פרק 4: טיפול במחרוזות

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

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

4.1 אופרטורים של מחרוזות

my ($a, $b, $c) = ("Abra", "kadabra", 3); 
print $a . $b, "\n"; # prints "Abrakadabra" 
print $a x $c, "\n"; # prints "AbraAbraAbra"

4.2 מחרוזות ומשתנים

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

$temperature = 37; 
$str = "The temp. today is $temperature degrees"; 
$str = "The temp. today is " . $temperature . " degrees";

באותה דרך, אפשר לשלב מערך, איבר המערך, כמה איברים ממערך, טבלת האש שלמה או חלק ממנה, וכו'. כל דבר שמתחיל ב-$, @ או %, יכול להיכנס:

@temps=(23,53,25,34,11,7,0); 
$str = "The temps in the whole week where @temps degrees, each day"; 
$str = "The temp. yesterday was $temps[6] degrees"; 
$str = "The temps yesterday and the day before were @temps[6,5] degrees";

אם אתה רוצה להכניס את התווים $, @ או % עצמם למחרוזת, פשוט שים '\' לפניהם.

print "\$temp = $temperature\n";

אם אתה רוצה לעצב את המחרוזת בצורה מיוחדת, אתה תמיד יכול להשתמש בפקודה sprintf, המוכרת משפת C, ונתמכת בפרל.

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

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

#!/usr/bin/perl -w 
use strict;
my $str = "string";
print "Found!\n" if ($str =~ m/regexp/);

הדבר היחידי שאתה לא מכיר בתוכנית זה התנאי של ה-if. מה שאני שואל שם, זה האם הסקלר $str מכיל את המחרוזת "regexp". התשובה היא לא.
קודם כל – תנסה לשחק עם המחרוזות, כדי לקבל אמת. (כלומר, תחליף גם את "string" וגם את "regexp", כדי לראות מתי אתה מקבל אמת ומתי שקר) קח לך עשר דקות. אני אחכה.
עכשיו, נשנה את המחרוזת למשהו מעניין יותר:

my $str = "Semuel casts: Abra Kadabra! sugoi"

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

Abra Kadabra
כלומר, בתוכנית יהיה כתוב:

print "Found!\n" if ($str =~ m/Abra Kadabra/);

וזה יהיה אמת. המחרוזת שלנו באמת מכילה "Abra Kadabra".
עכשיו אפשר להתחיל לשחק:
הנקודה (.) מסמנת כל אות. אם למשל, במקום d לא אכפת לנו שיהיה z, J, 5 או סימן כלשהו ^, (אות, מספר או סימן כלשהו) אנחנו יכולים להחליף אותה בנקודה:

$str =~ m/Abra Ka.abra/

המחרוזת באמת מכילה את התת-מחרוזת "Abra Ka", שאחריה יש תו כלשהו יחיד, (במקרה הזה יש את התו "d") ואחריו יש את התת-מחרוזת "abra". לכן, יחזיר אמת.
גם המקרים הבאים יחזירו אמת:

my $str = "Semuel casts: Abra KaZabra sugoi";
my $str = "Abra Ka5abra ahoi";
my $str = "blaBLAblaAbra Ka^abraapchiEWE!";

כמו שאתה רואה, לא חשוב לשאלה הזאת מה יש לפני המחרוזת המבוקשת, מה יש אחריה, ואיזה תו בדיוק עומד במקום הנקודה. כל עוד המחרוזת עומדת בתנאי, יוחזר אמת.
ננסה משהו אחר. נגיד שבמקום ה-b השני אנחנו רוצים שיהיה או b, או z או 5, ורק אלה. כלומר, נשים בתוך הסוגריים של ה-if את הביטוי הבא:

m/Abra Ka.a[bz5]ra/

יוחזר אמת. במחרוזת באמת יש את התת-מחרוזת "Abra Ka", אחריה יש תו כלשהו, ("d") אחריו יש "a", אחריו יש אחד מהתווים "b", "z" או "5", ("b") ואחריו יש "ra".
רגע, בעצם התכוונתי, שיהיה כל דבר חוץ מ b, או z או 5:

Abra Ka.a[^bz5]ra

יוחזר שקר. כי במקום שאנו מתעקשים שלא יהיה "b", ולא "z" ולא "5", ויש "b". אי לכך, המחרוזת לא תואמת לתנאי, ולכן שקר.
הסימן + מסמן אחד-או-יותר, על הדבר שלפניו. למשל, איפה שיש כרגע K, אני מוכן שיהיה יותר מ-K יחיד: (כלומר, לקבל גם את המחרוזת "Abra KKKagakra")

Abra K+a.a[^bz5]ra

