אסור להשתמש בערך שפוענח מקוד QR כמו שהוא — תיקון שגיאות שהצליח אינו מבטיח את הערך

· עודכן בתאריך: · · קוד QR, ברקוד, תיקון שגיאות, אימות קלט, איכות נתונים, מערכות עסקיות, C#, תכנון, עבודה בשטח

מסוף הבדיקה במחסן משמיע “ביפ”. המערכת לוקחת את הצליל הזה כאות שהקוד QR נקרא, מזכה את התעודה ומאשרת את המשלוח. מה שצריך להפריד כאן הוא בין “המפענח החזיר ערך” לבין “מותר להמשיך לעבוד עם הערך הזה”.

לקוד QR יש תיקון שגיאות שמשחזר נתונים מלכלוך ומנזק. מרמה L ועד H הוא מסוגל לשחזר בערך 7% עד 30% מה-codewords.1 אבל מכאן אי אפשר להסיק ש”הערך שחזר נכון בהכרח”.

במאמר הזה נעבור לפי הסדר על מה קורה בפועל, עד היכן מגיע תפקידו של תיקון השגיאות, ומה האפליקציה חייבת לאמת, בעזרת דוגמאות QR אמיתיות ומדידות משני מפענחים שונים. את הקודים שמוצגים כאן אפשר לנסות עם קורא שיש לכם ביד. דוגמאות שנועדו להראות שקוד אינו ניתן לקריאה מסומנות במקומן.

הכנו גם כלי להשוואת פענוח קודי QR שמחליף בדפדפן בין jsQR ל-OpenCV.js. הוא מציג את תוצאות הפענוח של הדוגמאות ואת ההבדלים שקידוד התווים יוצר. עם זאת, את ההבדל בין גרסת ה-Python של OpenCV לגרסת הדפדפן, שמתואר בהמשך, יש להתייחס אליו בנפרד.

1. קודם כל המסקנה

מתייחסים לערך שפוענח כ”קלט חיצוני שלא עבר אימות”, בדיוק כמו קלט מהמקלדת. הבסיס הוא שלושה שלבים: בדיקת מבנה, אחריה ספרת ביקורת, ואחריה אימות עסקי. תיקון השגיאות אינו תחליף לאימות הזה.

מה בודקים מה “עבר” לבדו לא מלמד מה עושה הצד המקבל
האם הקוד QR פוענח האם הוא תואם לערך המקורי. האם זהו קטע של Structured Append או mojibake בודקים את המבנה — אורך, סוגי תווים, prefix — בהתאמה מלאה ומדויקת
האם הוא עקבי כמערכת קידוד האם כמה תווים השתנו, או שמדובר במספר חוקי אחר אחרי ספרת הביקורת מצליבים מול master data ומול ההקשר העסקי
האם קיימת תעודה שמישה האם זו התעודה של הקופסה, המשלוח או המשימה שצריך לטפל בהם עכשיו משווים אותה מול יעד העבודה שזוהה בדרך אחרת
האם היא ממתינה למשלוח עכשיו האם מסוף אחר מריץ את אותה פעולה באותו זמן מונעים עיבוד כפול עם מעבר מצב אטומי בזמן הביצוע, או עם idempotency key

גם את אופן הקריאה של המדידות צריך להפריד מראש. בשיבוש מודולים אקראי ובהשחתה אקראית של codewords, ב-9,700 ניסיונות לא הוחזר אף ערך שגוי. לעומת זאת, בנזק שנבנה בכוונה, שינוי של 7 מתוך 26 ה-codewords הספיק כדי ש-004873 ייקרא כ-104873, ושני מפענחים החזירו את אותה שגיאה. “קיים דפוס שנקרא באופן שגוי” ו”במה תדירות זה קורה בשטח” הן שתי שאלות שונות.

Structured Append, קידוד תווים, ותפיסה של קוד QR אחר מתוך הפריים — כל אלה קורים מחוץ לתיקון השגיאות. גם האם התוצאה תהיה אזהרה, חריגה או מחרוזת ריקה תלוי במימוש, ולכן אין להסתמך על “לא נזרקה שגיאה” לבדה כסימן להצלחה.

לפי מה להתחיל, בהתאם למה שרוצים לדעת

מה רוצים לדעת איפה לקרוא
האם באמת חוזר ערך שונה הדוגמאות האמיתיות בפרק 2
למה תיקון השגיאות לא מונע זאת המנגנון בפרק 3, והמדידות והמגבלות בפרק 4
לאיזו תקלה עסקית זה מתגלגל ארבעת דפוסי הכשל בפרק 5
איך לתקן את האפליקציה תכנון האימות, דוגמת ה-C#, ושאר אמצעי המיגון בפרק 6
איך להחליט על תכנון התווית ונוהלי השטח התפעול בפרק 7 ושלבי השחזור בפרק 8

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 16, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

2. דוגמאות — שני קודי QR שנראים כמעט זהים

2.1. זה נראה כמו הצלחה, אבל מספר התעודה שונה

שני הקודים שלמטה פוענחו בבדיקות של המאמר הזה בלי שגיאה. B, התחתון, הוא דוגמה שהנזק בה נבנה בכוונה כך שתיקרא כערך אחר. היא לא מלמדת דבר על התדירות שבה לכלוך רגיל מייצר תוצאה כזאת. קודם כל מאשרים את התוצאה: ערך חזר, והוא שגוי.

למעלה: קוד QR ללא נזק. למטה: קוד QR עם פס נזק אנכי אחד. השניים נראים כמעט זהים

מילה אחת לפני שמנסים: המצלמה הסטנדרטית של iOS עלולה שלא להגיב. אם הקריאה לא עובדת, נסו אפליקציית קורא QR (הסיבה בהערה שלמטה).

  • A (העליון)NO:20260725-004873
  • B (התחתון)NO:20260725-104873

אלה קודים אמיתיים, וניתן לנסות אותם ישירות עם קורא שיש לכם ביד. מספר התעודה בנוי לפי התבנית “8 ספרות של תאריך הזמנה + 5 ספרות של מספר רץ + ספרת ביקורת אחת”, ולכן מה שהשתנה הוא החלק הרץ — 00487 הפך ל-10487, מה שמצביע על תעודה אחרת, במרחק 10,000 רשומות.

המצלמה הסטנדרטית של iOS עלולה שלא להגיב. המצלמה הסטנדרטית בנויה לתת עדיפות לתוכן שאפשר “לפתוח”, כמו כתובת URL, ולכן קוד QR שנושא רק מחרוזת, כמו אלה שבמאמר הזה, עלול לא להציג דבר. אפליקציית קורא QR תקרא אותו. אותה תמונה בדיוק, ובכל זאת ההתנהגות משתנה עם המימוש בצד הקורא — נושא המאמר הזה הוא משהו שמתנסים בו כבר בדוגמה הראשונה.

2.2. רק 31 מודולים השתנו, ובכל זאת אין הודעה

B נבדל מ-A ב-31 מודולים בלבד, כולם בתוך פס אנכי אחד בעמודות 10 עד 14 משמאל. זה 15% מתוך 208 המודולים באזור ה-codewords.

תרשים שמסמן במסגרת אדומה את 31 המודולים ב-B שהשתנו לעומת A. הם מפוזרים ברצועה אנכית צרה וארוכה

והחלק המכריע הוא שהמפענח לא מחזיר שגיאה עבור אף אחד מהשניים. אין אפילו הודעה של “בוצע תיקון”. מבחינת האפליקציה, שתיהן קריאות מוצלחות באותה מידה.

