Regex برای اعداد، شماره تلفن و آدرسهای IP
الگوهای آماده برای ورودی عددی، شماره تلفن و آدرس IP که همه آزمایش شدهاند، همراه با توضیح اینکه کجا یک parser یا کتابخانه کار را بهتر انجام میدهد.
عددها، شماره تلفنها و آدرسهای IP هدفهای سادهای برای regex به نظر میرسند، چون بیشترشان رقماند. اما هرکدام یک دام دارد: \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 |
regex شکل را بررسی میکند، نه مقدار را. برای یک بازه مثل ۱ تا ۱۰۰، رقمها را با الگو تطبیق دهید و بعد خود عدد را در کد مقایسه کنید. اما regex را کنار نگذارید و فقط به تبدیل تکیه نکنید: در JavaScript، Number('') برابر 0 است، Number(' 42 ') برابر 42، Number('0x1A') برابر 26 و parseInt('12abc') برابر 12.
آیا \d فقط با ۰ تا ۹ تطبیق پیدا میکند؟
نه همهجا. ما ^\d+$ را روی ارقام عربی-هندی ٣٤ آزمایش کردیم:
| موتور | ارقام غیر ASCII را پذیرفت؟ | برای محدود کردن به ۰ تا ۹ |
|---|---|---|
| JavaScript | نه، حتی با پرچم u | کاری لازم نیست؛ \p{Nd} همراه با u با همه ارقام دهدهی تطبیق پیدا میکند |
| Python 3 (الگوهای str) | بله، ارقام تمامعرض 12 را هم | [0-9] یا پرچم re.ASCII |
| PHP (PCRE) | نه؛ با modifier u بله | [0-9] |
| .NET | بله | [0-9] یا RegexOptions.ECMAScript |
| Perl 5 | بله | [0-9] یا modifier /a |
این وقتی اهمیت پیدا میکند که مقداری از regex عبور کند و بعد به کدی برسد که آن را طور دیگری میخواند. int('٣٤') در Python مقدار 34 را برمیگرداند، در حالی که ممکن است یک ستون پایگاه داده یا سرویس دیگری آن را رد کند. وقتی منظورتان رقمهای ASCII است، [0-9] بنویسید؛ این عبارت در همه موتورها یک معنا دارد.
شماره تلفن
تنها قالبی که تعریف ساده و بینالمللی دارد E.164 است: یک علامت بهعلاوه، یک کد کشور که با ۰ شروع نمیشود، و در مجموع حداکثر ۱۵ رقم.
^\+[1-9]\d{1,14}$ E.164, plus sign required (best for storage)
^\+?[1-9]\d{1,14}$ plus sign optional
خط اول علامت بهعلاوه را الزامی میکند و برای ذخیرهسازی بهترین گزینه است؛ در خط دوم این علامت اختیاری است. وقتی علامت بهعلاوه الزامی باشد، +14155552671 و +442071838750 پذیرفته میشوند، اما +0123456789، یک عدد ۱۶ رقمی و 004914155552671 (که با پیشوند بینالمللی ۰۰ نوشته شده) رد میشوند. کاربران فاصله، نقطه، خط تیره و پرانتز تایپ میکنند، پس اول اینها را با 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 گوگل یا یکی از نسخههای آن برای زبانهای دیگر استفاده کنید: libphonenumber-js برای JavaScript، phonenumbers برای Python و giggsey/libphonenumber-for-php برای PHP. نتیجه را در قالب E.164 ذخیره کنید و اگر مهم است، مالکیت شماره را با یک کد پیامکی تأیید کنید.
آدرسهای IPv4
میانبر محبوب ^\d{1,3}(?:\.\d{1,3}){3}$ هم 256.1.1.1 را پذیرفت و هم 999.999.999.999 را. برای اینکه هر بخش به ۰ تا ۲۵۵ محدود شود، بازهها را جزءبهجزء بنویسید:
^(?:(?: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 را روی همه عددهای ۰ تا ۹۹۹ که با یک، دو و سه رقم نوشته شده بودند آزمایش کردیم، و الگوی کامل را روی ۲۰۰٬۰۰۰ آدرس تصادفی؛ نتیجه در همه موارد با 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 را رد میکند.
رد کردن صفرهای ابتدایی عمدی است. بعضی parserها آنها را مبنای هشت (octal) میخوانند: 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: از parser استفاده کنید
IPv6 اجازه میدهد :: جای یک رشته از گروههای صفر را بگیرد، صفرهای ابتدایی اختیاری باشند، IPv4 داخل آدرس بیاید، مثل ::ffff:192.0.2.1، و شناسه zone داشته باشد، مثل fe80::1%eth0. regexی که همه اینها را پوشش دهد طولانی است و خیلی راحت ممکن است به شکلی ظریف اشتباه از آب دربیاید. یک parser کوتاهتر و بهتر آزمایششده است:
- Node.js:
net.isIPv6(value)، یاnet.isIP(value)که ۴، ۶ یا ۰ برمیگرداند. هر دو IPv4 داخلی و شناسههای zone را پذیرفتند. - Python:
ipaddress.ip_address(value)در صورت نامعتبر بودن، خطایValueErrorمیدهد. IPv4 داخلی و شناسههای zone را پذیرفت. - PHP:
filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6). IPv4 داخلی را پذیرفت اماfe80::1%eth0را رد کرد. - مرورگرها: اعتبارسنج داخلی IP وجود ندارد، پس از کتابخانهای مثل
ipaddr.jsاستفاده کنید.
اگر باز هم به regex نیاز دارید، مثلاً در یک schema یا فایل پیکربندی که فقط regex میپذیرد، این الگوی ۴۰۸ نویسهای شکلهای هگزادسیمال را پوشش میدهد و عمداً IPv4 داخلی و شناسههای zone را رد میکند. روی ۵۰۰٬۰۰۰ رشته تولیدشده و جهشیافته بدون نقطه و علامت درصد، نتیجه آن در همه موارد با 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}|:))$
اشتباههایی که ورودی نادرست را راه میدهند
- نبودن anchor.
/\d+/.test('abc123')مقدارtrueمیدهد، چون رقمها را هر جای رشته پیدا میکند. برای اعتبارسنجی به^و$نیاز دارید. - انتخاب (alternation) بیرون از گروه.
^cat|dog$یعنی «با cat شروع شود، یا با dog تمام شود»، پس باcats and dogs!تطبیق پیدا کرد.^(?:cat|dog)$بنویسید. - پرچم
m. با این پرچم،^و$در هر خط تطبیق پیدا میکنند، پس/^\d+$/mرشته دوخطی"abc\n123"را پذیرفت. هنگام اعتبارسنجی یک مقدار تکی هرگز ازmاستفاده نکنید. $پیش از خط جدید پایانی. در Python، PHP و .NET،^\d+$رشته"42\n"را پذیرفت. در Python ازre.fullmatch()، در PHP از modifierD، یا در PHP و .NET از\zاستفاده کنید. JavaScript آن را رد کرد.- پرچم
gهمراه باtest(). regexی که پرچمgداردlastIndexرا به خاطر میسپارد، پس سه بار صدا زدن/^\d+$/g.test('42')روی یک شیء یکسان، به ترتیب true، false و true برگرداند. - گزینههای بدون anchor با ترتیب اشتباه. جستوجوی
255.255.255.255با octetی که به شکل(?:\d|1\d\d|2[0-4]\d|25[0-5])نوشته شده بود، فقط255.255.255.2را پیدا کرد، چون موتور اولین گزینهای را که جواب بدهد برمیدارد. گزینههای طولانیتر را اول بگذارید، یا الگو را anchor کنید.
الگو را پیش از انتشار آزمایش کنید
یک الگو را بدون اسلشها در تستکننده Regex بچسبانید، پرچمها را روی gm بگذارید و نمونههایتان را هرکدام در یک خط وارد کنید: نمونههای معتبر، نامعتبر و حالتهای دردسرساز همین صفحه. هر خطی که تطبیق پیدا کند هایلایت میشود. این ابزار با موتور JavaScript مرورگر شما اجرا میشود، پس اگر الگو قرار است در Python، PHP یا .NET اجرا شود، تفاوتهای \d و $ را که بالاتر گفته شد دوباره بررسی کنید. دنبال اعتبارسنجی آدرس ایمیل هستید؟ راهنمای Regex اعتبارسنجی ایمیل را ببینید.