从 URL 中获取子域名

113

从URL中获取子域名一开始听起来很容易。

http://www.domain.example

查找第一个句点,然后返回“http://”之后的任何内容...

然后你记住了

http://super.duper.domain.example

哦,那么你认为,好的,找到最后一个句号,往回走一个单词并获取前面的所有内容!

然后你记得了

http://super.duper.domain.co.uk

你回到了起点。除了存储所有顶级域名的列表之外,还有没有其他好的想法?


1
这个问题在这里已经被问过了: 获取URL的部分 编辑:在这里也有一个类似的问题被问过 :) - jb.
1
你能澄清一下你想要什么吗?看起来你想要 URL 的“官方”域部分(即 domain.co.uk),无论在它之前有多少个 DNS 标签? - Alnitak
1
我认为这不是同一个问题 - 这似乎更多地涉及域名中的管理削减,仅通过查看字符串无法解决。 - Alnitak
1
我同意。请详细说明您的最终目标是什么。 - BuddyJoe
1
请查看此答案:https://dev59.com/WnE85IYBdhLWcg3wpFKu#39307593 - Ehsan Chavoshi
显示剩余2条评论
18个回答

77

除了存储所有顶级域名(TLD)的列表外,还有什么好的想法吗?

没有,因为每个TLD在子域、二级域等方面有所不同。

请记住,有顶级域名、二级域名和子域。严格来说,除TLD外的所有内容都是子域。

在domain.com.uk的示例中,“domain”是一个子域,“com”是一个二级域,“uk”是TLD。

因此,问题比表面上看起来更加复杂,这取决于每个TLD的管理方式。您需要一个包括它们特定分区以及什么被认为是二级域和子域的所有TLD的数据库。虽然TLD并不多,但收集所有这些信息并不容易。可能已经存在这样的列表。

看起来http://publicsuffix.org/就是这样的列表——所有常见后缀(.com、.co.uk等)都在一个适合搜索的列表中。解析它仍然不容易,但至少您不必维护列表。

"公共后缀"是指互联网用户可以直接注册名称的后缀。一些公共后缀的例子包括".com"、".co.uk"和"pvt.k12.wy.us"。公共后缀列表是所有已知公共后缀的列表。

公共后缀列表是Mozilla Foundation的一个倡议。它可用于任何软件,但最初是为了满足浏览器制造商的需求而创建的。它允许浏览器执行以下操作:

  • 避免针对高级域名后缀设置损害隐私的"超级cookie"
  • 在用户界面中突出显示域名的最重要部分
  • 准确地按站点对历史记录条目进行排序

查阅该列表, 您会发现这不是一个微不足道的问题。我认为列表是实现这一目标的唯一正确方式...


Mozilla有使用这项服务的代码。该项目是因为原始cookie规范将TLD与cookie信任相关联,但从未起作用而被分离出来的。 "Cookie Monster" bug是第一个问题,架构从未得到修复或替换。 - benc
虽然没有列出解决此问题的首选语言,但有一个开源项目在C#代码中使用了这个列表,可以在这里找到:http://code.google.com/p/domainname-parser/ - Dan Esparza
1
一个域名是否是“公共后缀”应该通过DNS协议本身提供,可能通过EDNS标志来实现。在这种情况下,所有者可以设置它,而无需维护单独的列表。 - Pieter Ennes
@PieterEnnes EDNS是用于“传输相关”标志的,不能用于内容相关的元数据。我确实同意这些信息最好放在DNS本身中。我记得在即将到来的温哥华IETF会议上有一个“BoF会议”来讨论这个问题。 - Alnitak
感谢您提供 http://publicsuffix.org 的链接,我已经根据您的答案发布了一些基于 [tag:shell] 和 [tag:bash] 函数的内容:https://dev59.com/0nVC5IYBdhLWcg3wcwwm#63761712。谢谢(+1)! - F. Hauri - Give Up GitHub
还有一个 Perl 模块 Net::PublicSuffixList 可以管理对此列表的查询。 - Jacques