כדי להבין את התוצאה הזאת אין להסתכל רק על שטח הנזק, אלא גם על איזה דפוס, כקבוצת codewords, הוא התקרב אליו. פרק 3 מסביר את המנגנון, סעיף 4.2 את אופן הבנייה, וסעיף 4.3 את המרחק מלכלוך אמיתי.

3. למה זה קורה — מה יש בפנים תיקון השגיאות

3.1. יחידת התיקון, ומרווח ההגנה שהתקן השאיר

תיקון השגיאות בקוד QR מבוסס על קוד Reed-Solomon, והוא פועל לפי codeword, ביחידות של 8 סיביות. הנה הפירוט של סימבול גרסה 1 ברמת תיקון שגיאות M (21×21 מודולים) שמשמש כאן.2

סה”כ codewords codewords של נתונים codewords של תיקון שגיאות יכולת התיקון לפי התקן
26 16 10 4 codewords

העמודה האחרונה היא זו שמושכת את העין. עם 10 codewords של תיקון שגיאות, קוד Reed-Solomon מסוגל לתקן עד 5. אבל יכולת התיקון שהתקן קובע היא 4. ה-codeword האחד של ההפרש מוקצה במכוון כcodewords להגנה מפני פענוח שגוי p (בגרסה 1-M, p = 2). הערת שוליים לטבלה 13 בתקן קובעת במפורש שיכולת התיקון נקבעת לפחות ממחצית מספר ה-codewords של תיקון השגיאות כדי להקטין את ההסתברות לפענוח שגוי.2

במילים אחרות, התקן עצמו תוכנן מתוך הנחה שתיקון השגיאות יכול להחזיר ערך שגוי. באותו סעיף נכתב גם כך.

כיוון שקוד QR הוא סימבולוגיה מטריצית, פגם שהופך מודול מכהה לבהיר (או להיפך) גורם לכך שסימבול התו המתאים מפוענח באופן שגוי כ-codeword אחר, שנראה תקין אך שונה.2

3.2. הוא לא “יודע” את הערך המקורי — הוא מחפש מועמד שאפשר לתקן

הסיבה נעוצה בעקרון התיקון עצמו. מה שפענוח Reed-Solomon עושה הוא לחפש codeword בתוך מרחק קבוע — יכולת התיקון — מהדפוס שהתקבל. אם נמצא כזה, הוא מוחזר כתשובה; אם לא נמצא, התוצאה היא “לא ניתן לקרוא”. הוא לא יוצא לחפש את ה-codeword הקרוב ביותר בכל מחיר, לא משנה כמה הסימבול ניזוק.

התכונה הזאת מפצלת את התוצאות לשניים. נזק אקראי מתפזר הרחק מכל codeword, ולכן בדרך כלל נופל לצד של “לא נמצא, ולכן לא ניתן לקרוא”. המקרה המסוכן הוא כשהנזק נופל במקרה בסביבה של codeword אחר. המפענח מחליט אז שה-codeword הזה הוא התשובה הנכונה ומחזיר אותו. ה-codeword שחזר עקבי לחלוטין בעצמו, ולכן אין דרך לדעת שהוא שגוי.

4. עד היכן התיקון מגיע, ומאיפה מתחיל הסיכון

כאן נמדדים בנפרד נזק אקראי ונזק שנבנה כך שיתקרב לערך אחר. כמות הנזק לבדה אינה מלמדת דבר על נכונות הערך שחזר. עם אותו מנגנון תיקון, המקום שבו פגע הנזק פיצל את התוצאות ל”לא ניתן לקרוא” ול”מחזיר ערך אחר”.

לפני שמסתכלים על המספרים, אלה תנאי הניסוי של הפרק הזה. הם ההנחות שמאפשרות לשחזר את הבדיקה בעצמכם.

פריט פרטים
הסימבול הנבדק גרסה 1-M / 21×21 (26 codewords = 16 נתונים + 10 תיקון שגיאות)
יצירה segno 1.6.6 / Python 3.11
מפענח 1 OpenCV 5.0.0 cv2.QRCodeDetector (Python 3.11)
מפענח 2 jsQR 1.4.0 (Node.js 22)
אופן יצירת הנזק (סעיף 4.1) היפוך מודולים שנבחרו באקראי מתוך 208 באזור ה-codewords / השחתה אקראית של codewords שלמים / טשטוש, רעש והנמכת ניגודיות אחידים על הסימבול כולו
אופן יצירת הנזק (סעיף 4.2) ניסוי ממצה של כל שילוב של codewords שהוזזו לעבר ערך היעד B
מספר הניסיונות 3,900 היפוכי מודולים (300 בכל רמה), 1,800 השחתות codewords (200 בכל רמה) ועוד 4,000 ניסיונות המשך מעבר ליכולת התיקון, 1,000 מקרי פגיעה באיכות התמונה (200 בכל תנאי). סעיף 4.2 הוא 792 ו-495 השילובים הממצים
סיווג ערך ההחזרה של המפענח מסווג כ”נקרא נכון” אם הוא תואם לערך המצופה, כ”לא ניתן לקרוא” אם לא התקבל ערך, וכ”ערך שגוי” אם הוחזר ערך לא ריק ושונה

הסביבה של המאמר כולו, כולל הדוגמאות בפרק 5, מרוכזת ב”סביבת הבדיקה” בסוף המאמר.

4.1. לכלוך אקראי נופל לצד של “לא ניתן לקרוא”

היפוך מודולים באקראי

על סימבול בגרסה 1-M הופכו באקראי מספר מודולים מתוך 208 באזור ה-codewords, והתוצאות סווגו (300 ניסיונות בכל רמה, 3,900 בסך הכול).

השוואה בין קוד QR עם 6 מודולים מהופכים לקוד עם 9 מודולים מהופכים. העליון נקרא, התחתון לא

בעליון הופכו 6 מודולים, בתחתון 9. העליון נקרא נכון; התחתון לא נקרא כלל. הבדל של 3 מודולים כמעט אינו נראה לעין. הגבול נמצא במקום שלא נראה במראה.

מספר המודולים שהופכו נקרא נכון לא ניתן לקרוא ערך שגוי
0 עד 5 1,799 1 0
6 131 169 0
7 32 268 0
8 11 289 0
9 עד 12 0 1,200 0

יכולת הקריאה קורסת בין 5 ל-6 מודולים, ומעל 9 היא נמחקת לגמרי. ולא הופיע אפילו ערך שגוי אחד. נזק שאי אפשר לתקן במלואו נופל לצד של “לא ניתן לקרוא” — אלה החדשות הטובות והפשוטות. גם ב-jsQR המספרים היו כמעט זהים (כל 1,800 הניסיונות בטווח 0 עד 5 נקראו נכון, ומעל 6 בתוך ניסיון אחד מ-OpenCV).

השחתת codewords שלמים, והשוואה בין יכולת התקן לבין מה שהמימושים עושים

הסתכלות על אותו דבר לפי codeword מחדדת את הגבול (200 ניסיונות בכל רמה, 1,800 בסך הכול).

מספר ה-codewords שהושחתו נקרא נכון לא ניתן לקרוא ערך שגוי
0 עד 5 1,194 6 0
6 עד 8 0 600 0

התיקון מגיע עד 5 codewords. כפי שראינו בסעיף הקודם, יכולת התיקון בתקן היא 4, והשאר היה מרווח לגילוי בלי תיקון. jsQR קרא נכון את כל 1,200 הניסיונות בטווח 0 עד 5, ולכן שני המימושים מוציאים את כל המרווח הזה על תיקון. אי אפשר לסמוך ברמת המימוש על המרווח שהתקן הקצה להגנה מפני פענוח שגוי.

