Sanyanggae LogoSanyanggae

💻 ווב ופרונטאנד

מה ההבדל בין Unicode ל־UTF-8? קוריאנית משובשת, בתים, אורך מחרוזת ונרמול

נשווה נקודות קוד של קוריאנית ואימוג'י לבתי UTF-8 ולאורך מחרוזות JavaScript, ונפריד פענוח שגוי מבעיות גופן ונרמול משימור המקור.

사냥개 (Sanyanggae)פורסם עודכן 12 דק׳

זהו תרגום של המקור בקוריאנית. מקור בקוריאנית · בדיקת תרגום: נבדק בבינה מלאכותית

תרשים הסבר בשלושה תאים המקשר תווים ונקודות קוד, בתי UTF-8, תצוגה ונרמול
תרשים הסבר מקורי המפריד בין מספרי תווים ב־Unicode, בתי אחסון ב־UTF-8 וכללי תצוגה והשוואה; לא צילום תקלה אמיתית ולא תוצאת מדידה.

1. מספר התו והבתים הנשמרים הם דברים שונים

נניח שייצאתם מסמך בקוריאנית, וכשפתחתם אותו שוב הכותרת הופיעה כתווים מוזרים. שפת הכותב לא השתנתה; ייתכן שכללי קריאת המספרים השמורים לא התאימו. Unicode הוא תקן המקצה לתווים ולסימנים מספרים המכונים נקודות קוד, ואילו UTF-8 הוא קידוד המייצג את הערכים המתאימים כרצף בתים. למשל, נקודת הקוד של ההברה הקוריאנית '가' היא U+AC00. הכתיב אחרי U+ הוא הקסדצימלי; הוא אינו מציין מיקום בקובץ או כתובת בזיכרון. המספר גם אינו קובע את צורת הגופן או את מיקום הפיקסלים. אותה נקודת קוד עשויה להיראות אחרת בגופנים ובמערכות עימוד שונות. לכן תמיכה ב־Unicode, שמירה ב־UTF-8 והתקנת גופן קוריאני קשורות זו לזו, אך צריך לבדוק כל אחת בנפרד. הסבר UTF של Unicode הוא נקודת מוצא להפרדה בין התו לצורת אחסונו.

ב־UTF-8 ההברה '가' היא שלושת הבתים EA B0 80 בכתיב הקסדצימלי. לעומתה A הלטינית היא U+0041, וב־UTF-8 היא בית אחד, 41. בית מורכב משמונה סיביות, ומספר הבתים לתו עשוי להשתנות גם באותו מסמך. גם הסיומת .txt או .csv לבדה אינה מזהה את הקידוד האמיתי. UTF-8 מייצג ערכים סקלריים תקינים של Unicode בבית אחד עד ארבעה בתים, ולכן אי אפשר לחשב במדויק את גודל הקובץ באמצעות הכפלת מספר הסימנים הנראים בקבוע. אין להחיל את דוגמת ההברה הזאת על כל סימן הקשור לקוריאנית או על כל שפה. קידוד הוא כלל להעברת מידע על תווים לבתים; הוא אינו דחיסה או הצפנה. אפילו לאותו משפט עשוי להיות גודל כולל שונה בגלל מידע נוסף של פורמט הקובץ, מעברי שורה ודחיסת תעבורה. הפרדת ההמרה בין בתים לתווים שב־WHATWG Encoding Standard מגודל הקובץ המלא ומסודיות מסייעת לצמצם את מקור הבעיה.

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

קריאת length של מחרוזת JavaScript מחזירה את מספר יחידות הקוד של UTF-16. זה אינו מספר הבתים בקובץ UTF-8 או מספר התווים שאדם תופס. למשל, הפנים '😀' הן נקודת קוד אחת, U+1F600, אך שתי יחידות UTF-16, ולכן '😀'.length הוא 2. הייצוג ב־UTF-8 הוא F0 9F 98 80, כלומר ארבעה בתים. המספר העשרוני 128512 שמתקבל מ־codePointAt(0) הוא אותו מספר כמו U+1F600, ולא כמות תווים או גודל קובץ. לעומת זאת ל־'가' יש נקודת קוד אחת, יחידת UTF-16 אחת ושלושה בתי UTF-8. המספרים השונים לאותה מחרוזת נובעים מיחידות מדידה שונות. ההסבר של MDN על אורך מחרוזת מציין במפורש מה length סופר. גם אי אפשר להסיק ממנו כמה בתי זיכרון מנוע JavaScript מקצה בפועל.

