我能优化这个电话号码正则表达式吗?

9

好的,我有这个正则表达式:

( |^|>)(((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{2})(-)?( )?)?)([0-9]{7}))|((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{3})(-)?( )?)?)([0-9]{6}))|((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{1})(-)?( )?)?)([0-9]{8})))( |$|<)

它可以格式化荷兰和比利时的电话号码(我只需要这些,因此国家代码为31和32)。
虽然不太容易理解,但正如您所看到的,它也有很多重复。但现在它处理得非常准确。
所有以下欧洲格式的电话号码都被接受。
0031201234567
0031223234567
0031612345678
+31(0)20-1234567
+31(0)223-234567
+31(0)6-12345678
020-1234567
0223-234567
06-12345678
0201234567
0223234567
0612345678

而以下格式不正确的都不是

06-1234567 (mobile phone number in the Netherlands should have 8 numbers after 06 )
0223-1234567 (area code with home phone)

与此相反,这是好的。
020-1234567 (area code with 3 numbers has 7 numbers for the phone as opposed to a 4 number area code which can only have 6 numbers for phone number)

正如您所看到的,'-'字符使它有点困难,但我需要它在其中,因为它是人们通常使用的格式的一部分,并且我想能够解析它们所有。现在我的问题是...您是否看到简化此正则表达式的方法(或者如果您发现错误,甚至改进它),同时保持相同的规则?您可以在regextester.com上测试它。('( |^|>)'用于检查它是否位于单词开头,并可能在前面加上换行符或'>'。我在HTML页面中搜索电话号码。)

我的第一个问题是:你真的需要所有这些捕获吗?你不能只获取重要部分并重新格式化吗?哪些是相关部分? - Axeman
我在一堆文本中搜索电话号码,但不知道号码在哪里以及以何种常规方式格式化。找到后,基本上就不再需要它了。 - youri
5个回答

13

第一点观察:阅读正则表达式是一场噩梦。它呼唤Perl的 /x 模式。

第二点观察:在表达式中有大量的捕获括号(如果我数对了的话就有42个,而42当然是“生命、宇宙和万物的答案”——如果需要解释,请参考道格拉斯·亚当斯的《银河系漫游指南》)。

Bill the Lizard 注意到你多次使用 '(-)?( )?'。与 '-? ?' 或可能的 '[- ]?' 相比,这没有明显的优势,除非你真的想要分别捕获实际的标点符号 (但是由于有如此多的捕获括号,很难弄清楚哪些 '$n' 项将被使用)。

因此,让我们尝试编辑您一行代码的副本:

( |^|>)
(
    ((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{2})(-)?( )?)?)([0-9]{7})) |
    ((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{3})(-)?( )?)?)([0-9]{6})) |
    ((((((\+|00)(31|32)( )?(\(0\))?)|0)([0-9]{1})(-)?( )?)?)([0-9]{8}))
)
( |$|<)

好的 - 现在我们可以看到您的正则表达式的常规结构。

从这里可以进行更多的分析。是的,可以对正则表达式进行大量改进。首先,明显的改进是提取国际前缀部分,并应用一次(可选地,或要求前导零),然后应用国家规则。

( |^|>)
(
    (((\+|00)(31|32)( )?(\(0\))?)|0)
    (((([0-9]{2})(-)?( )?)?)([0-9]{7})) |
    (((([0-9]{3})(-)?( )?)?)([0-9]{6})) |
    (((([0-9]{1})(-)?( )?)?)([0-9]{8}))
)
( |$|<)

然后我们可以像之前注意到的那样简化标点,并删除一些可能是多余的括号,以及改进国家代码识别器:

( |^|>)
(
    (((\+|00)3[12] ?(\(0\))?)|0)
    (((([0-9]{2})-? ?)?)[0-9]{7}) |
    (((([0-9]{3})-? ?)?)[0-9]{6}) |
    (((([0-9]{1})-? ?)?)[0-9]{8})
)
( |$|<)

我们可以观察到,该正则表达式并没有强制要求移动电话代码的规则(例如,并没有坚持要求'06'后面跟着8个数字)。它似乎也允许1、2或3位数字的“交换”代码可选,即使有国际前缀——这可能不是您想要的,修复它会去掉一些括号。在此之后,我们可以进一步删除括号,得到:

( |^|>)
(
    (((\+|00)3[12] ?(\(0\))?)|0)    # International prefix or leading zero
    ([0-9]{2}-? ?[0-9]{7}) |        # xx-xxxxxxx
    ([0-9]{3}-? ?[0-9]{6}) |        # xxx-xxxxxx
    ([0-9]{1}-? ?[0-9]{8})          # x-xxxxxxxx
)
( |$|<)

然后你可以从这里开始进一步优化,我希望如此。


1
谢谢,我已经将它分解开来,以便查看是否可以实现这一点,但我一定做错了什么...感谢您的帮助。 - youri
1
虽然很老的帖子,但我刚看到关于42的那部分...真不错 :P 谢谢伙计 :P - youri
你如何使用PHP和preg_replace使其工作? - Sanne

8

天哪,这太混乱了! :) 如果你有高级语义或业务规则(比如你所描述的欧洲数字、荷兰数字等),最好将单个正则表达式测试分成几个独立的正则表达式测试,每个测试都针对你的高级规则。

if number =~ /...../  # Dutch mobiles
  # ...
elsif number =~ /..../  # Belgian landlines
  # ...
# etc.
end

这样做可以使阅读、维护和更改变得更加容易。

按照最有可能匹配的顺序(假设您足够了解人口统计信息),对测试进行排序。 - tvanfosson

3

(31|32) 看起来不太好。当匹配 32 时,正则表达式引擎将首先尝试匹配 31(2个字符),失败,并回溯两个字符以匹配 31。更有效的方法是首先匹配 3(一个字符),尝试 1(失败),回溯一个字符并匹配 2。

当然,您的正则表达式无法匹配 0800- 开头的号码;它们不是10位数字。


我不需要0800号码,但你评论中的其他部分很有用,谢谢。 - youri

3
将其拆分为多个表达式。例如(伪代码)...
phone_no_patterns = [
    /[0-9]{13}/, # 0031201234567
    /+(31|32)\(0\)\d{2}-\d{7}/ # +31(0)20-1234567
    # ..etc..
]
def check_number(num):
    for pattern in phone_no_patterns:
        if num matches pattern:
            return match.groups

然后您只需循环每个模式,检查每个模式是否匹配。

将模式拆分使得修复特定数字引起的问题变得容易(如果使用单个庞大的正则表达式,这将是可怕的)。


2

这不是一种优化,而是你在使用

(-)?( )?

在你的正则表达式中使用三次这个符号。这将使你匹配到像下面这样的电话号码

+31(0)6-12345678
+31(0)6 12345678

但也将匹配包含破折号和空格的数字,例如
+31(0)6- 12345678

您可以替换 <\p>
(-)?( )?

使用

(-| )?

匹配破折号空格。


那很好。你的解决方案省了一个字符。我在避免多余打字。 :) - Bill the Lizard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接