עוגנים והיגדים#

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

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

תחילת המחרוזת וסופה#

^ מתאים בתחילת המחרוזת. $ מתאים בסוף, או ממש לפני שורה חדשה סופית.

"housekeeper" =~ /keeper/;    # matches — 'keeper' appears somewhere
"housekeeper" =~ /^keeper/;   # does not match — not at start
"housekeeper" =~ /keeper$/;   # matches — at end
"housekeeper\n" =~ /keeper$/; # matches — before the trailing newline

בשימוש משולב, ^…$ כופה על התבנית להסביר את כל המחרוזת:

"bert"    =~ /^bert$/;     # matches
"bertram" =~ /^bert$/;     # does not match
"dilbert" =~ /^bert$/;     # does not match
""        =~ /^$/;         # matches — empty string

עבור מחרוזת מילולית, $s eq "bert" מהיר יותר וברור יותר. ^…$ מצדיק את עצמו רק כשהאמצע משתמש בתכונות regex אמיתיות.

עוגנים מוחלטים לעומת עוגנים יחסיים לשורה#

תחת המתאם /m, ^ ו־$ מתעגנים בכל גבול שורה בתוך המחרוזת, ולא רק בקצוות החיצוניים:

my $x = "There once was a girl\nWho programmed in Perl\n";

$x =~ /^Who/;    # does not match — 'Who' is not at string start
$x =~ /^Who/m;   # matches — 'Who' is at start of second line

עבור הקצוות המוחלטים ללא תלות ב־/m, יש להשתמש בעוגנים הייעודיים:

  • \A — תחילת מחרוזת מוחלטת.

  • \z — סוף מחרוזת מוחלט.

  • \Z — סוף המחרוזת, או ממש לפני שורה חדשה סופית. דומה ל־$ אך אינו מושפע מ־/m.

$x =~ /^Who/m;    # matches, as above
$x =~ /\AWho/m;   # does not match — \A is string start, always

$x =~ /girl$/m;   # matches, end of first line
$x =~ /girl\Z/m;  # does not match — 'girl' is not at string end
$x =~ /Perl\Z/m;  # matches — end or before final newline
$x =~ /Perl\z/m;  # does not match — \z is strict end

כלל אצבע: \A ו־\z כשמתכוונים לכל המחרוזת; ^ ו־$ כשמתכוונים לשורות (בדרך כלל עם /m). \A ו־\z גם מהירים יותר: המנוע יודע שהם מגבילים מיקום, ומדלג על ניסיונות התקדמות צעדית ש־^ מתיר.

ארבעת מצבי השורה החדשה ניתנים לסיכום בטבלה. יש לבחור את זה התואם למה ש־^, $ ו־. אמורים להיות עבור הקלט:

מצב

^ מתאים ב־

$ מתאים ב־

. מתאים ל־\n?

ברירת מחדל

תחילת מחרוזת בלבד

סוף מחרוזת (ולפני \n סופי)

לא

/m

תחילת כל שורה

סוף כל שורה

לא

/s

תחילת מחרוזת בלבד

סוף מחרוזת (ולפני \n סופי)

כן

/sm

תחילת כל שורה

סוף כל שורה

כן

\A, \Z, \z אינם מושפעים מאף אחד מהמתאמים.

גבולות מילה#

\b מתאים למיקום בין תו מילה (\w) לתו שאינו תו מילה (\W), או בין אחד מהם לבין קצה המחרוזת. הוא אינו צורך אף תו.

my $x = "Housecat catenates house and cat";

$x =~ /cat/;      # matches 'cat' inside 'Housecat'
$x =~ /\bcat/;    # matches 'cat' inside 'catenates'
$x =~ /cat\b/;    # matches 'cat' inside 'Housecat'
$x =~ /\bcat\b/;  # matches the standalone 'cat' at end

\B הוא השלילה — מיקום שאינו גבול מילה.

