יוניקוד בקטנה

07/04/2018

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

1. איך מידע נשמר בקבצים

היחידה הבסיסית ביותר של מידע שמחשב יודע לשמור היא הביט. אפשר לחשוב על ביט כנורה שיכולה להיות ״דולקת״ או ״כבויה״ ולכן אנחנו אוהבים לייצג את הערכים שביט מאחסן במספרים 1 ו-0 (אחד מצין ״דולק״ ו-0 מצין ״כבוי״). כל המידע שנשמר בכל הקבצים שלכם במחשב שמור כאוסף של ביטים, חלקם דולקים וחלקם מכובים.

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

כדי שיהיה לנו נוח אנשים נוהגים להסתכל על הביטים בקבוצות: אם ניקח 4 ביטים נקבל את היכולת לייצג 16 ערכים שונים, כי כל ביט יכול להיות דלוק או מכובה ו 2 בחזקת 4 הוא 16. לרביעיה כזו אנו קוראים Nibble ומייצגים אותה כספרה בודדת. מאחר ויש לנו רק 10 ספרות מוסיפים את האותיות a,b,c,d,e,f וזה נקרא בסיס ספירה 16.

לדוגמא רצף הביטים 0000 מיוצג על ידי הסיפרה 0; הרצף 0001 הוא הסיפרה 1; לרצף 0010 אנחנו קוראים 2 ול 0011 קוראים 3. כך ממשיכים עד 1110 שנקרא e ו 1111 שנקרא f.

אם ניקח שני Nibbles נקבל יחידה שנקראת בית (באנגלית Byte). כדי לכתוב בית נשתמש בשתי ספרות בבסיס 16. לדוגמא הבית 00101101 מיוצג על ידי הספרות 2d, כי 0010 זה 2 ו 1101 הוא d.

לכן מבחינת המחשב המידע נשמר כרצפים של ביטים, בשביל שיהיה לנו נוח אנחנו מסמנים כל ביט בספרות 0 ו-1 ובשביל שיהיה עוד יותר נוח אנחנו מסתכלים על שמיניות של ביטים ומסמנים כל שמיניה בשתי ספרות בבסיס 16.

2. ייצוג מידע טקסטואלי

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

בימים הישנים והטובים עורכי טקסט השתמשו בטבלת המרה שנקראת טבלת ASCII. הטבלא כללה מיפוי בין ערך של בית לבין האות או הסימן שמתאים לו. לדוגמא האות איי גדולה (A) מיוצגת על ידי הקוד 41, כלומר על ידי רצף הביטים 01000001. בקישור https://www.rapidtables.com/code/text/ascii-table.html תוכלו למצוא רשימה של כל הקודים.

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

ASCII stands for American Standard Code for Information Interchange.
Computers can only understand numbers,
so an ASCII code is the numerical representation of a character

באמצעות xxd אוכל להציג את המידע הגולמי בקובץ ולקבל:

$ xxd hello.txt
00000000: 4153 4349 4920 7374 616e 6473 2066 6f72  ASCII stands for
00000010: 2041 6d65 7269 6361 6e20 5374 616e 6461   American Standa
00000020: 7264 2043 6f64 6520 666f 7220 496e 666f  rd Code for Info
00000030: 726d 6174 696f 6e20 496e 7465 7263 6861  rmation Intercha
00000040: 6e67 652e 0a43 6f6d 7075 7465 7273 2063  nge..Computers c
00000050: 616e 206f 6e6c 7920 756e 6465 7273 7461  an only understa
00000060: 6e64 206e 756d 6265 7273 2c0a 736f 2061  nd numbers,.so a
00000070: 6e20 4153 4349 4920 636f 6465 2069 7320  n ASCII code is 
00000080: 7468 6520 6e75 6d65 7269 6361 6c20 7265  the numerical re
00000090: 7072 6573 656e 7461 7469 6f6e 206f 6620  presentation of 
000000a0: 6120 6368 6172 6163 7465 720a            a character.

בצד שמאל רואים את הבתים כמידע גולמי ובצד ימין את הטקסט שהם מייצגים בקידוד ASCII. כבר אנחנו רואים שהסימן הראשון 41 מתאים לאות A.

3. ואז הגיעו השפות הזרות

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

במחשבים כמו במחשבים מהנדסים מיהרו לפתור את הבעיה: קודם כל מהר ורק אחרי זה נכון. הפיתרון המהיר היה לשים לב שיש לנו מקום פנוי בטבלא! כל אות מיוצגת על ידי בית (שמונה ביטים, כלומר 256 ערכים אפשריים), אבל טבלת ASCII סטנדרטית כוללת רק 127 שורות. מיהרו מי שמיהרו לנצל את המקום הפנוי לשפה שלהם. כך הצרפתים לקחו את ה 127 ערכים הבאים לאותיות המיוחדות שלהם, הרוסים לאותיות שלהם וגם הישראלים שמרו שם אותיות בעברית. כל קבוצה כזו יצרה בעצם טבלת ASCII חדשה שזהה לטבלא המקורית ב 127 הערכים הראשונים ונבדלת ב 127 ערכים האחרונים.

הטבלא העברית נקראת ISO8859-8. היא יכולה לייצג טקסטים באנגלית ובעברית וזהו. אחרי 256 שורות נגמר לנו המקום. הטבלא ISO8859-11 מייצגת אותיות באנגלית ובתאילנדית. אחרי זה נגמר שם המקום. לכן בעולם של ASCII לא ניתן לכתוב קובץ בעברית, אנגלית ותאילנדית: הקידוד פשוט מתנגש.

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

00000000: e4f6 e2fa 20f2 e1f8 e9fa 20e1 eee7 f9e1  .... ..... .....
00000010: 20e4 e9e0 20ee f9e9 eee4 20ec e020 f4f9   ... ..... .. ..
00000020: e5e8 e420 e1eb ecec 0aeb e920 ebec 20f4  ... ....... .. .
00000030: f2ed 20f9 f7e5 f8e0 e9ed 20f7 e5e1 f50a  .. ....... .....
00000040: f6f8 e9ea 20ec e3f2 fa20 e1e0 e9e6 e420  .... .... ..... 
00000050: f7e9 e3e5 e320 ebfa e1f0 e520 e0e5 fae5  ..... ..... ....
00000060: 0a                                       .

הסימן הראשון e4 מייצג את הערך 228 בבסיס ספירה עשרוני ולכן חורג מה 127 שורות הראשונות של טבלת ה ASCII. רק אם נדע באיזה טבלא השתמשו כדי לכתוב את הקובץ נוכל לקרוא אותו ולהציג את התווים. בלי מידע זה המידע שיוצג יהיה חסר היגיון.

4. קידוד קבצים מודרני: יוניקוד

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

יוניקוד כולל היום 136,755 תווים אפשריים ומכסה את כל השפות בעולם (ועוד כמה שפות שאינן בעולם). הוא מכסה את כל האימוג'ים, סימני הניקוד וכל התווים שאתם מכירים מהמחשב.

בעיה בייצוג מספר כל כך גדול של אפשרויות היא שזה עשוי להיות בזבזני: מספר הביטים המינימלי שדרוש כדי לייצג את כל האפשרויות הוא 24, או שלושה בתים. אם הקובץ שלי מכיל טקסט אנגלי בלבד אז במקום להשתמש ב-8 ביטים כדי לייצג כל אות אני צריך עכשיו 24, מה שאומר שהקובץ יהיה גדול פי 3.

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

אותו קובץ טקסט מהדוגמא הקודמת יראה כך בקידוד UTF-8:

00000000: d794 d7a6 d792 d7aa 20d7 a2d7 91d7 a8d7  ........ .......
00000010: 99d7 aa20 d791 d79e d797 d7a9 d791 20d7  ... .......... .
00000020: 94d7 99d7 9020 d79e d7a9 d799 d79e d794  ..... ..........
00000030: 20d7 9cd7 9020 d7a4 d7a9 d795 d798 d794   .... ..........
00000040: 20d7 91d7 9bd7 9cd7 9c0a d79b d799 20d7   ............. .
00000050: 9bd7 9c20 d7a4 d7a2 d79d 20d7 a9d7 a7d7  ... ...... .....
00000060: 95d7 a8d7 90d7 99d7 9d20 d7a7 d795 d791  ......... ......
00000070: d7a5 0ad7 a6d7 a8d7 99d7 9a20 d79c d793  ........... ....
00000080: d7a2 d7aa 20d7 91d7 90d7 99d7 96d7 9420  .... .......... 
00000090: d7a7 d799 d793 d795 d793 20d7 9bd7 aad7  .......... .....
000000a0: 91d7 a0d7 9520 d790 d795 d7aa d795 0a    ..... .........

וכך בקידוד UTF-16:

00000000: 05d4 05e6 05d2 05ea 0020 05e2 05d1 05e8  ......... ......
00000010: 05d9 05ea 0020 05d1 05de 05d7 05e9 05d1  ..... ..........
00000020: 0020 05d4 05d9 05d0 0020 05de 05e9 05d9  . ....... ......
00000030: 05de 05d4 0020 05dc 05d0 0020 05e4 05e9  ..... ..... ....
00000040: 05d5 05d8 05d4 0020 05d1 05db 05dc 05dc  ....... ........
00000050: 000a 05db 05d9 0020 05db 05dc 0020 05e4  ....... ..... ..
00000060: 05e2 05dd 0020 05e9 05e7 05d5 05e8 05d0  ..... ..........
00000070: 05d9 05dd 0020 05e7 05d5 05d1 05e5 000a  ..... ..........
00000080: 05e6 05e8 05d9 05da 0020 05dc 05d3 05e2  ......... ......
00000090: 05ea 0020 05d1 05d0 05d9 05d6 05d4 0020  ... ........... 
000000a0: 05e7 05d9 05d3 05d5 05d3 0020 05db 05ea  ........... ....
000000b0: 05d1 05e0 05d5 0020 05d0 05d5 05ea 05d5  ....... ........
000000c0: 000a                                     ..

אבל כשתקראו אותו בעורך טקסט יופיעו האותיות:

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

5. דוגמאות קוד

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

הפונקציה הראשונה תכתוב את השורה Hello World לקובץ ולאחריה בשורה חדשה את האות אלף. היא תעשה זאת בקידוד ברירת המחדל של המערכת שהוא כמעט תמיד UTF-8:

def write_default():
    with open('out.txt', 'w') as f:
        f.write('Hello World\n')
        f.write('\u05d0')
        f.write('\n')

הרבה יותר מומלץ לבחור את הקידוד בעת פתיחת הקובץ לכתיבה כדי שהקוד יעבוד על כל מכונה (ובכלל כדאי להתרגל לציין קידודים תמיד כי אז זה יעבוד גם כשתעברו לשפות אחרות). הקוד הזה כותב את אותו קובץ אבל הפעם מציין את הקידוד במפורש:

def write_utf8():
    with open('out.txt', 'w', encoding='utf-8') as f:
        f.write('Hello World\n')
        f.write('\u05d0')
        f.write('\n')

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

def write_iso88598():
    with open('out.txt', 'w', encoding='iso8859-8') as f:
        f.write('Hello World\n')
        f.write(b'\xe0'.decode('iso8859-8'))
        f.write('\n')

בשלב הקריאה נציין את הקידוד בדיוק באותו האופן. הקוד הבא לדוגמא יקרא וידפיס קובץ טקסט המקודד כ ISO8859-8:

def read_iso88598():
    with open('out.txt', 'r', encoding='iso8859-8') as f:
        for line in f:
            sys.stdout.write(line)

 

6. לקחים ומסקנות

ברוב הפעמים שתכתבו טקסט באנגלית לקבצים קידוד ברירת המחדל יהיה UTF-8. בקידוד כזה ובאותיות אנגליות אי אפשר לטעות: גם אם תפתחו את הקובץ בכל אחד מקידודי ה ASCII או ה Extended ASCII תקבלו את אותן אותיות בדיוק.

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

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

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