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

匹配

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$

其他回答

前半段邮政编码有效格式

[a - z] [a - z][0 - 9]的[a -ž] [a - z] [a - z] [0 - 9] [0 - 9] [a - z] [0 - 9] [0 - 9] [a - z] [a - z] [0 - 9] [a - z] [a - z]的[a -ž] [a - z][0 - 9]的[a -ž] [a - z] [0 - 9]

异常 位置1 - QVX未使用 位置2 -除GIR 0AA外,IJZ不使用 位置3 - AEHMNPRTVXY只使用 位置4 - ABEHMNPRVWXY

邮政编码的后半部分

[0 - 9] [a - z]的[a -ž]

异常 位置2+3 - CIKMOV未使用

记住,不是所有可能的代码都被使用了,所以这个列表是有效代码的必要条件,而不是充分条件。只是匹配所有有效代码的列表可能会更容易?

根据维基百科的表格

这种模式适用于所有情况

(?:[A-Za-z]\d ?\d[A-Za-z]{2})|(?:[A-Za-z][A-Za-z\d]\d ?\d[A-Za-z]{2})|(?:[A-Za-z]{2}\d{2} ?\d[A-Za-z]{2})|(?:[A-Za-z]\d[A-Za-z] ?\d[A-Za-z]{2})|(?:[A-Za-z]{2}\d[A-Za-z] ?\d[A-Za-z]{2})

当在Android / Java上使用它时,使用\\d

通过经验测试和观察,以及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)

我一直在寻找一个英国邮政编码正则表达式的最后一天左右,无意中发现了这个线程。我尝试了上面的大部分建议,但没有一个对我有用,所以我想出了自己的正则表达式,据我所知,它捕获了截至1月13日的所有有效的英国邮政编码(根据皇家邮政的最新文献)。

The regex and some simple postcode checking PHP code is posted below. NOTE:- It allows for lower or uppercase postcodes and the GIR 0AA anomaly but to deal with the, more than likely, presence of a space in the middle of an entered postcode it also makes use of a simple str_replace to remove the space before testing against the regex. Any discrepancies beyond that and the Royal Mail themselves don't even mention them in their literature (see http://www.royalmail.com/sites/default/files/docs/pdf/programmers_guide_edition_7_v5.pdf and start reading from page 17)!

注意:在皇家邮政自己的文献中(链接以上),第3和第4位的位置略有模糊,如果这些字符是字母,则例外。我直接联系了皇家邮政,用他们自己的话说,“AANA NAA格式的出境代码的第4个位置的信件没有例外,而第3个位置的例外只适用于ANA NAA格式的出境代码的最后一个字母。”直接从马嘴里说出来的!

<?php

    $postcoderegex = '/^([g][i][r][0][a][a])$|^((([a-pr-uwyz]{1}([0]|[1-9]\d?))|([a-pr-uwyz]{1}[a-hk-y]{1}([0]|[1-9]\d?))|([a-pr-uwyz]{1}[1-9][a-hjkps-uw]{1})|([a-pr-uwyz]{1}[a-hk-y]{1}[1-9][a-z]{1}))(\d[abd-hjlnp-uw-z]{2})?)$/i';

    $postcode2check = str_replace(' ','',$postcode2check);

    if (preg_match($postcoderegex, $postcode2check)) {

        echo "$postcode2check is a valid postcode<br>";

    } else {

        echo "$postcode2check is not a valid postcode<br>";

    }

?>

我希望它能帮助其他遇到这条线索寻找解决方案的人。

看起来我们将使用^(GIR ?0AA|[a - pr - uwyz]([0-9]{1,2}|([a - hk - y][0-9]([0-9ABEHMNPRV-Y])?)|[0-9][a - hjkps - uw])? [0-9][ABD-HJLNP-UW-Z]{2})$,这是上面Minglis建议的略有修改的版本。

然而,我们将不得不调查到底是什么规则,因为上面列出的各种解决方案似乎适用于不同的规则,哪些字母是允许的。

经过一番研究,我们找到了更多的信息。显然,“govtalk.gov.uk”上的一个页面会指向邮政编码规范govtalk-postcodes。它指向XML schema中的一个XML模式,该模式提供了邮政编码规则的“伪正则表达式”语句。

我们用它做了一些修改,得到了下面的表达式:

^((GIR &0AA)|((([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][ABEHMNPRV-Y]))) &[0-9][ABD-HJLNP-UW-Z]{2}))$

这使得空格是可选的,但限制您只能使用一个空格(将'&'替换为'{0,}表示无限空格)。它假定所有文本都必须是大写的。

如果你想要允许小写,任意数量的空格,使用:

^(([gG][iI][rR] {0,}0[aA]{2})|((([a-pr-uwyzA-PR-UWYZ][a-hk-yA-HK-Y]?[0-9][0-9]?)|(([a-pr-uwyzA-PR-UWYZ][0-9][a-hjkstuwA-HJKSTUW])|([a-pr-uwyzA-PR-UWYZ][a-hk-yA-HK-Y][0-9][abehmnprv-yABEHMNPRV-Y]))) {0,}[0-9][abd-hjlnp-uw-zABD-HJLNP-UW-Z]{2}))$

这并不包括海外领土,只是强制执行格式,而不是不同地区的存在。它基于以下规则:

可接受以下格式:

“秋天” A9 9 zz A99 9 zz AB9 9 zz AB99 9 zz A9C 9 zz AD9E 9 zz

地点:

9可以是任何一位数。 A可以是除Q、V或X之外的任何字母。 B可以是除I、J或Z之外的任何字母。 C可以是除I、L、M、N、O、P、Q、R、V、X、Y或Z之外的任何字母。 D可以是除I、J或Z之外的任何字母。 E可以是A, B, E, H, M, N, P, R, V, W, X或Y中的任意一个。 Z可以是C、I、K、M、O或V之外的任何字母。

最好的祝愿

科林