השוואה בין־מנועית#

ביטויים רגולריים של Perl אינם הסוג היחיד. קורא שמכיר Perl היטב יתקל, במוקדם או במאוחר, בביטוי רגולרי שנכתב עבור כלי אחר — sed, awk, חיפוש מאגר Emacs Lisp, שירות Go, ספריית PCRE2 מוטמעת — ויצופה ממנו לקרוא אותו. ההבדלים התחביריים בדרך כלל קטנים, ההבדלים הסמנטיים לפעמים משמעותיים, וההבדלים הארכיטקטוניים (אילו קלטים הופכים את המנוע לאיטי קטסטרופלית) יכולים להיות מכריעים.

פרק זה הוא העמוד היחיד שמשיב על השאלה ״כיצד נראה X במקום אחר?״ עבור חמש משפחות המנועים שקורא מאומן ב־Perl ככל הנראה יפגוש. אין זה מדריך להעברת קוד; זוהי הפניית ההשוואה.

המנועים המכוסים#

חמש משפחות, נבחרו מכיוון שכל אחת נפוצה בנישה שלה וגם שונה משמעותית מ־Perl. מנועים שנפוצים אך מעוצבים ברובם בסגנון Perl (java.util.regex של Java, re של Python, ECMAScript, .NET) מחוץ לתחום; ההבדלים מינוריים וקורא Perl יכול לקרוא אותם במבט.

מנוע

היכן פוגשים אותו

משפחה

PCRE2

nginx, preg_* של PHP, pcre2grep בשורת הפקודה

Perl־מורחב

Emacs

חיפוש ביטוי רגולרי ב־Emacs Lisp, M-x replace-regexp

משפחה משלו

POSIX BRE

sed (ברירת מחדל), grep (ברירת מחדל), ed

POSIX Basic

POSIX ERE

awk, egrep / grep -E, sed -E / sed -r

POSIX Extended

RE2 / regexp של Go

ספריית התקן של Go, Cloud Bigtable, חיפוש קוד

היברידי בזמן ליניארי

גרסאות שעוגנו מולן בפרק זה: PCRE2 10.44, Emacs 30.x, POSIX 1003.1-2024, Go 1.22 (RE2 עוקב אחר מנוע הביטויים הרגולריים של Go). הערכים בטבלאות שלמטה אומתו מול גרסאות אלה; הם יסטו עם הזמן, ועדכונים ספציפיים למנוע צריכים לאמת מחדש לפני הסתמכות על שורה.

מה אינו בתחום#

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

  • java.util.regex של Java — נגזר מ־Perl; ההבדלים הם איותי דגלים ומעט מוסכמות בריחה.

  • re של Python — נגזר מ־Perl; ההפרש הגדול ביותר הוא שתמיכה בהצצה לאחור באורך משתנה הגיעה מאוחר יותר מאשר ב־Perl.

  • ECMAScript (JavaScript) — נגזר מ־Perl; הצצה לאחור רק מאז ES2018, ללא כמתים רכושניים, דגל דביק y במקום \G. V8 / SpiderMonkey מודרניים מתכנסים לקבוצת התכונות של Perl.

  • ביטוי רגולרי של .NET — מעוצב כ־Perl, ובנוסף קבוצות מאוזנות ((?<name-pop>…)), שהן ייחודיות. מספיק נישה כדי לדלג.

  • regex של Rust — שושלת RE2; הסיפור בעיקרו של Go עם משטח תכונות שונה במקצת (יש בו הצצה לאחור ברוחב קבוע, ב־RE2 אין).

  • Vim — סמנטיקת מצב קסם שונה; פנים־Vim.

  • Boost.Regex — נישת C++; עוקב מקרוב אחר PCRE2.

חמש המשפחות, פסקה לכל אחת#

PCRE2#

Perl-Compatible Regular Expressions, גרסה 2 (PCRE2 החליף את ספריית PCRE המקורית ב־2015). המימוש הסטנדרטי ל״ביטוי רגולרי של Perl מחוץ ל־Perl״. נקרא כמעט זהה לתבניות Perl 5.42 ותומך בכמעט אותה קבוצת תכונות: הפניות לאחור, הצצות סביב, קבוצות אטומיות, כמתים רכושניים, תת־תבניות רקורסיביות, לכידות בשם, תבניות מותנות, callouts. ל־PCRE2 יש גם מהדר JIT שלעיתים מהיר משל perl.