גם ספירה של נקודות הקוד אחת־אחת לא תמיד שווה למספר התווים שהמשתמש רואה. לתו בסיס יכולים להצטרף סימנים משלבים, ואימוג'י אחד יכול להיות מורכב מכמה נקודות. כללי חלוקת הטקסט של Unicode מתארים גבולות של אשכולות גרפמה מורחבים. שיטה הפורסת את המחרוזת לפי נקודות קוד, כמו Array.from(text), אינה סופרת אוטומטית את הקבוצות מרובות הנקודות. בדפדפן אפשר לבדוק את התמיכה ואז לשקול שימוש ב־Intl.Segmenter כאשר granularity מוגדר כ־'grapheme'. עם זאת, אשכול אינו בהכרח צורה גרפית אחת בגופן, וציפיות העריכה יכולות להשתנות בין שפות. בשדה תיאור יש להציג ספירת תווים שהמשתמש מבין, ולצידה מגבלות בתים נפרדות לאחסון ולתקשורת. הצגת '20 תווים' כאשר בפנים חותכים רק יחידות UTF-16 עלולה לקטוע אימוג'י או רצף משולב באמצע.

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

שיבוש יכול להיווצר כאשר קידוד כתיבת הבתים אינו תואם לפענוח בזמן הקריאה. קריאת קובץ UTF-8 בקידוד אחר יכולה להציג סימנים לא נכונים בלי שהקובץ עצמו השתנה. ב־HTML בודקים יחד את פורמט השמירה האמיתי, את charset בכותרת HTTP מסוג Content-Type ואת הצהרת meta charset בתחילת המסמך. המאפיין lang='ko' מציין את שפת התוכן; תפקידו שונה מ־charset שבוחר כיצד לקרוא את הבתים. מדריך W3C להצהרת קידוד מסביר שלא מספיק לשנות את ההצהרה ל־UTF-8: יש לשמור באמת ב־UTF-8 ולהתאים בין השרת למסמך. סימן זיהוי של קידוד יכול להשפיע גם על סדר הקדימות, ולכן meta אינו בהכרח גובר על כל מידע אחר. אם פתיחה ישירה וקבלה מהשרת מציגות תוצאות שונות, הגדרות שלב ההעברה הן רמז חשוב.

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

4. בדיקת אותם ערכים בעזרת TextEncoder ו־TextDecoder

התחילו ניסוי פשוט במחרוזת קצרה שיצרתם בעצמכם. TextEncoder ממיר מחרוזת JavaScript למערך בתי UTF-8. התוצאה של new TextEncoder().encode('가') בהקסדצימלי היא EA B0 80, ו־length של המערך הוא 3. כלי הפיתוח עשויים להציג את אותם ערכים כ־234, 176, 128 בעשרוני; אלה אינם נתונים אחרים אלא בסיס מספרים אחר. השוואה ל־length הרגיל של המחרוזת, שהוא 1, מדגימה את הבדל היחידות. קריאת המערך באמצעות new TextDecoder('utf-8').decode(bytes) מחזירה '가'; bytes כאן הוא המערך שנוצר קודם. המקודד יוצר UTF-8 ואינו ממיר כללי ששומר בכל קידוד ישן שנבחר. כשבודקים קובץ אמיתי צריך להתחיל מהבתים המקוריים שהושגו. קידוד מחדש ל־UTF-8 של טקסט שכבר נקרא לא נכון על המסך אינו מוכיח מה היו בתי הקובץ הראשון.

אם מציינים 'utf-8' ואת { fatal: true } ב־בנאי TextDecoder, אפשר לטפל בקלט שאינו מתאים באמצעות שגיאה במקום החלפה שקטה. למשל, הבית הבודד FF אינו UTF-8 תקין, ולכן הפענוח במצב זה ייכשל. במצב ההחלפה הרגיל החלק הלא תקין עשוי להפוך לתו ההחלפה U+FFFD. אך היעדר שגיאה אינו מעיד שנמצא הקידוד המקורי או שהתוכן אומת. יש בתים שחופפים בין קידודים, ותוצאה של פענוח שגוי קודם יכולה כבר להישמר שוב כ־UTF-8 תקין. הבדיקה קובעת אם המפענח הנבחר מקבל את הקלט; היא אינה מאמתת אוטומטית את מקור המסמך, את התאמתו למקור או שלא שונה בדרך.

