使用PHP /正则表达式验证美国电话号码

12

编辑:我混合并修改了下面两个答案,形成了完整的函数,现在它可以实现我想要的功能,甚至更多...所以我想把它发布在这里,以防其他人寻找同样的东西。

/*
 * Function to analyze string against many popular formatting styles of phone numbers
 * Also breaks phone number into it's respective components
 * 3-digit area code, 3-digit exchange code, 4-digit subscriber number
 * After which it validates the 10 digit US number against NANPA guidelines
*/
function validPhone($phone) {

  $format_pattern = '/^(?:(?:\((?=\d{3}\)))?(\d{3})(?:(?<=\(\d{3})\))?[\s.\/-]?)?(\d{3})[\s\.\/-]?(\d{4})\s?(?:(?:(?:(?:e|x|ex|ext)\.?\:?|extension\:?)\s?)(?=\d+)(\d+))?$/';
  $nanpa_pattern = '/^(?:1)?(?(?!(37|96))[2-9][0-8][0-9](?<!(11)))?[2-9][0-9]{2}(?<!(11))[0-9]{4}(?<!(555(01([0-9][0-9])|1212)))$/';

  //Set array of variables to false initially
  $valid = array(
    'format' => false,
    'nanpa' => false,
    'ext' => false,
    'all' => false
  );

  //Check data against the format analyzer
  if(preg_match($format_pattern, $phone, $matchset)) {
    $valid['format'] = true;    
  }

  //If formatted properly, continue
  if($valid['format']) {

    //Set array of new components
    $components = array(
      'ac' => $matchset[1], //area code
      'xc' => $matchset[2], //exchange code
      'sn' => $matchset[3], //subscriber number
      'xn' => $matchset[4], //extension number
    );

    //Set array of number variants
    $numbers = array(
      'original' => $matchset[0],
      'stripped' => substr(preg_replace('[\D]', '', $matchset[0]), 0, 10)
    );

    //Now let's check the first ten digits against NANPA standards
    if(preg_match($nanpa_pattern, $numbers['stripped'])) {
      $valid['nanpa'] = true;
    }

    //If the NANPA guidelines have been met, continue
    if($valid['nanpa']) {
      if(!empty($components['xn'])) {
        if(preg_match('/^[\d]{1,6}$/', $components['xn'])) {
          $valid['ext'] = true;
        }
      }
      else {
        $valid['ext'] = true;
      }
    }

    //If the extension number is valid or non-existent, continue
    if($valid['ext']) {
      $valid['all'] = true;
    }
  }
  return $valid['all'];
}

我认为存在问题。您的格式允许区号是可选的,但nanpa模式(我认为是正确的)要求有适当的区号。此外,如果没有给出区号,但确实提供了扩展名,那该怎么办呢?当您剥离原始号码时,移除非数字字符,然后盲目地获取包括扩展名在内的前10个数字。保证电话号码符合NANPA标准的唯一方法是知道区号,因此我认为必须有区号才能返回true。请参见:http://rubular.com/r/xxoCmSft8H - Alexander Bird
此外,format_pattern 不允许前导 1,但 nanpa 模式可以。 - Alexander Bird
另外,上面的NANPA模式中有"(?",这不是一个正确的正则表达式模式。我猜你的意思是"(?:"。顺便说一下,我之所以一直在这里留下评论,是因为这是目前在互联网上找到的最好的编译版本:)。我正在尝试使用它并帮助其他谷歌用户。 - Alexander Bird
5个回答