היכן ש־PCRE2 שונה מ־Perl 5.42:

  • (?{...}) ו־(??{...}) — Perl מריצה קוד Perl שרירותי; ל־PCRE2 יש callouts ((?C) / (?Cn)) שקוראים בחזרה ליישום המארח. אין זה הבדל אבטחה; פשוט ממשק שונה.

  • חופן איותי דגלים שונים (PCRE2_CASELESS מול /i, וכדומה) ברמת ה־API, לא בתחביר התבנית.

  • אטומיות של תת־תבנית רקורסיבית: (?R) הוא אטומי ב־PCRE2 כברירת מחדל; Perl מאפשרת חזרה לאחור לתוך קבוצה שעברה רקורסיה.

  • קבוצת (*VERB) זהה ברובה, כאשר PCRE2 מוסיף (*UTF), (*UCP), ואפשרויות אתחול נוספות.

עבור רוב התבניות, ״נראה אותו דבר״ נכון.

Emacs#

מנוע הביטויים הרגולריים של GNU Emacs הוא משפחה משלו. על פני השטח הוא נראה בסגנון BRE — \(...\) לקיבוץ, \{m,n\} לספירות — אך בפנים יש בו תכונות בסגנון Perl־קל (כמתים לא־חמדנים, גבולות מילה) ותכונות ספציפיות ל־Emacs (מחלקות תחביר \sw, מחלקות קטגוריה \cC) שאין באף מנוע אחר.