25

正如Adam所说,并不容易,目前唯一可行的方法是使用列表。

即使这样也有例外情况-例如在.uk中,有少数几个域在该级别立即有效,但不在.co.uk中,因此必须将其作为例外添加。

这是目前主流浏览器如何处理的方式-必须确保example.co.uk无法为.co.uk设置Cookie,否则将发送到.co.uk下的任何其他网站。

好消息是,已经有一个列表可供使用,在http://publicsuffix.org/上。

IETF也正在进行一些工作,以创建某种标准,允许顶级域声明其域结构。然而,像.uk.com之类的东西会稍微复杂些,它被操作得就像一个公共后缀,但却不是由.com注册机构出售。


1
额,IETF 应该知道不要让他们的 URL 失效。该草案(最后更新于 2012 年 9 月)现在可以在此处访问:http://tools.ietf.org/html/draft-pettersen-subtld-structure - IMSoP
IETF关于DBOUND主题的工作组已经关闭。 - Patrick Mevzek
请注意,自本文写作以来,.uk 域名注册局现在允许直接在第二级别进行注册。这已经相应地反映在 PSL 中。 - Alnitak

23

2
但请记住,这不仅仅是解析的问题! Publicsuffix.org 上的此列表是一个非官方项目,不完整(例如缺少 eu.org),不会自动反映顶级域名的政策,并且随时可能变得无人维护。 - bortzmeyer
另外,Ruby: github.com/weppos/public_suffix_service - fractious
8
publicsuffix.org的列表并不比Mozilla做的其他事情更“非官方”。鉴于Mozilla、Opera和Chrome都在使用它,它不太可能变得不再维护。至于它是否不完整,像eu.org这样的域名运营者如果希望被包含在内,可以申请 inclusion,并且他们明白这样做的后果。如果您想添加域名,请让所有者申请。是的,它并不能自动反映TLD政策,但是没有什么能够这样做-没有可编程的信息源。 - Gervase Markham
1
dagger/android: okhttp会为您提供topPrivateDomain。 - bladerunner

9
如Adam和John所说,publicsuffix.org是正确的方法。但如果出于任何原因您无法使用此方法,这里是一个基于一种假设的启发式算法,适用于99%以上的所有域:
有一个属性可以区分(并非全部,但几乎全部)“真实”域名和子域和顶级域名,那就是DNS的MX记录。您可以创建一个搜索算法来查找此记录:逐个删除主机名的部分,并查询DNS,直到找到MX记录。例如:
super.duper.domain.co.uk => no MX record, proceed
duper.domain.co.uk       => no MX record, proceed
domain.co.uk             => MX record found! assume that's the domain

以下是 PHP 的一个示例:

function getDomainWithMX($url) {
    //parse hostname from URL 
    //http://www.example.co.uk/index.php => www.example.co.uk
    $urlParts = parse_url($url);
    if ($urlParts === false || empty($urlParts["host"])) 
        throw new InvalidArgumentException("Malformed URL");

    //find first partial name with MX record
    $hostnameParts = explode(".", $urlParts["host"]);
    do {
        $hostname = implode(".", $hostnameParts);
        if (checkdnsrr($hostname, "MX")) return $hostname;
    } while (array_shift($hostnameParts) !== null);

    throw new DomainException("No MX record found");
}

这是IETF也在这里建议的吗? - Ellie Kesselman
1
即使 publicsuffix.org 说(见第六段),正确的做法也是通过 DNS,就像你在回答中所说的一样! - Ellie Kesselman
1
除此之外,您完全可以拥有一个没有MX记录的域。而且通配符记录会欺骗算法。另一方面,您也可以拥有具有MX记录的顶级域名(例如.ai.ax等)。 - Patrick Mevzek
@Patrick:我完全同意;就像我在介绍中说的那样,这个算法并不是百分之百可靠的,只是一种出奇地有效的启发式方法。 - Francois Bourgeois
该算法应返回具有MX记录的最短主机名。有些域接受子域中的邮件,通常是邮件列表(name@lists.example.net),但一些大型组织也曾经为某些部门使用单独的服务器。 - Ale

