Regex برای اعداد، شماره تلفن و آدرس‌های IP

الگوهای آماده برای ورودی عددی، شماره تلفن و آدرس IP که همه آزمایش شده‌اند، همراه با توضیح اینکه کجا یک parser یا کتابخانه کار را بهتر انجام می‌دهد.

همین حالا امتحان کنید: تست‌کننده Regex

عددها، شماره تلفن‌ها و آدرس‌های IP هدف‌های ساده‌ای برای regex به نظر می‌رسند، چون بیشترشان رقم‌اند. اما هرکدام یک دام دارد: \d در زبان‌های مختلف معنای متفاوتی دارد، شماره تلفن‌ها از قاعده‌های ملی پیروی می‌کنند که هیچ الگویی نمی‌تواند همه را پوشش دهد، و یک آدرس IP فقط وقتی معتبر است که همه بخش‌هایش در بازه مجاز باشند. الگوهای زیر در Node.js آزمایش شده‌اند، مگر جایی که نام موتور دیگری آمده باشد، و همه مثال‌های پذیرفته‌شده یا ردشده را خودمان اجرا کرده‌ایم.

فقط عدد

چه می‌خواهیدالگومی‌پذیردرد می‌کند
فقط رقم^[0-9]+$123، 007-42، 3.14، فاصله در ابتدا، ورودی خالی
عدد صحیح با علامت اختیاری^[+-]?[0-9]+$-42، +42، 0073.14، 1e3
عدد صحیح بدون صفرهای ابتدایی^-?(?:0|[1-9][0-9]*)$0، -42007، +42
عدد اعشاری^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$3.14، -0.5، .55.، 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، 1001000، 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 از modifier D، یا در 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 اعتبارسنجی ایمیل را ببینید.

سوالات متداول

چه regexی فقط عدد را مجاز می‌داند؟

برای فقط رقم از ^[0-9]+$ استفاده کنید، یا اگر علامت هم مجاز است از ^[+-]?[0-9]+$. [0-9] را به \d ترجیح دهید، چون در Python، .NET و PHP با modifier u، \d با ارقام خط‌های دیگر هم تطبیق پیدا می‌کند.

چه regexی شماره تلفن بین‌المللی را اعتبارسنجی می‌کند؟

برای قالب E.164، پس از حذف فاصله‌ها، نقطه‌ها، خط تیره‌ها و پرانتزها، از ^\+[1-9]\d{1,14}$ استفاده کنید. این الگو فقط شکل را بررسی می‌کند، پس برای اینکه مطمئن شوید شماره برای کشورش واقعاً معتبر است، از libphonenumber یا یکی از نسخه‌های آن استفاده کنید.

regex یک آدرس IPv4 معتبر چیست؟

^(?:(?: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)$ هر بخش را به ۰ تا ۲۵۵ محدود می‌کند و صفرهای ابتدایی را رد می‌کند. برای IPv6 از یک parser مثل net.isIPv6 در Node.js یا ipaddress در Python استفاده کنید.

چرا regex من ورودی‌ای را که در انتهایش خط جدید دارد می‌پذیرد؟

در Python، PHP و .NET، $ درست پیش از خط جدید پایانی هم تطبیق پیدا می‌کند. در Python از re.fullmatch، در PHP از modifier D، یا به‌جای $ از \z استفاده کنید.
همین حالا امتحان کنید: تست‌کننده Regex