גם השחתה שברור שהיא מעבר ליכולת התיקון (6 codewords ו-8 codewords) נבדקה שוב, 2,000 פעם כל אחת, ושוב לא היה אף ערך שגוי; כל הניסיונות הסתיימו ב”לא ניתן לקרוא”.

החלת פגיעה אחידה באיכות התמונה על הסימבול כולו

גם פגיעה שמקורה במצלמה מציגה את אותה נטייה. אלה תוצאות של טשטוש, רעש והנמכת ניגודיות אחידים על הסימבול כולו (200 ניסיונות בכל תנאי, 1,000 בסך הכול).

טשטוש σ רעש σ ניגודיות נקרא נכון לא ניתן לקרוא ערך שגוי
0 0 1.00 200 0 0
1.5 10 0.90 183 17 0
3.0 20 0.70 1 199 0
4.5 30 0.50 0 200 0
6.0 40 0.35 0 200 0

התוצאה היא או “נקרא נכון” או “לא ניתן לקרוא”, בלי אמצע. ככל שהפגיעה גוברת שיעור ההצלחה יורד, אבל כל מה שאבד הופך לכשל קריאה.

זה נכון לפגיעה אחידה. לרעידת יד אמיתית יש כיוון, וצילום בזווית או תאורה לא אחידה מקלקלים רק חלק מהתמונה. כפי שנראה בסעיף הבא, הסכנה נובעת מנזק מרוכז, ולכן אין להכליל מכאן ש”בעיות איכות תמונה לא גורמות לפענוח שגוי”.

4.2. כשהנזק נופל במקום הלא נכון, מתקבל ערך אחר באופן ודאי

דוגמה B בפרק 2 נבנתה בכוונה כדי לייצר בדיוק סוג כזה של נזק מרוכז.

כשמסדרים את 26 ה-codewords של NO:20260725-004873 (A) ושל NO:20260725-104873 (B), 12 מהם שונים: 2 codewords של נתונים, ועוד 10 codewords של תיקון שגיאות שנגררו אחריהם.

הזזה של 7 מתוך 12 אלה לערכים של B מציבה את הדפוס המתקבל במרחק 7 codewords מ-A ו-5 codewords מ-B. אם יכולת התיקון היא 5, המפענח קורא את זה כ”B עם 5 codewords מלוכלכים” ומתקן אותו ל-B.

תנאי שילובים שנבדקו מספר הפענוחים השגויים כ-B
7 codewords שהוזזו לעבר B (מרחק 5 מ-B) 792 792 (100%)
8 codewords שהוזזו לעבר B (מרחק 4 מ-B) 495 495 (100%)

כל שילוב שנבדק כאן פוענח באופן שגוי כ-B, בלי יוצא מן הכלל. השורה התחתונה היא מרחק 4 מ-B, כלומר בתוך יכולת התיקון שהתקן קובע, כפי שהיא נראית מ-B. אפילו מימוש שמכבד את ה-codewords להגנה מפני פענוח שגוי p נותן את אותה תוצאה ברגע שהנזק גדל ב-codeword אחד נוסף. p רק מקטין את ההסתברות; הוא לא מונע זאת.

B כפי שהוא מוצג בפרק 2 הוא השילוב שבו הנזק מתאסף לפס אנכי אחד (31 מודולים). במזעור אפשר היה להוריד אותו ל-23 מודולים. שני מימושים בלתי תלויים, OpenCV 5.0.0 ו-jsQR 1.4.0, מחזירים שניהם NO:20260725-104873.

4.3. איך לקרוא את הפער הזה

למען האמת, נזק כזה אינו סביר שייווצר באקראי. עם לכלוך אקראי, 9,700 ניסיונות הניבו אפס פענוחים שגויים. זה לא מקרה של “זה יכול לקרות מחר”.

אף על פי כן, יש שלוש סיבות שאי אפשר להתעלם מהן.

  • נזק בעולם האמיתי אינו אקראי. קמט רץ בקו ישר, שחיקה בזמן הובלה מתרכזת באותו קצה, וראש דיו סתום מייצר פס אנכי. הנזק בצורת פס שכאן הוא דוגמה אחת לנזק כזה, שיש לו הטיה מיקומית. עם זאת, B כולל שינויים בשני הכיוונים — 17 מודולים מלבן לשחור ו-14 משחור ללבן — ולכן אי אפשר לשחזר אותו בפגם שמסיר דיו בלבד. שני הכיוונים מתרחשים יחד במקרים כמו צל של קמט שמזיז את סף ה-binarization, לכלוך והדפסה דהויה שחופפים, או מדבקה נוספת שהודבקה חלקית מעל.
  • מספר הסריקות גדול בסדרי גודל. הסתברות שזניחה בסריקה אחת היא סיפור אחר באתר שקורא עשרות אלפי קודים ביום. וכיוון שפענוח שגוי לא מפיק שגיאה, הוא גם לא משאיר רשומה, ובסופו של דבר מטופל כפער ספירה בלתי מוסבר.
  • אם אפשר לבנות את זה בכוונה, גם אחרים יכולים לבנות את זה. הדפוס כאן נבנה באופן מכני אחרי בחירת ערך היעד. בקודי QR שיש להם מניע לשכתב את הערך, כמו תגי מחיר וקופונים, זה הופך לטכניקת תקיפה.

5. “זה נקרא, אבל זה שגוי” — מקרים שאינם קשורים לתיקון השגיאות

ארבע הבעיות הבאות מתרחשות גם כשהתיקון שגיאות עובד כראוי: נאסף רק קטע, התווים פורשו אחרת, נבחר קוד QR אחר, או שהתווית עצמה שגויה. אלה המקרים שפוגשים בתדירות גבוהה יותר בעבודה המעשית, והפלט של המפענח לבדו אינו יכול לגלות אותם.

5.1. קריאה של הסימבול הראשון בלבד מתוך קבוצת Structured Append

לקודי QR יש מנגנון (Structured Append) שמפצל נתונים ארוכים על פני כמה סימבולים ומחבר אותם בצד הקורא. למטה מופיע הראשון מבין שלושת הסימבולים שאליהם פוצל NO:20260725-004873/LOT:AB-77/QTY:120/EXP:20270131.

הראשון מבין שלושה סימבולים של Structured Append. בקריאה בנפרד הוא מחזיר מספר תעודה שחסר בסופו

הוא נראה כמו קוד QR רגיל, בלי שום רמז שהוא אחד משלושה. בקריאה בנפרד, OpenCV מחזיר את זה.

NO:20260725-00487

בלי שגיאה, בלי אזהרה. מספר תעודה שנראה לגמרי סביר, שחסרה בו רק ספרת הביקורת 3 שבסוף. הסימבול השני והשלישי מחזירים 3/LOT:AB-77/QTY: ו-120/EXP:20270131. אותה תמונה בדיוק ל-jsQR החזירה מחרוזת ריקה. מה קורה כשאפליקציה שלא מצפה ל-Structured Append סורקת במקרה את הסימבול הראשון משתנה בין מפענחים.

וכך זה נראה במערכת עסקית. במסך שמחפש מספרי תעודות בהתאמת prefix או ב-LIKE, המספר NO:20260725-00487 שחסרה בו הספרה האחרונה פוגע בתעודה המקורית בכל זאת, ומכיוון ש”זה נקרא, והתעודה עלתה” — איש לא מבחין במשהו חריג. אם האורך אינו נבדק כערך קבוע, הקטע הזה זורם עד הסוף כקריאה תקינה.

5.2. קידוד תווים ו-ECI

הבא הוא קוד QR שמכיל 部品番号 東-004873 ב-Shift_JIS, בלי ציון ECI.

קוד QR שנוצר ב-Shift_JIS בלי ציון ECI. קריאה שלו מפיקה טקסט משובש

גם זה קוד אמיתי. קראו אותו עם קורא שיש לכם ביד ותקבלו 部品番号 東-004873, או מחרוזת משובשת, או כלום — מה שמלמד באיזו פרשנות משתמש הקורא שלכם.

העבירו את התמונה הזאת לכלי להשוואת פענוח קודי QR ותראו את רצף הבתים הגולמי ש-jsQR הוציא, לצד התוצאות של פירוש מחדש שלו כ-UTF-8, Shift_JIS, EUC-JP וכן הלאה. אפשר לראות במקום כיצד אותו רצף בתים הופך למשהו אחר לגמרי בהתאם לקידוד התווים.

שינוי תנאי היצירה והשוואה של תוצאות המפענחים

אלה תוצאות של יצירת אותו תוכן עם קידודי תווים וציוני ECI שונים והעברתו לשני המפענחים. כל ארבעת הסימבולים האלה נמצאים בין הדוגמאות של הכלי. עם זאת, הערכים בטבלה נמדדו עם cv2 של Python, ורק השורה השלישית שונה מגרסת הדפדפן (מיד אחרי הטבלה). מה שהכלי מאפשר לאמת הוא ההתנהגות של גרסת הדפדפן, ולכן כדי לשחזר את “נופל בחריגה” של השורה השלישית דרוש cv2 של Python.

תנאי יצירה OpenCV 5.0.0 jsQR 1.4.0
Shift_JIS / בלי ECI מחזיר מחרוזת משובשת כהצלחה מחרוזת ריקה
UTF-8 / בלי ECI 部品番号 東-004873 部品番号 東-004873
Shift_JIS / עם ECI מפיק אזהרה ונכשל בפענוח מחרוזת ריקה
UTF-8 / עם ECI 部品番号 東-004873 部品番号 東-004873

השורה הראשונה היא הגרועה ביותר. OpenCV פירש את רצף הבתים כ-Latin-1 והחזיר את המחרוזת השבורה \x95\x94\x95i... בלי שום שגיאה. מבחינת האפליקציה זו קריאה תקינה, ואם היא נכנסת ישר למסד הנתונים נוצרה רשומה משובשת אחת.

וכך זה נראה במערכת עסקית. מחרוזת משובשת נרשמת בשדה שם הפריט של רשומת קליטה, ולמחרת זה הופך לפנייה שאומרת “חיפוש לפי מספר הפריט הזה לא מחזיר רשומה”. קריאה חוזרת של אותה תווית מכניסה את אותו ערך, ולכן השטח מדווח ש”החיפוש במערכת שבור”, וההלוך-חזור נמשך עד שמישהו מבין שהסיבה היא קידוד התווים בצד הקריאה.

כשה-ECI לא מצוין, וכשהוא מצוין אבל המימוש לא יודע לטפל בו

זה לא באג של OpenCV — זוהי התנהגות שתואמת בדיוק את פרשנות ברירת המחדל (ISO/IEC 8859-1) שהתקן הנוכחי מגדיר.3 מה שסוטה מהתקן הוא דווקא הכנסת Shift_JIS בלי ECI.

גם את השורה השלישית אין לפספס. למרות ש-ECI — המנגנון שמצהיר במפורש על קידוד התווים — צוין כראוי, OpenCV הפיק את האזהרה QR: ECI is not supported properly ואז נפל בחריגה, בלי יכולת לפרש את ערך ההחזרה כ-UTF-8. ההיפוך שבו קוד QR שנבנה בנאמנות לתקן הוא דווקא זה שאי אפשר לקרוא קורה בפועל.

אין להתייחס לגרסת ה-Python ולגרסת הדפדפן כתוצאה זהה

יותר מזה, השורה השלישית הזאת מניבה תוצאות שונות גם בתוך אותו OpenCV, בהתאם ל-language binding. הטבלה שלמעלה היא התוצאה עם cv2 של Python, אבל העברת אותה תמונה לגרסת הדפדפן (opencv.js 5.0.0) אינה מעוררת חריגה: היא מחזירה כהצלחה את המחרוזת ���i��� ��-004873, מלאה בתווי החלפה. הסיבה היא שכאשר Emscripten ממיר std::string כ-UTF-8, הוא מפיל בתים לא חוקיים ל-U+FFFD במקום לזרוק חריגה. אותה גרסה, אותה תמונה; הדבר היחיד שהשתנה הוא שפת הקריאה. Python, שבו חריגה לפחות מאפשרת להבחין בבעיה, הוא הטוב מבין השניים; גרסת הדפדפן אומרת “זה נקרא” ובמקביל מחזירה ערך שבור. אפשר לאמת זאת עם הדוגמאות של הכלי.

5.3. כמה קודי QR בתוך הפריים

כמה קודי QR שמודפסים על תעודה אחת, או התווית של הקופסה הבאה שנכנסת לשדה הראייה — מצבים שגרתיים. ניסינו שלושה קודי QR בשורה זה לצד זה.

תמונה עם שלושה קודי QR זה לצד זה. משמאל: NO:, ITEM:, LOT:

API של קריאה בודדת אינו מבטיח שהוא דוחה כמה סימבולים

קודם כל, בתמונה הזאת ה-API של קריאה בודדת של OpenCV לא החזיר דבר. אבל זו אינה הבטחה ש”הוא דוחה קלט כשיש כמה”. ה-API של קריאה בודדת מתועד כמזהה ומפענח קוד QR אחד בלבד; אין תיעוד שהוא דוחה כמה סימבולים, ובהתאם לפריסה הוא עשוי בהחלט להחזיר אחד מהם. אין להשתמש בקיומו או בהיעדרו של ערך החזרה מה-API של קריאה בודדת כתחליף לזיהוי כמה קודים.

גם עם API של קריאה מרובה, אי אפשר להשתמש בסדר התוצאות לזיהוי

שימוש ב-API של קריאה מרובה לא אומר שאפשר לסמוך גם על סדר ההחזרה.

אלה תוצאות של קריאת התמונה שלמעלה (NO: / ITEM: / LOT: משמאל) כפי שהיא, בשינוי ממדי הפיקסלים בלבד. זה תואם סורק אמיתי שבו המרחק ליעד או רזולוציית המצלמה משתנים.

רוחב התמונה סדר ההחזרה
1,001px NO: / LOT: / ITEM:
1,502px לא מוחזר דבר
2,002px NO: / LOT: / ITEM:
3,003px NO: / ITEM: / LOT:
4,004px LOT: / NO: / ITEM:

אותה תמונה בדיוק, ובכל זאת הסדר משתנה רק מפני שהרזולוציה השתנתה. הוא לא משמאל לימין ולא מהגדול לקטן. יש אפילו רזולוציה שבה לא נקרא דבר. הסדר נקבע לפי העבודה הפנימית של אלגוריתם הזיהוי, וכיוון שהוא אינו מוגדר, זו התוצאה שמתקבלת.

מכאן שקוד שלוקח את index 0 מתוך הנחה ש”הראשון הוא בטח מספר התעודה” עשוי לעבוד היום במקרה, ומחר לתפוס קוד אחר רק מפני שהמצלמה התקרבה. זה סוג של פגם שקשה לשחזר וקשה לאתר.

