فصل ۲۵: Regular Expressions
زبان regular expression الگوهای کاراکتری را شناسایی میکند. Typeهای .NET برای regex بر پایهٔ regular expressionهای Perl 5 هستند و هم search و هم search/replace را پشتیبانی میکنند.
Regex برای کارهایی مانند validation ورودی متن ــ از جمله password و phone number ــ، parsing دادهٔ متنی به ساختارهای منظمتر، و جایگزینی patternهای متن در document استفاده میشود. این فصل هم بخش آموزشی برای مبانی regex در .NET دارد و هم بخش مرجع syntax زبان. همهٔ typeهای regular expression در System.Text.RegularExpressions تعریف شدهاند.
مبانی Regular Expression
یکی از operatorهای رایج، quantifier است. علامت ? item قبلی را صفر یا یک بار match میکند، یعنی آن item optional است. Item میتواند یک character یا ساختار پیچیدهتری در square bracket باشد. الگوی colou?r با color و colour match میشود ولی با colouur نه:
Console.WriteLine (Regex.Match ("color", @"colou?r").Success); // True
Console.WriteLine (Regex.Match ("colour", @"colou?r").Success); // True
Console.WriteLine (Regex.Match ("colouur", @"colou?r").Success); // False
Regex.Match داخل string بزرگتر search میکند. object برگشتی propertyهای Index، Length، Value و Success دارد:
Match m = Regex.Match ("any colour you like", @"colou?r");
Console.WriteLine (m.Success); // True
Console.WriteLine (m.Index); // 4
Console.WriteLine (m.Length); // 6
Console.WriteLine (m.Value); // colour
Console.WriteLine (m.ToString()); // colour
میتوان Regex.Match را نسخهٔ قدرتمندتر string.IndexOf دانست؛ با این تفاوت که pattern را جستوجو میکند نه literal string. IsMatch میانبری برای Match و سپس بررسی Success است.
Regex engine بهطور پیشفرض از چپ به راست کار میکند و بنابراین leftmost match برمیگردد. با NextMatch matchهای بعدی و با Matches همهٔ matchها قابل دریافتاند:
Match m1 = Regex.Match ("One color? There are two colours in my head!",
@"colou?rs?");
Match m2 = m1.NextMatch();
Console.WriteLine (m1); // color
Console.WriteLine (m2); // colours
foreach (Match x in Regex.Matches
("One color? There are two colours in my head!", @"colou?rs?"))
Console.WriteLine (x);
Alternator با | گزینههای جایگزین را بیان میکند. برای مثال:
Console.WriteLine (Regex.IsMatch ("Jenny", "Jen(ny|nifer)?")); // True
Parenthesis اطراف alternator، alternativeها را از بقیهٔ expression جدا میکند.
Compiled Regular Expressions
اگر pattern یکسان را بارها با static methodهای Regex استفاده میکنید، میتوان یک Regex instance با RegexOptions.Compiled ساخت:
Regex r = new Regex (@"sausages?", RegexOptions.Compiled);
Console.WriteLine (r.Match ("sausage"));
Console.WriteLine (r.Match ("sausages"));
Compiled از code generation سبک ــ DynamicMethod در Reflection.Emit ــ برای ساخت و compile کد ویژهٔ همان pattern استفاده میکند؛ matching سریعتر میشود اما هزینهٔ اولیهٔ compilation دارد. Regex instance بدون Compiled هم ممکن است و instanceها immutable هستند. Engine حتی بدون compilation نیز سریع است و simple match معمولاً کمتر از یک microsecond طول میکشد.
RegexOptions
enum پرچمی RegexOptions رفتار matching را تغییر میدهد. نمونهٔ رایج، ignore case است:
Console.WriteLine (Regex.Match ("a", "A", RegexOptions.IgnoreCase));
Console.WriteLine (Regex.Match ("a", "A", RegexOptions.IgnoreCase |
RegexOptions.CultureInvariant));
بیشتر optionها را میتوان با code تکحرفی داخل expression فعال یا غیرفعال کرد:
Console.WriteLine (Regex.Match ("a", @"(?i)A"));
Console.WriteLine (Regex.Match ("AAAa", @"(?i)a(?-i)a"));
IgnorePatternWhitespace یا (?x) whitespace بدون escape را از pattern نادیده میگیرد و خوانایی expression پیچیده را بیشتر میکند. NonBacktracking از .NET 7 یک الگوریتم forwards-only میخواهد؛ معمولاً کندتر است و قابلیتهایی مثل lookahead/lookbehind را غیرفعال میکند، اما time complexity قابل پیشبینیتری دارد و در برابر ReDOS مفید است.
Table 25-1 ـ Regular expression options| Enum | Code | معنی |
| None | | رفتار پیشفرض |
| IgnoreCase | i | نادیدهگرفتن case |
| Multiline | m | ^ و $ را برای ابتدا/انتهای line تفسیر میکند |
| ExplicitCapture | n | فقط groupهای صریحاً named یا numbered را capture میکند |
| Compiled | | Compile به IL |
| Singleline | s | . را با همهٔ characterها match میکند |
| IgnorePatternWhitespace | x | Whitespace بدون escape را از pattern حذف میکند |
| RightToLeft | r | جستوجو از راست به چپ |
| ECMAScript | | وادارکردن به ECMA compliance |
| CultureInvariant | | خاموشکردن رفتار وابسته به culture در string comparison |
| NonBacktracking | | غیرفعالکردن backtracking برای performance قابل پیشبینیتر |
Character Escapeها
Metacharacterهای regex معنای ویژه دارند:
\ * + ? | { [ () ^ $ . #
برای استفادهٔ literal از metacharacter معمولاً آن را با backslash escape میکنیم:
Console.WriteLine (Regex.Match ("what?", @"what\?")); // what?
Console.WriteLine (Regex.Match ("what?", @"what?")); // what
داخل character set یا square bracket، این قاعده برای بسیاری از metacharacterها اعمال نمیشود و آنها literal تفسیر میشوند. متدهای Regex.Escape و Regex.Unescape نیز metacharacterها را به فرم escaped و برعکس تبدیل میکنند.
نمونههای کتاب از verbatim string در C# یعنی prefixِ @ استفاده میکنند تا escape mechanism خود C# با backslashهای regex تداخل نداشته باشد. بدون @، یک backslash literal ممکن است به چهار backslash در source نیاز داشته باشد. مگر اینکه (?x) فعال باشد، space نیز literal است.
Character Setها
Character setها wildcard برای مجموعهای از characterها هستند:
| Expression | معنی | Inverse |
[abcdef] | یک character از list | [^abcdef] |
[a-f] | یک character از range | [^a-f] |
\d | Unicode digit؛ در ECMAScript تقریباً [0-9] | \D |
\w | word character؛ وابسته به culture | \W |
\s | whitespace مطابق char.IsWhiteSpace | \S |
\p{category} | character در Unicode category مشخص | \P |
. | در حالت پیشفرض هر character جز newline؛ در Singleline همهٔ characterها | |
Console.Write (Regex.Matches ("That is that.", "[Tt]hat").Count); // 2
Console.Write (Regex.Match ("quiz qwerty", "q[^aeiou]").Index); // 5
Hyphen داخل set برای range استفاده میشود. Unicode categoryها امکان match کردن letter، number، punctuation، mark، symbol، separator و control character را بدون list کردن تکتک characterها میدهند.
Quantifierها
| Quantifier | معنی |
|---|
* | صفر یا بیشتر |
+ | یک یا بیشتر |
? | صفر یا یک |
{n} | دقیقاً n بار |
{n,} | حداقل n بار |
{n,m} | بین n و m بار |
برای filenameهایی مانند cv.docx میتوان patternهایی ساخت که تعداد occurrenceها را محدود کنند؛ period در extension باید با backslash escape شود.
Greedy در برابر Lazy
Quantifierها بهطور پیشفرض greedy هستند: تا حد ممکن character مصرف میکنند و سپس در صورت نیاز backtrack میکنند. با suffixِ ? quantifier lazy میشود و تا حداقل مقدار لازم مصرف میکند. برای مثال .*? در استخراج چند fragment مجزا معمولاً از .* مناسبتر است.
Zero-Width Assertionها
Regex میتواند شرطی را دربارهٔ موقعیت فعلی بررسی کند بدون اینکه characterی مصرف کند. Lookahead سمت راست و lookbehind سمت چپ را میسنجد. Positive lookahead با (?=expr) و negative با (?!expr) است؛ positive lookbehind با (?<=expr) و negative با (?<!expr).
پس از lookahead موفق، matching طوری ادامه پیدا میکند که گویی preview رخ نداده است. کاربرد نمونه، password rule است: ابتدا با lookahead بررسی کنید جایی در string digit یا symbol وجود دارد، سپس طول کل را match کنید.
Anchorها
| Expression | معنی |
|---|
^ | ابتدای string یا line در multiline |
$ | انتهای string یا line در multiline |
\A | ابتدای string مستقل از multiline |
\z | انتهای string مستقل از multiline |
\Z | انتهای line یا string |
\G | محل شروع search |
Anchor یک position را match میکند نه character را. Word boundary با \b و non-boundary با \B بیان میشود.
Groupها
Parenthesis علاوه بر precedence، subexpression را capture میکند. Group صفر کل match است. Groupهای دیگر از collectionِ Groups قابل خواندناند و در خود regex نیز با back-reference قابل استفادهاند. Noncapturing group با (?:expr) ساخته میشود.
Named Groupها
در expressionهای طولانی، نامگذاری group خوانایی را بیشتر میکند. Syntaxهایی مانند (?'name'expr) یا فرم زاویهبراکت برای capture و \k'name'/\k<name> برای back-reference استفاده میشوند. مثال سادهٔ XML/HTML میتواند نام tag شروع را capture و برای tag پایان دوباره استفاده کند.
جایگزینی و تقسیم متن
Regex.Replace مانند string.Replace است اما pattern میپذیرد. Replacement میتواند از groupها استفاده کند: $0 کل match، $1 group شمارهدار و ${name} named group را جایگزین میکند.
Overload دیگری از Replace یک MatchEvaluator میگیرد که برای هر match اجرا میشود. این روش زمانی مفید است که replacement باید از calculation یا transformation هر match ساخته شود، مثلاً escape کردن Unicode characterها برای HTML.
Regex.Split نسخهٔ قدرتمندتر string.Split است و delimiter را بهصورت regex pattern میپذیرد.
Cookbook: الگوهای آماده
US Social Security Number و Phone Number
Regex میتواند قالبهای شماره را با digitها، separator اختیاری و parenthesis کنترل کند. نمونهٔ phone number منبع:
string phone = @"(?x)
( \(\d{3}\) | \d{3})
[\s-]?
\d{3}[-\s]?
\d{4}";
Console.WriteLine (Regex.IsMatch ("123-456-7890", phone)); // True
Console.WriteLine (Regex.IsMatch ("(123) 456-7890", phone)); // True
استخراج pairهای name = value
string r = @"(?m)^\s*(?'name'\w+)\s*=\s*(?'value'.*)\s*(?=\r?$)";
این pattern با multiline directive هر line را پردازش میکند و groupهای name و value را میدهد.
اعتبارسنجی Password قوی
string r = @"(?x)^(?=.* ( \d | \p{P} | \p{S} )).{6,}";
Console.WriteLine (Regex.IsMatch ("abc12", r)); // False
Console.WriteLine (Regex.IsMatch ("abcdef", r)); // False
Console.WriteLine (Regex.IsMatch ("ab88yz", r)); // True
این expression حداقل شش character و وجود حداقل یک digit، punctuation یا symbol را کنترل میکند.
Lineهای حداقل 80 character
string r = @"(?m)^.{80,}(?=\r?$)";
Parsing date/time عددی
string r = @"(?x)(?i)
(\d{1,4}) [./-]
(\d{1,2}) [./-]
(\d{1,4}) [\sT]
(\d+):(\d+):(\d+) \s? (A\.?M\.?|P\.?M\.?)?";
این pattern قالبهای گوناگون عددی date/time را میپذیرد، چه year اول باشد چه آخر. البته صحت تقویمی date/time را validate نمیکند.
Roman Numeral
string r =
@"(?i)\bm*" +
@"(d?c{0,3}|c[dm])" +
@"(l?x{0,3}|x[lc])" +
@"(v?i{0,3}|i[vx])" +
@"\b";
حذف word تکراری
string r = @"(?'dupe'\w+)\W\k'dupe'";
string text = "In the the beginning...";
Console.WriteLine (Regex.Replace (text, r, "${dupe}"));
// In the beginning
Word Count
string r = @"\b(\w|[-'])+\b";
string text = "It's all mumbo-jumbo to me";
Console.WriteLine (Regex.Matches (text, r).Count); // 5
GUID
string r =
@"(?i)\b" +
@"[0-9a-fA-F]{8}\-" +
@"[0-9a-fA-F]{4}\-" +
@"[0-9a-fA-F]{4}\-" +
@"[0-9a-fA-F]{4}\-" +
@"[0-9a-fA-F]{12}" +
@"\b";
Parsing یک XML/HTML Tag
string r =
@"<(?'tag'\w+?).*>" +
@"(?'text'.*?)" +
@"</\k'tag'>";
این pattern برای fragmentهای HTML ــ حتی وقتی document کاملاً well-formed نیست ــ مفید است؛ group tag نام tag و group text محتوا را capture میکند.
تقسیم camelCase
string r = @"(?=[A-Z])";
foreach (string s in Regex.Split ("oneTwoThree", r))
Console.Write (s + " "); // one Two Three
ساخت filename قانونی
string input = "My \"good\" <recipes>.txt";
char[] invalidChars = System.IO.Path.GetInvalidFileNameChars();
string invalidString = Regex.Escape (new string (invalidChars));
string valid = Regex.Replace (input, "[" + invalidString + "]", "");
Console.WriteLine (valid); // My good recipes.txt
Escape کردن Unicode برای HTML
string htmlFragment = "© 2007";
string result = Regex.Replace (htmlFragment, @"[\u0080-\uFFFF]",
m => @"&#" + ((int)m.Value[0]).ToString() + ";");
Unescape کردن HTTP Query String
string sample = "C%23 rocks";
string result = Regex.Replace (
sample,
@"%[0-9a-f][0-9a-f]",
m => ((char) Convert.ToByte (m.Value.Substring (1), 16)).ToString(),
RegexOptions.IgnoreCase
);
Parsing عبارتهای Google از Web Log
string sample =
"http://google.com/search?hl=en&q=greedy+quantifiers+regex&btnG=Search";
Match m = Regex.Match (sample, @"(?<=google\..+search\?.*q=).+?(?=(&|$))");
string[] keywords = m.Value.Split (
new[] { '+' }, StringSplitOptions.RemoveEmptyEntries);
در منبع توصیه شده این نمونه با unescape کردن characterهای query string ترکیب شود.
مرجع زبان Regular Expression
جدولهای 25-2 تا 25-12 grammar و syntax پشتیبانیشده در implementation .NET را خلاصه میکنند.
Table 25-2 ـ Character Escapeها
| Escape | معنی | Hex |
|---|
\a | Bell | U+0007 |
\b | Backspace | U+0008 |
\t | Tab | U+0009 |
\r | Carriage return | U+000A |
\v | Vertical tab | U+000B |
\f | Form feed | U+000C |
\n | Newline | U+000D |
\e | Escape | U+001B |
\nnn | ASCII به octal | |
\xnn | ASCII به hex | |
\cl | ASCII control character | |
\unnnn | Unicode character با hex | |
\symbol | Symbol با escape | |
Table 25-3 و 25-4 ـ Character Set و Category
| Category | معنی |
|---|
\p{L} | Letterها |
\p{Lu} | Uppercase letter |
\p{Ll} | Lowercase letter |
\p{N} | Numberها |
\p{P} | Punctuation |
\p{M} | Diacritic mark |
\p{S} | Symbolها |
\p{Z} | Separatorها |
\p{C} | Control characterها |
Table 25-5 ـ Quantifierها
همان *، +، ?، {n}، {n,} و {n,m} هستند. suffixِ ? هر quantifier را از greedy به lazy تبدیل میکند.
Table 25-6 ـ Substitutionها
| Expression | معنی |
|---|
$0 | متن matchشده |
$group-number | group شمارهدار |
${group-name} | group نامدار |
Substitution فقط در replacement pattern مشخص میشود.
Table 25-7 ـ Zero-Width Assertionها
| Expression | معنی |
|---|
^ | ابتدای string یا line |
$ | انتهای string یا line |
\A | ابتدای string |
\z | انتهای string |
\Z | انتهای line یا string |
\G | محل آغاز search |
\b | روی word boundary |
\B | خارج word boundary |
(?=expr) | Positive lookahead |
(?!expr) | Negative lookahead |
(?<=expr) | Positive lookbehind |
(?<!expr) | Negative lookbehind |
(?>expr) | Atomic subexpression؛ یک بار match و بدون backtrack |
Table 25-8 ـ Grouping Constructها
| Syntax | معنی |
|---|
(expr) | Capture در indexed group |
(?number) | Capture در group number مشخص |
(?'name') | Capture در named group |
(?'name1-name2') | Undefine کردن name2 و ذخیرهٔ interval/current group در name1؛ اگر name2 تعریف نشده باشد backtrack میکند |
(?:expr) | Noncapturing group |
Table 25-9 ـ Back Referenceها
| Syntax | معنی |
|---|
\index | Reference به group قبلاً captureشده بر اساس index |
\k<name> | Reference به group بر اساس name |
Table 25-10 ـ Alternation
| Syntax | معنی |
|---|
| | Logical OR |
(?(expr)yes|no) | اگر expr match شد yes وگرنه no؛ no اختیاری است |
(?(name)yes|no) | اگر named group match داشته باشد yes وگرنه no |
Table 25-11 ـ Constructهای متفرقه
| Syntax | معنی |
|---|
(?#comment) | Inline comment |
#comment | Comment تا انتهای line، فقط در IgnorePatternWhitespace |
Table 25-12 ـ Optionهای داخل Expression
| Option | معنی |
|---|
(?i) | Case-insensitive |
(?m) | Multiline؛ تغییر معنای ^ و $ |
(?n) | فقط capture صریح named/numbered |
(?c) | Compile به Intermediate Language |
(?s) | Single-line؛ . همه characterها را match میکند |
(?x) | حذف whitespace بدون escape از pattern |
(?r) | Search از راست به چپ؛ midstream قابل تعیین نیست |