פרק 9: עברית בפרל

[הקודם : אינטרנט][חזרה לתוכן העניינים][הבא : תרבות וסביבה]

9.1 הקדמה

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

9.2 מושגי יסוד

בתחלה היה האסקיי. (ASCII) אסקיי זוהי טבלה הממפה מספר (מ-0 עד 127) לתו, ומכילה את כל הספרות, האותיות הגדולות והקטנות באנגלית, סימנים נפוצים, ועוד כמה תווים שונים.
ואז בא אדם חכם (?) ואמר, בואו נוסיף פה עברית. טבלת האסקיי היא רק עד 127, כלומר רק 7 ביט, ולנו יש בכל בית (Byte) שמונה ביטים, כלומר עד 255, בואו נדחוף את האותיות העבריות מעל 127, והכל יהיה טוב.
צצו עם זה שלוש בעיות: אחת, שגם הצרפתי, הגרמני, הספרדי והטיוואני חשבו ככה. וכולם שמו את האותיות שלהם באותו תחום. (128-255) כלומר, לאותו מספר היו כמה אפשרויות לתצוגה.
הבעיה השניה היא שבגלל שיהודים לא יכולים להסכים אפילו על השעה, היו כמה יהודים ששמו את האותיות העבריות בתחום ההפקר, וכל אחד שם אותם במיקום שונה. כלומר, לאותו תו היו כמה אפשרויות למספרים.
הבעיה השלישית היא שיופי שלימדנו את המחשב את האותיות העבריות, עדיין נשאר לנו ללמד אותו לדבר מימין לשמאל.
בסופו של דבר, הטבלאות השונות הסתדרו במה שנקרא דפי-קידוד. (Code-Page) לכל שפה יש טבלה משלה, המקצה מספר לכל תו. כל דף מזוהה ע"י מספר. בעברית למשל יש את iso-8859-8 ואת CP1255. כל אחד משניהם מצה מקום לעברית, אבל במקום שונה מהדף השני.
חסרון אחד ברור של השיטה הזאת הוא שאי אפשר להציג כמה שפות באותו המסמך. כי כל השפות דורכות אחת על השניה. חסרון אחר זה שמישהו (מי?) צריך לומר למחשב באיזה דף קידוד אנחנו רוצים להשתמש.

9.3 יוניקוד

לעזרתנו בא איגוד היוניקוד. (UniCode) איגוד היוניקוד שם לו למטרה להקצות מספר אחד לכל אות שקיימת, ולא משנה באיזו שפה. כמובן שעבור זה צריך יותר משמונה ביטים. אז מה.
האיגוד יצר טבלה ענקית, הכוללת את כל הסימנים בכל השפות. כמובן, שום דבר לא מושלם, ויש כמה מקרי קצה, (של תו שיש לו כמה מספרים, ומספר שיש לו כמה תווים) אבל בגדול, זה עושה את חיינו טובים יותר.
עדיין, הטבלה הזאת לא מיתרגמת ישירות לדף-קידוד. וזה מהסיבה בפשוטה שהיא גדולה מדי. ולתו בודד יש רק שמונה ביטים, לא יותר.
אז מה עושים? יש שתי כיוונים עיקריים:
שיטה אחת אומרת: מהיום, לתו יש יותר משמונה ביטים. מהיום, יהיה לו 16 ביט. או אפילו 32 ביט. אלה שיטות הנקראות ucs2 ו-ucs4, בהתאמה.
היתרון שלהם זה שלכל תו יש מספר ביטים קבוע. זה חשוב בטיפול בקבצים, שאם אתה רוצה לגשת לתו השלושים ושמונה, לא מעניין אותך מה יש לפניו. אתה פשוט קופץ לבית ה-38 כפול 2 או 4. החיסרון הוא שלכל תו יש מספר ביטים קבוע. אם אני רוצה לכתוב רק אנגלית, אני עדיין מבזבז 4 בתים על כל אות, בעוד שבאסקיי זה היה רק בית אחד על כל אות.
שיטה אחרת אומרת: אנגלית נשארת בית אחד לכל תו. רק אם אני אצטרך, אני אוסיף בתים. ולזה נשתמש בביט השמיני של התו. אם הוא דלוק, (כלומר, ערכו 1) אזי אני מוסיף גם את הבית הבא לתו. אם הוא כבוי, אני נשאר עם בית אחד לתו אחד. שיטה זאת נקראת utf8.
היתרון הוא בקומפקטיות, אם אני כותב אנגלית, יש לי בית אחד לכל תו. החיסרון הוא באורך תו משתנה. אני כבר לא יכול לדעת מראש איפה נמצא התו ה-38.

9.4 חזרה לפרל

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

use utf8;

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

binmode(STDIN, ":utf8");

וזה יגיד לפרל להתייחס לקלט שלו כמקודד ב-utf8. אפשר להפעיל את אותה הפקודה על הפלט ועל כל קובץ פתוח. כדי לפתוח קובץ עם קידוד כבר, אפשר לכתוב:

open(my $fh, "<:encoding(cp1255)", "file"); 
open(my $fh, ">:encoding(UTF-8)", "file");
open(my $fh, "<:utf8", "file"); # utf8 have it's own shortcut

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

use Encode qw{from_to encode decode};
# from iso-8859-8 to Perl's internal encoding (adding the flag)
$string = decode("iso-8859-8", $octets);
# from Perl's internal encoding to iso-8859-8 (removing the flag)
$octets = encode("iso-8859-8", $string);
# from legacy to utf-8 - "in-place"
from_to($data, "iso-8859-8", "utf8");

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

@all_encodings = Encode->encodings(":all");

9.5 זרימת טקסט

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

use Locale::Hebrew qw/hebrewflip/;
$visual = Locale::Hebrew::hebrewflip($logical);

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

Hello לאומש! How are you?

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

Hello !שמואל How are you?
Hello שמואל! How are you?

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

9.6 זהירות - הגדרות

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

9.7 ביטויים רגולריים

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

use utf8;
$str = "Hello עולם!";
$str =~ m/(\p{InHebrew}+)/;
print $1, "\n";

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

9.8 תוכנית דוגמא

תוכנית קטנה שמשחקת עם עברית:

#!/usr/bin/perl -w
use strict;
use warnings;
use Encode;

binmode(STDOUT, ":utf8");
my $orig_str = "some עברית writen";
print "undecoded: ", (join ", ", $orig_str =~ m/\w+/g) ,"\n";
my $dstring = decode("utf8", $orig_str);
print "decoded: ", (join ", ", $dstring =~ m/\w+/g) ,"\n";
my $cpstring = encode("cp1255", $dstring);
print "re-encoded ($cpstring): ", (join ", ", $cpstring =~ m/\w+/g) ,"\n";

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

undecoded: some, writen
decoded: some, עברית, writen
re-encoded (some òáøéú writen): some, written

הסביבה בה אני עובד היא komodo, התומך בפלט utf8.
אני מתחיל ממחרוזת המקודדת כ-utf8, אבל בהתחלה פרל לא יודע מזה. אז הביטוי הרגולרי הראשון לא מזהה את העברית בתור אותיות. מצד שני המחרוזת המקודדת (decoded) מוצגת כמו שצריך, ובה הביטוי הרגולרי זיהה את האותיות העבריות כאותיות.
ומה זה ה-"òáøéú"? לקחנו את המחרוזת והמרנו אותה לקידוד עברית-חלונות. למחרוזת החדשה אין את הסימון של פרל שזו מחרוזת בפורמט הפנימי שלו. עכשיו התוכנית מדפיסה את המחרוזת לפלט הסטנדרטי, שמסומן כ-utf8. פרל מסתכלת על המחרוזת, רואה תווים שהם לא אסקי. מניחה שמדובר בקידוד latin1, וממירה מהקידוד הזה ל-utf8.
אגב, ככה נראה הפלט של אותה התוכנית בקופסת דוס אצלי במחשב:

undecoded: some, writen
decoded: some, ╫ó╫ס╫¿╫ש╫, writen
re-encoded (some òáøéú writen): some, writen

בערך. במקום ה-ש וה-ס היו תווים מוזרים, שלא רצו לעבור בהעתק\הדבק.
איך זה יראה אצלך תלוי בסביבה שלך ובאם הצלחת לשמור את הקובץ בקידוד המתאים.

[הקודם : אינטרנט][חזרה לתוכן העניינים][הבא : תרבות וסביבה]

נכתב ע"י שמואל פומברג, כל הזכויות שמורות © ראה פרק 1.5 לתנאי רשיון