וכך זה נראה במערכת עסקית. על שולחן הבדיקה נכנסים לשדה הראייה בו-זמנית התעודה הרצויה והתווית של הקופסה הסמוכה. אפליקציה שלוקחת index 0 מזכה את התעודה השכנה, והוראת המשלוח מאושרת לפיה. המפעיל משוכנע שהמצלמה כוונה לתווית הנכונה, ולכן איש לא מבחין בכך עד שמישהו אומר אחרי המשלוח “הגיע מוצר אחר”.

אמצעי המיגון: לבחור לפי תוכן, ולוודא שיש מועמד אחד בדיוק

הצד המקבל חייב לבחור לפי תוכן, לא לפי סדר. לאסוף הכול עם ה-API של קריאה מרובה, לקבל רק את הרשומות שה-prefix והמבנה שלהן תואמים, ולהחזיר שגיאה אם יש אפס התאמות או שתיים ומעלה — כך כותבים את זה בצורה בטוחה.

5.4. מלכתחילה התוכן אינו בהכרח נכון

יש שכבה שעיבוד תמונה אינו יכול לזהות אפילו באופן עקרוני. נתוני המקור שהודפסו שגויים, תווית ישנה מלפני הדבקה מחדש עדיין נמצאת על הקופסה, תווית של ספק אחר התערבבה, או שהתווית היא העתק.

קוד QR אומר לכם רק מה כתוב בו. האם זה נכון, והאם החברה שלכם הנפיקה אותו — את זה יכול לאמת רק הצד שמקבל אותו.

וכך זה נראה במערכת עסקית. תווית קודמת עדיין נמצאת על הדופן של קופסה שנעשה בה שימוש חוזר, וקוראים אותה במקום את התווית החדשה שעל המכסה. כערך, הקוד QR תקין לחלוטין, ולכן הוא עובר את בדיקת המבנה, את ספרת הביקורת, וגם בדיקת master data שמאמתת רק קיום — והחבילה יוצאת ליעד הקודם. בדיקת הערך לבדו אינה אומרת דבר על השאלה אם זו התווית של הקופסה שלפניכם. מה שנדרש הוא ההשוואה מול היעד שצריך לטפל בו עכשיו, כפי שמוסבר בפרק 6.

6. מה עושים עם הערך שהתקבל

האמצעי מצטמצם לדבר אחד: להעמיס אימות משלכם מחוץ לתיקון השגיאות.

שלב מה מאומת מה נתפס
1. בדיקת מבנה התאמה מלאה של אורך, סוגי תווים, מפרידים ו-prefix קריאה של קוד אחר, קטע של Structured Append, mojibake
2. אימות עצמי ספרת ביקורת מזהה באופן ודאי שינוי של תו אחד. מפספס חלק מהשינויים של כמה תווים
3. אימות עסקי בדיקת master data, והאם הוא תואם ליעד שצריך לטפל בו עכשיו תווית ישנה, תווית של חברה אחרת, תפיסה של תעודה אחרת

מספר התעודה בדוגמה הוא NO: ועוד 8 ספרות של תאריך הזמנה, 5 ספרות של מספר רץ וספרת ביקורת אחת, והספרה שבסוף משתמשת באותה שיטת modulus 10 weight 3 כמו GS1. הפענוח השגוי NO:20260725-104873 מפרק 2 נעצר כאן: ספרת הביקורת הנכונה עבור 2026072510487 היא 0, והיא אינה תואמת ל-3 שעל התווית.

מה שלושת השלבים האלה לא מצליחים להגן עליו

לפני המעבר למימוש, נסתכל על המגבלות של כל שלב. ספרת הביקורת, בדיקת הקיום והמצב, והרצת הפעולה — לכל אחד תפקיד אחר.

שינוי של כמה תווים לא נעצר בספרת ביקורת בלבד

ספרת הביקורת מפספסת שינויים של כמה תווים. מה ש-modulus 10 weight 3 תופס באופן ודאי הוא שגיאה של תו אחד. ואכן, 2026072500487 ו-2026072517487 מניבים שניהם ספרת ביקורת 3, ולכן NO:20260725-174873 חולף ישר דרך שלב 2. תיקון שגוי לא בהכרח משנה תו אחד בלבד, ולכן אי אפשר לוותר על שלב 3.

קיום ומצב תקינים עדיין לא הופכים אותו ליעד העבודה הנוכחי

אל תתנו לבדיקת ה-master data להסתיים באימות קיום. הקריאה repo.Find() ובדיקת המצב בדוגמת ה-C# שלמטה מאמתות רק שקיימת תעודה שמישה איפשהו. אם המפעיל קורא את התווית של הקופסה הסמוכה, גם התווית הזאת עומדת במבנה, בספרת הביקורת ובמצב “ממתין למשלוח”, ולכן היא חולפת ישר. את הערך שפוענח צריך להשוות מול היעד שאמורים לטפל בו עכשיו — האם הוא תואם לרשומה הבאה ברשימת הליקוט, האם הוא קשור למזהה מכולה שכבר נסרק, האם היעד זהה למשלוח שנמצא בעבודה. מה שמשווים מולו משתנה מתהליך עסקי אחד למשנהו, ולכן זה המקום היחיד שאינו מתרגם לקוד גנרי.

עיבוד כפול נמנע בצד הביצוע, אחרי האימות

אימות לא מונע עיבוד כפול. אם שני מסופים קוראים את אותה תווית בערך באותו זמן, שניהם מאשרים את מצב “ממתין למשלוח” ואז מעדכנים אותו, ולכן שניהם עוברים. מניעת זה היא תפקידו של צד הביצוע: להפוך מעבר מצב מותנה כמו UPDATE ... WHERE status = 'WaitingForShipment' לפעולה אטומית אחת, או לספוג את ההרצה החוזרת עם idempotency key. אימות הוא החלטה בנקודת הכניסה; הוא אינו תחליף לשליטה על מקביליות.

6.1. קודם כל מחליטים על קידוד התווים ועל המבנה

החליטו על הנחת קידוד התווים לפני שכותבים שורת קוד. המימוש שלמטה מניח שמספר התעודה מורכב מספרות ASCII בלבד, ומחשב את ספרת הביקורת כ”תו פחות '0'”. אם ספרות ברוחב מלא, או רצף הבתים המשובש שראינו בסעיף 5.2, יגיעו לחישוב הזה — התוצאה חסרת משמעות. לכן הביטוי הרגולרי של בדיקת המבנה כתוב עם [0-9] ולא עם \d, בסדר שמונע מכל ספרה שאינה ASCII להגיע אי פעם לחישוב ספרת הביקורת. קודם מחליטים על ההנחה, אחר כך מבטיחים אותה בבדיקת המבנה, ורק אחר כך מחשבים. ברגע שהסדר הזה נשבר, כל בדיקה בהמשך מסתובבת בלי לעשות דבר.

6.2. מימוש אימות בנקודת הכניסה ב-C#

למטה מופיעה דוגמה של בדיקת המבנה, ספרת הביקורת ובדיקת הקיום והמצב של התעודה עבור ערך מפוענח אחד. ISlipRepository, SlipStatus וסוג התעודה מגיעים מהמימוש בצד העסקי. הדוגמה הזאת לבדה אינה משלימה את ההשוואה ליעד העבודה או את מניעת העיבוד הכפול. גם הטיפול במקרה שבו המפענח זורק חריגה מטופל בנפרד בצד הקורא.

using System.Linq;
using System.Text.RegularExpressions;

public sealed record ScanOutcome(bool Accepted, string? SlipNo, string Reason);

