Regex للأرقام وأرقام الهواتف وعناوين IP
أنماط جاهزة للنسخ للمدخلات الرقمية وأرقام الهواتف وعناوين IP، كلٌّ منها مُختبَر، مع ملاحظات عن المواضع التي يؤدي فيها محلّل أو مكتبة المهمة على نحو أفضل.
تبدو الأرقام وأرقام الهواتف وعناوين IP أهدافًا سهلة للتعابير النمطية لأنها في معظمها أرقام. لكن لكلٍّ منها فخًّا: فالرمز \d يعني أشياء مختلفة في لغات البرمجة المختلفة، وأرقام الهواتف تتبع قواعد وطنية لا يستطيع أي نمط الإحاطة بها، وعنوان IP لا يكون صالحًا إلا إذا كان كل جزء منه ضمن النطاق المسموح. اختُبرت الأنماط التالية في Node.js ما لم يُذكر محرك آخر، وكل مثال مقبول أو مرفوض هنا شغّلناه فعلًا.
الأرقام فقط
| ما تريده | النمط | يقبل | يرفض |
|---|---|---|---|
| أرقام فقط | ^[0-9]+$ | 123، 007 | -42، 3.14، مسافة في البداية، مدخل فارغ |
| عدد صحيح بإشارة اختيارية | ^[+-]?[0-9]+$ | -42، +42، 007 | 3.14، 1e3 |
| عدد صحيح دون أصفار بادئة | ^-?(?:0|[1-9][0-9]*)$ | 0، -42 | 007، +42 |
| عدد عشري | ^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$ | 3.14، -0.5، .5 | 5.، 1e3، 1,000 |
| عدد بأسلوب JSON | ^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$ | 1.5E-7، -0 | .5، 01.5، +42 |
| فواصل الآلاف | ^[0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]+)?$ | 1,000، 12,345,678.90، 100 | 1000، 1,00 |
يتحقق التعبير النمطي من الشكل لا من القيمة. فإن أردت نطاقًا مثل 1 إلى 100، فطابِق الأرقام ثم قارن العدد في الشيفرة. ولا تتخطَّ التعبير النمطي معتمدًا على التحويل وحده: ففي JavaScript تعطي Number('') القيمة 0، وتعطي Number(' 42 ') القيمة 42، وتعطي Number('0x1A') القيمة 26، وتعطي parseInt('12abc') القيمة 12.
هل يطابق \d الأرقام من 0 إلى 9 فقط؟
ليس في كل مكان. اختبرنا النمط ^\d+$ على الأرقام العربية الهندية (Arabic-Indic) ٣٤:
| المحرك | هل طابق أرقامًا غير ASCII؟ | لحصر المطابقة في 0–9 |
|---|---|---|
| JavaScript | لا، حتى مع الراية u | لا حاجة لشيء؛ و\p{Nd} مع u يطابق كل الأرقام العشرية |
| Python 3 (أنماط str) | نعم، وكذلك الأرقام كاملة العرض 12 | [0-9] أو الراية re.ASCII |
| PHP (PCRE) | لا؛ ونعم مع المعدِّل u | [0-9] |
| .NET | نعم | [0-9] أو RegexOptions.ECMAScript |
| Perl 5 | نعم | [0-9] أو المعدِّل /a |
وهذا مهم حين تجتاز قيمةٌ التعبيرَ النمطي ثم تصل إلى شيفرة تقرؤها بطريقة مختلفة. فالدالة int('٣٤') في Python تعيد 34، بينما قد يرفض القيمة نفسها عمود في قاعدة بيانات أو خدمة أخرى. اكتب [0-9] حين تقصد أرقام ASCII، فمعناها واحد في كل المحركات.
أرقام الهواتف
الصيغة الوحيدة التي لها تعريف دولي بسيط هي E.164: علامة زائد، ثم رمز دولة لا يبدأ بـ 0، وبحد أقصى 15 رقمًا إجمالًا.
^\+[1-9]\d{1,14}$ E.164, plus sign required (best for storage)
^\+?[1-9]\d{1,14}$ plus sign optional
السطر الأول يشترط علامة الزائد وهو الأنسب للتخزين، والثاني يجعلها اختيارية. وحين تكون علامة الزائد مطلوبة يجتاز +14155552671 و+442071838750 النمط، بينما يفشل +0123456789، ورقم من 16 خانة، و004914155552671 (المكتوب بالبادئة الدولية 00). والناس يكتبون المسافات والنقاط والشرطات والأقواس، فأزلها أولًا بـ value.replace(/[\s().-]/g, '')، فيتحول +1 415 555 2671 إلى +14155552671.
وتظهر الحدود سريعًا. فالنمط يتحقق من الشكل فقط، لذا يجتاز +12 مع أنه لا يوجد رقم حقيقي بهذا القِصر. والأسوأ أن +49 (0)30 1234567 يصبح +490301234567 بعد التنظيف ويجتاز النمط، لكنه رقم خاطئ: فالصفر بين القوسين بادئة اتصال محلية (trunk prefix) يجب حذفها عند الاتصال من الخارج.
والصيغ الوطنية ليست أفضل حالًا. فالنمط الأمريكي الشائع ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$ قبِل (415-555-2671 بقوس غير مغلق، وقبِل 000-000-0000 الذي لا يمكن أن يكون رقمًا حقيقيًا. ولكل دولة أطوالها وبادئاتها ونطاقات أرقام الجوال الخاصة بها، وهذه كلها تتغير. وللتحقق والتنسيق الفعليين استخدم مكتبة libphonenumber من Google أو إحدى نسخها: libphonenumber-js لـ JavaScript، وphonenumbers لـ Python، وgiggsey/libphonenumber-for-php لـ PHP. خزّن الناتج بصيغة E.164، وتأكد من ملكية الرقم برمز يُرسل عبر SMS إن كان ذلك مهمًا.
عناوين IPv4
الاختصار الشائع ^\d{1,3}(?:\.\d{1,3}){3}$ قبِل 256.1.1.1 و999.999.999.999. ولحصر كل جزء بين 0 و255 اكتب النطاقات صراحةً:
^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$
اختبرنا جزء الثُّمانية (octet) على كل عدد من 0 إلى 999 مكتوبًا برقم واحد وبرقمين وبثلاثة أرقام، واختبرنا النمط الكامل على 200,000 عنوان عشوائي، فتوافق مع net.isIPv4() في Node في كل مرة. وهو يقبل 0.0.0.0 و192.168.1.1 و255.255.255.255، ويرفض 192.168.1 و192.168.1.1.1 والأصفار البادئة مثل 192.168.01.1.
ورفض الأصفار البادئة مقصود. فبعض المحلّلات تقرؤها بالنظام الثماني: أعطتنا socket.inet_aton('010.0.0.1') في Python العنوان 8.0.0.1. كما رفضت كلٌّ من net.isIPv4() في Node وipaddress.ip_address() في Python وfilter_var($ip, FILTER_VALIDATE_IP) في PHP العنوان 192.168.01.1.
ولإيجاد العناوين داخل سطر من ملف سجل، احذف ^ و$، وأضف \b على الجانبين، واستخدم الراية g. على النص from 10.0.0.5 to 192.168.1.300 via 8.8.8.8 أعاد ذلك 10.0.0.5 و8.8.8.8. ودون \b أنتج النص 1192.168.1.300x مطابقة خاطئة هي 192.168.1.30. ويبقى قيد واحد: في 1.2.3.4.5 ما زالت النسخة ذات \b تطابق 1.2.3.4.
عناوين IPv6: استخدم محلّلًا
يسمح IPv6 بوضع :: مكان سلسلة واحدة من المجموعات الصفرية، وبالأصفار البادئة الاختيارية، وبعناوين IPv4 المضمّنة مثل ::ffff:192.0.2.1، وبمعرّفات النطاق (zone ID) مثل fe80::1%eth0. والتعبير النمطي الذي يغطي كل ذلك طويل، ومن السهل أن يتسلل إليه خطأ دقيق. أما المحلّل فأقصر وأفضل اختبارًا:
- Node.js:
net.isIPv6(value)، أوnet.isIP(value)التي تعيد 4 أو 6 أو 0. وقد قبلت كلتاهما عناوين IPv4 المضمّنة ومعرّفات النطاق. - Python:
ipaddress.ip_address(value)ترميValueErrorحين يكون العنوان غير صالح. وقد قبلت عناوين IPv4 المضمّنة ومعرّفات النطاق. - PHP:
filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6). قبلت عناوين IPv4 المضمّنة لكنها رفضتfe80::1%eth0. - المتصفحات: لا يوجد مدقّق مدمج لعناوين IP، لذا استخدم مكتبة مثل
ipaddr.js.
وإن كنت لا تزال تحتاج إلى تعبير نمطي، في مخطط (schema) أو ملف إعدادات لا يقبل غيره مثلًا، فهذا النمط المؤلف من 408 محارف يغطي الأشكال الست عشرية، ويرفض عمدًا عناوين IPv4 المضمّنة ومعرّفات النطاق. وعلى 500,000 نص مولَّد ومعدَّل لا تحتوي نقاطًا ولا علامات نسبة مئوية، توافق مع net.isIPv6() في كل واحد منها.
^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$
أخطاء تسمح بمرور المدخلات السيئة
- غياب المراسي. تعطي
/\d+/.test('abc123')القيمةtrueلأنها تجد أرقامًا في أي موضع. والتحقق يحتاج إلى^و$. - التناوب خارج مجموعة. النمط
^cat|dog$يعني «يبدأ بـ cat، أو ينتهي بـ dog»، لذا طابقcats and dogs!. اكتب^(?:cat|dog)$. - الراية
m. معها يطابق^و$عند كل سطر، لذا قبِل/^\d+$/mالنص ذا السطرين"abc\n123". لا تستخدمmأبدًا عند التحقق من قيمة واحدة. $قبل سطر جديد أخير. في Python وPHP و.NET قبِل^\d+$النص"42\n". استخدمre.fullmatch()في Python، أو المعدِّلDفي PHP، أو\zفي PHP و.NET. أما JavaScript فرفضته.- الراية
gمعtest(). التعبير النمطي ذو الرايةgيتذكرlastIndex، لذا فإن استدعاء/^\d+$/g.test('42')ثلاث مرات على الكائن نفسه أعاد true ثم false ثم true. - بدائل غير مثبّتة بالمراسي وبترتيب خاطئ. البحث في
255.255.255.255بنمط يكتب الثُّمانية على شكل(?:\d|1\d\d|2[0-4]\d|25[0-5])طابق255.255.255.2فقط، لأن المحرك يأخذ أول بديل ينجح. ضع البدائل الأطول أولًا، أو ثبّت النمط بالمراسي.
اختبر نمطك قبل اعتماده
الصق النمط في مختبِر التعابير النمطية دون الشرطتين المائلتين، واضبط الرايات على gm، وأدخل نماذجك سطرًا لكل نموذج: الصالحة، وغير الصالحة، والحالات الصعبة الواردة في هذه الصفحة. يُظلَّل كل سطر يطابق النمط. ويعمل المختبِر بمحرك JavaScript في متصفحك، لذا أعد التحقق من فروق \d و$ المذكورة أعلاه إن كان النمط سيعمل في Python أو PHP أو .NET. تريد التحقق من عناوين البريد الإلكتروني بدلًا من ذلك؟ راجع التحقق من البريد الإلكتروني بـ Regex.