קידוד תוים
From The Joel on Software Translation Project
המינימום ההכרחי שכל מתכנת חייב לדעת על Unicode ומשפחות תוים (בלי תרוצים!)
מאת יואל ספולסקי
המאמר המקורי: Unicode תורגם ע"י מוטי פורת. 2006-06-02
אי-פעם תהיתם על התג המסתורי Content-Type? נו, זה שצריך לשים בתחילת קובץ HTML ואתם אף-פעם לא בטוחים מה צריך לכתוב בו?
קיבלתם פעם מייל מהחברים שלכם בבולגריה, עם כותרת בסגנון "????? ??????? ??? ????"?
נדהמתי לגלות שכל-כך הרבה מפתחי מערכות לא לגמרי שולטים בכל העולם הזה של משפחות תוים, קידוד, Unicode וכל העניין הזה. לפני כמה שנים, בודק גירסת בטה של FogBugs תהה אם התוכנה יכולה לטפל באימיילים נכנסים ביפנית. יפנית? יש אימיילים ביפנית? לא היה לי מושג. כשבדקתי היטב את פקד ה-ActiveX המסחרי שבו השתמשנו לפענוח הודעות בפורמט MIME, גילינו שהוא עושה בדיוק את הדבר הלא נכון בקשר למשפחות תוים, ולכן ממש הצטרכנו לכתוב קוד גדול כדי לבטל את ההמרה שהפקד עשה ולעשות אותה נכון. כשבדקתי ספריית קוד מסחרית אחרת, גם בה היה מימוש שגוי לגמרי של הטיפול בקידוד טקסט. התכתבתי עם המפתח של אותה ספריה והוא ענה, פחות או יותר, שהם "לא יכולים לעשות שום דבר בנדון". כמו מתכנתים רבים, הוא פשוט קיווה שזה יעלם איכשהו.
אבל זה לא. כשגיליתי שכלי הפיתוח הפופולרי לרשת, PHP, מגלה בורות כמעט מוחלטת בכל נושא הקידוד ומתייחס בשמחה לכל תו כ-8 סיביות, ובכך גורם שכמעט אי אפשר לבנות אפליקציות רשת בין-לאומיות טובות, חשבתי מספיק זה מספיק.
לכן אני מצהיר בזאת: אם אתה מתכנת שעובד ב-2008 ולא יודע את הנושאים הבסיסיים של תוים, משפחות תוים, קידוד ו-Unicode, ואני אתפוס אותך, אני הולך לשלוח אותך לחצי שנה של עבודות שירות בצוללת. אני נשבע.
ועוד משהו:
במאמר הזה אני אשלים לך את הידע שכל מתכנת פעיל צריך. כל הקטע הזה של "טקסט פשוט = ASCII = כל תו הוא 8 סיביות", לא רק שהוא שגוי, הוא חסר תקנה, ואם אתה עדיין מתכנת ככה, אתה לא הרבה יותר טוב מרופא שלא מאמין בחיידקים. אנא, אל תכתוב שורת קוד נוספת לפני שתגמור לקרוא את המאמר.
לפני שנתחיל, עלי להזהירך שאם אתה אחד מהאנשים הנדירים שיודעים איך להתאים תוכנה לריבוי שפות, המאמר ייראה לך פשטני מדי. אני רק קובע את הרף המינימלי כך שכולם יוכלו להבין מה קורה, ולכתוב קוד שבתקווה יצליח לעבוד לא רק עם מילים באנגלית שאין להן סימנים מיוחדים. ואני מדגיש שטיפול בתוים הוא רק חלק קטן מהנושא של פיתוח תוכנות רב לשוניות; אבל אני יכול לכתוב רק על נושא אחד כל פעם, אז היום נעסוק בתוים.
מבט היסטורי
הדרך הכי טובה להבין היא ללכת בסדר כרונולוגי. אולי אתה חושב שאני הולך לדבר על משפחות תוים עתיקות כמו EBCDIC, אז אל דאגה, EBCDIC אינו רלוונטי לחיים שלך. לא נלך עד כדי כך אחורה...
בימים הקצת-קדומים, כש-UNIX התפתחה וקרניגהן וריצ'י פיתחו את שפת C, הכל היה פשוט. EBCDIC היה בדרך החוצה, וכל מה שרצינו היה התוים הישנים והטובים באנגלית - אותיות בלי סימנים מעליהן. היה קוד עבור תוים אלה - קוד ASCII - והוא יכול לייצג כל תו ע"י מספר בין 32 ל-127. רווח הוא 32, A גדולה היא 65 וכו'. מספרים כאלה ניתן לשמור ב-7 סיביות, אך רוב המחשבים עובדים עם בתים בגודל 8 סיביות. לכן נותרה לך סיבית שלמה לנצל, ואם אתה מרושע אתה יכול להשתמש בה למטרות עקומות: הטמבלים ב-WordStar הדליקו את הסיבית העליונה כדי לציין סוף מילה, ובכך גזרו על WordStar לעבוד באנגלית בלבד. קודים מתחת 32 נקראים בלתי נראים והשתמשו בהם בקללות. סתם... הם שימשו לשליטה, למשל תו 7 גרם למחשב להשמיע צפצוף. תו 12 גרם לדף הנוכחי להתעופף מהמדפסת ולדף חדש להכנס אליה.
והכל הלך טוב, בהנחה שאתה דובר אנגלית.
מכיוון שבבתים יש שמונה סיביות, הרבה אנשים חשבו "היי, אפשר להשתמש בקודים 128-255 למטרות שלנו". הבעיה היא שהרבה אנשים חשבו על כך בו-זמנית, כל אחד ודעותיו הוא על מה שצריך להיות בקודים 128-255. מחשבי IBM-PC הגיעו עם מה שנקרא תווי OEM, שהכילו אותיות עם סימנים לשפות אירופאיות ואוסף של תווים לציור טבלאות; מלבנים אופקיים; מלבנים אנכיים; מלבנים אופקיים עם קישוטים מתנדנדים בצד ימין וכו', שאיתם יכולת לצייר חלונות חמודים במסך DOS, שאותם עדיין תראה אם תציץ על המחשב במכולת. כשמחשבים נמכרו מחוץ לאמריקה, אנשים חלמו על כל מיני סוגים של תווי OEM, שכולם השתמשו ב-128 הקודים העליונים. כך, למשל, במחשבים רבים קוד 130 מייצג את האות é ואילו בישראל אותו קוד הוא האות ג'. אז אם אמריקאי שולח את קורות החיים (résumés) שלו לישראל, הן יגיעו כ- rגsumגs. בכמה מקרים, כגון רוסית, היו כמה רעיונות בקשר לשימוש ב-128 הקודים העליונים, כך שאפילו אי אפשר להתכתב ברוסית באופן אמין.
לבסוף, ה-OEM-לכל-דורש נכנס רשמית לתקן ה-ANSI, שבו כולם הסכימו שמה שמתחת ל-128 יהיה בערך כמו ב-ASCII, אבל עדיין היו הרבה דרכים שונות לטפל בתוים 128 ומעלה, בהתאם למקום שבו אתה חי. כל הדרכים הללו נקראו code pages. אז, למשל, בישראל השתמשו ב-code page 862, בעוד שביוון השתמשו ב-737. שניהם היו זהים ב-127 ומטה, אבל שונים בתוים 128 ומעלה, איפה שהאותיות המצחיקות גרות. הגרסאות המותאמות של MS-DOS הכילו עשרות code pages שטיפלו בהכל, מאנגלית עד איסלנדית, וכמה מהם אפילו הכילו קודים רב-לשוניים. למשל, אפשר היה לכתוב באספרנטו ובגליציאית באותו מחשב! וואו! אבל לעבוד, למשל, בעברית ויוונית באותו מחשב היה בלתי אפשרי במצב טקסט, אלא רק בתוכנות גראפיות. זאת משום ששתי השפות השתמשו ב-code pages שונים, שנתנו פירושים שונים לקודים הגבוהים.
בינתיים, באסיה, קרו דברים עוד יותר משוגעים, כי בשפות האסייתיות יש אלפי תווים. זה בחיים לא יוכל להכנס לבתים של 8 סיביות. לכן המציאו את השיטה המבולגנת DBCS - סט תוים בבתים כפולים - שבה חלק מהאותיות יוצגו ע"י בית אחד והאחרות ע"י שניים. היה קל להתקדם לאורך מחרוזות, אבל כמעט בלתי אפשרי ללכת אחורה. מתכנתים הצטרכו להשתמש לא בפקודות כמו ++s ו- --s כדי לעבור קדימה ואחורה במחורוזת, אלא בפונקציות מיוחדות כמו AnsiNext ו-AnsiPrev שידעו להתעסק עם כל הבלגן.
אבל עדיין, אנשים העמידו פנים שבית הוא תו ותו הוא 8 סיביות, וכל עוד אתה לא צריך להעביר מחרוזות בין מחשבים שונים או לדבר ביותר משפה אחת, הכל הסתדר איכשהו. אך כמובן, ברגע שצץ האינטרנט, העברת מחרוזות בין מחשבים הפכה לעניין שבשגרה, וכל הבעיות נחתו עלינו בסערה. למרבה המזל, אז הומצא ה-Unicode.
Unicode
ה-Unicode הוא נסיון נועז ליצור משפחת תוים אחת לכל התוים הסבירים שמשמשים את האנושות, וגם כמה כאלה של כאילו-שפות, כגון קלינגון. כמה אנשים סבורים בטעות שיוניקוד הוא פשוט קוד שמייצג את כל התוים ב-16 סיביות, כך שיש 65536 תוים אפשריים. זה לא נכון. זוהי הטעות הכי נפוצה בעניין היוניקוד, אז אל תרגיש רע אם גם אתה נפלת בה.
למעשה, ליוניקוד יש דרך אחרת לחשוב על תוים, ואתה חייב להבין אותה כדי להבין על מה אנו הולכים לדבר.
עד עכשיו חשבנו שאות מתמפה לכמה סיביות שאותן ניתן לשמור בזכרון או בדיסק, למשל:
ביוניקוד, כל אות מתמפה למה שנקרא נקודת קוד (code point), שהיא מושג תאורטי. איך נקודת הקוד מיוצגת בזכרון או בדיסק זה סיפור אחר לגמרי.
ביוניקוד, האות A היא רעיון אפלטוני שמרחף לו בחלל. ה-A האפלטונית שונה מ-B והיא גם שונה מ-a, אבל היא זהה ל-A או A. הרעיון ש-A גדולה בגופן אחד היא בדיוק אותו תו כמו A גדולה בכל גופן אחר אבל היא תו שונה מ-a קטנה, נתפס היטב. אבל בכמה שפות עצם ההגדרה של 'מהי אות' יכולה לעורר מחלוקת. האם האות הגרמנית ß היא אות אמיתית או רק דרך מסוגננת לכתוב ss? אם אות מסוימת לובשת צורה שונה כשהיא מופיעה בסוף מילה, האם היא אות אחרת? העברית טוענת שכן, הערבית טוענת שלא. בכל אופן, האנשים החכמים בוועדת יוניקוד עבדו על מציאת פתרונות לכל זה במשך העשור האחרון, מה שלווה בלא מעט ויכוחים פוליטיים. כעת אתה לא צריך לדאוג לעניינים האלה.
לכל אות אפלטונית בכל אלף-בית בעולם נקבע מספר קסם ע"י ועדת יוניקוד, שנראה כך: U+0645. המספר הזה הוא 'נקודת הקוד' שהזכרנו קודם. ה- +U מייצג את 'יוניקוד' והמספרים הם הקסדצימליים. U+FEC9 הוא האות הערבית 'עין'. A גדולה באנגלית היא U+0041. ניתן למצוא את קודי היוניקוד בתוכנית charmap ב-Windows 2000 ומעלה, או באתר יוניקוד.
אין ממש גבול למספר האותיות שאפשר לקודד ביוניקוד, ולמעשה הם כבר עברו את ה-65536, כך שלא ניתן לדחוס כל אות בשני בתים, אבל ניחא, כבר אמרנו שזה מיתוס.
טוב, אז נגיד שיש לנו מחרוזת:סתם כמה נקודות קוד. מספרים, למעשה. עוד לא אמרנו דבר על איך לשמור אותם בזכרון או לייצג אותם באימייל.
קידוד
כאן נכנס הקידוד לתמונה.
הרעיון הראשוני לקידוד יוניקוד, שממנו נולד המיתוס על התוים של שני בתים, היה: היי, פשוט נאחסן כל תו בשני בתים. אז Hello הופך ל-
?
במשך כמה זמן נדמה היה שזה מספיק טוב, אבל אז התלוננו מתכנתים: "תראו את כל האפסים האלה!" מכיוון שהם היו אמריקאים ועבדו עם טקסטים באנגלית, שרק לעיתים נדירות הכילו נקודות קוד מעל U+00FF. בנוסף, הם היו היפים ליברליים מקליפורניה שדאגו לשימור (חחח...). אם הם היו מטקסס לא היה אכפת להם לזלול כמות כפולה של בתים, אבל הנמושות האלה מקליפורניה לא יכלו לסבול את הרעיון של הכפלת כמות הבתים הדרושה לאחסון מחרוזות. מה גם שכבר קיימים מיליוני מסמכים ב-ASCII או ב-DBCS, ומי אמור להמיר את כולם ליוניקוד? אני? מסיבה זו רוב האנשים העדיפו להתעלם מיוניקוד במשך עוד כמה שנים, ובינתיים המצב נעשה גרוע יותר.
לכן המציאו את הרעיון המבריק של . UTF-8 UTF-8 היא עוד שיטה לאחסון תוי יוניקוד, האלה עם ה- +U, בבתים של 8 סיביות. ב- UTF-8 כל נקודת קוד בתחום 0-127 נשמרת בבית אחד. רק נקודות הקוד מ-128 ומעלה נשמרות ב-2, 3, למעשה עד 6 בתים.

יש לכך תוצר לוואי מגניב: טקסטים באנגלית נראים בדיוק אותו דבר ב-UTF-8 וב-ASCII, כך שאמריקאים לא צריכים להטריד את עצמם בהבדלים בין השיטות. רק כל שאר העולם צריך לעשות שמיניות באויר. בפרט, ה-Hello שהוא U+0048 U+0065 U+006C U+006C U+006F, נשמר בתור
שזה - הפלא ופלא - בדיוק כמו ב-ASCII, ב-ANSI ובכל סט של OEM על-פני כדור הארץ. עכשיו, אם אתה כל-כך אמיץ בכדי להשתמש באותיות ביוונית או בשפת קלינגון, תצטרך להשתמש ביותר מבית אחד כדי לשמור אותן. אבל אמריקאים לא יבחינו בהבדל. (ל-UTF-8 יש עוד תכונה נחמדה: עבור הערכים שמפוצלים לכמה בתים, אף בית לא שווה 0. לכן תוכנות שמשתמשות בבית ששווה 0 כדי לסמן את סוף המחרוזת לא יקצצו מחרוזות).
עד כה ראינו שלוש דרכים לקידוד יוניקוד: השיטות הישנות של שמור-הכל-בשני-בתים נקראות UCS-2 (על שם שני הבתים) או UTF-16 (בגלל 16 הסיביות), ואני סופר אותן כשתי שיטות כי עדיין צריך לדעת לזהות אם טקסט נתון הוא big-endian UCS-2 או little-endian UCS-2. וישנו תקן UTF-8 החדש והפופולרי, שעובד בצורה נאה גם עם השילוב ה'מוצלח' של טקסטים באנגלית בלבד ומתכנתים רפי-שכל שאינם מודעים בכלל לקיומו של משהו חוץ מ-ASCII.
למעשה יש עוד כמה שיטות קידוד, כמו UTF-7 שהיא די דומה ל-UTF-8 אבל מבטיחה שהסיבית העליונה בכל בית תהיה 0, כך שאם אתם שולחים מייל דרך מדינה עם חוקים דרקוניים שאומרת ש-7 סיביות זה מספיק למדי, תודה הוא יצליח לעבור בשלום. ישנו גם UCS-4 ששומר כל תו בארבעה בתים. מה שנוח בו הוא שכל התווים נשמרים ביחידה באותו גודל. אך אבוי, אפילו הטקסאנים לא יהיו כל-כך נועזים כדי לבזבז כך-כך הרבה זכרון.
בעצם, עכשיו כשחושבים על תוים בתור ישויות אפלטוניות, אפשר גם לקודד אותם בכל דרך מהאסכולה הישנה. למשל, את מחרוזת היוניקוד עבור (Hello (U+0048 U+0065 U+006C U+006C U+006F אפשר לקודד ב-ASCII, ב-OEM של יוונית, ב-ANSI של עברית ובכל אחת ממאות השיטות שהומצאו עד כה. עם מלכודת אחת: כמה מהתוים לא יופיעו! אם אתה מנסה לשמור נקודת-קוד של יוניקוד שאין דרך לשמור אותה בקידוד שבחרת, תקבל סימן שאלה קטן, או, אם אתה ממש טוב, ריבוע. מה יצא לך? -> �
יש מאות קידודים מסורתיים שיכולים ליצג כמה נקודות קוד והופכים את השאר לסימני שאלה. כמה קידודים נפוצים הם Windows-1252 (הסטנדרט של Windos 9x לשפות מערב אירופאיות) ו-ISO-8859-1 (הנקרא גם Latin-1) שגם הוא שימושי לכל שפה במערב אירופה. אבל נסה לשמור אותיות ברוסית או בעברית בשיטות הללו ותקבל אוסף של סימני שאלה. UTF-7, 8, 6 או 32, לעומתם, יכולים לשמור כל תו יוניקוד שיש.
העובדה הכי חשובה על קידוד
אם שכחת לגמרי את כל מה שכרגע הסברתי, לפחות זכור עובדה אחת חשובה מאוד. אין משמעות להחזיק מחרוזת בלי לדעת באיזה קידוד היא משתמשת. אתה לא יכול יותר לתקוע את הראש בחול ולהעמיד פנים שטקסט "פשוט" הוא ASCII.
אם יש לך מחרוזת בזכרון, בקובץ או בהודעת אימייל, אתה חייב לדעת באיזה קידוד היא או שלא תוכל לפרש אותה או להציג אותה נכון למשתמש.
כמעט כל בעיה טיפשית של "האתר שלי נראה כמו ג'יבריש" או "היא לא יכולה לקרוא את המיילים שלי כשאני משתמש באותיות עם סימנים" מתחילה ונגמרת במתכנת תמים אחד שלא הבין את העובדה הפשוטה שאם אתה לא אומר לי אם מחרוזת מסוימת מקודדת ב-UTF-8 או ב-ASCII או ב-(ISO-8859-1 (Latin-1 או ב-(Windows-1252 (Western European, אתה פשוט לא יכול להציג אותה נכון או אפילו לגלות איפה היא נגמרת. יש יותר ממאה קידודים ומעל נקודת-הקוד 127 כל ההימורים מפספסים.
איך שומרים את המידע על הקידוד שבו מחרוזת נתונה כתובה? ובכן, יש דרכים סטנדרטיות לעשות זאת. בהודעת אימייל, אמורה להיות כותרת מהצורה
בדף ברשת, הרעיון המקורי היה שהשרת יחזיר כותרת כזאת יחד עם הדף - לא בתוך ה-HTML עצמו אלא כחלק מההודעות שעוברות בפרוטוקול התקשורת. זה גרם לבעיה: נניח שיש שרת גדול המפעיל הרבה אתרים עם מאות דפים שנתרמו ע"י הרבה אנשים בהרבה שפות שונות, וכל אחד משתמש בקידוד שהעורך שלו (למשל Microsoft FrontPage) בחר כמתאים ביותר. השרת עצמו לא ממש יודע באיזה קידוד נכתב כל קובץ, לכן הוא לא יכול להיות אחראי על שליחת ה-Content-Type ללקוח. כך הוחלט שתג ה-Content-Type יירשם בקובץ ה-HTML עצמו. הרעיון הזה משגע את התאורטיקנים: איך אתה יכול לקרוא את דף ה-HTML לפני שאתה יודע באיזה קידוד הוא נשמר?! למרבה המזל, כמעט כל הקידודים שומרים בדיוק באותו אופן את התוים בתחום 32-127, כך שאפשר להתחיל את ה-HTML עם הכותרות הללו, שאין בהן תווים מיוחדים:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">אבל תג ה-meta הזה צריך להיות ממש בתחילת קטע ה-<head> בקובץ, כי ברגע שהדפדפן קורא אותו הוא מפסיק לקרוא ולפענח את הדף ומתחיל לעשות זאת מחדש, הפעם עם הקידוד שכתוב בתג הזה.
מה הדפדפן עושה אם אין אף הגדרת Content-Type, לא בקובץ עצמו ולא ב"שיחה" שלו עם השרת? Internet Explorer עושה דבר מעניין: הוא מנסה לנחש, לפי התדירות שבה קודים מסוימים מופיעים בטקסטים אופיניים ובקידודים אופיניים בשפות שונות, מהם השפה והקידוד של הדף. מכיוון שהשיטות הישנות של 8 סיביות נוטות לתת ערכים שונים לאותיות המיוחדות שלהן, ומכיוון שבכל שפה אנושית יש התפלגות אופיינית של שכיחות האותיות, יש לזה ממש סיכוי לעבוד. זה אולי מוזר, אבל נראה שזה באמת עובד לעיתים מספיק קרובות, כך שמפתחי אתרים תמימים שמעולם לא ידעו שצריך את תג ה-Content-Type, מסתכלים על האתרים שלהם ואומרים זה נראה טוב, עד שיום אחד הם כותבים משהו שלא בדיוק מתאים להתפלגות האותיות בשפה שלהם, ואז Internet Explorer מחליט שהדף כתוב בקוריאנית ומציג אותו כך. זה מוכיח, לדעתי, שחוק פוסטל - "היה שמרן במה שאתה שולח וליברלי במה שאתה מקבל" - הוא, למען האמת, לא עקרון הנדסי כל-כך טוב. בכל אופן, מה הקורא המסכן של אותו אתר, שנכתב בבולגרית אבל מוצג בתור קוריאנית (ואפילו לא בתרגום נכון...) יכול לעשות? הוא פותח את תפריט View | Encoding ומנסה כמה אפשרויות (יש לפחות תריסר אפשרויות לשפות מזרח אירופאיות) עד שהתמונה מתבהרת. כלומר, אם הוא יודע לעשות את זה, מה שרוב האנשים לא יודעים.
בגרסה האחרונה של CityDesk, התוכנה לניהול אתרים שפותחה בחברה שלי, החלטנו לעשות את כל הדברים הפנימיים ב-UCS-2 Unicode (שמירת כל תו בשני בתים), כי זה מה ש-Visual Basic, COM ו-Windows NT ומעלה עושים עם טיפוס המחרוזת הטבעי שלהם. בקוד המקור ב- ++C פשוט הכרזנו על המחרוזות כבעלות טיפוס (wchar_t (wide char במקום char והשתמשנו בפונקציות שמתחילות ב-wcs במקום אלה שמתחילות ב-str (למשל wcscat ו-wcslen במקום strcat ו-strlen). כדי להגדיר קבוע מטיפס מחרוזת רחבה בקוד ב-C, פשוט הצמד את האות L לתחילתו: "L"Hello.
כאשר CityDesk מפרסמת דף ברשת, היא ממירה אותו לקידוד UTF-8, שנתמך היטב ע"י הדפדפנים כבר שנים. כך מקודדות כל הגרסאות של "יואל על תוכנה" ב-29 שפות, ועוד לא קיבלתי תלונה אחת על בעיות בצפייה בהן.
המאמר הזה כבר נהיה ארוך ואני בוודאי לא יכול לכסות את כל מה שיש לדעת על יוניקוד וקידוד טקסט, אבל אני מקווה שאם קראת עד כאן, אתה יודע מספיק כדי לחזור לתכנת, תוך שימוש באנטיביוטיקה במקום עלוקות ולחשים, משימה שאני אשאיר לך עכשיו.