public static class SlipScanValidator
{
    // NO: + 8 ספרות של תאריך הזמנה + '-' + 5 ספרות של מספר רץ + ספרת ביקורת אחת
    // המסיים הוא \z ולא $. ב-.NET גם $ תואם מיד לפני שורה חדשה בסוף,
    // ולכן הוא היה מאפשר ל-"NO:20260725-004873\n" לעבור.
    // הספרות הן [0-9] ולא \d. ב-.NET, \d תואם ספרות Unicode בכלליות,
    // כולל ספרות ברוחב מלא, אבל חישוב ספרת הביקורת שלמטה מניח ASCII
    private static readonly Regex Format =
        new(@"\ANO:(?<date>[0-9]{8})-(?<seq>[0-9]{5})(?<cd>[0-9])\z", RegexOptions.Compiled);

    public static ScanOutcome Validate(string? raw, ISlipRepository repo)
    {
        // 1. אין להפוך "לא ניתן לקרוא" ל"הצלחה ריקה".
        //    מפענחים שונים מדווחים על כשל כ-null, כמחרוזת ריקה או כחריגה
        if (string.IsNullOrEmpty(raw))
            return new(false, null, "לא ניתן לקרוא את הקוד. סרקו שוב");

        // 2. בדיקת מבנה. לא התאמת prefix, אלא התאמה מלאה שכוללת את האורך.
        //    הקטע "NO:20260725-00487" מ-Structured Append נדחה כאן
        var m = Format.Match(raw);
        if (!m.Success)
            return new(false, null, $"לא במבנה של קוד QR של תעודה ({Describe(raw)})");

        // 3. אימות עצמי. אם תיקון שגוי שיבש ספרה, זה נתפס כאן
        var body = m.Groups["date"].Value + m.Groups["seq"].Value;
        if (Modulus10Weight3(body) != m.Groups["cd"].Value[0] - '0')
            return new(false, null, "ספרת הביקורת אינה תואמת. בדקו אם התווית מלוכלכת");

        // 4. אימות עסקי. האם היא קיימת, והאם היא במצב שמותר לטפל בו עכשיו
        var slip = repo.Find(raw);
        if (slip is null)
            return new(false, null, "לא קיימת תעודה מתאימה");
        if (slip.Status != SlipStatus.WaitingForShipment)
            return new(false, null, $"התעודה הזאת '{slip.Status}'. היא אינה יעד משלוח");

        return new(true, raw, "OK");
    }

    // אותו modulus 10 weight 3 כמו GS1. משקלים 3,1,3,1... מהספרה הימנית
    private static int Modulus10Weight3(string body)
    {
        var sum = 0;
        for (var i = 0; i < body.Length; i++)
        {
            var weight = (body.Length - i) % 2 == 1 ? 3 : 1;
            sum += (body[i] - '0') * weight;
        }
        return (10 - sum % 10) % 10;
    }

    // הערך המפוענח הוא קלט חיצוני. לפני שמציגים אותו על המסך או כותבים אותו ללוג,
    // משאירים רק ASCII מודפס.
    // char.IsControl מסיר רק Unicode Control (Cc) ומעביר תווי Format (Cf)
    // כמו U+202E (right-to-left override). יש לכתוב allowlist, לא denylist
    private static string Describe(string raw)
    {
        var kept = raw.Where(c => c >= ' ' && c <= '~').Take(40).ToArray();
        if (kept.Length == 0) return "(לא ניתן להציג את המחרוזת)";
        var safe = new string(kept);
        return kept.Length < raw.Length ? safe + "... (הוסרו חלק מהתווים)" : safe;
    }
}

תכנון ספרות הביקורת עצמן מוסבר בתכנון קודים למערכות עסקיות — קביעת קודי מוצר ולקוח וספרות ביקורת, שם מפורטים הנוסחאות ואיך בוחרים ביניהן. מה שרלוונטי בהקשר של המאמר הזה הוא שהבדיקה פועלת לא רק מול טעויות הקלדה של אדם אלא גם מול פענוח שגוי של מכונה.

“תקלות” ו”תקיפות” הן בעיות שונות

האימות עד כאן הוא מיגון מפני תקלות: פענוח שגוי בגלל לכלוך, החמצת סימבול של Structured Append, mojibake, תווית ישנה שמתערבבת. יש לתכנן אותו כך שיכלול גם את ההשוואה ליעד העבודה ואת מניעת העיבוד הכפול בצד הביצוע. לעומת זאת, מיגון מפני גורם שמשכתב את הערך בכוונה הוא דרישה נפרדת.

בשימושים שיש להם מניע לשכתב את הערך — תגי מחיר, קופונים, כרטיסי כניסה, תשלומים — שום דבר מזה אינו הגנה. תוקף יכול לעמוד במבנה, לחשב מחדש את ספרת הביקורת, ולהפיק בחופשיות קוד QR שמצביע על מספר אחר שקיים באמת. בדיקת master data בודקת רק קיום, ולכן הוא חולף ישר.

מיגון מפני זיוף דורש דרך לאמת את המנפיק

אם עצם ההחזקה בקוד QR מעניקה ערך או הרשאה, הערך עצמו חייב לשאת אותנטיות. או להפוך אותו ל-token שאי אפשר לנחש, שהשרת מנפיק — מספר אקראי באורך מספיק — כך שאי אפשר להסיק מספר אחד מאחר, או לצרף ל-payload MAC עם מפתח או חתימה דיגיטלית ולתת לצד המקבל לאמת עם המפתח. בשני המקרים מנהלים בצד השרת את מצב השימוש כדי למנוע שימוש כפול בעותק.

החלפה של קוד QR אמיתי נמנעת על ידי קישור ליעד

אותנטיות לבדה אינה עוצרת החלפה. אם מקלפים קוד QR חוקי ממוצר זול ומדביקים אותו על מוצר יקר, גם ה-token וגם החתימה נשארים אמיתיים. במקום שבו המדיום משמש שוב ושוב, כמו תג מחיר שאפשר להעביר, גם בדיקת מצב השימוש לא עוזרת. מה שעובד כאן הוא אותו רעיון כמו בשלב 3: לאמת בדרך שאינה הערך עצמו האם קוד ה-QR הזה שייך לאובייקט שלפניכם — לקחת את הזהות של המוצר בדרך אחרת ולהשוות, להצליב מול ההקשר העסקי כמו קבלת הקופה או חלון הזמן של הכניסה, או לקשור פיזית עם תווית tamper-evident שנקרעת כשמסירים אותה.

לא ספרת הביקורת ולא בדיקת ה-master data מבטיחות דבר בנוגע לאותנטיות. וגם האותנטיות עצמה אינה מבטיחה שהערך שייך לאובייקט שלפניכם. הנקודה המרכזית היא לא לנסות לכסות מיגון מפני פענוח שגוי, מיגון מפני זיוף ומיגון מפני החלפה באותו מנגנון אחד.

7. מה מחליטים בצד התפעול

ההחלטות התפעוליות מסתדרות בקלות רבה יותר כשמחלקים אותן לתכנון התווית, לטיפול בכשל קריאה, ולשלב האישור לפני הקביעה.