16
您可以使用前瞻断言来解决这个问题。我们需要的是一系列特定的字母(e、ex、ext、x、extension),后面跟着一个或多个数字。但我们也要考虑没有扩展名的情况。
顺便提一下,您不需要在单个字符周围加上方括号,比如[\s]或后续的[x]。此外,您可以分组所需放置在同一位置的字符。因此,您可以使用[\s\./]而不是\s?\.?/?,意思是“其中任何一个字符”。
以下是更新后的正则表达式,还包含了代码解释。
<?php
    $sPattern = "/^
        (?:                                 # Area Code
            (?:                            
                \(                          # Open Parentheses
                (?=\d{3}\))                 # Lookahead.  Only if we have 3 digits and a closing parentheses
            )?
            (\d{3})                         # 3 Digit area code
            (?:
                (?<=\(\d{3})                # Closing Parentheses.  Lookbehind.
                \)                          # Only if we have an open parentheses and 3 digits
            )?
            [\s.\/-]?                       # Optional Space Delimeter
        )?
        (\d{3})                             # 3 Digits
        [\s\.\/-]?                          # Optional Space Delimeter
        (\d{4})\s?                          # 4 Digits and an Optional following Space
        (?:                                 # Extension
            (?:                             # Lets look for some variation of 'extension'
                (?:
                    (?:e|x|ex|ext)\.?       # First, abbreviations, with an optional following period
                |
                    extension               # Now just the whole word
                )
                \s?                         # Optionsal Following Space
            )
            (?=\d+)                         # This is the Lookahead.  Only accept that previous section IF it's followed by some digits.
            (\d+)                           # Now grab the actual digits (the lookahead doesn't grab them)
        )?                                  # The Extension is Optional
        $/x";                               // /x modifier allows the expanded and commented regex

    $aNumbers = array(
        '123-456-7890x123',
        '123.456.7890x123',
        '123 456 7890 x123',
        '(123) 456-7890 x123',
        '123.456.7890x.123',
        '123.456.7890 ext. 123',
        '123.456.7890 extension 123456',
        '123 456 7890', 
        '123-456-7890ex123',
        '123.456.7890 ex123',
        '123 456 7890 ext123',
        '456-7890',
        '456 7890',
        '456 7890 x123',
        '1234567890',
        '() 456 7890'
    );

    foreach($aNumbers as $sNumber) {
        if (preg_match($sPattern, $sNumber, $aMatches)) {
            echo 'Matched ' . $sNumber . "\n";
            print_r($aMatches);
        } else {
            echo 'Failed ' . $sNumber . "\n";
        }
    }
?>

输出结果:

Matched 123-456-7890x123
Array
(
    [0] => 123-456-7890x123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123.456.7890x123
Array
(
    [0] => 123.456.7890x123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123 456 7890 x123
Array
(
    [0] => 123 456 7890 x123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched (123) 456-7890 x123
Array
(
    [0] => (123) 456-7890 x123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123.456.7890x.123
Array
(
    [0] => 123.456.7890x.123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123.456.7890 ext. 123
Array
(
    [0] => 123.456.7890 ext. 123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123.456.7890 extension 123456
Array
(
    [0] => 123.456.7890 extension 123456
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123456
)
Matched 123 456 7890
Array
(
    [0] => 123 456 7890
    [1] => 123
    [2] => 456
    [3] => 7890
)
Matched 123-456-7890ex123
Array
(
    [0] => 123-456-7890ex123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123.456.7890 ex123
Array
(
    [0] => 123.456.7890 ex123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 123 456 7890 ext123
Array
(
    [0] => 123 456 7890 ext123
    [1] => 123
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 456-7890
Array
(
    [0] => 456-7890
    [1] => 
    [2] => 456
    [3] => 7890
)
Matched 456 7890
Array
(
    [0] => 456 7890
    [1] => 
    [2] => 456
    [3] => 7890
)
Matched 456 7890 x123
Array
(
    [0] => 456 7890 x123
    [1] => 
    [2] => 456
    [3] => 7890
    [4] => 123
)
Matched 1234567890
Array
(
    [0] => 1234567890
    [1] => 123
    [2] => 456
    [3] => 7890
)
Failed () 456 7890

我喜欢这个,因为对我来说稍微容易理解一些... 然而,就像ebynum在下面指出的我的原始正则表达式模式一样,这个也允许匹配当区号括号内不包含任何值的情况...如此:() 456-7890 ext1234 = 匹配 然而,它应该失败,因为区号标识符被放置,但没有填充。然而,就这个问题的“原始请求”而言,您的正则表达式模式几乎已经涵盖了这部分。 - Josh
我结合了你和ebynum的答案来形成完整的函数...我使用了你的方法来检查格式本身,然后使用ebynum的方法来验证经过消毒处理的数字字符串是否符合NANPA准则。谢谢! - Josh
1
这个正则表达式目前允许反斜杠作为分隔符,例如 (xxx)xxx\xxxx。它使用了冗余的前瞻(执行前瞻搜索以查看是否有扩展名,然后要求扩展名)。 - ebynum
@ebynum 你说得对。我以OP的模式为基础。不确定你说的冗余前瞻是什么意思。它基本上是在寻找单词/缩写和数字,这是有意的,因为似乎没有类似xxx-xxx-xxxx-xxx的电话号码。 - enobrev
1
我不同意你的偏好(但这只是个人喜好)。我完全可以接受像1-800-555-1212 1234这样的数字。然而,你的正则表达式仍然是这样的:……允许一个单词/缩写作为扩展名,但只有在它后面跟着一个或多个数字,并且它必须后面跟着一个或多个数字。如果你去掉前瞻,留下:((?:(?:(?:e|x|ex|ext).?|extension)\s?)(\d+))?, 它仍然必须同时包含单词和数字(或者什么都没有)。 - ebynum
啊,好的,我明白你的意思了。谢谢你的澄清。 - enobrev

4
当前的正则表达式
/^[\(]?(\d{0,3})[\)]?[\.]?[\/]?[\s]?[\-]?(\d{3})[\s]?[\.]?[\/]?[\-]?(\d{4})[\s]?[x]?(\d*)$/

这个正则表达式存在很多问题,导致它匹配了以下所有内容,包括但不限于:
(0./ -000 ./-0000 x00000000000000000000000)
()./1234567890123456789012345678901234567890
\)\-555/1212 x

我认为这个正则表达式更接近您要找的内容:

/^(?:(?:(?:1[.\/\s-]?)(?!\())?(?:\((?=\d{3}\)))?((?(?!(37|96))[2-9][0-8][0-9](?<!(11)))?[2-9])(?:\((?<=\(\d{3}))?)?[.\/\s-]?([0-9]{2}(?<!(11)))[.\/\s-]?([0-9]{4}(?<!(555(01([0-9][0-9])|1212))))(?:[\s]*(?:(?:x|ext|extn|ex)[.:]*|extension[:]?)?[\s]*(\d+))?$/

或者,爆炸式:
<?
    $pattern = 
    '/^                                                     #  Matches from beginning of string

        (?:                                                 #  Country / Area Code Wrapper [not captured]
            (?:                                             #  Country Code Wrapper [not captured]
                (?:                                         #  Country Code Inner Wrapper [not captured]
                    1                                       #  1 - CC for United States and Canada
                    [.\/\s-]?                               #  Character Class ('.', '/', '-' or whitespace) for allowed (optional, single) delimiter between Country Code and Area Code
                )                                           #  End of Country Code
                (?!\()                                      #  Lookahead, only allowed if not followed by an open parenthesis
            )?                                              #  Country Code Optional
            (?:                                             #  Opening Parenthesis Wrapper [not captured]
                \(                                          #  Opening parenthesis
                (?=\d{3}\))                                 #  Lookahead, only allowed if followed by 3 digits and closing parenthesis [lookahead never captured]
            )?                                              #  Parentheses Optional
            ((?(?!(37|96))[2-9][0-8][0-9](?<!(11)))?[2-9])  #  3-digit NANPA-valid Area Code [captured]
            (?:                                             #  Closing Parenthesis Wrapper [not captured]
                \(                                          #  Closing parenthesis
                (?<=\(\d{3})                                #  Lookbehind, only allowed if preceded by 3 digits and opening parenthesis [lookbehind never captured]
            )?                                              #  Parentheses Optional
        )?                                                  #  Country / Area Code Optional

        [.\/\s-]?                                           #  Character Class ('.', '/', '-' or whitespace) for allowed (optional, single) delimiter between Area Code and Central-office Code

        ([0-9]{2}(?<!(11)))                                 #  3-digit NANPA-valid Central-office Code [captured]

        [.\/\s-]?                                           #  Character Class ('.', '/', '-' or whitespace) for allowed (optional, single) delimiter between Central-office Code and Subscriber number

        ([0-9]{4}(?<!(555(01([0-9][0-9])|1212))))           #  4-digit NANPA-valid Subscriber Number [captured]

        (?:                                                 #  Extension Wrapper [not captured]
            [\s]*                                           #  Character Class for allowed delimiters (optional, multiple) between phone number and extension
            (?:                                             #  Wrapper for extension description text [not captured]
                (?:x|ext|extn|ex)[.:]*                      #  Abbreviated extensions with character class for terminator (optional, multiple) [not captured]
              |                                             #  OR
                extension[:]?                               #  The entire word extension with character class for optional terminator
            )?                                              #  Marker for Extension optional
            [\s]*                                           #  Character Class for allowed delimiters (optional, multiple) between extension description text and actual extension
            (\d+)                                           #  Extension [captured if present], required for extension wrapper to match
        )?                                                  #  Entire extension optional

    $                                                       #  Matches to end of string
    /x';                                                    // /x modifier allows the expanded and commented regex

?>

这个修改提供了几个改进:
  1. 它创建了一个可配置的项目组,可以作为扩展名匹配。您可以添加附加的分隔符以进行扩展名。扩展名还允许在扩展名定界符后添加冒号。
  2. 它将4个可选分隔符(点、空格、斜杠或连字符)的序列转换为只匹配单个字符类。
  3. 它适当地对项目进行分组。在给定的示例中,您可以在没有区号的情况下拥有开放括号,并且您可以在没有扩展名的情况下拥有扩展标记(空格-x)。这个备用正则表达式要求完整的区号或无区号,以及完整的扩展名或无扩展名。
  4. 号码的4个组成部分(区号、中央办公室代码、电话号码和扩展名)是反向引用元素,它们输入到preg_match()中的$matches。
  5. 使用前瞻/后顾要求在区号中匹配括号。
  6. 允许在数字之前使用1。(这假设所有数字都是美国或加拿大的数字,这似乎是合理的,因为匹配最终针对NANPA限制进行。还禁止混合国家代码前缀和用括号括起来的区号。
  7. 它合并NANPA规则以消除不可分配的电话号码。
    1. 它消除了0xx、1xx 37x、96x、x9x和x11形式的区号,这些区号是无效的NANPA区号。
    2. 它消除了0xx和1xx形式的中央办公室代码(无效的NANPA中央办公室代码)。
    3. 它消除了555-01xx形式的数字(从NANPA不可分配)。

它有一些小限制。它们可能不重要,但在此处被记录。

  1. 没有任何措施要求重复使用相同的分隔符,允许像800-555.1212、800/555 1212、800 555.1212等数字。
  2. 没有任何措施限制带有括号的区号后面的分隔符,允许像(800)-555-1212或(800)/5551212这样的数字。

NANPA规则是从以下REGEX调整而来,可以在此处找到:http://blogchuck.com/2010/01/php-regex-for-validating-phone-numbers/

/^(?:1)?(?(?!(37|96))[2-9][0-8][0-9](?<!(11)))?[2-9][0-9]{2}(?<!(11))[0-9]{4}(?<!(555(01([0-9][0-9])|1212)))$/

哇...我甚至没有想到你指出的缺陷。非常感谢您让我知道!我也没有考虑过尝试针对两个不同的模式运行它,以便首先验证格式,然后验证实际数字...我会尝试一下,看看会发生什么。谢谢! - Josh
嗯,当我尝试将一个测试数字通过第一个模式运行时,我得到了这个错误:Warning: preg_match() [function.preg-match]: Unknown modifier '\' - Josh
修复了一个可能解决的错误,转义了字符类中的 /。 - ebynum
我结合了你和enobrev的答案来形成完整的函数...我使用enobrev的来检查格式本身,然后使用你的来验证经过消毒处理的数字字符串是否符合NANPA准则。谢谢! - Josh

3

为什么不将任何一系列字母转换为"x"。那么你就可以将所有可能性都转换为"x"。

或者

检查3位数,3位数,4位数,1个或多个数字,并忽略中间的任何其他字符。

正则表达式:([0-9]{3}).*?([0-9]{3}).*?([0-9]{4}).+?([0-9]{1,})


不必寻找“[x]”或“x”,然后再加上数字,只需寻找“[\w\s]*\d+”。因此,您无需担心他们是否使用电话号码X扩展名或电话号码EXTENSION扩展名或电话号码EXT扩展名。 - Freiheit
@Josh - 类似于 ([0-9]{3}).*?([0-9]{3}).*?([0-9]{4}).+?([0-9]{1,}) 这样的正则表达式将忽略分隔符,只验证输入是否为有效数字。 - Mitch Dempsey
那个表达式匹配了很多他不想匹配的东西,比如:我的地址是123 Main Street,Cityville,ST。我真的很喜欢圆周率。它是我最喜欢的数字。它是3.1415926535。另外,我不确定.+?是否是一个有效的序列(相邻重复量词),但如果你想让"."可选,一个简单的.*就足够了。 - ebynum
@ebynum - .+? 表示一个或多个字符但是尽可能少的匹配(它是懒惰量词)。 - Mitch Dempsey
我总是忘记那些懒惰的量词。 - ebynum

3

或者,您可以使用一些非常简单直接的JavaScript来强制用户输入更具体的格式。jQuery的Masked 输入插件(http://digitalbush.com/projects/masked-input-plugin/)允许您将HTML输入掩码为电话号码,只允许用户以xxx-xxx-xxxx格式输入数字。它并不能解决您的扩展问题,但它确实提供了更清洁的用户体验。不要删除HTML标签。


1
注意:您仍然需要确认输入是否符合所要求的格式(例如,xxx-xxx-xxxx)。否则,没有启用 JavaScript 的人(或者绕过 JavaScript 的人)仍然可以插入无效内容。 - ebynum
我喜欢这个解决方案。同样,您也可以选择一种更低技术的方法,只需使用3或4个文本框:区号、号码(1或2个框)、分机号。 - Lèse majesté

0

你可以修改正则表达式,但这样做并不好--如果允许"extn",那么"extentn"呢?如果是"and then you have to dial"呢?

我认为做法上的"正确"方式是添加一个单独的数字扩展框。

但如果你真的想要正则表达式,我认为我已经修复了它。提示:对于单个字符,你不需要使用[x],直接用x即可。

/^\(?(\d{0,3})\)?(\.|\/)|\s|\-)?(\d{3})(\.|\/)|\s|\-)?(\d{4})\s?(x|ext)?(\d*)$/

您允许使用点、斜杠、破折号和空格字符。您应该只允许其中一种选项。您需要更新对$matches的引用;有用的组现在是0、2和4。

P.S. 这未经测试,因为我没有运行PHP的参考实现。如果有错误,请告诉我,我会尽力修复。

编辑

这比我能做到的更好地总结了此处


哈哈,我明白你的意思...这可能会变成一场永无止境的战斗...不过,为了简单起见,我认为最好一次性完成电话号码。不过,如果万不得已,我可以将分机号码拆分出来。 - Josh
目前的正则表达式允许 xext。如果您想添加其他选项,可以通过将 (x|ext) 更改为 (x|ext|spam|ham|eggs) 来添加它们。然而,我认为如果用户实际上想要输入一个扩展名,这将会导致无尽的混乱。 - Katriel

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