5. נרמול הוא פעולה נפרדת להתאמת ייצוגים

גם לשתי מחרוזות שנקראו נכון יכולים להיות רצפי נקודות קוד שונים. את '가' אפשר לייצג ב־U+AC00 אחד, או ברצף העיצור התחילי המשלב U+1100 והתנועה U+1161. שני הערכים האחרונים שונים מהצבה רגילה של ג'אמו התאימות 'ㄱ' ו־'ㅏ' זה לצד זה, ולכן צריך לבדוק גם את המספרים. הייצוגים נראים בדרך כלל כאותה הברה, אך בהשוואת מחרוזות גולמיות ב־JavaScript הם עשויים לא להיות שווים. החלת NFC על שניהם לפי כללי הנרמול של Unicode מביאה את הדוגמה השקולה קנונית לאותו ייצוג. NFD משתמש בפירוק קנוני. בדוגמה זו ייצוג NFC ב־UTF-8 הוא שלושה בתים, ושני סימני הג'אמו ב־NFD הם שישה; מראה זהה יכול לתפוס נפח שונה. אם שאילתת חיפוש ושם שמור נראים זהים אך אינם תואמים, אפשר לבדוק את ההבדל הזה, בנפרד משיבוש עקב פירוש שגוי של הבתים.

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

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

לפני שדורסים את הקובץ הראשון בחיפוש אחר מקור הנזק, שומרים את הבתים המקוריים ואת הדרך שבה התקבל. קוראים עותק בקידוד הצפוי ובודקים שגם שמירה חוזרת של התוצאה שנראית נכונה משמרת את אותו תוכן. הסבר W3C על BOM מציין שחתימת הפתיחה של UTF-8 היא EF BB BF, ושהבתים האלה אינם חובה. UTF-8 אינו מצריך בחירת סדר בתים, ולכן אין להבין חתימה זו כאילו תפקידה לבחור סדר כמו ב־UTF-16. גם הוספה או הסרה של BOM שונות מהמרת בתי גוף הטקסט לקידוד אחר. אם ערכים שלא נקראו כבר הוחלפו בתו החלפה או בסימן שאלה ונשמרו, כמה ערכי מקור יכלו להתמזג ולא מובטח שחזור אוטומטי. אפשר לבחון הפיכות רק כשידועים הקידוד המקורי והיסטוריית ההמרות; נרמול אינו כלי להשבת בתים שאבדו.

כשבונים תכונה רב־לשונית לא מסתפקים בשורה אחת באנגלית. כללו בקבוצת דוגמאות קצרה הברה קוריאנית, אות לטינית עם סימן משלב, אימוג'י ממישור משלים וטקסט מימין לשמאל כגון ערבית או עברית; השוו קלט, אחסון, קריאה חוזרת וייצוא. בכל שלב בודקים את המראה, נקודות הקוד, מספר בתי UTF-8 ויחידת ספירת התווים שהמוצר הגדיר. כיוון הקריאה אינו תכונה שמתקנים בהחלפת UTF-8 בקידוד אחר; גם שפת המסך והגדרת הכיוון נבדקות בנפרד. בעת שינוי מגבלת קלט או השוואת חיפוש שומרים גם כן את המקור ומסבירים את הכללים החדשים. בדקו שספירת הדפדפן ותנאי הקבלה של השרת משתמשים באותה יחידה; אם נחוצות מגבלות שונות, הציגו כל אחת. המספרים ודוגמאות הקוד כאן מדגימים המרה של מחרוזות קצרות ואינם מדידות של תקלה בשירות מסוים. בחינת מספר התו, בתי האחסון, יחידות המחרוזת, התצוגה וההשוואה לפי הסדר מאפשרת לשחזר מה השתנה בכל גבול.

ראיות ומקורות

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

על המפרסם ויצירת קשר

מאמרים נוספים בנושא