怎样用正则表达式匹配IPV4地址

写论文写累了,忽然想起以前面试时的一道题:怎样用正则表达式匹配IPV4地址?把相关内容翻译综合一下,与诸君共享。

写一个正则表达式匹配IPV4地址说容易也容易,说难也难,取决于需要的准确度。简单起见,这里只考虑点分十进制的IPV4地址。
最简单的方式是把IPV4地址看做四段十进制数字串,由三个点号隔开,可以采用如下写法:

^\d+\.\d+\.\d+\.\d+$

就其本身而言没有问题,但会错误地匹配"448.90210.0.65535"这样的字符串,而一个恰当的IPV4表示法中每个域中的值不应大于255。但写一个匹配0到255的整数的正则表达式并不容易,因为正则表达式不理解算术,而是单纯基于文本。考虑下面一些情况:

  • 单独的一个数字(表示0-9)
  • 一个非零数字后紧跟着另外一个数字(表示10-99)
  • "1"后面跟着两个数字(表示100-199)
  • "2"后面跟着一个"0"到"4"间的数字,后面又跟着一个数字(表示200-249)
  • "25"后面跟着一个"0"到"5"间的数字(表示250-255)
根据上面的分析,来试着写匹配0-255的正则表达式:

^\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]$
将前两项合并,上式变成:
^[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]$

然后,将上面这个式子重复4次,中间以点号隔开即可,如下所示:

^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$

好了,我们完成了点分十进制的简单描述,同时写了一个基本不可读的怪异的正则表达式。想象你正在维护一个程序时碰到了这个式子,你要花多长时间才能看懂它表达的意思?

可能这个式子还是不太对,因为有些解析器支持在每个十进制数前的前导零(例如,127.0.0.001127.0.0.1是相通的,但另外一些解析器将前导零作为八进制的前缀,orz)。好,现在我们考虑前导零,上面的正则表达式变成:

^0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$

这就是我对正则表达式既爱又恨的原因。简单模式有许多种表示方式,复杂模式的表示方式更是多得可怕。

啊哈,你可能看到了,浪费这么多时间写一个正则表达式真是个错误。
我们忘了去搞清楚:实际问题到底是什么?搞懂了这个可以解决一半的问题。现在问问自己:“我有一个字符串,想检查它是否是一个点分十进制的IPV4地址,我得写一个正则表达式!嘿,谁能帮我写一下?”

真正的问题不是“我怎么写一个识别点分十进制IPV4地址的正则表达式”,而是**“我怎么识别一个点分十进制IPV4地址”**。头脑里思考这个更大的目标,你会发现逼你自己写一个正则表达式纯粹是自寻烦恼。
来看看下面的函数:

#include <iostream>
#include <regex>
#include <string>

bool isDottedIPv4(const std::string& s) {
    std::regex ipv4_regex(R"(^(\d+)\.(\d+)\.(\d+)\.(\d+)$)");
    std::smatch match;

    // 检查是否匹配
    if (std::regex_match(s, match, ipv4_regex)) {
        // 检查每个部分是否在 0 到 255 之间
        for (size_t i = 1; i <= 4; ++i) {
            int num = std::stoi(match[i].str());
            if (num < 0 || num > 255 || (match[i].str().length() > 1 && match[i].str()[0] == '0')) {
                return false;
            }
        }
        return true;
    }
    return false;
}

int main() {
    std::cout << isDottedIPv4("127.0.0.001") << std::endl;      // 输出:0
    std::cout << isDottedIPv4("448.90210.0.65535") << std::endl; // 输出:0
    std::cout << isDottedIPv4("microsoft.com") << std::endl;     // 输出:0
    std::cout << isDottedIPv4("192.168.1.1") << std::endl;       // 输出:1
    return 0;
}

问题是不是已经搞定了?怎么样?想不想试试解析电子邮件的地址?
别让正则表达式做它们不适合的事情。如果你想匹配一个简单的模式,就匹配一个简单的模式。正如评论家Maurits所说:“诀窍不是花时间去造锤子和螺丝刀,直接用锤子和螺丝刀就OK了”。

恩,写得不错,都上升到方法学的角度上来了,不过我很赞同,解决问题永远是第一位的,死钻技术牛角尖并不可取,这是面对问题时我们应采取的态度。但为了研究技术,深入考究也是必须的!下面来看看另一篇文章里的写法。
检查IP地址的简单正则表达式:
^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$
来一个更准确的:
^(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$
用于从长文本中提取IP地址的正则表达式:
\b(?:[0-9]{1,3}\.){3}[0-9]{1-3}\b
\b(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b
将IP地址的四个部分完全展开,有:

^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$
^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.
(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.
(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.
(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.

这里有两个版本,一个是简单的,另一个是准确的。简单版利用[0-9]{3}表示IP地址的每个域,实际上支持0-999之间的数字,而非0-255。当你的输入只包含有效的IP地址时,简单版会更有效,这时你只要把IP地址和其他内容分开就行了。准确版用25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?来匹配IP地址的每个域,准确地匹配了0-255间的数字。(这个式子具体什么意思不解释了,第一篇有解释)
如果想检查一个串是否完全是个IP地址,可以再首尾分别加上^和$符号。如果想在长文本中查找一个IP地址,可以用字边界符\b。
使用(?:number.){3}number的形式,IP地址的前三个数字重复三次,且为非获取性匹配,最后一部分匹配IP地址的最后一个数字。使用非获取性匹配并重复三次可以使我们的正则表达式更短更有效。
要将IP地址的文本表示转换为整数表示,我们需要分别获取IP地址的四个数字。上面最后的两个正则表达式做的就是这样的工作,其不是重复三次,而是完全展开,这也是唯一的办法。
一旦获取了数字,将其转换为32位的数字就很简单了。

OK,写完了,对你有没有一点启发呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

q472599451

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值