【问题标题】:Validate a hostname string验证主机名字符串
【发布时间】:2011-02-01 16:31:18
【问题描述】:

跟进Regular expression to match hostname or IP Address? 并使用Restrictions on valid host names 作为参考,在 Python 中匹配/验证主机名/fqdn(完全限定域名)的最易读、最简洁的方法是什么?我已经在下面的尝试中回答了,欢迎改进。

【问题讨论】:

    标签: python regex validation hostname fqdn


    【解决方案1】:
    import re
    def is_valid_hostname(hostname):
        if len(hostname) > 255:
            return False
        if hostname[-1] == ".":
            hostname = hostname[:-1] # strip exactly one dot from the right, if present
        allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
        return all(allowed.match(x) for x in hostname.split("."))
    

    确保每个细分市场

    • 至少包含一个字符,最多包含 63 个字符
    • 仅包含允许的字符
    • 不以连字符开头或结尾。

    它还避免了双重否定(not disallowed),如果hostname. 结尾,那也没关系。如果hostname 以多个点结尾,它将(并且应该)失败。

    【讨论】:

    • 主机名标签也不应该以连字符结尾。
    • 您错误地使用了re.match - 请注意re.match("a+", "ab") 是匹配项,而re.match("a+$", "ab") 不是。您的函数也不允许在主机名末尾有一个点。
    • 我一直认为re.match 需要匹配整个字符串,因此不需要字符串结尾的锚点。但正如我现在发现的(谢谢!)它只将匹配绑定到字符串的开头。我相应地更正了我的正则表达式。但是,我不明白你的第二点。以点结尾的主机名是否合法?问题中链接的*文章似乎说不。
    • @Tim Pietzcker 是的,最后一个点是合法的。它将名称标记为完全限定的域名,这让 DNS 系统知道它不应该尝试将本地域附加到它。
    • 请注意,每个段也有 63 个字符的限制。以及整个主机名的全局 255 个字符。
    【解决方案2】:

    不要重新发明*。您可以使用库,例如验证器。或者你可以复制their code

    安装

    pip install validators
    

    用法

    import validators
    if validators.domain('example.com')
        print('this domain is valid')
    

    【讨论】:

    • 看起来是最好的选择,应该是公认的答案......如果有一个库已经实现了这个,那就使用它。
    • 这很酷,但还不够。域不是主机名。 validators.domain('localhost') 失败。
    • AFAIK 这个库还将 IP 地址验证为主机名,这在大多数情况下可能是不可取的。
    【解决方案3】:

    这里是Tim Pietzcker's answer 的更严格版本,具有以下改进:

    • 将主机名的长度限制为 253 个字符(去掉可选的尾随点之后)。
    • 将字符集限制为 ASCII(即使用 [0-9] 而不是 \d)。
    • 检查 TLD 是否不是全数字的。
    import re
    
    def is_valid_hostname(hostname):
        if hostname[-1] == ".":
            # strip exactly one dot from the right, if present
            hostname = hostname[:-1]
        if len(hostname) > 253:
            return False
    
        labels = hostname.split(".")
    
        # the TLD must be not all-numeric
        if re.match(r"[0-9]+$", labels[-1]):
            return False
    
        allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
        return all(allowed.match(label) for label in labels)
    

    【讨论】:

    • 根据 RFC 3936 (tools.ietf.org/html/rfc3696#section-2),只有 TLD 不应该是数字,所以我想说最后一个条件应该看起来像 if re.match(r"\.(\d+)$", hostname):
    • 这假定 TLD 只是 FQDN 中的最后一个标签。这是不正确的。有些人 TLD 由 2 或 3 个标签(即 co.uk)组成。这是公共后缀的好来源:github.com/publicsuffix/list/blob/master/public_suffix_list.dat
    • @bbak 你是不是随便对我的回答投了反对票?这仍然是比此处其他答案(包括accepted)中建议的更严格的验证。
    【解决方案4】:

    根据The Old New Thing,DNS 名称的最大长度为 253 个字符。 (一个最多允许 255 个八位字节,但其中 2 个被编码消耗。)

    import re
    
    def validate_fqdn(dn):
        if dn.endswith('.'):
            dn = dn[:-1]
        if len(dn) < 1 or len(dn) > 253:
            return False
        ldh_re = re.compile('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$',
                            re.IGNORECASE)
        return all(ldh_re.match(x) for x in dn.split('.'))
    

    根据自己的目的,可以争论是否接受空域名。

    【讨论】:

      【解决方案5】:

      我喜欢 Tim Pietzcker 的回答的彻底性,但我更喜欢从正则表达式中卸载一些逻辑以提高可读性。老实说,我不得不查找那些(?“扩展符号”部分的含义。此外,我觉得“双重否定”方法更明显,因为它将正则表达式的职责限制为仅查找任何无效字符。我确实喜欢 re.IGNORECASE 允许缩短正则表达式。

      所以这是另一个镜头;它更长,但读起来有点像散文。我想“可读”与“简洁”有些不一致。我相信到目前为止线程中提到的所有验证约束都已涵盖:

      
      def isValidHostname(hostname):
          if len(hostname) > 255:
              return False
          if hostname.endswith("."): # A single trailing dot is legal
              hostname = hostname[:-1] # strip exactly one dot from the right, if present
          disallowed = re.compile("[^A-Z\d-]", re.IGNORECASE)
          return all( # Split by labels and verify individually
              (label and len(label) <= 63 # length is within proper range
               and not label.startswith("-") and not label.endswith("-") # no bordering hyphens
               and not disallowed.search(label)) # contains only legal characters
              for label in hostname.split("."))
      

      【讨论】:

      • 您不需要将反斜杠作为续行符 - 它们隐含在括号中。
      • 这将为“1.1.1.1”(以及任何其他全数字主机名)返回True
      【解决方案6】:
      def is_valid_host(host):
          '''IDN compatible domain validator'''
          host = host.encode('idna').lower()
          if not hasattr(is_valid_host, '_re'):
              import re
              is_valid_host._re = re.compile(r'^([0-9a-z][-\w]*[0-9a-z]\.)+[a-z0-9\-]{2,15}$')
          return bool(is_valid_host._re.match(host))
      

      【讨论】:

      • 这是什么,被混淆的python?为什么将正则表达式作为函数属性的魔法?
      • 这样 re.compile 只需执行一次,而不是每次调用函数时。可能仅在您每秒多次调用此函数时才重要。
      • @imbolc +1 用于实现 idna 编码。
      • @btubbs Python 已经caches results of re.compile(),除非你使用了很多不同的模式。我同意@kaleissin 的观点,即这段代码相当模糊。只需将编译后的表达式插入到与is_valid_host() 相同范围内的私有变量中即可。您仍然只能在第一次调用时计算它并将其保存到变量中。
      【解决方案7】:

      对@TimPietzcker 答案的补充。 Underscore 是有效的主机名字符(但不适用于域名)。而双破折号通常用于 IDN punycode 域(例如 xn--)。端口号应该被​​剥离。这是代码的清理。

      import re
      def is_valid_hostname(hostname):
          if len(hostname) > 255:
              return False
          hostname = hostname.rstrip(".")
          allowed = re.compile("(?!-)[A-Z\d\-\_]{1,63}(?<!-)$", re.IGNORECASE)
          return all(allowed.match(x) for x in hostname.split("."))
      
      # convert your unicode hostname to punycode (python 3 ) 
      # Remove the port number from hostname
      normalise_host = hostname.encode("idna").decode().split(":")[0]
      is_valid_hostname(normalise_host )
      

      【讨论】:

      • 下划线无效 - 请参阅 *.com/a/2183140/500902 及其链接的 IETF 规范。当 _ 在主机名中时,许多事情都可以正常工作,但是各种正确符合的软件可能会失败,例如解析主机名带有“_”的 URL
      • 感谢您指出 encode("idna")。如果不是因为:,我会投票赞成 - 最初的问题没有提到任何相关内容。
      【解决方案8】:

      我认为这个正则表达式可能对 Python 有所帮助: '^([a-zA-Z0-9]+(\.|\-))*[a-zA-Z0-9]+$'

      【讨论】:

      • 请告诉我们为什么这比其他任何答案都好
      • @MofX 虽然这个正则表达式不准确,但它突出了其他人通常忽略的特定点;主机名部分可能在中间包含一些字形,但不在开头或结尾。具体来说,主机名不能以数字或连字符开头,也不能以连字符结尾。并且,虽然并不总是强制执行,但 _ 在主机名中很常见,但它应该只用于作为域密钥或服务记录的前缀。但是,尝试解析完全由 _ 组成的主机名仍然有效。即 _.example.com 是合法的并且适用于 RFC 7816。
      • DNS 实际上允许标签以数字开头,甚至完全由数字组成(尽管它最初没有;有趣的是,它被更改了,因为 411.com 或类似服务要求它)。
      【解决方案9】:

      通过排除无效字符并确保非零长度来单独处理每个 DNS 标签。

      def isValidHostname(hostname):
          disallowed = re.compile("[^a-zA-Z\d\-]")
          return all(map(lambda x: len(x) and not disallowed.search(x), hostname.split(".")))
      

      【讨论】:

      • return all(x and not disallowed.search(x) for x in hostname.split("."))
      • 主机名末尾的. 是有效的。哦,当然,如果你想支持 IDN,还有很多工作要做......
      【解决方案10】:

      如果您要验证现有主机的名称,最好的方法是尝试解析它。您永远不会编写正则表达式来提供这种级别的验证。

      【讨论】:

      • 如果他想知道一个还不存在的主机名是否合法呢? RFC 看起来很简单,所以我不明白为什么正则表达式不起作用。
      • 取决于您要显示的内容。如果名称无法解析,那么谁知道它的“含义”;真正的验证方法需要正则表达式无法拥有的信息(即访问 DNS)。尝试并处理失败会更容易。在考虑可能合法但尚未合法的名称时,唯一真正需要关心的是注册服务商。其他所有人都应该将这些事情留给旨在拥有该领域真正专业知识的代码。正如 JWZ 所指出的,应用 RE 会将一个问题变成两个问题。 (嗯,主要是……)
      • 我不同意。有两个单独的问题,并且都是有效的问题:(1)°争论给定的字符串是否可以在技术上和合理地作为有效的电子邮件地址、主机名等服务; (2)°证明一个给定的名字是被取用的,或者可能是免费的。 (1) 纯粹是句法考虑。由于 (2) 发生在网络上,因此有一点疑问:现在启动的主机可能会在一秒钟内关闭,我现在订购的域可以在我的邮件到达时被占用。
      • 这种方法已经在一个类似的问题(*.com/questions/399932/…)中提出,甚至还有一个 Python 项目来促进这一点(code.google.com/p/python-public-suffix-list)。我稍微修改了问题标题,因为我对需要网络查找的解决方案不感兴趣。
      最近更新 更多