מבנים בולטים ספציפיים ל־Emacs:

  • עוגני מאגר: \` (גרש הפוך) הוא תחילת המאגר, \' הוא סוף המאגר. קרוב יותר ל־\A / \z של Perl מאשר ל־^ / $.

  • מחלקות תחביר: \sw מתאים לתו ״תחביר מילה״ כפי שמוגדר על ידי טבלת התחביר של המאגר הנוכחי; \s- מתאים לתחביר־רווח לבן. זה תלוי במצב — אותו ביטוי רגולרי מתאים לתווים שונים ב־C-mode מול Lisp-mode.

  • מחלקות קטגוריה: \cC מתאים לתו בקטגוריה C. בשימוש כבד לסיווג טקסט CJK.

  • אין קיצור \d — Emacs דורש [0-9] או [[:digit:]]. (\w, \s, \b קיימים, אך \d לא.)

  • אין הצצה קדימה, הצצה לאחור, או \K.

אם תקראו Emacs Lisp ב־2026 תראו תבניות \\(\\) ללא הרף — זוהי הבריחה הכפולה הנדרשת מכיוון שמילוליי מחרוזות של Emacs Lisp צורכים רמה אחת של לוכסן הפוך לפני שמפענח הביטוי הרגולרי רואה את השאר.

POSIX BRE — sed, grep, ed#

POSIX Basic Regular Expressions. המנוע שמתקבל מ־sed, grep, ו־ed ללא דגלים. הדבר המפתיע ביותר עבור קורא Perl: קבוצת התווים המטא קטנה יותר. +, ?, {m,n}, (, ), ו־| חשופים הם תווים מילוליים. צורות התווים המטא הן הוריאנטים עם לוכסן הפוך: \+, \?, \{m,n\}, \(, \), \| (האחרון הוא הרחבה של GNU; ל־POSIX BRE קפדני אין ברירה כלל).

# POSIX BRE, finds the literal text "a+b":
echo 'a+b' | grep 'a+b'

# To match "one or more a", under BRE:
echo 'aaab' | grep 'a\+b'

הפניות לאחור (\1\9) עובדות ב־BRE (זה ישן מכלל ה־ללא־הפניות־לאחור של POSIX ERE). מחלקות POSIX [[:digit:]] וכדומה עובדות. \< ו־\> (הרחבת GNU) מספקים גבולות מילה. רצפי בריחה מקוצרים של תווים כמו \d, \s, \w אינם נתמכים.

grep ו־sed מספקים את אותו מנוע אך ברירת המחדל של הדגלים שונה למשפחות שונות: grep -P של GNU מפעיל את PCRE2, grep -E הוא ERE, ברירת המחדל grep היא BRE. sed -E הוא ERE, ברירת המחדל sed היא BRE.

POSIX ERE — awk, egrep, sed -E#

POSIX Extended Regular Expressions, המנוע ש־awk מוגדר אליו כברירת מחדל וזה שנבחר על ידי egrep (עכשיו grep -E) ו־sed -E / sed -r. +, ?, {m,n}, (...), ו־| חשופים הם תווים מטא כאן; זוהי המשפחה הקרובה ביותר ל״מה שמתכנת מאומן ב־Perl מצפה לו״ בין הקבוצה תואמת־POSIX.

מה ש־POSIX ERE קפדני אינו כולל, על פי המפרט:

  • הפניות לאחור בתבנית (\1, \2, …). הן חוקיות בהחלפות החלפה של sed אך לא בתבניות התאמה. הרחבות GNU מוסיפות אותן; סקריפטים ניידים לא צריכים להסתמך על זה.

  • הצצות סביב.

  • כמתים רכושניים, קבוצות אטומיות.

  • \d, \s, \w, \b (יש להשתמש ב־[0-9], [[:space:]], וכו«).

ההפתעה הנפוצה ביותר: \( ב־ERE הוא סוגריים פותחים מילוליים. הוספת לוכסן הפוך לפיסוק היא לעיתים תו מטא ולעיתים ללא־פעולה, כשהכלל הפוך מ־BRE. יש לזכור: לוכסן הפוך ב־BRE מאפשר תווים מטא; לוכסן הפוך ב־ERE משבית אותם.

RE2 / regexp של Go#

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

הפשרה: RE2 אינה יכולה לתמוך בתכונות שמוציאות שפות רגולריות ממשפחת השפות הרגולריות. באופן ספציפי:

  • אין הפניות לאחור בתבניות. \1, \g{1}, \k<name> — אין. תבנית כמו (.)\1 פשוט אינה ניתנת להידור.

  • אין הצצה סביב כללית. RE2 תומך בעוגנים וב־\b (טענות ברוחב־אפס), אך לא ב־(?=...), (?<=...), (?!...), או (?<!...).

  • אין קבוצות אטומיות, אין כמתים רכושניים, אין תת־תבניות רקורסיביות, אין קוד מוטמע.

מה ש־RE2 כן תומך בו, ומה שהופך אותה לנעימה למקרה הנפוץ:

  • לכידות בשם עם תחביר בסגנון Python (?P<name>...).

  • ASCII כברירת מחדל עבור \d, \w, \s; מצב יוניקוד מאופשר עם (?u).

  • תחבירי המתאם (?flags) ו־(?flags:...).

  • דגל החלפת ברירת המחדל של החמדנות (?U) — תחתיו, * הוא לא־חמדן ו־*? הוא חמדן. ההפך מ־Perl.

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

חבילת regexp הסטנדרטית של Go עוטפת את RE2 ומוסיפה כמה ייחודיויות של Go, שאף אחד מהן אינו משנה את היסודות.

טבלאות השוואת תכונות#

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

כמתים, קיבוץ, ברירה#

תכונה

Perl 5.42

PCRE2

Emacs

POSIX BRE

POSIX ERE

RE2 / Go

* אפס־או־יותר

מטא

מטא

מטא

מטא

מטא

מטא

+ אחד־או־יותר

מטא

מטא

מטא

מילולי (יש להשתמש ב־\+)

מטא

מטא

? אפס־או־אחד

מטא

מטא

מטא

מילולי (יש להשתמש ב־\?)

מטא

מטא

{m,n} טווח

מטא

מטא

\{m,n\}

\{m,n\}

מטא

מטא

| ברירה

מטא

מטא

מטא

\| (GNU)

מטא

מטא

(...) קיבוץ

מטא

מטא

\(...\)

\(...\)

מטא

מטא

(?:...) לא־לוכד

כן

כן

לא

לא

לא

כן

רכושני *+, ++, ?+

כן

כן

לא

לא

לא

לא

לא־חמדן *?, +?

כן

כן

כן

לא

לא

כן

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

כן

כן

לא

לא

לא

לא

השורה היחידה בעלת הערך הגבוה ביותר עבור קורא Perl שפוגש ב־BRE: + ו־? הם מילוליים. ביטוי רגולרי כמו colou?r נקרא כ״מתאים colou?r בדיוק״ תחת BRE. הכלל עקבי — קבוצת התווים המטא של BRE קטנה; ERE הרחיב אותה; Perl הרחיבה אותה עוד יותר.

מחלקות תווים מקוצרות#

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

קיצור

Perl 5.42 (ברירת מחדל)

Perl 5.42 + /u

PCRE2 (ברירת מחדל)

Emacs

POSIX BRE

POSIX ERE

RE2 / Go (ברירת מחדל)

\d

ASCII (או יוניקוד תחת /u)

יוניקוד

ASCII

לא

לא

לא

ASCII; יוניקוד תחת (?u)

\w

ASCII או יוניקוד

יוניקוד

ASCII

כן (מונע על ידי טבלת תחביר)

לא

לא

ASCII; יוניקוד תחת (?u)

\s

ASCII או יוניקוד

יוניקוד

ASCII

כן

לא

לא

ASCII; יוניקוד תחת (?u)

\b גבול מילה

כן

כן

כן

כן (\</\> מסורתי)

לא

לא

כן

\h רווח לבן אופקי

כן

כן

כן

לא

לא

לא

לא

\v רווח לבן אנכי

כן

כן

כן

לא

לא

לא

לא

שתי הפתעות כאן עבור קורא Perl:

  1. POSIX BRE ו־ERE שניהם חסרים \d, \w, \s. הצורות הניידות הן [0-9], [[:alnum:]_], [[:space:]]. כלים המקבלים \d (grep של GNU, awk בחלק מהמימושים) עושים זאת כהרחבה לא־ניידת.

  2. ל־Emacs יש \w ו־\s אך אין \d. אחרי התו \s בא תו מחלקת תחביר — \s- עבור רווח לבן, \sw עבור מילה — שייחודי ל־Emacs.

מחלקות סוגריים של POSIX כמו [[:digit:]] ו־[[:alpha:]] עובדות בכל מנוע בטבלה; הן האיות הנייד.

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

תכונה

Perl 5.42

PCRE2

Emacs

POSIX BRE / ERE

RE2 / Go

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

כן

כן

תחילת מאגר

כן

כן

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

/m

(?m)

תמיד (מוכוון־שורות)

לא מוגדר

(?m)

$ סוף מחרוזת

כן

כן

סוף מאגר

כן

כן

$ לפני \n הסופי

כן

כן

לא

משתנה

כן

. מתאים \n

/s

(?s)

לא

לא

(?s)

\A / \z

כן

כן

\` / \'

לא

כן (\A, \z)

\Z (לפני \n סופי אופציונלי)

כן

כן

לא

לא

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

\G (מיקום ההתאמה האחרונה)

כן

כן

לא

לא

לא

Emacs משתמשת ב־\` וב־\' עבור עוגני המאגר, היכן שרוב המנועים המודרניים משתמשים ב־\A וב־\z. האיות של Emacs ישן יותר; הוא קודם ל־POSIX. בלבול בין ^ ו־\A חשוב בקוד Emacs מוכוון־שורות יותר מאשר ב־Perl, מכיוון שהתאמות Emacs לעיתים מופעלות בהקשרים שסורקים מאגר שלם.

\G ספציפי ל־Perl (ול־PCRE2). RE2 / Go משתמשים ב־API שונה על עצם ההתאמה (מתודת Loc שמחזירה את המיקום הבא) במקום להטמיע את המיקום בתבנית.

מבני הצצה־סביב ואטומיים#

תכונה

Perl 5.42

PCRE2

Emacs

POSIX

RE2 / Go

הצצה קדימה (?=…) / (?!…)

כן

כן

לא

לא

לא

הצצה לאחור ברוחב קבוע (?<=…)

כן

כן

לא

לא

לא

הצצה לאחור באורך משתנה

כן (ניסיוני)

כן

לא

לא

לא

\K שמירה שמאלה

כן

כן

לא

לא

לא

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

כן

כן

לא

לא

לא

כמתים רכושניים

כן

כן

לא

לא

לא

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

לכידות והפניות לאחור#

תכונה

Perl 5.42

PCRE2

Emacs

POSIX BRE

POSIX ERE

RE2 / Go

לכידות ממוספרות

כן

כן

כן

כן

קפדני לא, GNU כן

כן

הפניות לאחור בתבנית (\1–)

כן (1–99)

כן

כן

כן (\1\9)

קפדני לא, GNU כן

לא

לכידות בשם (?<name>…)

כן

כן

לא

לא

לא

כן ((?P<name>…))

איפוס ענפים (?|…)

כן

כן

לא

לא

לא

לא

תת־תבניות רקורסיביות (?R)

כן

כן

לא

לא

לא

לא

תבניות מותנות (?(c)y|n)

כן

כן

לא

לא

לא

לא

(?{…}) קוד מוטמע

כן

callouts

לא

לא

לא

לא

מערך @{^CAPTURE}

כן

לא

לא

לא

לא

לא (API שונה)

המסקנה החשובה ביותר: RE2 / regexp של Go אינו תומך בהפניות לאחור. תבניות כמו /(.)\1/ אינן ניתנות להידור. אין זה באג של המנוע; זהו המחיר ש־RE2 משלם עבור זמן ליניארי מובטח. תבניות המכילות הפניות לאחור אינן שפות רגולריות ו־DFA אמיתי אינו יכול להתאים אותן בזמן ליניארי.

אם אתם מעבירים ביטויים רגולריים של Perl לשירות Go, שאלת הביקורת היא ״האם התבנית הזו משתמשת בהפניות לאחור?״ — אם כן, ההמרה אינה מכנית.

סיכום ארכיטקטורת המנוע#

תכונה

PCRE2 / Perl 5.42

Emacs

POSIX BRE / ERE (מימוש טיפוסי)

RE2 / Go

אלגוריתם

NFA מסורתי + JIT (PCRE2)

NFA מסורתי

DFA (GNU) / NFA (אחרים)

היברידי NFA → DFA עצלן

זמן במקרה הגרוע

מעריכי

מעריכי

ליניארי (DFA)

ליניארי (מובטח)

מודל חזרה לאחור

כן

כן

DFA: לא

לא

סיכון לחזרה לאחור קטסטרופלית

כן

כן

לא (DFA)

לא

הפניות לאחור נתמכות

כן

כן

משתנה

לא

POSIX BRE/ERE תלוי במימוש: grep ו־awk של GNU משתמשים ב־DFA היברידי עם נפילה ל־NFA שהוא מהיר ואינו נתון לחזרה לאחור קטסטרופלית; מימושים מסורתיים ישנים יותר (awk היסטורי, grep מסורתי בכמה BSD) משתמשים ב־NFA עם חזרה לאחור. ההנחה הניידת היא ״זמן ליניארי, אך לאמת אם הקלט עלול להיות עוין״.

בחירת מנוע כאשר יש לכם את הבחירה#

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

  • תוכנית Perl: המובנה של Perl. אין סיבה לפנות החוצה.

  • תוכנית C המטמיעה ביטוי רגולרי: PCRE2. ה־regex.h של ספריית התקן הוא POSIX BRE/ERE; PCRE2 הוא מה שרוצים עבור התנהגות בסגנון Perl.

  • שירות Go המטפל בקלט לא־אמין: regexp (RE2). הבטחת הזמן הליניארי היא התכונה המרכזית; ההפניות לאחור והצצות־סביב החסרות לרוב מקובלות.

  • תוכנית Rust: ה־crate של regex. שושלת RE2; אותה הבטחת זמן ליניארי.

  • סקריפטים של מעטפת: יש להעדיף grep -E (ERE) ו־sed -E (ERE) על פני ברירות המחדל של BRE לקריאוּת בסגנון Perl. יש להשתמש ב־grep -P (PCRE2) רק כאשר ERE באמת אינו מספיק.

  • פונקציית Emacs Lisp: של Emacs. אחרת יש לעזוב את Emacs.

ראו גם#

  • פרק character classes — שורת הקיצורים־לפי־מנוע מופיעה בסרגל הצד L3 שלו.

  • פרק anchors and assertions — שורות ההצצה־סביב והעוגנים מופיעות בסרגל הצד L3 שלו.

  • פרק groups and captures — שורת הלכידות־וההפניות־לאחור מופיעה בסרגל הצד L3 שלו.

  • פרק performance — מודל NFA מסורתי וחזרה לאחור קטסטרופלית, הסיבה הארכיטקטונית לקיומה של RE2.