修复澳大利亚/新西兰电话号码的Python正则表达式
我有一个Python脚本,用来处理包含用户输入的电话号码的CSV文件。因为用户输入的格式可能会很奇怪,所以我们需要把这些号码拆分成不同的部分,并且修正一些常见的输入错误。
我们的电话号码是来自澳大利亚的悉尼或墨尔本,或者新西兰的奥克兰,格式是国际标准的。
标准的悉尼号码看起来是这样的:
+61(2)8328-1972
它有国际区号+61
,后面是一个用括号括起来的地区代码2
,然后是本地号码的两部分,用连字符分开,像这样8328-1972
。
墨尔本的号码只是在地区代码上用3
代替2
,例如:
+61(3)8328-1972
奥克兰的号码也类似,但它们的本地号码部分是7位(前3位后4位),而不是正常的8位。
+64(9)842-1000
我们还处理了一些常见错误的匹配。我把正则表达式分成了自己的类。
class PhoneNumberFormats():
"""Provides compiled regex objects for different phone number formats. We put these in their own class for performance reasons - there's no point recompiling the same pattern for each Employee"""
standard_format = re.compile(r'^\+(?P<intl_prefix>\d{2})\((?P<area_code>\d)\)(?P<local_first_half>\d{3,4})-(?P<local_second_half>\d{4})')
extra_zero = re.compile(r'^\+(?P<intl_prefix>\d{2})\(0(?P<area_code>\d)\)(?P<local_first_half>\d{3,4})-(?P<local_second_half>\d{4})')
missing_hyphen = re.compile(r'^\+(?P<intl_prefix>\d{2})\(0(?P<area_code>\d)\)(?P<local_first_half>\d{3,4})(?P<local_second_half>\d{4})')
space_instead_of_hyphen = re.compile(r'^\+(?P<intl_prefix>\d{2})\((?P<area_code>\d)\)(?P<local_first_half>\d{3,4}) (?P<local_second_half>\d{4})')
我们有一个用于标准格式的号码,还有其他一些用于各种常见错误的情况,比如在地区代码前面多加一个零(02
而不是2
),或者本地号码部分缺少连字符(例如83281972
而不是8328-1972
)等等。
然后我们通过一系列的if/elif语句来调用这些匹配:
def clean_phone_number(self):
"""Perform some rudimentary checks and corrections, to make sure numbers are in the right format.
Numbers should be in the form 0XYYYYYYYY, where X is the area code, and Y is the local number."""
if not self.telephoneNumber:
self.PHFull = ''
self.PHFull_message = 'Missing phone number.'
else:
if PhoneNumberFormats.standard_format.search(self.telephoneNumber):
result = PhoneNumberFormats.standard_format.search(self.telephoneNumber)
self.PHFull = '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half')
self.PHFull_message = ''
elif PhoneNumberFormats.extra_zero.search(self.telephoneNumber):
result = PhoneNumberFormats.extra_zero.search(self.telephoneNumber)
self.PHFull = '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half')
self.PHFull_message = 'Extra zero in area code - ask user to remediate.'
elif PhoneNumberFormats.missing_hyphen.search(self.telephoneNumber):
result = PhoneNumberFormats.missing_hyphen.search(self.telephoneNumber)
self.PHFull = '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half')
self.PHFull_message = 'Missing hyphen in local component - ask user to remediate.'
elif PhoneNumberFormats.space_instead_of_hyphen.search(self.telephoneNumber):
result = PhoneNumberFormats.missing_hyphen.search(self.telephoneNumber)
self.PHFull = '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half')
self.PHFull_message = 'Space instead of hyphen in local component - ask user to remediate.'
else:
self.PHFull = ''
self.PHFull_message = 'Number didn\'t match recognised format. Original text is: ' + self.telephoneNumber
我的目标是尽量让匹配变得严格,同时至少能捕捉到常见的错误。
不过,我上面做的有几个问题:
- 我用
\d{3,4}
来匹配本地号码的前半部分。但理想情况下,我们只想在新西兰号码(即以+64(9)
开头)时捕捉到3位数的前半部分。这样,我们就可以标记缺少数字的悉尼/墨尔本号码。我可以把奥克兰号码单独分成一个正则表达式,但这样就无法捕捉到与错误情况(多余的零、缺少连字符、用空格代替连字符)结合的号码。所以,除非我为奥克兰号码重新创建一个版本,比如auckland_extra_zero
,但这似乎太重复了,我不知道怎么简单地解决这个问题。 - 我们没有捕捉到错误组合的情况,比如如果有一个多余的零和一个缺少的连字符,我们就无法识别。这有没有简单的方法用正则表达式来做到这一点,而不需要明确创建不同错误的排列组合?
我想解决上面两个问题,并希望能稍微收紧一下,以捕捉到我遗漏的任何情况。有没有更聪明的方法来实现我上面尝试的目标?
谢谢,
Victor
附加说明:
以下内容只是为了提供一些背景信息:
这个脚本是为一家全球公司设计的,悉尼、墨尔本和奥克兰各有一个办公室。
这些号码来自内部的员工活动目录(也就是说,这不是客户名单,而是我们自己的办公室电话)。
因此,我们并不是在寻找一个通用的澳大利亚电话号码匹配脚本,而是想要一个通用的脚本来解析来自三个特定办公室的号码。一般来说,只有最后四个数字应该不同。
手机号码不是必需的。
这个脚本的目的是解析活动目录的CSV导出,并将号码重新格式化为另一个程序(QuickComm)所需的可接受格式。
这个程序来自外部供应商,需要的号码格式正是我在上面的代码中生成的格式——这就是为什么号码会显示为0283433422。
我写的脚本不能更改记录,只能在它们的CSV导出上工作——记录存储在活动目录中,唯一能访问并修正它们的方法是给员工发邮件,让他们登录并更改自己的记录。
所以这个脚本是由一个助理运行的,以生成这个程序所需的输出。她/他还会得到一份格式不正确的号码的人员名单——因此有关于要求用户修正的消息。理论上,这样的人应该很少。然后我们会给这些员工发邮件或打电话,要求他们修正自己的记录——这个脚本每月运行一次(号码可能会变化),我们还需要标记那些新员工,以防他们输入错误的记录。
@John Macklin:你是建议我放弃正则表达式,直接从字符串中提取特定位置的数字吗?
我在寻找一种方法来捕捉常见错误的组合情况(例如,用空格代替连字符,加上多余的零),但这是否不容易实现?
3 个回答
电话号码这样格式化是为了让人们更容易记住,实际上没有必要把它存储成那样。为什么不直接用逗号分开,把每个数字提取出来,只保留数字部分呢?
>>> import string
>>> def parse_number(number):
n = ''
for x in number:
if x in string.digits:
n += x
return n
一旦你这样处理了,就可以根据国际电话区号和地区代码进行验证。(比如如果第三位是3,那后面应该还有7位数字,等等)
验证完后,拆分成各个部分就简单了。前两位是前缀,接下来的是地区代码,等等。你可以检查一些常见的错误,而不需要使用复杂的正则表达式。输出结果在这种情况下也很简单。
别用复杂的正则表达式。把除了数字以外的所有东西都删掉——非数字的部分容易出错。如果第三个数字是0,就把它删掉。
你应该期待以61开头,后面跟着有效的澳大利亚区号(一般来说是[23478],注意4是手机号码),然后是8个数字;或者以64开头,后面跟着有效的新西兰区号(不管是什么),再接7个数字。其他的都不行。在有效的号码中,适当地插入+()-这些符号。
顺便说一下,区号2是新南威尔士州和澳大利亚首都地区的,不仅仅是悉尼;区号3是维多利亚州和塔斯马尼亚州的。现在很多人没有固定电话,只有手机,而且人们通常会比固定电话或邮寄地址更长时间保持同一个手机号码,所以手机号码在模糊匹配客户记录时非常有用——所以我很好奇你为什么不包括它们。
以下内容会告诉你关于澳大利亚和新西兰电话号码格式的所有信息,甚至更多。
关于正则表达式的评论:
(1) 你使用了带有“^”前缀的搜索方法。用没有前缀的匹配方法会更简单一些。
(2) 你似乎没有检查电话号码字段后面是否有多余的内容:
>>> import re
>>> standard_format = re.compile(r'^\+(?P<intl_prefix>\d{2})\((?P<area_code>\d)\
)(?P<local_first_half>\d{3,4})-(?P<local_second_half>\d{4})')
>>> m =standard_format.search("+61(3)1234-567890whoopsie")
>>> m.groups()
('61', '3', '1234', '5678')
>>>
你可以选择(a) 在一些正则表达式的结尾加上\Z(而不是 $),这样它们就不会在后面有多余内容时匹配成功,或者(b) 增加另一个组来捕捉多余的内容。
还有一个社会工程的评论:你有没有测试过用户对工作人员执行这个指令的反应:“本地部分用空格代替连字符——请用户自行修正”?难道脚本不能直接修复并继续吗?
还有关于代码的一些评论:
self.PHFull代码
(a) 非常重复(如果你必须使用正则表达式,把它们放在一个列表中,配上相应的操作代码和错误信息,然后遍历这个列表)。
(b) 对于“错误”情况和标准情况是一样的(那你为什么要让用户“自行修正”呢???)
(c) 把国家代码丢掉,替换成0,也就是说你标准的 +61(2)1234-5678 被保留为 0212345678,真让人抓狂……即使你在地址中存储了国家信息,如果一个新西兰人移民到澳大利亚,地址更新了但电话号码没变,那也没用。请不要说你依赖于当前(没有新西兰客户在奥克兰以外的地方???)区号的不重叠……
在完整故事揭晓后的更新
对你和员工来说,保持简单。使用Active Directory的员工的指示应该是(根据不同的办公室)“填写+61(2)9876-7
,后面跟上你的3位分机号码”。如果他们尝试几次都做不好,那就该让他们接受培训了。
所以你每个办公室用一个正则表达式,填入固定部分,比如说悉尼办公室的号码格式是+61(2)9876-7ddd
,你就用正则表达式r"\+61\(2\)9876-7\d{3,3}\Z"
。如果正则表达式匹配成功,就去掉所有非数字的部分,然后用"0" + the_digits[2:]
来处理下一个应用。如果没有正则表达式匹配,就发个警告。