2

如前所述,公共后缀列表只是正确解析域名的一种方式。对于PHP,您可以尝试使用TLDExtract。以下是示例代码:

$extract = new LayerShifter\TLDExtract\Extract();

$result = $extract->parse('super.duper.domain.co.uk');
$result->getSubdomain(); // will return (string) 'super.duper'
$result->getSubdomains(); // will return (array) ['super', 'duper']
$result->getHostname(); // will return (string) 'domain'
$result->getSuffix(); // will return (string) 'co.uk'

2
对于一个C库(使用Python生成数据表),我编写了 http://code.google.com/p/domain-registry-provider/,它既快速又占用空间小。
该库使用约30kB的数据表和约10kB的C代码。由于表在编译时构建,因此没有启动开销。有关更多详情,请参见http://code.google.com/p/domain-registry-provider/wiki/DesignDoc
要更好地理解生成表的代码(Python),请从这里开始:http://code.google.com/p/domain-registry-provider/source/browse/trunk/src/registry_tables_generator/registry_tables_generator.py 要更好地了解C API,请参见:http://code.google.com/p/domain-registry-provider/source/browse/trunk/src/domain_registry/domain_registry.h

1
我还有一个 C/C++ 库,它有自己的列表,尽管也会与 publicsuffix.org 列表进行检查。它叫做 libtld,在 Unix 和 MS-Windows 下工作 http://snapwebsites.org/project/libtld - Alexis Wilke
这里有一个DesignDoc的存档副本。遵循相同设计的简化实现(但不需要Python)在这里(以单个test.c文件的形式)提供。 - Ale

1

我刚用Clojure编写了一个程序,基于publicsuffix.org的信息:

https://github.com/isaksky/url_dom

例如:

(parse "sub1.sub2.domain.co.uk") 
;=> {:public-suffix "co.uk", :domain "domain.co.uk", :rule-used "*.uk"}

1

版本

除了Adam Davis给出的正确答案,我想发表我自己的解决方案。

由于列表非常庞大,因此有很多不同的测试解决方案...

首先以以下方式准备您的TLD列表:

wget -O - https://publicsuffix.org/list/public_suffix_list.dat |
    grep '^[^/]' |
    tac > tld-list.txt

注意:tac 会翻转列表以确保测试 .co.uk 在测试 .uk 之前。

shell 版本

splitDom() {
    local tld
    while read tld;do
        [ -z "${1##*.$tld}" ] &&
            printf "%s : %s\n" $tld ${1%.$tld} && return
    done <tld-list.txt
}

测试:

splitDom super.duper.domain.co.uk
co.uk : super.duper.domain

splitDom super.duper.domain.com
com : super.duper.domain

版本

为了减少分支(避免使用myvar=$(function..)语法),在bash函数中,我更喜欢设置变量而不是将输出转储到标准输出:

tlds=($(<tld-list.txt))
splitDom() {
    local tld
    local -n result=${2:-domsplit}
    for tld in ${tlds[@]};do
        [ -z "${1##*.$tld}" ] &&
            result=($tld ${1%.$tld}) && return
    done
}

然后:

splitDom super.duper.domain.co.uk myvar
declare -p myvar
declare -a myvar=([0]="co.uk" [1]="super.duper.domain")

splitDom super.duper.domain.com
declare -p domsplit
declare -a domsplit=([0]="com" [1]="super.duper.domain")

更快的版本:

在做好相同的准备工作后,然后执行以下步骤:

