【问题标题】:How to format a string based on a regular expression?如何根据正则表达式格式化字符串?
【发布时间】:2016-05-09 21:07:47
【问题描述】:

我正在编写一个 PHP 应用程序,它从 API(我们称之为 A)获取数据并写入另一个 API(我称之为 B)。我在一个特定的领域苦苦挣扎:邮政编码。

API A 将所有邮政编码作为 7 位字符串返回,不带任何分隔符。如果特定邮政编码的位数少于 7 位,则会在左侧填充 0(零)。这样,50-224(来自波兰的邮政编码)变为0050224。我无法控制此输出,并且可能以这种方式存储。我知道这是波兰邮政编码,因为回复中还提供了国家/地区代码PL

问题在于 API B 会验证邮政编码并需要正确的格式。

我找到了一个PHP library on GitHub,它有一个带有每个国家/地区邮政编码格式的正则表达式。像这样:resources/address_format/PL.json

我想要做的是使用该库提供的表达式来格式化 A 返回的值。

我当前的代码如下所示:

use CommerceGuys\Addressing\Repository\AddressFormatRepository;

$country = 'US';
$postalcode = '0031401';
$repo = new AddressFormatRepository();
$pattern = $repo
    ->get($country)
    ->getPostalCodePattern()
    ;
$postalcode = preg_replace(
    '/^.*(' . $pattern . ')$/',
    '$1',
    $potalcode
);

对于上述情况,美国邮政编码,它可以正常工作,因为代码的第二部分在表达式中是可选的:(\d{5})(?:[ \-](\d{4}))?。当其他国家出现时,我开始遇到问题,特别是邮政编码中包含字母和数字以外的其他字符。

顺便说一句,我在 S.O. 上查看了几个问题,但是,他们似乎都没有询问我想要实现的目标。

更新

尽管上面是波兰语示例,但我的代码应该适用于任何国家/地区。我只是想提供一些关于我正在尝试做的事情的背景。正如我在问题标题中所述,我希望利用寻址库中的正则表达式。

更多示例,来自其他国家/地区:

Country | Postal code
--------+------------
PH      | 0002010
LB      | 0001201
JO      | 0000962

【问题讨论】:

  • 简而言之 - 你不能使用匹配模式来格式化邮政编码,api A 是错误的,不要使用它(即使你必须 - 不要使用它)
  • 您能否确认此波兰邮政编码(如果存在)50-1 将由 api A 这样存储:0050001
  • @CasimiretHippolyte 不幸的是,我无法向 API A 添加新数据。但我认为它会存储为0000501。无论如何,您的示例似乎不是有效的邮政编码。为了更清楚起见,我的代码应该适用于任何国家/地区。
  • 您不能使用这些模式。您需要为每种可能的格式编写自己的模式。
  • 正则表达式用于验证字符串,而不是格式化它。一个处理输入,另一个处理输出。您的库正在向preg_match 提供第一个参数,但您必须提供第二个参数。您的第一个 API 似乎非常缺乏。巴西邮政编码或美国 ZIP+4 将如何处理?

标签: php regex


【解决方案1】:

您可以从正则表达式生成所有可能的组合。例如,Faker 使用其regexify 格式化程序。

问题在于有效的邮政编码是可能匹配的子集。例如,美国 5 位邮政编码正则表达式 (\d{5}) 产生 100,000 个候选人,但只有(大约)43,000 5 位邮政编码。

在我看来,这听起来像是 GIGO - Garbage In, Garbage Out 的经典案例。您将获得一个非规范化数据点,并要求您根据第一原则对其进行规范化。这很难。有时是不可能的。

如果我是你,我会从一个简单的格式列表开始,例如基于联合国列表的 this one(或 this one,如果原件离线)。然后从您的输入中一次拉出一个字符,反向并匹配它。举个例子吧。

