Menu

Regexp ב-Golang: התאמה, חיפוש, submatches והחלפה

איך משתמשים בביטויים רגולריים ב-Go עם החבילה regexp: MustCompile, MatchString, FindString ו-FindAllString, קבוצות לכידה וקבוצות עם שם, ReplaceAllString והמגבלות של תחביר RE2, כמו היעדר lookbehind.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

הידור פעם אחת, ואז התאמה

כתבו תבניות כ-raw string literals בתוך backticks. במחרוזת רגילה במירכאות כפולות צריך להכפיל כל לוכסן הפוך ("\\d+"), וזה הופך מהר לבלתי קריא.

regexp.MustCompile גורמת ל-panic אם התבנית לא תקינה, וזה בדיוק מה שרוצים לתבנית שכתובה בקוד המקור: שגיאת הקלדה נכשלת מיד בהפעלה, ולא בבקשה הראשונה. לתבניות שמגיעות ממשתמשים או מהגדרות, השתמשו ב-regexp.Compile וטפלו בשגיאה:

re, err := regexp.Compile(userPattern)
if err != nil {
	return fmt.Errorf("bad pattern: %w", err)
}

הדרו פעם אחת. הידור יקר בהרבה מהתאמה. regexp.MustCompile בתוך פונקציה שרצה בכל בקשה או בכל שורה חוזר על העבודה הזאת בכל פעם. שמרו את ה-*regexp.Regexp המהודר ב-var ברמת החבילה או בשדה של struct. regexp מהודר בטוח לשימוש מהרבה goroutines בו זמנית.

regexp.MatchString(pattern, s) מהדרת ומתאימה בקריאה אחת. היא מתאימה לבדיקה חד פעמית ובזבזנית בתוך לולאה.

חיפוש התאמות

שמות המתודות בנויים לפי תבנית: Find + All? + String? + Submatch? + Index?.

חלקמשמעות
Allכל ההתאמות שלא חופפות; מקבלת מגבלה n (-1 לכולן)
Stringעובדת על string; בלעדיו, המתודה מקבלת ומחזירה []byte
Submatchמחזירה גם את קבוצות הלכידה
Indexמחזירה היסטים בבתים במקום הטקסט

FindString מחזירה "" כשאין התאמה, ואי אפשר להבדיל את זה מהתאמה למחרוזת ריקה. כשהתבנית יכולה להתאים לטקסט ריק, השתמשו ב-FindStringIndex (מחזירה nil כשאין התאמה) או קודם ב-MatchString. גרסאות ה-All מחזירות nil כששום דבר לא מתאים.

קבוצות לכידה

סוגריים לוכדים. FindStringSubmatch מחזירה את ההתאמה המלאה באינדקס 0, ואחריה רשומה אחת לכל קבוצה:

תמיד בדקו nil לפני גישה לפי אינדקס לתוצאה; כשאין התאמה, m[1] גורם ל-panic.

(?:...) מקבץ בלי ללכוד, לחלופות או לחזרות: (?:ab)+. קבוצות עם שם נכתבות (?P<name>...), ומאז Go 1.22 גם (?<name>...).

החלפה

מלכודת קלאסית בהפניות לקבוצות: "$1x" נקרא כקבוצה בשם 1x, שלא קיימת, ומתפרש כמחרוזת ריקה. כתבו "${1}x".

למחרוזת קבועה, strings.ReplaceAll, strings.Contains ו-strings.Split פשוטות ומהירות יותר מ-regexp. פנו ל-regexp כשלטקסט שאתם מחפשים יש צורה ולא ערך קבוע.

תקציר תחביר

Go משתמשת בתחביר RE2, שמכסה את היכולות המוכרות בסגנון Perl:

תחבירמתאים ל
.כל תו חוץ משורה חדשה (עם (?s), גם שורה חדשה)
\d \w \sספרה, תו מילה [0-9A-Za-z_], רווח לבן (ASCII בלבד)
\D \W \Sהשלילות שלהם
[abc] [^abc] [a-z]מחלקות תווים
\pL \p{Greek}מחלקות Unicode: כל אות, כל תו יווני
* + ? {n,m}חזרה, חמדנית
*? +? ??חזרה, עצלה
^ $תחילת הטקסט וסופו (של השורה, עם (?m))
\bגבול מילה (ASCII)
a|bחלופה
(?i)בלי הבחנה בין אותיות גדולות לקטנות מאותה נקודה ועד סוף הקבוצה העוטפת; בדרך כלל נכתב בתחילת התבנית

\d ו-\w מתאימים רק ל-ASCII. ל"כל אות בכל שפה", השתמשו ב-\pL, ולכל ספרת Unicode, ב-\p{Nd}. regexp.QuoteMeta(s) מבצעת escape לכל תו מיוחד ב-s, ואת זה צריך כשבונים תבנית מקלט של משתמש.

מה ש-RE2 לא יודע לעשות

regexp של Go מבטיח שההתאמה לוקחת זמן ליניארי ביחס לאורך הקלט. יכולות שהיו שוברות את ההבטחה הזאת לא נתמכות:

  • אין lookahead או lookbehind: (?=...), (?!...), (?<=...), (?<!...) הן שגיאות הידור. מכיוון ש-Go 1.22 התחילה לקבל (?<name>...) לקבוצות עם שם, lookbehind נכשל עכשיו עם ההודעה המבלבלת invalid named capture, כפי שהתוכנית הבאה מראה.
  • אין backreferences: אי אפשר לכתוב (\w)\1 כדי למצוא אות כפולה.
  • אין כמתים רכושניים או קבוצות אטומיות.

הפתרון כמעט תמיד זהה: ללכוד קצת יותר ממה שצריך, ואז לסנן או לחתוך ב-Go. התמורה היא ש-regexp של Go לא יכול להידחף ל-backtracking קטסטרופלי. תבנית כמו (a+)+$, שיכולה להקפיא מנוע PCRE על קלט קצר, רצה כאן בזמן ליניארי, וזה חשוב כשהתבנית או הקלט מגיעים מבחוץ.

טעויות נפוצות

  • הידור בתוך לולאה או handler. הדרו פעם אחת לתוך משתנה ברמת החבילה.
  • תבניות במירכאות כפולות. "\d" הוא אפילו לא מחרוזת Go תקינה; השתמשו ב-backticks.
  • שכחת עוגנים באימות. \d{5} מתאים בתוך "abc123456xyz". השתמשו ב-^\d{5}$.
  • גישה לפי אינדקס לתוצאת submatch בלי בדיקת nil.
  • ציפייה ש-\w או \b יבינו טקסט שאינו ASCII. השתמשו במחלקות Unicode כמו \pL.
  • שימוש ב-regexp למחרוזות קבועות. החבילה strings ברורה ומהירה יותר.

שאלות נפוצות

איך בודקים אם מחרוזת מתאימה ל-regex ב-Go?

הדרו את התבנית פעם אחת עם regexp.MustCompile, וכתבו אותה כ-raw string בתוך backticks כדי שלא יהיה צורך לבצע escape ללוכסנים ההפוכים, ואז קראו ל-re.MatchString(s). בלי עוגנים (^ ו-$), MatchString מחזירה true אם התבנית מתאימה בכל מקום במחרוזת.

האם regexp של Go תומך ב-lookahead וב-lookbehind?

לא. regexp של Go משתמש בתחביר RE2, שאין בו lookahead, lookbehind או backreferences. בתמורה, ההתאמה רצה בזמן ליניארי ביחס לגודל הקלט, כך שתבנית או קלט זדוניים לא יכולים לתקוע את התוכנית. השתמשו במקום זה בקבוצות לכידה ובקצת קוד Go, או פצלו את הבדיקה לשני ביטויים רגולריים.

מה ההבדל בין regexp.Compile לבין regexp.MustCompile?

Compile מחזירה את התבנית המהודרת ושגיאה. MustCompile גורמת ל-panic כשהתבנית לא תקינה. השתמשו ב-MustCompile לתבניות שכתובות בקוד המקור, בדרך כלל במשתנה ברמת החבילה, כך ששגיאת הקלדה תיכשל כבר בהפעלה. השתמשו ב-Compile לתבניות שמגיעות ממשתמשים או מקובצי הגדרות.

איך מקבלים את קבוצות הלכידה של regex ב-Go?

השתמשו ב-FindStringSubmatch, שמחזירה slice שבו אינדקס 0 הוא ההתאמה המלאה ואינדקסים 1, 2, ... הם הקבוצות, או nil אם אין התאמה. לכל ההתאמות השתמשו ב-FindAllStringSubmatch(s, -1). את הקבוצות עם שם (?P<year>\d{4}) אפשר למצוא עם re.SubexpIndex("year").

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל