ברירה#
ברירה היא האופרטור |. הוא בוחר בין שתי תת־תבניות או יותר באותו מיקום.
"cats and dogs" =~ /cat|dog|bird/; # matches 'cat'
"cats and dogs" =~ /dog|cat|bird/; # matches 'cat'
סדר החלופות אינו משנה היכן התבנית הכוללת מתאימה. המנוע עדיין מכבד את הכלל ״המיקום המוקדם ביותר מנצח״ — שתי התבניות שלמעלה מתאימות במיקום 0 כי זה המיקום המוקדם ביותר שבו חלופה כלשהי יכולה להתאים.
החלופה השמאלית ביותר מנצחת במיקום נתון#
בתוך מיקום התחלה יחיד, החלופות נבדקות משמאל לימין והראשונה שמצליחה היא זו שנבחרת:
"cats" =~ /c|ca|cat|cats/; # matches 'c' — first alternative wins
"cats" =~ /cats|cat|ca|c/; # matches 'cats' — first wins, longer
אם חלופה אחת היא קידומת של אחרת ורוצים את ההתאמה הארוכה יותר, יש למקם אותה ראשונה. המנוע אינו מסתכל מעבר לחלופה המוצלחת הראשונה במיקום הנוכחי.
זוהי התנהגות NFA מסורתי, וזה מה ש־Perl, PCRE2, Python ורוב המנועים המודרניים מיישמים. מנועים תואמי־POSIX (בעיקר מימושי awk ו־grep מסוימים) עוקבים אחר כלל הארוך-משמאל במקום — הם יתאימו את cats ללא קשר לסדר החלופות. בפרק cross-engine מופיעה ההשוואה.
פרידל מנסח זאת בתמציתיות:
ברירה חמדנית היא לא־חמדנית ב־NFA מסורתי.
התבנית tour|to|tournament מול three tournaments won מתאימה את tour, לא את tournament. החלופה הראשונה מצליחה והמנוע מתחייב אליה; חלופות ארוכות יותר בהמשך הרשימה אינן נבדקות אף פעם.
השלכות על הסדר#
ברירה שהיא חלק מתבנית גדולה יותר עשויה לקבל את ביצועיה ואת נכונותה מסדר החלופות:
ספציפיות תחילה. כאשר רוצים את הארוכה ביותר מבין כמה קידומות, יש למקם את הארוכה ביותר ראשונה.
/web|website|websites/מתאימה אתwebאפילו על הקלטwebsite;/websites|website|web/מתאימה אתwebsites.המקרה הנפוץ תחילה. ברירה נבדקת משמאל לימין; אם 90% מהקלטים פוגעים בחלופה 3, המנוע מבזבז זמן על חלופות 1 ו־2 בכל פעם. יש לסדר מחדש לפי תדירות.
לכידות אחיות. כאשר החלופות לוכדות, הסדר משפיע על איזה
$nנקבע — ראו ברירה ולכידה למטה.
כלל פורמלי לשילוב חלקים#
״Combining RE Pieces״ של perlre מספק את הניסוח המדויק שביסוד הכלל ״השמאלי ביותר מנצח״. עבור שני חלקי תבנית S ו־T:
כאשר
Sיכול להתאים, זוהי התאמה טובה יותר מאשר כאשר רקTיכול להתאים.
זוהי הגרסה הפורמלית של הכלל. ״טובה יותר״ פירושו שהמנוע מעדיף אותה. עבור שתי התאמות S, חל אותו סידור פנימי (כללי חמדנות בתוך החלופה); באותו אופן גם עבור התאמות T. בין חלופות, קיומה של התאמת S מוצלחת שולל את שקילת T.
זו הסיבה ש־S|T אינה ניתנת לסידור מחדש על ידי המנוע מיוזמתו: Perl אינה מחפשת את החלופה הטובה ביותר על פני הדיסיונקציה — היא מתחייבת ל־S בכל פעם ש־S מצליחה.
קדימות קיבוץ מול ברירה#
ל־| יש קדימות נמוכה מאוד. הוא מפצל את התבנית ברמה החיצונית ביותר המכילה אותו:
/ab|cd/; # 'ab' OR 'cd'
/^ab|cd$/; # '^ab' OR 'cd$' — probably not what you meant!
/^(ab|cd)$/; # '^' + ('ab' or 'cd') + '$' — what you meant
כדי לצמצם ברירה לחלק מתבנית, יש לעטוף אותה בקבוצה. קבוצה לא־לוכדת (?:…) עדיפה אלא אם יש צורך בלכידה:
/house(?:cat|keeper)/; # 'housecat' or 'housekeeper'
/house(cat|keeper)/; # same, but $1 will be 'cat' or 'keeper'
הקבוצה יוצרת תחום מקומי עבור |. מחוץ לקבוצה | חוזר לתפקידו ברמה העליונה:
/^(?:foo|bar|baz)$|^xyz$/; # ('foo'/'bar'/'baz') or 'xyz'
חלופות ריקות#
חלופה ריקה מתאימה את המחרוזת הריקה — תכסיס שימושי עבור ״זה או כלום״:
/house(cat|)/; # 'housecat' or 'house'
/(19|20|)\d\d/; # '19xx', '20xx', or just 'xx'
סגנון מודרני מעדיף (?:…)? על פני (?:…|); הם שקולים, אך צורת ה־? ברורה יותר:
/house(?:cat)?/; # same as house(cat|), no capture
יש לשים לב לעלות החזרה לאחור כאשר חלופה ריקה משולבת עם כמת — המנוע עלול לחקור מחדש את אותו מיקום פעמים רבות. ראו את פרק performance על סיום התאמת אורך אפס.
הערה בין־מנועית#
POSIX קפדני, lex, ורוב מימושי awk הישנים אוסרים חלופות ריקות — (this|that|) הוא שגיאת תחביר. Perl 5.42, PCRE2, ה־crate של regex ב־Rust, re של Python, ומימושי מנועים מודרניים מקבלים אותן. אם תבנית צריכה להיות ניידת לכלי POSIX ישנים יותר, יש לכתוב (?:this|that)? במקום — זה מבטא את אותו רעיון והוא נייד בין המנועים שאינם מקבלים אף אחת מהצורות.
ברירה בתוך מחלקות תווים#
מחלקות תווים הן כמעט תמיד מה שרוצים כאשר בוחרים בין תווים בודדים. /a|b|c/ ו־/[abc]/ מתאימות את אותן המחרוזות, אך [abc] מהיר יותר, תמציתי יותר וברור יותר:
/a|b|c/; # works, but verbose
/[abc]/; # use this
ברירה מיועדת לחלופות הארוכות מתו אחד (או כאלה שהן עצמן תבניות). כאשר כל חלופה היא תו בודד, יש להשתמש במחלקה. המנוע מתייחס למחלקה כבחירה אטומית יחידה; לברירה כ־N בחירות שיש לחקור בסדר.
פירוק קידומת משותפת#
תבנית כמו /this|that|then|those/ מסכלת את אופטימיזציית בדיקת המחרוזת הקבועה של המנוע: אין קידומת מילולית שהמנוע יכול לסרוק בזול. שכתוב כדי לחשוף את הקידומת המשותפת הופך את הברירה למשהו שהאופטימייזר יכול לעבוד איתו:
/this|that|then|those/; # no common prefix visible
/th(?:is|at|en|ose)/; # common prefix 'th' exposed
שתי התבניות מתאימות את אותן המחרוזות, והשנייה מהירה משמעותית על קלטים גדולים. המנוע סורק אחר th בעזרת Boyer-Moore, ואז מריץ את הברירה הקטנה רק במיקומי המועמדים.
הטכניקה הכללית:
למצוא את הקידומת המילולית הארוכה ביותר המשותפת לכל החלופות.
להוציא אותה אל מחוץ לברירה.
לעטוף את היתרה ב־
(?:…)כך שהברירה תישאר מקומית.
עבור רשימות של מילים עם מספר קידומות שונות, ניתן ליישם את השכתוב באופן רקורסיבי. עבור רשימות ארוכות מאוד, ראו את הסעיף על התאמת מחרוזות רבות בפרק performance — כאשר הרשימה מגיעה לאלפים, מנגנון התאמה ייעודי מנצח כל ברירה.
ברירה ולכידה#
רק חלופה אחת בתוך קבוצה יכולה להתאים בכל פעם, ולכן הקבוצה לוכדת את החלופה המתאימה:
if ("bert" =~ /(cat|dog|bert|ernie)/) {
print "matched $1\n"; # matched bert
}
קבוצות אחיות מחוץ לברירה שומרות על המספור הרגיל שלהן:
/^(\w+):\s*(yes|no|maybe)$/;
# $1 = the key, $2 = the verdict
בתוך ברירה מקוננת, הקבוצות ממוספרות משמאל לימין לפי הסוגר הפותח, אפילו בין ענפים:
/(a)|(b)/;
# On match of 'a': $1 = 'a', $2 undef
# On match of 'b': $1 undef, $2 = 'b'
יש לבדוק עם defined $n, לא עם ערך אמת — לכידה ריקה שונה מלכידה נעדרת.
איפוס ענפים: (?|…)#
תבניות לכידה מקבילה הן הסיבה הרגילה לבחור ב־(?|…). בתוך (?|…), כל ענף מתחיל למספר את הלכידות שלו באותה משבצת. אחרי הקבוצה, המספור מתחדש באחד מעבר למקסימום על פני כל הענפים.
# Without (?|…): need to know which branch matched.
if ($time =~ /(\d\d|\d):(\d\d)|(\d\d)(\d\d)/) {
my ($h, $m) = ($1, $2);
($h, $m) = ($3, $4) unless defined $h;
}
# With (?|…): $1 and $2 come from whichever branch matched.
if ($time =~ /(?|(\d\d|\d):(\d\d)|(\d\d)(\d\d))/) {
my ($h, $m) = ($1, $2);
}
עם חלק קבוע נגרר:
if ($time =~ /(?|(\d\d|\d):(\d\d)|(\d\d)(\d\d))\s+([A-Z]{3})/) {
# $1 = hours, $2 = minutes, $3 = zone (numbered after the group)
print "hour=$1 minute=$2 zone=$3\n";
}
כללים בתוך (?|…):
כל ענף ממספר את קבוצות הלכידה שלו באופן עצמאי החל מספירת הקבוצה הנוכחית.
אחרי הקבוצה, המספור החיצוני ממשיך באחד גבוה יותר מהספירה המקסימלית שהושגה בכל ענף.
קבוצות בשם שומרות על שמותיהן; ניתן לחזור על שם בין ענפים. יש להשתמש באותם שמות באותו סדר בכל ענף, אחרת צצות הפתעות (ראו את פרק groups and captures).
איפוס ענפים הוא הדרך הנקייה ביותר לבטא ״לפענח X באחד ממספר פורמטים שקולים, ואז לגשת לאותם משתנים לאחר מכן״.
ברירה ב־split#
split מקבלת תבנית של ביטוי רגולרי, כך שברירה עובדת גם שם:
my @words = split /\s+|-/, "one-two three four-five";
# ('one', 'two', 'three', 'four', 'five')
אם תבנית המפריד מכילה קבוצות לכידה, split כוללת את הטקסט הנלכד ברשימת הפלט — לעיתים מפתיע. יש להשתמש ב־(?:…) אלא אם רוצים בכך:
split /(?:\s+|-)/, "a-b c"; # ('a', 'b', 'c')
split /(\s+|-)/, "a-b c"; # ('a', '-', 'b', ' ', 'c')
הצורה הלוכדת היא לעיתים מה שרוצים — שמירת המפרידים המדויקים בין השדות. ברוב המקרים רוצים לא־לוכדת.
סיכום#
|מפריד בין חלופות; השמאלית ביותר שמתאימה במיקום הנוכחי מנצחת.יש לעטוף ברירות ב־
(?:…)כדי למקם אותן.יש להעדיף מחלקות תווים על פני ברירות של תו בודד.
יש להוציא קידומות משותפות אל מחוץ לברירה כאשר רוצים שאופטימיזציית הסריקה המילולית של המנוע תפעל.
(?|…)מאפס את מספור הלכידות בין ענפים — יש להשתמש בו כאשר הענפים לוכדים את אותם שדות מושגיים.
ראו גם#
פרק groups and captures — מספור לכידות בתוך
(?|…), ושאר מנגנון הלכידה־בשם.פרק performance — סידור לפי סבירות, פירוק קידומת משותפת, והתאמת מחרוזות רבות.
פרק cross-engine — הבדלי תחביר ברירה בין משפחות מנועים.
split— ברירה כתחביר מפריד.