我在一个正则表达式后,将验证一个完整的复杂的英国邮政编码只在输入字符串。所有不常见的邮政编码形式必须包括以及通常。例如:

匹配

CW3 9不锈钢 SE5 0EG SE50EG Se5 0eg WC2H 7LT

不匹配

aWC2H 7LT WC2H 7LTa WC2H

我怎么解决这个问题?


当前回答

我最近发表了一个关于英国R语言邮政编码的答案。我发现英国政府的正则表达式模式是不正确的,不能正确地验证一些邮政编码。不幸的是,这里的许多答案都是基于这个错误的模式。

我将在下面概述其中的一些问题,并提供一个实际工作的修改后的正则表达式。


Note

我的回答(以及一般的正则表达式):

只验证邮政编码格式。 不能确保邮政编码合法存在。 为此,使用适当的API!更多信息请看本的回答。


如果你不关心糟糕的正则表达式,只想跳到答案,向下滚动到答案部分。

糟糕的正则表达式

不应使用本节中的正则表达式。

这是英国政府提供给开发者的失败正则表达式(不确定这个链接会持续多久,但你可以在他们的批量数据传输文档中看到它):

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$

问题

问题 1 - 复制/粘贴

在这里查看正则表达式的使用。

正如许多开发人员可能会做的那样,他们复制/粘贴代码(特别是正则表达式)并粘贴它们,希望它们能够工作。虽然这在理论上很好,但在这种特殊情况下行不通,因为从这个文档复制/粘贴实际上会将其中一个字符(空格)更改为换行符,如下所示:

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))
[0-9][A-Za-z]{2})$

大多数开发人员会做的第一件事就是毫不犹豫地删除换行符。现在正则表达式将不匹配带有空格的邮政编码(GIR 0AA邮政编码除外)。

要解决这个问题,换行符应该替换为空格字符:

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                                                                                     ^

问题2 -边界

在这里查看正则表达式的使用。

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
^^                     ^ ^                                                                                                                                            ^^

邮政编码正则表达式不正确地锚定正则表达式。如果fooA11 1AA这样的值通过,任何使用这个正则表达式验证邮政编码的人都可能会感到惊讶。这是因为它们锚定了第一个选项的开始和第二个选项的结束(彼此独立),正如上面的正则表达式所指出的那样。

这意味着^(断言在行开始位置)只对第一个选项([Gg][Ii][Rr] 0[Aa]{2})有效,因此第二个选项将验证以邮政编码结尾的任何字符串(不管前面是什么)。

类似地,第一个选项没有锚定到行$的末尾,所以GIR 0AAfoo也被接受。

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$

为了解决这个问题,两个选项都应该被包装在另一个组(或非捕获组)中,并在其周围放置锚点:

^(([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2}))$
^^                                                                                                                                                                      ^^

问题3 -不正确的字符集

在这里查看正则表达式的使用。

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                       ^^

正则表达式在这里缺少一个-来指示字符范围。按照目前的情况,如果邮政编码的格式是ANA NAA(其中a代表字母,N代表数字),并且它的开头不是a或Z,那么它将失败。

这意味着它将匹配A1A 1AA和Z1A 1AA,但不匹配B1A 1AA。

为了解决这个问题,字符-应该放在A和Z之间,在各自的字符集:

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                        ^

问题4 -错误的可选字符集

在这里查看正则表达式的使用。

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                                                                        ^

我发誓他们在网上公布之前都没测试过。他们设置了错误的可选字符集。他们在选项2(组9)的第四个子选项中设置了[0-9]选项。这允许正则表达式匹配格式不正确的邮政编码,如AAA 1AA。

为了解决这个问题,将下一个字符类改为可选的(随后使集合[0-9]精确匹配一次):

^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?)))) [0-9][A-Za-z]{2})$
                                                                                                                                                ^

问题5 -性能

这个正则表达式的性能非常差。首先,他们在开始时放置了最不可能匹配GIR 0AA的模式选项。有多少用户可能使用这个邮政编码而不是其他邮政编码;可能从来没有?这意味着每次使用正则表达式时,它必须先用完这个选项,然后再进行下一个选项。要了解性能如何受到影响,请检查在翻转选项(22)后,原始regex执行的步数(35)相对于相同的regex。

性能的第二个问题是由于整个正则表达式的结构方式。如果一个选项失败了,那么回溯每个选项就没有意义了。当前正则表达式的结构方式可以大大简化。我在答案部分提供了一个修复方法。

问题6:空格

在这里查看正则表达式的使用