בעצם, למה להגביל אותנו ל-K. נשלב בין שני הקודמים, ונסכים לקבל גם שבאותו מקום יוכל להיות גם M, T ו-R (כלומר, גם "Abra MTRTKTMRagakra" מתקבל)

Abra [KTMR]+a.a[^bz5]ra

או סתם כל אות גדולה מה-ABC:

Abra [A-Z]+a.a[^bz5]ra

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

Abra [A-Z]+a.*a[^bz5]ra

זהירות עם הכוכבית. זה ת'כלס יכול לבלוע לך קבצים שלמים. {איש אחד נכנס עם פיל לקולנוע. השומר אומר לו, אדוני, אתה לא יכול להיכנס עם פיל לקולנוע. האיש מסתובב והולך. אחרי חמש דקות הוא חוזר עם הפיל, ששתי פרוסות לחם תקועות לפיל באוזניים. השומר אומר לו, אדוני, אתה לא יכול להיכנס עם פיל לקולנוע. הוא עונה, אתה לא תגיד לי מה לשים בסנדוויץ' שלי!} אז תיזהרו שלא יהיה לכם פה סנדוויץ' פיל, בסדר?
הסימן כובע (^), בנוסף לפעילותו בתוך הסוגריים המרובעות, גם מסמן תחילת מחרוזת, או אם זוהי מחרוזת עם כמה שורות טקסט בפנים, אז תחילת שורה. בדוגמא הבאה, מחפשים משהו שמתחיל ב-Abra [A..Z]+a, אחריו לא משנה מה, שמסתיים בשורה חדשה ו-a[^bz5]ra.

Abra [A-Z]+a.*^a[^bz5]ra

אגב, די פופולרי הצרוף הזה, “.*^”. זה בגלל הקושי לסמן שורה חדשה, או שאתה לא בדיוק יודע מה יש בסוף השורה.
הנה סתם קשר 'או'. אני מוכן שיהיה באותו המקום במחרוזת או 'bra' או 'adr'.

A(bra|adr) [A-Z]+a.*^a[^bz5]ra

בלי הסוגריים, זה עובד רק על אות בודדת: (בסוף, יש r או z)

A(bra|adr) [A-Z]+a.*^a[^bz5]r|za

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

A(bra|adr) [A-Z]+a.*^a[^bz5](?r)a

טוב, די. אין לדבר הזה סוף. מה שקיבלנו כרגע, שאנחנו רוצים מחרוזת שמתחילה ב-A, אחריה יש bra או adr, אח"כ רוווח, אח"כ אחת או יותר מהאותיות הגדולות באנגלית, (A..Z) אח"כ את האות a, אח"כ משהו, לא משנה כמה ומה. אח"כ שורה חדשה, a, ואז סימן כלשהו שיכול להיות כל דבר חוץ מ-b, z או 5, ואז אולי r ואולי לא, ובסוף עוד a. ברור, לא?
רק שים לב, שהמון תווים נחשבים לתווי בקרה בביטויים רגולריים, ואם אתם רוצים דווקא לחפש אותם, פשוט תשימו סלש-כפול (\\) לפניהם. למה כפול? כי אל תשכחו שמה שאתם כותבים זה בסופו של דבר מחרוזת, ובמחרוזת סלש מרמז על תו מיוחד. סלש-כפול נהפך אחרי העיבוד של המחרוזת לסלש רגיל, שאומר לביטוי הרגולרי שפה מדובר בתו עצמו, ולא באיזה תו בקרה.
בנוסף לכל מה שאמרנו, יש גם דרכי קיצור לתיאור של תווים. במקום לכתוב [A-Za-z0-9], (שזה כל האותיות באנגלית, פלוס כל הספרות) אפשר להשתמש בקיצור \w, שאומר בדיוק את אותו הדבר.
וזוהי טבלת הקיצורים:
משמעותהגדרהצרוף
אות גדולה, קטנה, מספר או קו תחתי[a-zA-Z_0-9]\w
רווח לבן (רווח, שורה חדשה, טאב וכו')[ \t\n\r\f]\s
סיפרה[0-9]\d
הכול חוץ מאות\מספר\קו תחתי[^a-zA-Z_0-9]\W
הכול חוץ מרווח לבן [^ \t\n\r\f]\S
הכל חוץ מסיפרה[^0-9]\D
מתקיים כאשר מצד אחד שלו יש \w, ומהצד השני \W. מסמן קצה מילה.קצה מילה\b
ניתן כמה דוגמאות, בעזרת הפקודה split. הפקודה מקבלת מחרוזת וביטוי רגולרי, ושוברת את המחרוזת בכל מקום שהיא מצאה רצף המתאים לביטוי הרגולרי.

split("\\|","Start|middle|end"); # gets ("Start","middle","end") 
split("\\||d","Start|middle|end"); # gets ("Start","mi","","le","en","") 
split("[td]+","Start|middle|end"); # gets ("S","ar","|mi","le|en","") 
m/<a [^<>]*href="[^"]*"[^<>]*>[^<>]*<\/a>/

