سلسلة ++c للمحترفين الدرس 36: التعبيرات النمطية Regular expressions في Cpp


محمد الميداوي

التعبيرات النمطية (تُسمّى أحيانًا 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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن