التعبيرات النمطية (تُسمّى أحيانًا regexs أو regexps) هي صِيغ نصّية تمثّل الأنماط التي يمكن مطابقتها في السلاسل النصّية، وقد تدعم التعبيرات النمطيّة التي قُدِّمت في C++ 11 -اختياريًا- إعادة مصفوفة من السلاسل النصّية المطابِقة، أو صيغة نصّية أخرى تحدّد كيفيّة استبدال الأنماط المتطابقة في السلاسل النصية.
الصياغة الأولى للدالة regex_match
:
bool regex_match(BidirectionalIterator first, BidirectionalIterator last, smatch& sm, const regex& re, regex_constraints::match_flag_type flags)
يمثّل BidirectionalIterator
أيَّ مُكرّر محارف يوفّر عامليْ الزيادة (increment) والإنقَاص (decrement). والوسيط smatch
يمكن أن يكون كائنًا cmatch
أو أيّ متغيّر آخر من الصنف match_results
يقبل النوع BidirectionalIterator
، ويمكن حذف هذا الوسيط إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي. وتعيد ما إذا كان التعبير re
يطابق كامل تسلسل المحارف المُعُرّف بواسطة first
و last
.
الصياغة الثانية للدالة regex_match
:
bool regex_match(const string& str, smatch& sm, const regex re&, regex_constraints::match_flag_type flags)
قد تكون string
من النّوع const char*
أو قيمة نصّية يسارية، وتُحذَف الدوالّ التي تقبل سلسلة نصيّة يمينية R- بشكل صريح. الوسيط smatch
يمكن أن يكون كائنًا cmatch
أو أيّ متغيّر آخر من الصنف match_results
يقبل سلسلة نصية. ويمكن حذف الوسيط smatch
إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي .تعيد الدالة regex_match
ما إذا كان التعبير re
قد طابق كامل السلسلة النصية str
.
أمثلة عن regex_match و regex_search
const auto input = "Some people, when confronted with a problem, think \"I know, I'll use regular expressions.\""s; smatch sm; cout << input << endl;
إن انتهى input
بعلامة تنصيص تحتوي كلمة تبدأ بالكلمة "reg" وكلمة أخرى تبدأ بـ "ex" فالتقط الجزء السابق من input.
if (regex_match(input, sm, regex("(.*)\".*\\breg.*\\bex.*\"\\s*$"))) { const auto capture = sm[1].str(); cout << '\t' << capture << endl; // الخرج: "\tSome people, when confronted with a problem, think\ n ";
ابحث في الجزء الملتقط عن "a problem" أو "problems #".
if (regex_search(capture, sm, regex("(a|d+)\\s+problems?"))) { const auto count = sm[1] == "a"s ? 1 : stoi(sm[1]); cout << '\t' << count << (count > 1 ? " problems\n" : " problem\n"); // الخرج: --> "\t1 problem\ n " cout << "Now they have " << count + 1 << " problems.\n"; // الخرج: "Now they have 2 problems\ n " } }
هذا مثال حيّ على ذلك.
مثال عن مُكرّر التعبيرات النمطية regex_iterator
تُعد regex_iterator
خيارًا ممتازًا عند معالجة الخرج الملتقَط بشكل متكرر، وسيعيد تحصيل regex_iterator
كائن match_result
، وهذا يفيد في الالتقاطات الشرطية (conditional captures) أو الالتقاطات المترابطة.
لنقل أنّنا نريد تقطيع (tokenize) مقتطف من شيفرة C++:
enum TOKENS { NUMBER, ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION, EQUALITY, OPEN_PARENTHESIS, CLOSE_PARENTHESIS };
يمكننا تقطيع هذه السلسلة النصّية:
const auto input = "42/2 + -8\t=\n(2 + 2) * 2 * 2 -3"s
باستخدام مكرّر تعبيرات نمطية regex_iterator
على النحو التالي:
vector<TOKENS> tokens; const regex re { "\\s*(\\(?)\\s*(-?\\s*\\d+)\\s*(\\)?)\\s*(?:(\\+)|(-)|(\\*)|(/)|(=))" }; for_each(sregex_iterator(cbegin(input), cend(input), re), sregex_iterator(), [& ](const auto &i) { if (i[1].length() > 0) { tokens.push_back(OPEN_PARENTHESIS); } tokens.push_back(i[2].str().front() == '-' ? NEGATIVE_NUMBER : NON_NEGATIVE_NUMBER); if (i[3].length() > 0) { tokens.push_back(CLOSE_PARENTHESIS); } auto it = next(cbegin(i), 4); for (int result = ADDITION; it != cend(i); ++result, ++it) { if (it->length() > 0 U) { tokens.push_back(static_cast<TOKENS> (result)); break; } } }); match_results<string::const_reverse_iterator > sm; if (regex_search(crbegin(input), crend(input), sm, regex { tokens.back() == SUBTRACTION ? "^\\s*\\d+\\s*-\\s*(-?)" : "^\\s*\\d+\\s*(-?)" })) { tokens.push_back(sm[1].length() == 0 ? NON_NEGATIVE_NUMBER : NEGATIVE_NUMBER); }
هذا مثال حيّ.
ينبغي أن يكون وسيط regex
قيمة يسارية (L-value)، إذ أنّ القيم اليمينيّة لن تعمل.
المراسي (Anchors)
توفّر C++ أربع مراسي فقط:
-
^
- تمثّل بداية السلسلة النصّية -
$
- تمثّل نهاية السلسلة النصّية -
\b
- تمثّل محرف\W
أو بداية أو نهاية السلسلة النصّية. -
\B
- تمثّل محرف\w
.
في المثال التالي سنحاول التقاط عددٍ مع إشارَتِه:
auto input = "+1--12 * 123/+1234"s; smatch sm; if (regex_search(input, sm, regex { "(?:^|\\b\\W)([+-]?\\d+)" })) { do { cout << sm[1] << endl; input = sm.suffix().str(); } while (regex_search(input, sm, regex { "(?:^\\W|\\b\\W)([+-]?\\d+)" })); }
هذا مثال حيّ
لاحظ أنّ المرساة لا تستهلك أيّ محرف.
مثال على استخدام regex_replace
تأخذ هذه الشيفرة عدّة أنماط من الأقواس، وتعيد تنسيقها إلى نمط K&R أو كيرنيجان وريتشي (إشارة إلى أسلوب الأقواس المستخدم في نواة يونكس الأولى).
const auto input = "if (KnR)\n\tfoo();\nif (spaces) {\n foo();\n}\nif (allman)\n{\n\tfoo();\n}\nif (horstmann)\n{\tfoo();\n}\nif (pico)\n{\tfoo(); }\nif (whitesmiths)\n\t{\n\tfoo();\n\t}\n"s; cout << input << regex_replace(input, regex("(.+?)\\s*\\{?\\s*(.+?;)\\s*\\}?\\s*"), "$1{\n\t$2\n}\n") << endl;
مثال على استخدام regex_token_iterator
std::regex_token_iterator
هي أداة مفيدة للغاية لاستخراج عناصر من ملف يحتوي قيمًا مفصولة بفواصل، كما أنّها قادرة أيضًا على التقاط الفواصل، على خلاف الطرق الأخرى التي تجد صعوبة في ذلك:
const auto input = "please split,this,csv, ,line,\\,\n"s; const regex re{ "((?:[^\\\\,]|\\\\.)+)(?:,|$)" }; const vector<string> m_vecFields{ sregex_token_iterator(cbegin(input), cend(input), re, 1), sregex_token_iterator() }; cout << input << endl; copy(cbegin(m_vecFields), cend(m_vecFields), ostream_iterator<string>(cout, "\n"));
ينبغي أن يكون الوسيط regex
قيمة يسارية (L-value)، فالقِيم اليمينيّة (R-value) لن تعمل.
المحدِّدات الكمية
لنفترض أنّ لدينا سلسلة نصية ثابتة (const string input
) تحتوي رقم هاتف وعلينا أن نتحقّق من صحّته. يمكن أن نبدأ بطلب مدخلات رقمية مع أيّ عدد من المحدِّدات الكمية regex_match(input, regex("\\d*"))
، أو مع محدِّد كمي regex_match(input, regex("\\d+"))
واحد أو أكثر، بيْد أنّ كليهما سيفشلان إذا كانت المدخلات input
تحتوي على سلسلة نصّية رقمية غير صالحة مثل: "123".
سنستخدم n أو أكثر من المحدِّدات الكمية للتحقّق من أنّنا حصلنا على 7 أرقام على الأقل:
regex_match(input, regex("\\d{7,}"))
سيضمن هذا أنّنا سنحصل على العدد الصحيح من أرقام الهاتف، لكن يمكن أن تحتوي input
أيضًا على سلسلة رقمية أطول ممّا ينبغي مثل: "123456789012"، لذلك فالحلّ هو استخدام محدِّد كمي بين n و m بحيث يكون عدد أحرف input
محصورًا بين 7 و 11 رقمًا
regex_match(input, regex("\\d{7,11}"));
هذا أفضل، لكن ما تزال هنا مشكلة، إذ ينبغي أن ننتبه إلى السلاسل الرقمية غير القانونية التي تقع في النطاق [7، 11]، مثل: "123456789"، لذا دعنا نجعل رمز البلد (country code) اختياريًا عبر استخدام محدِّد كمي كسول(lazy quantifier):
regex_match(input, regex("\\d?\\d{7,10}"))
من المهمّ أن تعلم أنّ المحدِّد الكمي الكسول يحاول مطابقة أقل عدد ممكن من المحارف، وعليه فإنّّ الطريقة الوحيدة للمطابقة هي إذا كانت هناك فعليًا 10 محارف متطابقة مع \d{7,10}
. (لمطابقة الحرف الأول بطمع (greedy)، سيكون علينا استخدام: \d{0,1}
}.) يمكن ضمّ المحدِّد الكمي الكسول (lazy quantifier) إلى أيّ محدِّد كمي آخر.
الآن، كيف يمكننا جعل رمز المنطقة اختياريًا، وعدم قبول رمز الدولة إلّا في حال كان رمز المنطقة موجودًا؟
regex_match(input, regex("(?:\\d{3,4})?\\d{7}"))
تتطّلب \d{7}
في هذا التعبير النمطي النهائي سبعة أرقام، ومسبوقة -اختياريًا- إما بثلاثة أو أربعة أرقام. لاحظ أنّنا لم نضم المحدِّد الكسول : \d{3,4}?\d{7}
، إذ أنّ \d{3,4}?
يمكن أن يطابق إمّا 3 أو 4 محارف، بيْد أنّه يُفضّل الاكتفاء بـ 3 محارف، ( لهذا يُسمّونه كسولًا). بدلاً من ذلك، جعلنا المجموعة غير الملتقِطة (non-capturing group) لا تنجح في المُطابقة إلّا مرّة واحدة على الأكثر، مع تفضيل عدم التطابق. وهذا يتسبّب في منع التطابق إذا لم تتضمن input
رمز المنطقة، كما في: "1234567".
أود أن أشير في ختام موضوع المحدِّدات الكمية، إلى محدِّد آخر يمكنك استخدامه، وهو المحدِّد الكمي المُتملِّك (possessive quantifier). كلا المحدِّديْن سواءً المكمّم القنوع أو المكمّم المتملّك، يمكن ضمّهما إلى أيّ مكمّم آخر. وظيفة المكمّم المتملّك الوحيدة هي مساعدة محرّك التعبير النمطي عبر إخباره بأخذ ما أمكن من الأحرف المطابقة وعدم التخلي عنها حتى لو تسبّب ذلك في فشل التعبير النمطي.
على سبيل المثال، التعبير التالي لا معنى له: regex_match(input, regex("\\d{3,4}+\\d{7}))
لأنّ مُدخلًا input
مثل:" 1234567890 " لن يُطابَق بالتعبير \d{3,4}+
، وسيُطابق دائمًا بأربعة محارف، حتى لو كانت مطابقة 3 محارف كافية لإنجاح التعبير النمطي.
يُستخدم المحدِّد المتملّك عادة عندما تحدّ الوحدة المحدَّدة كميًا عددَ المحارف القابلة للمطابقة. على سبيل المثال:
regex_match(input, regex("(?:.*\\d{3,4}+){3}"))
يمكن استخدامها إذا كانت input
تحتوي أيًّا ممّا يلي:
123 456 7890 123-456-7890 (123)456-7890 (123) 456 - 7890
بيْد أنّ فائدة هذا التعبير النمطي تظهر عندما تتضمّن input
مُدخلاً غير صالح، مثل:
12345 - 67890
بدون استخدام المحدِّد الكمي المتملّك، سيتعيّن على محرّك التعبير النمطي الرجوع واختبار كل توليفات .*
، سواء مع 3 أو 4 محارف للتحقّق ممّا إن كان يستطيع العثور على تركيبة مطابقة. سيبدأ التعبير النمطي، باستخدام المحدِّد المتملّك، من حيث توقّف المحدِّد المتملّك الثاني، أي المحرف "0"، ثمّ سيحاول محرّك التعبير النمطي ضبط .*
للسماح بمطابقة \d{3,4}
؛ وفي حال تعذّر ذلك سيفشل التعبير النمطي، ولن يرجِع للخلف للتحقّق ممّا إذا كان من الممكن إنجاح المطابقة عبر إعادة ضبط .*
في مرحلة أبكر.
تقطيع سلسلة نصية Splitting a string
هذا مثال توضيحيّ على كيفية تقسيم سلسلة نصّية:
std::vector<std::string> split(const std::string &str, std::string regex) { std::regex r{ regex }; std::sregex_token_iterator start{ str.begin(), str.end(), r, -1 }, end; return std::vector<std::string>(start, end); } split("Some string\t with whitespace ", "\\s+"); // "Some", "string", "with", "whitespace"
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 70: Regular expressions من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.