האחרון הוא תנאי המחפש לינק של HTML בתוך מחרוזת. נשתמש בו בהמשך.

4.4 פקודת התאמת מחרוזת

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

while ( $text =~ m/Semuel is a(?n) (\w+) guy/g ) { 
    print $1,"\n"; 
}

הפקודה שבתוך התנאי של פקודת ה-while משמעותו, לחפש בתוך $text את הביטוי המובא בין שני הסלשים. אם היא לא מצאה, היא תחזיר undef, שהוא שקר, והפקודה תסתיים. אם מצאה, היא תחזיר בכל פעם את המחרוזת שהיא מצאה, ומחרוזת מלאה זה אמת, שתבצע את הפקודה. כשיגמרו ההופעות של תת המחרוזת בתוך המחרוזת, היא תחזיר undef, ובא לציון גואל.
שים לב לכך שבכל הקוד הוספתי פרמטר g לפקודה. זה אומר לו שיחפש בתוך המחרוזת שוב ושוב, עד שנגמרות לו התשובות, כמו שמוסבר בפסקה הקודמת. בלי g, הלולאה שלמעלה תרוץ לעד, שכל פעם היא מוצאת את אותו הדבר.
עכשיו, מאיפה צץ $1 ?! שימו לב לסוגריים סביב ה-'\w+'. הסוגריים אומרים, שאני מעוניין בקטע הזה של המחרוזת בנפרד. אז הערך מושם ב-$1. אם יש יותר מסוגריים יחיד, אזי שמים את הבאים ב- $2, $3 וכו'. הסוגריים עם ה-n בפנים לא נחשבים, כי זה חלק מהביטוי הרגולרי.
אגב, זו פקודה שמתנהגת שונה תחת הקשר רשימה. בהקשר רשימה, היא מחזירה רשימה של כל תת המחרוזות המתאימות לביטוי. דוגמא:

@arr = $text =~ m/Semuel is a(?n) \w+ guy/g;

דוגמא ללולאה. אם $contect מכיל קובץ html שלם, אז כדי לקבל את הלינקים שלו, עושים:

while( $context =~ m/<a [^<>]*href="([^"]*)"[^<>]*>([^<>]*)<\/a>/g ) { 
    print "The url is: $1\n"; 
    print "The text is: $2\n"; 
}

תשאל, למה אני משתמש בכל מקום ב-'[^<>]*', במקום בסתם '.*'. התשובה היא שהאלגוריתם שעובד פה הוא חמדן. זה אומר שאם אני אשתמש ב-''.*", הוא ייקח מהלינק הראשון של הקובץ עד האחרון, תחת אחד ה-'.*' (סנדוויץ' פיל). ככה, אני מכריח אותו שלא לקחת את התווים '<>', מה שמגביל אותו שהוא לא יכול להתפשט ליותר מלינק אחד, כי זה מכריח שימוש ב-'<>'.
אפשר להוסיף אופציות לפקודה, כגון האם להתחשב באותיות גדולות וקטנות ועוד. האופציות יתווספו אחרי הסלש השני של הפקודה. למשל:

$text =~ m/abra/i;

4.5 פקודת החלפה

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

my $text = "This is a long text, written slowing in the evening"; 
$text =~ s/long/short/; 
$text =~ s/written \w+ (\w+)/scratched $1 fast/;

בפקודה הראשונה, אני פשוט מחליף את המילה "long" במילה "short". בפקודה השניה, אני מחפש שלושה מילים, שהראשונה מהם היא "written", ומחליף אותם במילים "scratched fast", והמילה האחרונה מועברת לצד השני של הפקודה, ומשובצת שם. התוצאה תהיה:

"This is a short text, scratched in fast the evening"

4.6 חלק ממחרוזת

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

$newstr = substr($str,5,3);

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

my $text = "This is a long text, written slowing in the evening"; 
print $text, "\n";
print substr($text,5,5,"xxmmxxmmxx"), "\n";
print $text, "\n";

4.7 מציאת מיקום בתוך מחרוזת

אם אתה מחפש איפה האות g נמצאת בתוך מחרוזת, אז משתמשים בפקודת ה-index. דוגמא:

my ($str, $pos) = ("Kenguru"); 
$pos = index($str,"g");