\b מחוץ ל־[…] הוא גבול מילה. \b בתוך […] הוא תו ה־backspace, \x08. המשמעות הכפולה היא המלכודת הנפוצה ביותר בתחביר ה־regex. יש לקרוא תמיד את ההקשר של \b לפני קריאת מה שהוא עושה.

\b ופריטים שמתחילים ב־\W — מלכודת ה־$3.75#

\b דורש מעבר בין תווי מילה לתווים שאינם תווי מילה. אם שני צדי המיקום הם תווים שאינם תווי מילה, אין גבול. זה תופס אנשים הבונים תבניות באמצעות שיבוץ:

my $item = '$3.75';
my $regex = qr/\b\Q$item\E\b/;

"is $3.75 plus tax" =~ /$regex/;   # does NOT match

מדוע? לאחר ש־\Q$item\E מתבשל, התבנית היא \b\$3\.75\b. המנוע מחפש מיקום מיד לפני $ שבו צד אחד של המיקום הוא תו מילה. התו שלפני $ בקלט הוא רווח (לא־תו־מילה), ו־$ עצמו אינו תו מילה. אין מעבר מילה/לא־מילה; \b נכשל.

כדי לעגן את תחילתו של $item ללא תלות בתו הראשון שלו:

my $regex = qr/(?:^|\s)\Q$item\E(?:\s|$)/;

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

וריאנטים של גבול מילה ב־Unicode#

\b רגיל פועל לפי ההגדרה של \w/\W, אשר תחת Unicode מכסה אותיות, ספרות, סימני ניקוד וסימני פיסוק מחברים. טקסט אמיתי — שפה טבעית עם אפוסטרופים, מקפים, מספרים עם פסיקים — דורש ניואנסים נוספים. Perl מספקת ארבעה וריאנטים סמנטיים של גבול:

גבול

משמעות

\b{wb}

גבול מילה ב־Unicode — מטפל ב־don't, state-of-the-art

\b{sb}

גבול משפט

\b{lb}

שבירת שורה (מתאים לעטיפת שורות)

\b{gcb}

גבול אשכול גרפמות (אותה השפעה כמו \X)

"don't" =~ /.+?\b{wb}/x;   # matches whole word — apostrophe is inside
"don't" =~ /.+?\b/x;       # stops at the apostrophe — plain \b splits

עבור עיבוד שפה טבעית, \b{wb} ו־\b{sb} הם כמעט תמיד הרצויים. \b רגיל מיועד להקשרי מזהי ASCII.

\G — מיקום ההתאמה האחרון#

\G מתעגן במיקום שבו הסתיימה ההתאמה המוצלחת הקודמת של /g באותה מחרוזת. הוא מה שהופך טוקניזציה עם /g בלולאות while ליציבה.

my $s = "12abc34";
while ($s =~ /\G(\d+|[a-z]+)/gc) {
    print "got '$1'\n";
}
# prints: '12', 'abc', '34'

ללא \G, אותה תבנית הייתה סורקת מחדש ממקום שבו היא התאימה לראשונה, מדלגת על תווים שלא התאימו — ומאבדת נתונים בשקט. המתאם /gc משמר את המיקום בכישלון; מכוסה בפרק modifiers.

מדוע \G חשוב: הבחירה בין סנכרון לדילוג#

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

my $s = "06192054410-44272-13901-44106-22134";
while ($s =~ /44(\d{3})/g) {
    print "got 44$1\n";
}

ללא \G, ההתקדמות הצעדית של המנוע שמחה למצוא את 44272 ו־44106 נכונות וגם למצוא את 44272 ממיקום התחלה אחר בכל פעם שהתאמה קודמת נדחתה. הפלט הוא ״התשובה הנכונה בתוספת נכונות־לא נוספות״.

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

while ($s =~ /\G\D*(44\d{3})/gc) {
    print "got $1\n";
}

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

\G קורא את הערך ש־pos($string) מחזיר. הצבת pos במפורש מאפסת את העוגן. באיטרציה הראשונה של לולאת /g (או כל תבנית שטרם הותאמה למחרוזת זו), \G שקול ל־\A.

Lookahead#

Lookahead בודק מה בא אחר כך מבלי לצרוך אותו. lookahead חיובי הוא (?=…):

my $x = "I catch the housecat 'Tom-cat' with catnip";

$x =~ /cat(?=\s)/;   # matches 'cat' in 'housecat' — space follows
$x =~ /cat(?!\s)/;   # matches 'cat' in 'catch' — no space follows

(?!…) הוא lookahead שלילי: התאמה רק אם התבנית הפנימית אינה חלה. רוחב אפס, בדיוק כמו ^ ו־$.

"foobar" =~ /foo(?!bar)/;   # does not match
"foobaz" =~ /foo(?!bar)/;   # matches

דוגמה מעובדת של lookahead: ספרות שאינן מלוות בנקודה#

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

הניסיון הראשון: \d+(?!\.). בהחלתו על OH 44272:

"OH 44272" =~ /\d+(?!\.)/;   # MATCHES, matches '44272'

זה נראה נכון, אך מהסיבה הלא־נכונה. ה־\d+ החמדן תפס תחילה את כל רצף הספרות 44272; התו הבא הוא סוף־מחרוזת (לא .), ולכן (?!\.) הצליח מיד — ה־lookahead במקרה התקיים מבלי שהמנוע נדרש לסגת. הקריאה האינטואיטיבית ״רצף הספרות מסתיים במשהו שאינו נקודה״ מתקיימת במקרה של ספרות בסוף, אך הדוגמה הבאה מראה מה קורה כשהיא לא מתקיימת.

כעת בהחלתו על 4423.45:

"4423.45" =~ /\d+(?!\.)/;   # matches '442', not '4423'

הניסיון הראשון של המנוע: \d+ מתאים ל־4423. (?!\.) בודק את התו הבא: הוא ., ה־lookahead השלילי נכשל. המנוע חוזר לאחור: \d+ מתאים ל־442, התו הבא הוא 3, לא ., הצלחה. התבנית מתאימה ל־442לא רצף הספרות המלא שלפני הנקודה. המכמת החמדן היה מוכן לסגת כדי לאפשר ל־lookahead להצליח.

התיקון: לדרוש שה־lookahead ידחה גם ספרות אחרות, כך שהרצף לא יוכל לסגת ל״ספרה ואחריה ספרה״.

"4423.45" =~ /\d+(?![\d.])/;   # matches '45' — correct

\d+(?![\d.]) נקרא ״התאם רצף ספרות שאינו מלווה בספרה אחרת או בנקודה״. המכמת החמדן עדיין מתפשט מקסימלית, אך ה־lookahead כעת מסרב להצליח באמצע רצף ספרות, ולכן המיקומים המתאימים היחידים הם קצוות רצף הספרות בפועל.

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

מקרה קשור: lookahead שלילי מוביל וההתקדמות הצעדית.

"cattle" =~ /(?!cat)\w+/;   # MATCHES, captures 'cattle'

ה־lookahead (?!cat) דוחה את מיקום 0 (שבו cat היה מתאים). המנוע מתקדם צעדית: מיקום 1 מתחיל ב־attle, שבו (?!cat) מצליח, ו־\w+ מתאים ל־attle.

כדי לכפות ״המילה הראשונה שאינה מתחילה ב־cat״, יש לעגן את ה־lookahead בגבול מילה:

"cattle" =~ /\b(?!cat)\w+/;   # does not match

כעת ה־lookahead חל בכל גבול מילה, ובמילה cattle זהו רק מיקום 0.

Lookbehind#

Lookbehind בודק מה היה קודם. lookbehind חיובי הוא (?<=…):

"cats and dogs" =~ /(?<=and )dogs/;   # matches 'dogs' after 'and '

lookbehind שלילי הוא (?<!…):

"prefix_foo suffix_foo" =~ /(?<!prefix_)foo/;
# matches 'foo' in 'suffix_foo'

Perl מודרנית תומכת ב־lookbehind באורך משתנה מ־1 עד 255 תווים. תבניות כמו (?<=cat|kitten) תקינות. תבניות שבהן ה־lookbehind עצמו מכיל קבוצות לכידה מפיקות אזהרה ניסיונית (תוכן הלכידות אינו מוגדר במלואו כאשר ל־lookbehind יש אורך משתנה).

מכיוון שה־lookbehind חייב להסתיים לפני המיקום הנוכחי, אורכו המרבי מוגבל ל־255 תווים תחת הסמנטיקה ברירת־המחדל. תחת /i, מספר תווים מתקפלים לרצפים רב־תוויים (ß ל־ss), מה שסופר את האורך המורחב כלפי החסם של 255 — כך ש־lookbehind בן 127 תווים המכיל ß הוא תקין, אך 128 תווים כאלה אינו.

פתרון עוקף מעשי ל־lookbehind ארוך יותר: יש להשתמש ב־\K (סעיף הבא), שאין לו חסם אורך.

\K — keep-left#

\K הוא חלופת ה־lookbehind. כל מה שהותאם לפני \K אינו נכלל ב־$&, אך חייב היה להיות מותאם. מבחינה מעשית: ״התאם את התחילית הזו, אך אל תכלול אותה בפלט.״

"feed the cat" =~ /the \Kcat/;   # matches; $& is 'cat'

שקול ל־(?<=the )cat, אך:

  • ללא חסם אורך. \K פועל אחרי כל תחילית.

  • לעיתים קרובות מהיר משמעותית מ־(?<=…), מכיוון שהמנוע אינו צריך להסתכל אחורה — הוא שוכח את תחילת ההתאמה במקום זאת.

  • שימושי במיוחד בהחלפה: s/foo\Kbar/QUUX/ נקי יותר מ־s/(foo)bar/$1QUUX/.

\K שוכח רק את $& ואת @-/@+[0]; קבוצות לכידה לפני \K עדיין נקבעות כלכודות. המבנה עשוי להופיע בתוך lookaround אחרים, אם כי ההתנהגות שם מתוארת כ״כיום לא מוגדרת היטב״ — השימוש הזהיר הוא ברמה העליונה של התאמה או החלפה.

כינויים בצורה ארוכה עבור lookaround#

לכל מבנה lookaround יש כתיב בסגנון פועל. כינויי הצורה הארוכה נקראים בבירור רב יותר בתבניות שכבר משתמשות ב־(*VERB:…) לבקרת חזרה לאחור:

צורה סטנדרטית

צורת פועל (קצרה)

צורת פועל (ארוכה)

(?=…)

(*pla:…)

(*positive_lookahead:…)

(?!…)

(*nla:…)

(*negative_lookahead:…)

(?<=…)

(*plb:…)

(*positive_lookbehind:…)

(?<!…)

(*nlb:…)

(*negative_lookbehind:…)

צורות הפועל שקולות בדיוק לצורות הסטנדרטיות. הן מתקבלות; הן אינן אידיומטיות ב־Perl שנכתבת ביד. ניתן לראותן בתבניות שנוצרו אוטומטית ובהסבות מ־PCRE2.

שילוב lookaround עם split#

עוגנים ו־lookaround מאפשרים ל־split להפריד מחרוזת במיקומים בלתי־נראים במקום בתווים נראים:

my $str = "one two - --6-8";
my @toks = split / \s+            # whitespace
                 | (?<=\S) (?=-)  # non-space followed by '-'
                 | (?<=-)  (?=\S) # '-' followed by non-space
                 /x, $str;
# @toks = ("one", "two", "-", "-", "-", "6", "-", "8")

החלופה השנייה והשלישית מתאימות בין תווים — ללא צריכה. הן מפרידי split חוקיים מפני ש־split שמח לפצל על התאמה ברוחב אפס.

שני היגדים ברוחב אפס המוצבים זה לצד זה מבוצעת עליהם פעולת AND#

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

$x =~ /^(\D*)(?=\d)(?!123)/;
# Matches the leading non-digit run, but only if a digit follows
# AND that digit run does not begin with '123'.

גם (?=\d) וגם (?!123) חלים באותו מיקום; המנוע מתייחס לצירופם כדרישה. ללא (?=\d), ה־lookahead השלילי (?!123) יכול להתקיים על־ידי כל המשך שאינו 123 — כולל סוף־מחרוזת או תו שאינו ספרה, שאף אחד מהם אינו מה שהמחבר התכוון אליו.

זה מכליל את המסגור של פרידל: סמיכות ב־regex תמיד פירושה AND, פרט לכשהיא נכתבת ב־|. /ab/ פירושו ״a ו־(אחר כך) b״, בדיוק כמו ש־/^$/ פירושו ״התחלה וסוף״ — רק שה־AND חוצה מיקומים עבור ab ובמיקום אחד עבור ה־lookaround.

השוואה בין מנועים: עוגנים ו־lookaround#

פרק cross-engine מכיל את הטבלה המלאה; השורות הרלוונטיות חולצו.

עוגנים וטיפול בשורה חדשה#

היבט

Perl 5.42

PCRE2

Emacs

POSIX BRE / ERE

RE2 / Go

^ תחילת מחרוזת

כן

כן

תחילת buffer

כן

כן

^ אחרי \n במצב רב־שורתי

/m

(?m)

תמיד (מונחה־שורות)

לא מצוין

(?m)

$ סוף מחרוזת

כן

כן

סוף buffer

כן

כן

$ לפני \n סופי

כן

כן

לא

משתנה

כן

\A / \z

כן

כן

\` / \'

לא

כן

\Z

כן

כן

לא

לא

לא (להשתמש ב־\z)

\G

כן

כן

לא

לא

לא

Lookaround ומבנים אטומיים#

תכונה

Perl 5.42

PCRE2

Emacs

POSIX

RE2 / Go

Lookahead (?=…) / (?!…)

כן

כן

לא

לא

לא

Lookbehind ברוחב קבוע

כן

כן

לא

לא

לא

Lookbehind באורך משתנה

כן (ניסיוני)

כן

לא

לא

לא

\K

כן

כן

לא

לא

לא

קבוצה אטומית (?>…)

כן

כן

לא

לא

לא

למעשה, רק PCRE2 (ומנועים אחרים שמקורם ב־Perl מחוץ לטבלה — Java, Python, ‎.NET) תומך ב־lookaround. כלי POSIX ו־RE2 / Go פשוט אינם תומכים בו. תבניות הסומכות על lookaround אינן ניידות מעבר לפער הזה.

סיכום#

עוגן

מתאים

^

תחילת מחרוזת, או תחילת שורה תחת /m

$

סוף מחרוזת (לפני \n סופי), או שורה תחת /m

\A

תחילת מחרוזת מוחלטת

\z

סוף מחרוזת מוחלט

\Z

סוף מחרוזת או לפני \n סופי, מתעלם מ־/m

\b

גבול מילה

\B

שאינו גבול מילה

\b{wb}

גבול מילה ב־Unicode

\b{sb}

גבול משפט

\b{lb}

שבירת שורה

\b{gcb}

גבול אשכול גרפמות

\G

סוף ההתאמה הקודמת של /g (או \A אם אין)

(?=…)

lookahead חיובי

(?!…)

lookahead שלילי

(?<=…)

lookbehind חיובי

(?<!…)

lookbehind שלילי

\K

keep-left (לשכוח את התחילית מ־$&)

ראו גם#

  • פרק modifiers/m, /s, /g, /c.

  • פרק unicode\b{wb}, \b{sb}, \b{lb}, \b{gcb} ומשמעותם.

  • פרק performance — קבוצות אטומיות (?>…), פעלי בקרת חזרה לאחור מיוחדים.

  • פרק cross-engine — טבלת תאימות מלאה של עוגנים ו־lookaround.

  • split — פיצול במיקומים ברוחב אפס.

  • pos — קריאה או קביעה של המיקום שבו \G מתעגן.