declare -A TLDS='()'
while read tld ;do
    if [ "${tld##*.}" = "$tld" ];then
        TLDS[${tld##*.}]+="$tld"
      else
        TLDS[${tld##*.}]+="$tld|"
    fi
done <tld-list.txt

这一步骤会慢很多,但是splitDom函数会变得更快:
shopt -s extglob 
splitDom() {
    local domsub=${1%%.*(${TLDS[${1##*.}]%\|})}
    local -n result=${2:-domsplit}
    result=(${1#$domsub.} $domsub)
}

我的树莓派测试:

这两个脚本已经过测试,使用的环境为:

for dom in dom.sub.example.{,{co,adm,com}.}{com,ac,de,uk};do
    splitDom $dom myvar
    printf "%-40s %-12s %s\n" $dom ${myvar[@]}
done

版本已使用详细的for循环测试,但所有测试脚本都产生了相同的输出:

dom.sub.example.com                      com          dom.sub.example
dom.sub.example.ac                       ac           dom.sub.example
dom.sub.example.de                       de           dom.sub.example
dom.sub.example.uk                       uk           dom.sub.example
dom.sub.example.co.com                   co.com       dom.sub.example
dom.sub.example.co.ac                    ac           dom.sub.example.co
dom.sub.example.co.de                    de           dom.sub.example.co
dom.sub.example.co.uk                    co.uk        dom.sub.example
dom.sub.example.adm.com                  com          dom.sub.example.adm
dom.sub.example.adm.ac                   ac           dom.sub.example.adm
dom.sub.example.adm.de                   de           dom.sub.example.adm
dom.sub.example.adm.uk                   uk           dom.sub.example.adm
dom.sub.example.com.com                  com          dom.sub.example.com
dom.sub.example.com.ac                   com.ac       dom.sub.example
dom.sub.example.com.de                   com.de       dom.sub.example
dom.sub.example.com.uk                   uk           dom.sub.example.com

完整的脚本包含文件读取和splitDom循环,在posix版本下需要大约2分钟,在基于$tlds数组的第一个bash脚本下需要大约1分29秒,但是在基于$TLDS 关联数组的最后一个bash脚本下只需要大约22秒。
                Posix version     $tldS (array)      $TLDS (associative array)
File read   :       0.04164          0.55507           18.65262
Split loop  :     114.34360         88.33438            3.38366
Total       :     114.34360         88.88945           22.03628

如果填充关联数组是一项较强的工作,splitDom 函数会变得更快!

0
使用URIBuilder,然后获取URIBUilder.host属性,在"."上将其拆分为数组,现在您有一个包含域的数组。

0

如果您想从任意URL列表中提取子域名和/或域名,这个Python脚本可能会有所帮助。但一定要小心,它不是完美的。通常来说,这是一个棘手的问题,并且如果您拥有预期的域名白名单,则非常有帮助。

  1. 从publicsuffix.org获取顶级域名
import requests

url = 'https://publicsuffix.org/list/public_suffix_list.dat'
page = requests.get(url)
domains = [] for line in page.text.splitlines(): if line.startswith('//'): continue else: domain = line.strip() if domain: domains.append(domain)
domains = [d[2:] if d.startswith('*.') else d for d in domains] print('找到 {} 个域名'.format(len(domains)))
  1. 构建正则表达式
import re

_regex = ''
for domain in domains:
    _regex += r'{}|'.format(domain.replace('.', '\.'))
subdomain_regex = r'/([^/]*)\.[^/.]+\.({})/.*$'.format(_regex) domain_regex = r'([^/.]+\.({}))/.*$'.format(_regex)
  1. 对URL列表使用正则表达式
FILE_NAME = ''   # 在此处放置CSV文件名
URL_COLNAME = '' # 在此处放置URL列名
import pandas as pd
df = pd.read_csv(FILE_NAME) urls = df[URL_COLNAME].astype(str) + '/' # 注意:添加/以帮助正则表达式
df['sub_domain_extracted'] = urls.str.extract(pat=subdomain_regex, expand=True)[0] df['domain_extracted'] = urls.str.extract(pat=domain_regex, expand=True)[0]
df.to_csv('extracted_domains.csv', index=False)

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