אפשר לעשות את זה על יותר מתו אחת, ואפשר גם להגיד לו מאיפה להתחיל לחפש. הנה קטע קוד פופולרי:

my $str = "Here we are today, there were the greens"; 
my ($pos,$old_pos)=(0,0); 
while (($pos = index($str,"re",$old_pos))>=0) { 
    print "Found \"re\" at $pos!\n"; 
    $old_pos = $pos+1; 
}

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

4.8 חיבור מחרוזות

מי שקופץ ואומר, "אמרנו שיש אופרטור נקודה שמחבר מחרוזות!", אז צודק. זה טוב אם אנחנו רוצים לחבר שתי מחרוזות, ביחד. אבל אם יש לנו רשימה של מחרוזות, אז במקום לעשות for שבתוכו אופרטור הנקודה, יש לנו פקודה בשביל זה:

my @strarr=("Here","we","are","today,","there","were","the","greens"); 
print join('_',@strarr);

זה ידפיס: “Here_we_are_today,_there_were_the_greens”. שים לב לקו התחתי - זה מה שאמרתי לו להכניס בין המחרוזות. זו הפקודה ההפוכה ל-split. שם חתכנו לפי איזה ביטוי, פה אנחנו מחברים בעזרת מחרוזת.

4.9 חיתוך שורה חדשה

אולי פעם קראת שורות מקובץ, וכל פעם טרחת לחתוך את סימני שורה-חדשה (\n). פה, אתה פשוט משתמש בפקודת chomp:

my $line; 
$line = <>; 
chomp($line);

זוכר את האופרטור "<>"? הזכרנו אותו בעבר. זה אופרטור שקורא שורה מה-stdin. (שזה מהמקלדת, או מקובץ) אז אני לוקח פה שורה מה-stdin, ומכניס אותה ל-line. אולי יש בפנים את סימן שורה-חדשה, אולי לא. לא יודע.
chomp לוקח את המחרוזת, מחפש בפנים סימני שורה-חדשה, וחותך אותם. לא משנה אם אתה עובד על חלונות או על לינוקס. זה יעבוד. בכל פעם שתראה בתוכנית פרל קריאה מקובץ, תראה תמיד מיד אחריה את chomp. יעיל בטרוף.

4.10 סיכום מחרוזות

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

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

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

#!/usr/bin/perl -w 
use strict; 
my @taglist=(); 
my ($line, $tag, $stag); 
while ($line=<>) { 
    chomp($line); 
    while($line=~m/<([^<>]*)>/g) { 
        $tag=$1; 
        $tag=~s/^\s+//; 
        $tag=~s/\s+$//; 
        ($stag)=($tag=~m/\w+/); 
        if (substr($tag,0,1) ne "/") { 
            print "Starting tag($#taglist): $tag\n"; 
            push(@taglist, $stag); 
        } else { 
            while (($#taglist>0) and ($stag ne pop(@taglist))) {} 
            print "Ending tag($#taglist): $tag\n"; 
        } 
    } 
}

התוכנית קוראת קובץ עם תגים, ועוקבת אחרי התחלה וסוף של כל תג. היא מתחילה בהגדרת ארבעה משתנים, ומתחילה לקרוא את הקלט שורה-שורה. עבור כל שורה, התוכנית מחפשת את התגים בשורה. (אל דאגה, התוכנית לא תיתקע על התג הראשון ותמצא אותו שוב ושוב, אלא בכל פעם שורת ה-while תחפש את התג הבא, עד שיגמרו התגים במחרוזת ואת הלולאה תסתיים)
התג שנמצא מאוכסן בתוך $1. (שים לב לסוגריים בתוך פקודת ההתאמה) אני מציב את הערך לתוך $tag, מוריד רווחים בהתחלה ובסוף, ומכניס את המילה הראשונה בתג לתוך $stag.
אם אין את הסימן "/" בתחילת התג, זה אומר שמדובר בתחילת תג. אני מוציא הודעה מתאימה, הכוללת גם את גודל המערך שלי ואת התג עצמו, ודוחף את התג לתוך רשימת התגים שלי. אם יש את הסימן "/" בתחילת התג, זה אומר שזה סוף תג. אני מוריד מהמערך איברים עד שאני מגיע לתג המתאים, ומוציא שוב הודעה מקבילה על סוף תג.
התוכנית מניחה שכל התגים הם באותיות קטנות, מכילים מילה אחת לפחות, ושהסגירה שלהם מתחילה באותה המילה. אם יהיה סגירה של תג בלי ההתחלה שלו, התוכנית תיגמר מאוד מהר. כמו כן התוכנית מניחה שאין שבירה של תג בין שתי שורות.

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

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