Unicode, UTF-8 וקידוד תווים: מה כל מפתח צריך לדעת
אתם מעתיקים שם מטופס אינטרנטי והוא נשמר במסד הנתונים כ-é. פותחים קובץ CSV ב-Excel ותווים עם ניקוד מוצגים בצורה שגויה. משתמש מדביק אמוג'י בשדה טקסט וכל פקודת ה-INSERT ב-MySQL נכשלת עם שגיאה קריפטית. מוסיפים BOM לקובץ כדי ש-Excel יקרא אותו נכון, ופתאום סקריפט ה-shell שלכם נשבר בשורה הראשונה.
אלה באגי קידוד תווים. הם מהבעיות המבלבלות ביותר שמפתח יכול להיתקל בהן, כי הסימפטומים נראים אקראיים — אותה מחרוזת עשויה להיות מוצגת נכון בהקשר אחד ולהפוך לזבל בהקשר אחר. הסיבה כמעט תמיד זהה: שתי תוכנות חלוקות על הדרך שבה בתים ממפים לתווים.
המדריך הזה מסביר את הממפה הזו מהיסוד: מה הם קידודי תווים, כיצד עובדים Unicode ו-UTF-8, וכיצד לאבחן ולתקן את הבאגים הנפוצים בעולם האמיתי.
עידן טרום-Unicode: ASCII וכאוס ASCII המורחב
כל תו המאוחסן במחשב הוא בסופו של דבר מספר. קידוד תווים הוא טבלת חיפוש הממפה מספרים לתווים ולהיפך.
הקידוד הראשון שאומץ בצורה רחבה היה ASCII (American Standard Code for Information Interchange), שפורסם ב-1963. ASCII משתמש ב-7 ביטים, ומעניק לו 128 מיקומים (0–127). זה מכסה את האלפבית האנגלי באותיות גדולות וקטנות, עשרת הספרות, פיסוק נפוץ ו-33 תווי בקרה שאינם מודפסים כמו שורה חדשה וטאב.
128 תווים מספיקים לאנגלית. הם רחוקים מלספיק לצרפתית, גרמנית, ספרדית, ערבית, סינית, יפנית, או אלפי השפות הכתובות האחרות בעולם.
בעיית ASCII המורחב
הביט השמיני בבייט היה פנוי, מה שאומר 128 מיקומים נוספים (128–255). ארגונים ומערכות הפעלה שונים מיהרו למלא את המיקומים הללו בצורות שונות:
- Windows-1252 (נקרא גם CP1252): בשימוש על ידי Microsoft Windows במערב אירופה. מציב תווים כמו
é,ñו-€בחצי העליון. - ISO-8859-1 (Latin-1): תקן בינלאומי לשפות מערב אירופה. חופף במידה רבה ל-Windows-1252 אך אינו זהה — הטווח 128–159 הוא תווי בקרה ב-Latin-1 אך תווים להדפסה ב-Windows-1252.
- ISO-8859-5: מכסה קירילית.
- ISO-8859-6: מכסה ערבית.
- Shift JIS, EUC-JP, Big5: סכימות רב-בייטיות שונות לחלוטין לשפות מזרח אסיה.
התוצאה הייתה שקובץ שנוצר על מערכת אחת היה מוצג כזבל על מערכת אחרת אלא אם שתיהן הסכימו על אותו קידוד — ולא הייתה דרך אמינה לציין איזה קידוד קובץ משתמש בו. תוכנות ניחשו, ולעיתים קרובות טעו.
זו הבעיה ש-Unicode נוצר לפתור.
Unicode: ערכת תווים אחת לכל שפה
Unicode אינו קידוד. הוא ערכת תווים — טבלת חיפוש ענקית שמקצה מספר ייחודי לכל תו בכל מערכת כתיבה בשימוש, בתוספת אלפי סמלים, אמוג'י וכתבים היסטוריים.
כל ערך בטבלה הזו נקרא נקודת קוד, ונכתב כ-U+ ואחריו מספר הקסדצימלי. כמה דוגמאות:
תקן Unicode מגדיר כיום מעל 140,000 תווים ומתעדכן באופן קבוע.
מישורי Unicode
נקודות הקוד של Unicode נעות בין U+0000 ל-U+10FFFF, ומעניקות קצת יותר מ-1.1 מיליון מיקומים אפשריים. המרחב הזה מחולק ל-17 מישורים של 65,536 נקודות קוד כל אחד.
- מישור 0 — מישור הרב-לשוני הבסיסי (BMP): U+0000 עד U+FFFF. מכיל כמעט כל תו בשימוש נפוץ היום: לטינית, יוונית, קירילית, ערבית, עברית, CJK ועוד.
- מישור 1 — מישור הרב-לשוני המשלים (SMP): U+10000 עד U+1FFFF. אמוג'י, כתבים היסטוריים, תווי תיוג מוזיקלי.
- מישורים 2–16: אידאוגרפים CJK נוספים, כתבים היסטוריים נדירים, ומרחב לא מוקצה.
האמוג'י שמפיל את טבלאות MySQL נמצאים במישור 1. הפרט הזה חשוב, כפי שנראה בקרוב.
UTF-8: כיצד Unicode מאוחסן בפועל
Unicode מגדיר איזה מספר כל תו מקבל. הוא אינו מפרט כיצד לאחסן את המספרים הללו כבתים על הדיסק או בזיכרון. זו המשימה של קידוד Unicode.
UTF-8 הוא קידוד ה-Unicode הנפוץ ביותר ברשת ובתוכנות מודרניות. הוא בעל רוחב משתנה, כלומר תווים שונים תופסים מספר בתים שונה:
כיצד רצפי הבתים של UTF-8 עובדים
UTF-8 הוא בעל סנכרון עצמי. הבייט הראשון של כל תו מספר לכם כמה בתים עוקבים:
- 0xxxxxxx — תו בייט-1 (ASCII). כל קובץ ASCII חוקי הוא UTF-8 חוקי.
- 110xxxxx 10xxxxxx — תו 2 בתים.
- 1110xxxx 10xxxxxx 10xxxxxx — תו 3 בתים.
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx — תו 4 בתים.
בתים המשכיים תמיד מתחילים ב-10, כך שאפשר לדעת מיד אם בייט נתון הוא תחילת תו חדש או אמצע תו קיים. זה מאפשר סנכרון מחדש לאחר קריאת זרם חלקי.
דוגמה קונקרטית — קידוד é (U+00E9):
U+00E9 בבינארי הוא 11101001. זה נופל בטווח 2 הבתים. התבנית 110xxxxx 10xxxxxx מייצרת 11000011 10101001 — בתים 0xC3 0xA9 — שזה בדיוק מה ש-UTF-8 מאחסן עבור é.
מדוע UTF-8 שולט
ל-UTF-8 שלוש תכונות שגרמו לו לנצח:
- תאימות ל-ASCII. כל קובץ ASCII בלבד הוא UTF-8 חוקי. מיגרציה של מערכות קיימות לא דרשה נגיעה בקבצים שלא כללו תוכן שאינו ASCII.
- יעילות מרחב לכתבים לטיניים. טקסט אנגלי משתמש בבייט אחד לתו, כמו ASCII. רק תווים מחוץ ל-ASCII עולים יותר בתים.
- אין עמימות בסדר הבתים. מבנה הבתים אינו דו-משמעי ללא כל כותרת. (השוו זאת ל-UTF-16, שדורש סימן סדר בתים.)
UTF-16 ו-UTF-32: מתי נתקלים בהם
UTF-8 אינו קידוד ה-Unicode היחיד, וסביבות מסוימות משתמשות באחרים.
UTF-16 מקודד תווי BMP כשני בתים (יחידת קוד 16-ביט אחת) ותווי מישור משלים כארבעה בתים (שתי יחידות קוד 16-ביט, הנקראות זוג surrogates). פנימיות Windows, מחרוזות Java, מחרוזות JavaScript (ברמת המנוע) ומחרוזות .NET כולם משתמשים ב-UTF-16 פנימית.
UTF-32 משתמש בדיוק בארבעה בתים לתו, תמיד. זה מבזבז מקום אבל מאפשר גישה אקראית לפי אינדקס תו במהירות טריוויאלית. חלק מהפנימיות של מסדי נתונים משתמשים בו.
סימן סדר הבתים (BOM)
ל-UTF-16 יש בעיית אנדיאניות. רצף של שני בתים יכול להיות מאוחסן כ-big-endian (0x00 0x41 עבור A) או little-endian (0x41 0x00). מקלט צריך לדעת איזה סדר שימש.
הפתרון הוא סימן סדר הבתים: נקודת קוד U+FEFF מוצבת בתחילת הקובץ. ב-UTF-16 big-endian, זה מופיע כבתים FE FF. ב-UTF-16 little-endian, זה מופיע כ-FF FE. קורא שרואה אחד מהם יודע את סדר הבתים.
ל-UTF-8 יש BOM טכנית גם כן (EF BB BF) אך הוא חסר משמעות עבור UTF-8 כי ל-UTF-8 אין עמימות בסדר הבתים. כאן הוא גורם לבעיות:
- Excel וקובצי CSV. Excel משתמש ב-BOM של UTF-8 כאות לפרש CSV כ-UTF-8. ללא הסימן, Excel ברירת מחדל לקידוד locale המערכת (לרוב Windows-1252), ומעוות תווים עם ניקוד. מפתחים רבים מוסיפים את ה-BOM במיוחד כדי לרצות את Excel.
- סקריפטי shell. סקריפט shell שמתחיל ב-
#!/bin/bashחייב שהרצף הזה יהיה הבתים הראשונים ממש. אם BOM של UTF-8 קודם ל-shebang, הגרעין רואהEF BB BF 23 21ואינו מזהה את ה-shebang, מה שגורם לסקריפט להיכשל או לרוץ עם המפרש הלא נכון.
כלל האצבע: הוסיפו BOM לקובצי CSV המיועדים ל-Excel, לעולם אל תוסיפו אותו לסקריפטי shell או לקובצי JSON.
באגי קידוד נפוצים בעולם האמיתי
Mojibake: קריאת UTF-8 כ-Latin-1
Mojibake (文字化け) הוא המונח היפני לטקסט מסולף הנגרם ממחלוקת קידוד. הגרסה הנפוצה ביותר ברשת המודרנית: מחרוזת UTF-8 נקראת כאילו היא Latin-1 (ISO-8859-1 או Windows-1252).
התו é הוא U+00E9. ב-UTF-8 הוא מאוחסן כשני הבתים 0xC3 0xA9. אם תוכנה קוראת שני הבתים האלה כ-Latin-1, היא רואה שני תווים נפרדים: 0xC3 הוא à ו-0xA9 הוא ©. המחרוזת café הופכת ל-café.
דפוס הנזק הספציפי הזה — é עבור é, è עבור è, à עבור à — הוא הסימן המובהק של UTF-8 המתפרש כ-Latin-1. תראו אותו לרוב ב:
- מסדי נתונים שבהם charset החיבור לא הוגדר ל-UTF-8 בזמן ה-insert.
- מיילים שנשלחו דרך שרתי דואר ישנים.
- קבצים שהועתקו בין מערכות עם הגדרות locale שאינן תואמות.
התיקון הוא לזהות היכן מתרחש הפער ולתקן את הצהרת הקידוד בשכבה הזו — לא לתקן את המחרוזת הפגומה בדיעבד.
תו ההחלפה (U+FFFD)
כאשר מפענח נתקל ברצף בתים שאינו חוקי בקידוד הצפוי, הוא חייב לעשות משהו. תקן Unicode ממליץ להחליף את תו ההחלפה U+FFFD, המוצג כ-<0xFFFD>. אם אתם רואים קופסאות ? או את התו <0xFFFD> בפלט, מפענח נתקל בבתים שלא יכול היה לפרש.
סיבות נפוצות:
- קיצוץ רצף UTF-8 רב-בתים. אם חותכים מחרוזת בגבול בייט שנמצא בתוך תו CJK של 3 בתים, הרצף החלקי אינו חוקי.
- העברת קובץ מקודד Latin-1 למפענח UTF-8 מחמיר.
- קבלת נתונים ממקור שמערבב קידודים.
utf8 לעומת utf8mb4 ב-MySQL: בעיית האמוג'י
זה אחד מבאגי הקידוד המפורסמים ביותר בפיתוח ווב. ל-MySQL יש שתי ערכות תווים בשם utf8 ו-utf8mb4.
ה-utf8 של MySQL אינו UTF-8 אמיתי. הוא תומך רק בנקודות קוד עד U+FFFF — בדיוק ה-Basic Multilingual Plane — באמצעות מקסימום שלושה בתים לתו. אמוג'י נמצאים במישור 1 (U+10000 ומעלה) ודורשים ארבעה בתים. כאשר מנסים להכניס תו UTF-8 של ארבעה בתים לעמודה המוגדרת כ-utf8, MySQL חותך בשקט את הערך או זורק שגיאה.
utf8mb4 הוא UTF-8 אמיתי עם תמיכה מלאה בארבעה בתים. הוא הוכנס במיוחד לטיפול באמוג'י ותווי מישור משלים אחרים.
הגישה הנכונה: השתמשו תמיד ב-utf8mb4 לטבלאות ומסדי נתונים חדשים. הגדירו גם את charset החיבור והcollation במפורש:
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE posts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;והגדירו את פרמטר החיבור בתצורת האפליקציה שלכם:
charset=utf8mb4
שימוש ב-utf8 ב-MySQL הוא מלכודת ירושתית. התייחסו אליו כמיושן.
אורך מחרוזת לעומת ספירת תווים ב-Python
Python 2 אחסנה מחרוזות כרצפי בתים כברירת מחדל. len("café") החזירה 5 עבור מחרוזת UTF-8 — כי é תופס שני בתים — לא 4. טיפול נכון ברב-בתים דרש literals unicode מפורשים u"" ומשמעת קפדנית של encode/decode.
Python 3 תיקנה את זה: str הוא רצף של נקודות קוד Unicode, כך ש-len("café") מחזיר 4. נתוני בייט עברו לסוג bytes עם קריאות מפורשות .encode() ו-.decode().
נשארת עדינות אחת. len() של Python 3 סופרת נקודות קוד, לא אשכולות גרפמה. התו é יכול להיות מיוצג כנקודת קוד אחת (U+00E9) או כ-e (U+0065) בתוספת אקסן חריף משלים (U+0301). שניהם מוצגים זהה אך מייצרים ערכי len() שונים. יישומים רב-לשוניים החושפים ספירת תווים למשתמשים צריכים להשתמש בספריית פילוח Unicode ולא ב-len() גולמי.
כאשר אתם בודקים נתוני JSON המכילים מחרוזות רב-לשוניות, ייתכן שתבחינו בכך — מה שנראה כתו בודד עשוי להיות שתי נקודות קוד פנימית.
טבלת השוואת קידודים
כיצד לזהות ולתקן בעיות קידוד
זיהוי
ב-Python:
import chardet
with open("mystery_file.txt", "rb") as f:
raw = f.read()
result = chardet.detect(raw)
print(result) # {'encoding': 'Windows-1252', 'confidence': 0.73, 'language': ''}ספריית chardet משתמשת בניתוח סטטיסטי לניחוש קידוד מתבניות בתים. ציון הביטחון הוא כן — זיהוי קידוד הוא היוריסטי, לא דטרמיניסטי.
בדפדפן: פתחו DevTools, עברו ללשונית Network, מצאו את התגובה לדף HTML ובדקו את כותרת Content-Type. היא צריכה לקרוא text/html; charset=utf-8. אם היא נעדרת או שגויה, הדפדפן ינחש, לעיתים קרובות בצורה שגויה.
במסד נתונים: בדקו את ערכת התווים ברמת מסד הנתונים, הטבלה והעמודה:
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE posts;תיקון מסד נתונים עם Mojibake
אם נתונים הוכנסו כ-UTF-8 אך אוחסנו כ-Latin-1 (טעות מיגרציה נפוצה), לפעמים ניתן לשחזר אותם:
-- קרא את הנתונים הפגומים כבינארי, ואז פרש כ-UTF-8
UPDATE posts
SET body = CONVERT(CAST(CONVERT(body USING latin1) AS BINARY) USING utf8mb4)
WHERE body REGEXP '[Ã]';זה עובד רק אם הבתים המקוריים שלמים ורק תוייגו בצורה שגויה. אם הנתונים נפגעו באמת — בתים נשמטו או הוחלפו — שחזור אינו אפשרי ללא גיבוי.
מניעה
- הצהירו תמיד על קידוד במפורש. לעולם אל תסתמכו על ברירות מחדל.
- הגדירו
<meta charset="UTF-8">בכל מסמך HTML. - הגדירו
Content-Type: application/json; charset=utf-8בתגובות API. - ציינו את charset החיבור בעת פתיחת חיבורי מסד נתונים.
- השתמשו ב-UTF-8 בעורך שלכם והגדירו את מערכת ניהול הגרסאות לאוכפו עבור קבצי טקסט.
למשימות קידוד מעבר לערכות תווים — כמו המרת נתונים בינאריים לייצוגים בטוחים לטקסט — הכלי encode and decode text ב-FileCrank מטפל בהמרת Base64 ללא כל הגדרה. מאמר הקידוד הקיים מכסה Base64, קידוד URL וישויות HTML אם אתם זקוקים לאלה. אם אתם עובדים עם קובצי תצורה וצריכים לוודא שמחרוזות רב-לשוניות נראות נכון, תוכלו לבדוק נתוני JSON ישירות בדפדפן.
מה לקחת איתכם
באגי קידוד תווים מרגישים מסתוריים כי מצב הכשל — תווים מסולפים או חסרים — מופיע רחוק מהסיבה האמיתית. אבל המנגנון הבסיסי פשוט: שתי תוכנות משתמשות בטבלאות חיפוש שונות להמרת בתים לתווים.
שמרו שלושה כללים אלה לפניכם:
- השתמשו תמיד ב-UTF-8 בכל מקום אלא אם מערכת ספציפית כופה משהו אחר. הגדירו אותו במפורש; לעולם אל תסתמכו על ברירות מחדל.
- ב-MySQL, השתמשו ב-
utf8mb4, לא ב-utf8. ההבדל חשוב ברגע שמשתמש שולח אמוג'י. - כאשר אתם רואים mojibake, מצאו את הגבול. חפשו היכן בתים עוברים מערכת שאינה יודעת את הקידוד שלהם. תקנו את ההצהרה בנקודה הזו, לא את הסימפטומים במורד הזרם.
הבנת המכניקה — נקודות קוד, רצפי בתים, הצהרות קידוד — אומרת שאתם יכולים לחשוב באופן שיטתי על באג קידוד חדש במקום לנחש. הפעם הבאה שאתם רואים é בפלט שלכם, תדעו מיד מה קרה ואיפה לחפש.
כל כלי FileCrank פועלים לחלוטין בדפדפן שלכם. לא מועלים קבצים לשרת. אם אתם עובדים עם טקסט המכיל תוכן רב-לשוני, נסו את JSON Viewer או את הכלי Base64 Encode/Decode — שניהם מטפלים ב-Unicode נכון ללא כל תצורה.