这本身可能不被认为是一个问题,但它确实引起了大多数开发人员的关注。正则表达式中的空格不是可选的,这意味着输入邮政编码的用户必须在邮政编码中放置空格。这是一个简单的解决方案,只需添加?空格后显示为可选。有关修复方法,请参阅答案部分。


回答

1. 修复英国政府的正则表达式

修复问题一节中概述的所有问题并简化模式,可以得到以下更短、更简洁的模式。我们还可以删除大多数组,因为我们是将邮编作为一个整体(而不是单个部分)进行验证:

在这里查看正则表达式的使用

^([A-Za-z][A-Ha-hJ-Yj-y]?[0-9][A-Za-z0-9]? ?[0-9][A-Za-z]{2}|[Gg][Ii][Rr] ?0[Aa]{2})$

可以通过删除一个大小写(大写或小写)中的所有范围并使用不区分大小写的标志来进一步缩短这个时间。注意:有些语言没有,所以请使用上面的长一点的。每种语言都以不同的方式实现不区分大小写标志。

在这里查看正则表达式的使用。

^([A-Z][A-HJ-Y]?[0-9][A-Z0-9]? ?[0-9][A-Z]{2}|GIR ?0A{2})$

再次用\d替换[0-9](如果你的regex引擎支持它):

在这里查看正则表达式的使用。

^([A-Z][A-HJ-Y]?\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$

2. 简化的模式

在不确保特定的字母字符的情况下,可以使用以下方法(请记住从1。修复英国政府的正则表达式也被应用在这里):

在这里查看正则表达式的使用。

^([A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$

甚至如果你不关心特殊情况GIR 0AA:

^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$

3.复杂的模式

我不建议过度核实邮编,因为任何时候都可能出现新的地区、地区和街道。我建议做的是,增加对边缘情况的支持。存在一些特殊情况,并在维基百科的这篇文章中概述。

下面是包含3的子节的复杂正则表达式。(3.1, 3.2, 3.3)。

与1中的模式相关。修复英国政府的正则表达式:

在这里查看正则表达式的使用

^(([A-Z][A-HJ-Y]?\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$

和2的关系。简化的模式:

在这里查看正则表达式的使用

^(([A-Z]{1,2}\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$

3.1英国海外领土

维基百科的文章目前陈述(一些格式略微简化):

AI-1111: Anguila ASCN 1ZZ: Ascension Island STHL 1ZZ: Saint Helena TDCU 1ZZ: Tristan da Cunha BBND 1ZZ: British Indian Ocean Territory BIQQ 1ZZ: British Antarctic Territory FIQQ 1ZZ: Falkland Islands GX11 1ZZ: Gibraltar PCRN 1ZZ: Pitcairn Islands SIQQ 1ZZ: South Georgia and the South Sandwich Islands TKCA 1ZZ: Turks and Caicos Islands BFPO 11: Akrotiri and Dhekelia ZZ 11 & GE CX: Bermuda (according to this document) KY1-1111: Cayman Islands (according to this document) VG1111: British Virgin Islands (according to this document) MSR 1111: Montserrat (according to this document)

一个只匹配英国海外领土的包罗万象的正则表达式可能是这样的:

在这里查看正则表达式的使用。

^((ASCN|STHL|TDCU|BBND|[BFS]IQQ|GX\d{2}|PCRN|TKCA) ?\d[A-Z]{2}|(KY\d|MSR|VG|AI)[ -]?\d{4}|(BFPO|[A-Z]{2}) ?\d{2}|GE ?CX)$

3.2英国军队邮局

虽然他们最近已经将其更改为更好地与英国邮政编码系统保持一致的bf#(其中#代表一个数字),但它们被认为是可选的替代邮政编码。这些邮政编码遵循BFPO的格式,后面跟着1-4位数字:

在这里查看正则表达式的使用

^BFPO ?\d{1,4}$

3.3圣诞老人?

还有一个关于圣诞老人的特殊情况(在其他答案中提到过):SAN TA1是有效的邮政编码。一个正则表达式非常简单:

^SAN ?TA1$

其他回答

这个允许两边有空格和制表符,以防你不想验证失败,然后在另一边修剪它。

^\s*(([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) {0,1}[0-9][A-Za-z]{2})\s*$)

我有英国邮政编码验证的正则表达式。

这是适用于所有类型的邮政编码,无论是内部或外部

^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))) || ^((GIR)[ ]?(0AA))$|^(([A-PR-UWYZ][0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$|^(([A-PR-UWYZ][0-9][0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$|^(([A-PR-UWYZ][A-HK-Y0-9][0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$|^(([A-PR-UWYZ][A-HK-Y0-9][0-9][0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$|^(([A-PR-UWYZ][0-9][A-HJKS-UW0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$|^(([A-PR-UWYZ][A-HK-Y0-9][0-9][ABEHMNPRVWXY0-9])[ ]?([0-9][ABD-HJLNPQ-UW-Z]{0,2}))$

这适用于所有类型的格式。

例子:

Ab10 -------------------->仅为外部邮政编码 A1 1 aa ------------------> (内部和外部)邮政编码的组合 WC2A --------------------> 外

通过经验测试和观察,以及https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation的确认,以下是我的Python正则表达式版本,可以正确地解析和验证英国邮政编码:

UK_POSTCODE_REGEX = r ' (? P < postcode_area > [a - z] {1,2}) (? P <区> (?:[0 - 9]{1,2})| (?:[0 - 9][a - z])) (? P <部门> [0 - 9])(? P <邮编> [a - z]{2})”

这个正则表达式很简单,并且有捕获组。它不包括所有合法的英国邮政编码的验证,而只考虑字母与数字的位置。

下面是我在代码中如何使用它:

@dataclass
class UKPostcode:
    postcode_area: str
    district: str
    sector: int
    postcode: str

    # https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation
    # Original author of this regex: @jontsai
    # NOTE TO FUTURE DEVELOPER:
    # Verified through empirical testing and observation, as well as confirming with the Wiki article
    # If this regex fails to capture all valid UK postcodes, then I apologize, for I am only human.
    UK_POSTCODE_REGEX = r'(?P<postcode_area>[A-Z]{1,2})(?P<district>(?:[0-9]{1,2})|(?:[0-9][A-Z]))(?P<sector>[0-9])(?P<postcode>[A-Z]{2})'

    @classmethod
    def from_postcode(cls, postcode):
        """Parses a string into a UKPostcode

        Returns a UKPostcode or None
        """
        m = re.match(cls.UK_POSTCODE_REGEX, postcode.replace(' ', ''))

        if m:
            uk_postcode = UKPostcode(
                postcode_area=m.group('postcode_area'),
                district=m.group('district'),
                sector=m.group('sector'),
                postcode=m.group('postcode')
            )
        else:
            uk_postcode = None

        return uk_postcode


def parse_uk_postcode(postcode):
    """Wrapper for UKPostcode.from_postcode
    """
    uk_postcode = UKPostcode.from_postcode(postcode)
    return uk_postcode

下面是单元测试:

@pytest.mark.parametrize(
    'postcode, expected', [
        # https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation
        (
            'EC1A1BB',
            UKPostcode(
                postcode_area='EC',
                district='1A',
                sector='1',
                postcode='BB'
            ),
        ),
        (
            'W1A0AX',
            UKPostcode(
                postcode_area='W',
                district='1A',
                sector='0',
                postcode='AX'
            ),
        ),
        (
            'M11AE',
            UKPostcode(
                postcode_area='M',
                district='1',
                sector='1',
                postcode='AE'
            ),
        ),
        (
            'B338TH',
            UKPostcode(
                postcode_area='B',
                district='33',
                sector='8',
                postcode='TH'
            )
        ),
        (
            'CR26XH',
            UKPostcode(
                postcode_area='CR',
                district='2',
                sector='6',
                postcode='XH'
            )
        ),
        (
            'DN551PT',
            UKPostcode(
                postcode_area='DN',
                district='55',
                sector='1',
                postcode='PT'
            )
        )
    ]
)
def test_parse_uk_postcode(postcode, expected):
    uk_postcode = parse_uk_postcode(postcode)
    assert(uk_postcode == expected)

上面的一些正则表达式有点限制性。请注意真正的邮政编码:“W1K 7AA”将失败,因为上面的“位置3 - AEHMNPRTVXY仅使用”规则将不允许“K”。

正则表达式:

^(GIR 0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]|[A-HK-Y][0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9][A-HJKPS-UW])[0-9][ABD-HJLNP-UW-Z]{2})$

似乎更准确一点,请参阅维基百科上题为“英国的邮政编码”的文章。

注意,这个正则表达式只要求大写字符。

更大的问题是,您是限制用户输入,只允许实际存在的邮政编码,还是只是试图阻止用户在表单字段中输入完全的垃圾。正确匹配每一个可能的邮政编码,并在未来校对,是一个更难的难题,除非你是HMRC,否则可能不值得这么做。

我从一个XML文档中窃取了这个,它似乎涵盖了没有硬编码的GIRO的所有情况:

%r{[A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][A-Z]{2}}i

(Ruby语法忽略大小写)