API A 告诉您0001201 是利比里亚。从列表中,您可以看到 Liberia 的格式为 9999。反转这两个字符串:分别为10210009999。现在一次走一个字符的格式,匹配。格式中的第一个字符是9,它是一个数字占位符。反向输入的第一个字符是数字吗?是的:1,记住这一点。好的,第二个角色。 90,零匹配所以记住它。重复直到我们用完格式或输入,或者我们在格式上遇到不匹配。

在此示例中,我们将在输入数字之前用完格式数字,并且我们不会遇到错误,发现反向输入 1021 与反向格式 9999 匹配。所以我们完成了,现在对匹配进行最后的反转:1021 变为 1201,这是一个有效的利比里亚邮政编码。

【讨论】:

  • 我尝试使用格式列表访问该链接,但现在无法正常工作。我喜欢这种处理问题的方式,但是,我不想手动创建格式列表,除非它是最后也是唯一的选择。所以我给了 Solarflare 的答案,因为它使用了一个已经编译好的源代码。
  • @GustavoStraube 完全不要用手做机器可以做的事情。这是the link from web.archive.org,所有邮政编码都可以使用。
【解决方案2】:

正如其他人指出的那样,没有从正则表达式中获取原始文本的通用方法,因为通常有很多可能性。

但是,由于您拥有“原始文本”的数字,因此您可以重新创建文本,以防这些特定数字是模式中唯一缺少的信息;例如,在您的波兰语示例\d{2}-\d{3} 中,您可以将模式中的 \d{2} 和 {3} 替换为来自 api A 的邮政编码的 2 位和 3 位数字,并且该模式将为您提供额外的“-”。

无法重构的案例示例:

  • SO:"[A-Z]{2}[ ]?\d{5}" 因为你没有从 api A 得到字母,所以你不能重建它们。
  • BR:“\d{5}[\-]?\d{3}”因为您没有从 api A 获得 8 位数字。
  • 任何带有可选内容的东西,原因,嗯,没有定义这些选项中的哪一个是正确的。可能有几种有效的解决方案可能取决于特殊条件(例如,对于拥有超过 10000 座房屋或类似情况的城市,您必须在 \d{4}(-\d{3})? 中使用额外的 3 位数字,或者您必须在 @987654324 中使用 - @ 仅用于州首府,或者您可以随意使用它。)这包括像 \d{1-4} 这样的术语,因为长度可能取决于其他值。如果代码中允许使用前导 0,您可能会遇到问题:对于输入 00000011010010001 可能是 \d{1-4} 的正确解决方案(虽然我会假设在实践中前导 0 只会以固定长度发生);对于\d{4}(-\d{3})?0001002 可能意味着0001-001(大城市)或1001(小城市)。

在这些(以及所有)情况下获取正确邮政编码的常用方法是按城市和街道名称在数据库中查找。 (您可以从当地邮政服务购买此类数据库的访问权限,或从例如 openstreetmap-data 创建数据库)。

话虽如此,这里有一些示例代码将重建仅缺少固定位数的代码,例如PL (\d{2}-\d{3})。它也适用于 FK(“FIQQ 1ZZ”)等模式,只要 A 中的代码为“0000001”。我认为它适用于大约 50%-60% 的国家/地区。

use CommerceGuys\Addressing\Repository\AddressFormatRepository;

$country = 'PL';
$postalcodeA = '0031401';
$repo = new AddressFormatRepository();
$pattern = $repo
    ->get($country)
    ->getPostalCodePattern()
    ;

$ok = 1;
$pospattern = 0;
$posA = 0;
$postalcodeB = '';

while ( ($pospattern < strlen($pattern)) and ($ok==1) ) {
  $pospattern += 1;
  $charact = substr($pattern, -$pospattern,1);
  if (strcmp($charact,'}') == 0) {
    if (strcmp(substr($pattern, -$pospattern - 4, 3),'\d{') == 0) {
      $cnt = substr($pattern, -$pospattern - 1,1);
      $postalcodeB = substr($postalcodeA, -$posA - $cnt, $cnt) . $postalcodeB;
      $posA += $cnt;
      $pospattern += 4;
    } else { $ok = 0; }
  } elseif ( ctype_digit($charact) ) {
    if ( strcmp($charact,substr($postalcodeA,-$posA-1,1)) !== 0) {
      $ok = 0;
    }
    $postalcodeB = $charact . $postalcodeB;
    $posA += 1;
  } elseif ( preg_match('/[\(\)\[\]\{\}\$\?\\\]/', $charact) ) {
    $ok = 0;
  } else {
    $postalcodeB = $charact . $postalcodeB;
  }
}

# USE WITH CARE! READ INFO!
# if ($ok == 0) {
#  $postalcodeB = preg_replace(
#    '/^.*(' . $pattern . ')$/',
#        '$1',
#            $postalcodeA
#            );
#  if (strcmp($postalcodeA,$postalcodeB) !== 0) {
#    $ok = 1;
#  }
#}

if (!preg_match('/^' . $pattern . '$/', $postalcodeB)) {
  $ok = 0;
}

if (!$ok) {
  echo "Pattern ",$pattern," not supported or no match to ",$postalcodeA,"\r\n";
} else {
  echo "Pattern ",$pattern," ok: ",$postalcodeA," -> ",$postalcodeB,"\r\n";
}

它将从字符串的末尾开始,将模式中每次出现的\d{n} 替换为 n 位。 如果它不理解模式(例如,因为它有可选的东西),你可能想试试preg_replace。我不会使用它(并将其注释掉),因为它会给您带来不可预测和错误的随机结果(请参见下面的波士顿市政厅示例),但我添加了它以防您想使用它,因为您例如可以确保 api A 的客户端永远不会允许输入 zip+4 代码。 作为最后一步,它将验证结果是否符合模式。

您可以轻松添加对\d(一位数)的支持。

您可以尝试通过例如添加对 \d{1-4} 等术语的支持。检查 api A 有多少位数以及在其他术语中没有使用多少位数,并使用剩余的位数(例如,\d{2}-\d{1-4} 与输入 0001245 有 4 位数字,第一个术语使用 2 \d{2} 所以它有 2 位数字对于\d{1-4},请记住我上面写的内容:如果零是开头的允许数字,您可能会得到错误的结果,例如00-124501-24512-34 可能是有效的结果(在此情况下,如果不在数据库中查找城市名称,您将无法恢复代码)。您将遇到\d{1-2}-\d{2-3} 的麻烦。

您应该添加最终检查以查看数字的数量是否适合 A 中的数字(例如,您可能希望连接结果中的所有数字并检查此字符串是否是由 A 给出的代码,用零填充)。这将防止您因例如preg_replace\d{1-2} 或其他可选内容。 例如,有人为波士顿市政厅输入了US zip+4 代码,即02201-1020。你的 api A 会给你0220110,或者更糟糕的是2011020,而preg_replace 会给你2011011020,这两者都是完全错误的(02201 可能是一个可以接受的妥协,但是您将无法生成此结果)。

然后,您可以使用随机代码为每个国家/地区运行一次,然后检查不起作用的模式。其中一些将不起作用,因为代码不正确(例如,FK 仅在输入为 0000001 时才起作用,而随机输入通常不是这种情况)。

如果你幸运的话,你不需要这些国家。

或者,作为最后的手段,您也许可以重写一些剩余的错误,但这需要一些手动工作:

一些模式将包含可选的东西,例如\d{2}[-]?\d{2}。对于这些情况,您可以检查 - 是否取决于例如在某些数字或城市名称上,或者如果它真的是可选的。如果它真的是可选的,你必须决定是否需要-,然后将其保存为新模式,例如\d{2}-\d{2}。但在大多数情况下,您无法进行一般替换,例如对于US,您可能决定省略+4,但如果客户输入波士顿市政厅的(正确)邮政编码+4,您仍然无法获得正确的结果,请参见上面的示例。

对于其他模式,可能存在一些允许的可能性,例如\d{4}|A-\d{3}。对于这种情况,您可能能够创建 2 个模式,例如\d{4}A-\d{3}。你可以做同样的事情,例如\d{2}(-\d{2})? 并手动生成\d{2}\d{2}-\d{2} 两种模式。然后,您必须为一个国家/地区测试所有这些模式(将整个事情放在一个 while 循环中并为每个子模式执行它)并选择适合的第一个。如果模式使用 A 中的所有给定数字并完成最终模式测试,则该模式将适合。尽管如果允许前导零,这通常会再次失败:输入0000123 可能意味着0123A-123,因此如果允许零,您可能必须检查其他资源(以及与波士顿市政厅类似的问题可能仍会发生)。但是这样你也许可以重建更多的国家。

但在大多数情况下,如果不在数据库中查找它们,就无法重写它们,甚至无法手动生成特定的邮政编码。

【讨论】:

  • 不错的方法!我尝试了一些启用了regex_replace 部分的邮政编码,我得到了一个空邮政编码的匹配项,然后我添加了一个额外的检查:if (!empty($postalcodeB) &amp;&amp; strcmp($postalcodeA,$postalcodeB) !== 0) { $ok =1; }。此外,对于来自 PH 的代码 0002010,无法正确格式化。正则表达式太复杂了。然后我简单地从左边修剪零:if ($ok == 0) { $postalcodeB = ltrim($postalcodeA, '0'); $ok = 1; }。由于它最后运行preg_match 来检查邮政编码,我认为这不是问题。谢谢顺便说一句!
【解决方案3】:
/*Try this out to format your postal code*/

/* preg_replace(pattern, Replacement,values) */
  $result = preg_replace('/(\d{3})(\d{3})$/', '$1-$2', '0050224');

 echo substr($result, 2);

// Out put : 050-224

点击给定的链接了解更多关于preg_replace的信息

【讨论】:

  • 您好,谢谢您的回答。不幸的是,我不想写新的表达方式。使用我提到的 lib 中的表达式会很棒。或者我可以使用的任何其他来源,而无需为每个国家/地区编写新的正则表达式。例如,对于波兰的情况,我有表达式\d{2}-\d{3}
  • 顺便说一句,您的正则表达式应该是 /(\d{3})(\d{3})$/ 以给出 050-224 作为结果。
【解决方案4】:

你可以用老式的方式来做,手工

将该库中的所有模式转储到一个文本文件中。
剪掉标点符号。在
周围放置捕获组 用标点分隔的部分。创建一个替代品。

Country            Regex Validation         Regex Conversion
                                                  Find               Replace
---------------------------------------------------------------------------------
NL Netherlands     \d{4}[ ][A-Z]{2}         (\d{4})([A-Z]{2})$        $1 $2         
 9999 AA

NI Nicaragua       \d{3}-\d{3}-\d           (\d{3})(\d{3})(\d)$       $1-$2-$3
 999-999-9

US United States   \d{5}                    (\d{5})$                  $1
 99999

SH Saint Helena    [A-Z]{4}[ ]\d[A-Z]{2}    ([A-Z]{4})(\d[A-Z]{2})$   $1 $2
 TDCU 1ZZ 

JM Jamaica         [A-Z]{5}\d{2}            ([A-Z]{5}\d{2})$          $1
 JMAAA99

【讨论】:

  • 这绝对是一种选择,但我想避免这种工作。无论如何,感谢您提供帮助。
  • @GustavoStraube - 您没有其他选择。讨厌成为坏消息的承担者。这是一次性的事情,你将无法避免它,对不起,伙计..
猜你喜欢
  • 2017-05-10
  • 1970-01-01
  • 1970-01-01
  • 2018-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-15
相关资源
最近更新 更多