7.1. לתכנן את התווית כך שיסדר את סיבות אי-הקריאה ואת אמצעי הבדיקה

  • תמיד להדפיס מחרוזת קריאה לאדם מתחת לקוד QR. זה אותו רעיון כמו Human Readable Interpretation (HRI) שמוגדר על ידי GS1.4 כשחושדים בפענוח שגוי, נשארת דרך שבה אדם יכול להשוות. בתפעול אמיתי זה לא נדיר שזו הדרך היחידה שבה פענוח שגוי מתגלה. עבודה בשטח עם ברקודים בכלל מסוכמת בתקני הברקוד של GS1 — היסודות ומלכודות התפעול.
  • לא להשתמש ב-Structured Append. אם הנתונים לא נכנסים, להעלות את הגרסה, או להכניס לקוד QR רק מזהה ולשלוף את השאר מ-master data. האפשרות השנייה גם מאפשרת להשאיר את התווית קטנה ולתקן את התוכן בלי להנפיק את התווית מחדש.
  • לפתור את בעיית קידוד התווים בכך שלא מכניסים תווים כאלה. לשמור קודי QR לשימוש עסקי בתוך ASCII. מצב בתים (byte mode) אינו מצהיר על קידוד תווים אלא אם צוין ECI, ופרשנות ברירת המחדל השתנתה בין מהדורות של התקן.3 כתיבה ב-UTF-8 בלבד אינה קונה תאימות הדדית, וסורק שמיישם פרשנות אחרת מפיק mojibake. אם בכל זאת חייבים להכניס תווים שאינם ASCII, הצהרה על UTF-8 עם ציון ECI היא התשובה הנכונה לפי התקן, אבל כפי שסעיף 5.2 מראה, קיימים בפועל מימושים שהטיפול שלהם ב-ECI מוטל בספק, ולכן אין מנוס מבדיקה על מכונה אמיתית של הדגמים שאתם מתכננים להשתמש בהם.

7.2. להגדיר נוהל לכשל קריאה, ולשמור תיעוד שאפשר לחקור

  • להחליט מה עושים כשלא מצליחים לקרוא. מספר הסריקות החוזרות המרבי, הנסיגה להקלדה ידנית, ומי מאשר אותה. אם זה מעורפל, השטח נוטה ל”להמשיך לשנות זווית ולנסות עד שזה נקרא”, מה שמעלה את ההסתברות לפענוח שגוי.
  • לתעד את הערכים שנדחו. אם הם מצטברים סביב תווית או מסוף מסוים, אפשר לאתר מוקדם מדפסת או סורק תקולים. אבל אין לכתוב את המחרוזת הגולמית ישר ללוג מונחה שורות. ערך שמכיל שורות חדשות או תווי בקרה יכול לזייף שורות בלוג או לשבור את התצוגה. את המקור יש לקצר, להעביר escaping ולשמור בשדה של לוג מובנה או בעמודה במסד נתונים, ולהוציא רק ייצוג מחוטא בשורות שאדם קורא — את ההפרדה הזאת בונים מההתחלה. אם אפשר, לשמור גם את תמונת הסריקה.

7.3. ככל שפעולה קשה יותר לביטול, כך מחזקים את האישור לפני הקביעה

  • להכניס אישור לפני פעולה בלתי הפיכה. בפעולות שקשה לבטל — אישור משלוח, הפחתת מלאי, סגירת תשלום מול חוב — להציג לאדם את שם הפריט ואת הסכום שנשלפו מהערך המפוענח. פענוח שגוי אולי נראה כערך סביר, אבל בהקשר העסקי הוא לעתים קרובות נראה לא במקום.

עד כמה להרחיק את האימות נקבע לפי הנזק שטעות גורמת.

שימוש בדיקת מבנה ספרת ביקורת בדיקת master data אישור אנושי
מיקומים ומספרי מדף פנימיים חובה רשות מומלץ לא נדרש
קליטה, משלוח וספירת מלאי חובה מומלץ חובה לא נדרש
אישור משלוח והפחתת מלאי חובה חובה חובה מומלץ
חיוב וסגירת תשלומים חובה חובה חובה חובה
מניעת ערבוב תרופות וחומרים מסוכנים חובה חובה חובה חובה

8. סיכום

תיקון השגיאות בקוד QR הוא מנגנון לשחזור ה-codewords המקוריים מדפוס מודפס. את העבודה הזאת הוא עושה היטב. גם במדידות, מול לכלוך אקראי ומול פגיעה אחידה באיכות התמונה, הוא קרא נכון בתוך הטווח שאפשר לתקן, וּויתר באופן נקי על הקריאה מעבר לו.

אבל זה עניין אחר לגמרי מכך שהמחרוזת שהאפליקציה קיבלה נכונה מבחינה עסקית. בהתאם למקום שבו נופל הנזק, התיקון יכול לעשות את עבודתו ולהפיק ערך אחר ותקף. Structured Append, קידוד תווים, וקוד נוסף בתוך הפריים — אלה בעיות שמחוץ לתיקון השגיאות לגמרי, ואפילו לא עניין של הסתברות.

העובדה שהתקן עצמו כותב “מפוענח באופן שגוי כ-codeword אחר, שנראה תקין אך שונה”, וטורח להקצות codewords להגנה מפני פענוח שגוי, מבטאת את המבנה הזה בבירור. וגם אחרי כל זה אפשר רק להקטין את ההסתברות — עד כאן מגיע התקן. הדבר היחיד שיכול לסגור את הפער שנותר הוא האפליקציה שמקבלת את הערך.

ערך שפוענח בהצלחה הוא קלט שלא עבר אימות, שהגיע מבחוץ. להתייחס אליו בדיוק כמו למחרוזת שהוקלדה במקלדת — זאת, לדעתי, הדרך הנכונה לעבוד עם קודי QR.

שלושה שלבים לנסות זאת על קודי ה-QR שלכם

בדקו גם את התוויות שלכם, כשהערך התקין והמימוש שמשמש לקריאה קבועים מראש. השלבים שלמטה הם דרך לבחון את התנהגות הפענוח; הם אינם אומרים שפגיעה בקוד תגרום בהכרח לפענוח שגוי. דוגמת הפענוח השגוי בסעיף 4.2 נבנתה בכוונה.

  1. ליצור. לקחת ערך אחד בפורמט שאתם באמת עובדים איתו ולהפוך אותו לקוד QR עם מחולל שיש לכם ביד (הדוגמאות במאמר הזה נוצרו עם segno 1.6.6). קודם כל לאמת שהוא נקרא כשהוא ללא פגע.
  2. לפגוע בו. בתוכנת עריכת תמונות, לצייר פס אנכי אחד שמדמה קמט או ראש דיו סתום. מה שחשוב הוא הריכוז ולא הכמות, ולכן במקום לפזר רעש חלש על הסימבול כולו, לרכז את הנזק באזור צר.
  3. להשוות בין שני מפענחים. להעביר אותו לכלי להשוואת פענוח קודי QR ולהשוות בין התוצאות של jsQR ל-OpenCV.js. התנהגויות כמו “רק אחד מהם מחזיר ערך”, או “שניהם מחזירים את אותו ערך והוא שונה מהמקור”, אפשר לאמת במקום.

אל תבדקו רק אם שתי התוצאות מסכימות זו עם זו, אלא גם אם הן מסכימות עם הערך שציפיתם לו כשיצרתם את הקוד. כפי שקורה בפרק 2, שניהם יכולים להחזיר את אותו ערך שגוי. אם התוצאות מתפצלות בין שני המפענחים, התנאי הזה הוא תנאי שתכנון האימות שלכם חייב לטפל בו. שווה להשקיע פעם אחת ולנסות על השולחן את השאלה “האם הסורקים אצלנו בסדר?”.


סביבת הבדיקה

פרקים 2 עד 4, שמסתכלים על התנהגות תיקון השגיאות, משתמשים לאורכם בגרסה 1-M (21×21 מודולים). הדוגמאות בפרק 5 משנות גרסה בהתאם לכמות הנתונים, ולכן הן מפורטות לפי תת-סעיף.

פריט פרטים
מפענח 1 OpenCV 5.0.0 cv2.QRCodeDetector
מפענח 2 jsQR 1.4.0 (Node.js 22)
יצירה segno 1.6.6 / Python 3.11
סימבולים בפרקים 2 עד 4 גרסה 1-M / 21×21 (26 codewords = 16 נתונים + 10 תיקון שגיאות)
סעיף 5.1 (Structured Append) כל שלושת הסימבולים בגרסה 1-M / 21×21
סעיף 5.2 (קידוד תווים ו-ECI) Shift_JIS בגרסה 2-Q ו-UTF-8 בגרסה 2-M (שניהם 25×25), כיוון שטקסט ביפנית של 18 עד 23 בתים אינו נכנס ב-16 codewords הנתונים של גרסה 1-M
סעיף 5.3 (כמה קודים) NO: ו-ITEM: בגרסה 1-M; LOT:AB-77 בגרסה 1-H כיוון שהנתונים קצרים ו-segno מעלה את רמת תיקון השגיאות (כולם 21×21)
  1. DENSO WAVE Incorporated, Error correction feature | QRcode.com 

  2. ISO/IEC 18004 Information technology — Automatic identification and data capture techniques — QR code bar code symbology specification. נוסחת יכולת תיקון השגיאות e + 2t ≦ d - p, הערך של ה-codewords להגנה מפני פענוח שגוי p, והאמירה על “מפוענח באופן שגוי כ-codeword אחר, שנראה תקין אך שונה” נמצאים ב-8.5.1 Error correction capacity; ה-(26,16,4) של גרסה 1-M והערת השוליים שיכולת התיקון נקבעת לפחות ממחצית מספר ה-codewords של תיקון השגיאות כדי להקטין את ההסתברות לפענוח שגוי נמצאים בטבלה 13 (הציטוטים מבוססים על מהדורת 2000). המהדורה הנוכחית היא ISO/IEC 18004:2024 2 3

  3. פרשנות ברירת המחדל במצב בתים כשלא מצוין ECI השתנתה בין מהדורות של התקן. סעיף 8.3.1 ב-ISO/IEC 18004:2000 קבע “The default interpretation for QR Code is ECI 000020 representing the JIS8 and Shift JIS character sets”, אבל מהמהדורה של 2006 (QR Code 2005) ואילך ברירת המחדל היא ECI 000003, כלומר ISO/IEC 8859-1. מה שאומר גם שהסתמכות על ברירת מחדל שלא הוצהרה משאירה את הפרשנות חשופה לשינוי רק מפני שמהדורת התקן השתנתה 2

  4. GS1, GS1 General Specifications 

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

אם אגדיר את רמת תיקון השגיאות ל-H, זה ימנע פענוח שגוי?
לא. העלאת הרמה מגדילה את כמות הלכלוך שאפשר לתקן, אבל היא אינה מבטלת את התופעה של "התיקון נכנס לפעולה ומפיק ערך אחר". פענוח Reed-Solomon עובד בכך שהוא מחפש בדפוס שהתקבל codeword שנמצא בתוך יכולת התיקון, ולכן אם הנזק נופל בסביבה של codeword אחר, הוא מוחזר כתשובה (נזק שנופל רחוק מסתיים סתם ב"לא ניתן לקרוא"). ISO/IEC 18004 קובע במפורש שזה "מפוענח באופן שגוי כ-codeword אחר, שנראה תקין אך שונה", ומקצה בנפרד מרווח של codewords להגנה מפני פענוח שגוי, אבל גם זה אמצעי שמקטין את ההסתברות, לא הבטחה. המקום היחיד שבו אפשר לתפוס פענוח שגוי הוא האפליקציה שמקבלת את הערך.
באיזו תדירות פענוחים שגויים של QR קורים במציאות?
עם נזק אקראי — כמעט לעולם לא. במדידות של המאמר הזה, מתוך 3,900 ניסיונות שהפכו מודולים באקראי ו-5,800 ניסיונות שהשחיתו codewords באקראי, היו אפס מקרים שבהם הוחזר ערך שגוי. נזק מעבר למה שאפשר לתקן נופל לצד של "לא ניתן לקרוא". אלא שנזק בעולם האמיתי אינו אקראי: קמטים, שחיקה וראשי דיו סתומים — לכולם יש הטיה מיקומית. יותר מזה, החמצה של סימבול Structured Append או תפיסה של הקוד הלא נכון מבין כמה אינן בכלל שאלה של הסתברות; בתנאים המתאימים זה קורה בכל פעם.
לקוד QR יש תיקון שגיאות, אז למה עוד ספרת ביקורת?
כן, צריך. הן מגנות על שכבות שונות. תיקון שגיאות עוסק בעקביות בתוך הסימבול, והוא מבטיח שחזור רק עד גבולות יכולת התיקון שלו. מעבר לכך, נזק יכול "להשתחזר" ל-codeword אחר ותקף. ספרת ביקורת, לעומת זאת, בודקת אם המחרוזת שהאפליקציה קיבלה תקינה כמערכת קידוד. היא תופסת באופן ודאי שינוי של תו אחד, אבל מפספסת מקרים שבהם כמה תווים משתנים. לערך ביקורת modulus 10 יש רק עשר אפשרויות, והמאמר הזה מביא דוגמה שבה שינוי של שתי ספרות עדיין תואם את ספרת הביקורת. לכן התייחסו לספרת הביקורת כשכבה שעוצרת את רוב הפענוחים השגויים, והשאירו את ההכרעה הסופית לבדיקת master data ולהצלבה ברמה העסקית. יתרון אחד הוא שאותה בדיקה מגנה גם על הקלדה ידנית, על העתקה ועל ייבוא שמגיע בדרכים אחרות.
אפליקציית המצלמה בטלפון שלי קראה את זה, אז הערך בטח נכון?
"זה נקרא" אומר רק שהמפענח החזיר מחרוזת לא ריקה; זה לא אומר דבר על השאלה אם התוכן נכון. גם איך מדווחים על כשל שונה בין מימושים — חריגה, מחרוזת ריקה או null, הכול אפשרי — ולכן גם השימוש ב"לא נזרקה חריגה" כקריטריון הצלחה הוא מסוכן. במדידות של המאמר הזה היו כמה מקרים שבהם OpenCV ו-jsQR החזירו תוצאות שונות לאותה תמונה. עבור קוד QR שמכיל טקסט ביפנית ב-Shift_JIS, אחד החזיר מחרוזת משובשת כהצלחה והשני החזיר מחרוזת ריקה. התוצאות נבדלו גם בסימבול הראשון של קבוצת Structured Append. העובדה שזה נקרא לא אומרת דבר על נכונות הערך.
האם כדאי להימנע מ-Structured Append (QR מפוצל) בשימוש עסקי?
אלא אם יש סיבה מיוחדת, הימנעות היא הבחירה הבטוחה יותר. Structured Append הוא מנגנון שבו הנתונים מפוצלים על פני כמה סימבולים והקורא אוסף ומחבר אותם, אבל ההתנהגות כשנותנים למפענח שאינו תומך בו רק את הסימבול הראשון תלויה במימוש. במדידות של המאמר הזה, OpenCV החזיר מספר תעודה קטוע בלי שגיאה, ואילו jsQR החזיר מחרוזת ריקה. כיוון שקיימים מימושים שיעבירו קטע הלאה כערך שנראה סביר, אם אינכם מצפים ל-Structured Append אתם זקוקים לאימות שדוחה תוצאות חלקיות. אם הנתונים לא נכנסים, בטוח יותר להעלות את גרסת ה-QR, או לקצר את הקוד ולהישען על בדיקת master data במקום.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג