从字符串中提取主机名

333

我想匹配文本字符串中URL的根而不是整个URL。给定:

http://www.youtube.com/watch?v=ClkQA2Lb_iE
http://youtu.be/ClkQA2Lb_iE
http://www.example.com/12xy45
http://example.com/random

我想获取解析到 www.example.comexample.com 域的最后两个实例。

我听说正则表达式很慢,这将是页面上第二个正则表达式,所以如果有不使用正则表达式的方法,请告诉我。

我正在寻找一个JS/jQuery版本的解决方案。


2
建议更改接受的答案,以便于新来的人进入这个问题,因为Robin的答案更好。 - Digital Ninja
3
(也许你应该从你的问题中删除“听说正则表达式很慢”,这样你就不会向新手传递错误的观念,因为在基准测试中,正则表达式是最快的解决方案。) - Digital Ninja
30个回答

340

一个不使用正则表达式的巧妙技巧:

var tmp        = document.createElement ('a');
;   tmp.href   = "http://www.example.com/12xy45";

// tmp.hostname will now contain 'www.example.com'
// tmp.host will now contain hostname and port 'www.example.com:80'

将上述内容包装在以下函数中,您就可以轻松地从URI中获取域名部分。

function url_domain(data) {
  var    a      = document.createElement('a');
         a.href = data;
  return a.hostname;
}

63
如果需要快速进行操作,请不要使用此方法。它的速度大约比gilly3的方法慢40-60倍。在jsperf上进行了测试:http://jsperf.com/hostname-from-url。 - cprcrack

326

我给你3个可能的解决方案:

  1. 使用一个 npmpsl,它可以提取任何你输入的内容。
  2. 使用我的自定义实现 extractRootDomain,它适用于大多数情况。
  3. URL(url).hostname 可以使用,但不适用于每种边缘情况。点击“运行代码片段”查看其针对这些情况的表现。

1. 使用 npm 包 psl (Public Suffix List)

“公共后缀列表”是所有有效域名后缀和规则的列表,不仅包括国家代码顶级域名,还包括会被视为根域名的 Unicode 字符(例如www.食狮.公司.cn、b.c.kobe.jp等)。在此处了解更多信息

尝试:

npm install --save psl

然后使用我的“extractHostname”实现运行:

let psl = require('psl');
let url = 'http://www.youtube.com/watch?v=ClkQA2Lb_iE';
psl.get(extractHostname(url)); // returns youtube.com

2. 我自定义的extractRootDomain实现

以下是我编写的实现方式,它可以运行在各种可能的URL输入上。

function extractHostname(url) {
  var hostname;
  //find & remove protocol (http, ftp, etc.) and get hostname

  if (url.indexOf("//") > -1) {
    hostname = url.split('/')[2];
  } else {
    hostname = url.split('/')[0];
  }

  //find & remove port number
  hostname = hostname.split(':')[0];
  //find & remove "?"
  hostname = hostname.split('?')[0];

  return hostname;
}

// Warning: you can use this function to extract the "root" domain, but it will not be as accurate as using the psl package.

function extractRootDomain(url) {
  var domain = extractHostname(url),
  splitArr = domain.split('.'),
  arrLen = splitArr.length;

  //extracting the root domain here
  //if there is a subdomain
  if (arrLen > 2) {
    domain = splitArr[arrLen - 2] + '.' + splitArr[arrLen - 1];
    //check to see if it's using a Country Code Top Level Domain (ccTLD) (i.e. ".me.uk")
    if (splitArr[arrLen - 2].length == 2 && splitArr[arrLen - 1].length == 2) {
      //this is using a ccTLD
      domain = splitArr[arrLen - 3] + '.' + domain;
    }
  }
  return domain;
}

const urlHostname = url => {
  try {
    return new URL(url).hostname;
  }
  catch(e) { return e; }
};

const urls = [
    "http://www.blog.classroom.me.uk/index.php",
    "http://www.youtube.com/watch?v=ClkQA2Lb_iE",
    "https://www.youtube.com/watch?v=ClkQA2Lb_iE",
    "www.youtube.com/watch?v=ClkQA2Lb_iE",
    "ftps://ftp.websitename.com/dir/file.txt",
    "websitename.com:1234/dir/file.txt",
    "ftps://websitename.com:1234/dir/file.txt",
    "example.com?param=value",
    "https://facebook.github.io/jest/",
    "//youtube.com/watch?v=ClkQA2Lb_iE",
    "www.食狮.公司.cn",
    "b.c.kobe.jp",
    "a.d.kyoto.or.jp",
    "http://localhost:4200/watch?v=ClkQA2Lb_iE"
];

const test = (method, arr) => console.log(
`=== Testing "${method.name}" ===\n${arr.map(url => method(url)).join("\n")}\n`);

test(extractHostname, urls);
test(extractRootDomain, urls);
test(urlHostname, urls);

无论是协议还是端口号,你都可以提取域名。这是一种非常简单的、非正则表达式的解决方案,所以我认为考虑到我们在问题中提供的数据集,这个解决方案足够了。
3. URL(url).hostname URL(url).hostname 是一个有效的解决方案,但它不能很好地处理一些边缘情况,我已经解决了这些问题。正如你在我的最后一个测试中看到的那样,它不喜欢一些网址。你肯定可以使用我的解决方案的组合来使它们全部起作用。
*感谢@Timmerz、@renoirb、@rineez、@BigDong、@ra00l、@ILikeBeansTacos、@CharlesRobertson提供的建议! @ross-allen,谢谢你报告错误!

1
121KB gzipped https://bundlephobia.com/package/psl@1.8.0。 这比React重17倍以上。除非你有一个真正好的理由不使用URL().hostname,否则这是一个非常糟糕的解决方案,可能是所有解决方案中最慢的(因为它的捆绑大小)。 - Robin Métral
@RobinMétral,它可能会变得缓慢和庞大,因为它必须处理此大列表https://publicsuffix.org/list/public_suffix_list.dat的所有要求。我不确定`URL().hostname`是否足够。 - Lewis Nakao
完全同意这里,这就是为什么我会默认使用URL().hostname,只有在需要更多东西时才会使用psl :) - Robin Métral
@robin-métral 请看我最后一组测试。在某些边缘情况下,URL().hostname 可能不适用。此外,我的 extractRootDomainextractHostname 实现不需要 psl - Lewis Nakao

301

无需解析字符串,只需将您的URL作为参数传递给URL构造函数:

const url = 'http://www.youtube.com/watch?v=ClkQA2Lb_iE';
const { hostname } = new URL(url);

console.assert(hostname === 'www.youtube.com');

9
2021年及其以后,这个答案是否应被接受? - Marius Butuc
1
是的,为什么不呢?使用对象解构非常完美、简洁。请参考以下链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#object_destructuring - sMyles
需要使用curly大括号,因为返回的是一个对象,而hostname是其中的一个属性。正如@sMyles在他的评论中所写的那样,这是解构赋值 - Timo
1
如果您需要带有 https:// 的主机名,则可以使用 const { hostname, protocol } = new URL(url); - Timo
@Timo 我相信你可以直接使用 { origin } 来处理这个问题,它会自动为你处理。 - Dror Bar

156

试试这个:

var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
var domain = matches && matches[1];  // domain will be null if no match is found
如果你想要从结果中排除端口号,可以使用这个表达式代替:
/^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i

编辑:为了防止特定域名匹配,请使用负向预查 (?!youtube.com)

/^https?\:\/\/(?!(?:www\.)?(?:youtube\.com|youtu\.be))([^\/:?#]+)(?:[\/:?#]|$)/i

131

对于这个问题有两个好的解决方案,具体取决于您是否需要优化性能(而且不需要外部依赖!):

1. 使用URL.hostname提高可读性

最干净、最简单的解决方案是使用URL.hostname

const getHostname = (url) => {
  // use URL constructor and return hostname
  return new URL(url).hostname;
}

// tests
console.log(getHostname("https://dev59.com/FWoy5IYBdhLWcg3wg-Ym"));
console.log(getHostname("https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname"));

URL.hostnameURL API 的一部分,除了 IE(caniuse)外,所有主流浏览器都支持它。如果需要支持旧版浏览器,请使用 URL polyfill

额外福利:使用 URL 构造函数还可以让您访问其他 URL 属性和方法


2. 使用正则表达式提高性能

URL.hostname 对于大多数情况来说是首选。但是,它仍然比这个正则表达式慢得多 (在 jsPerf 上自行测试):

const getHostnameFromRegex = (url) => {
  // run against regex
  const matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
  // extract hostname (will be null if no match is found)
  return matches && matches[1];
}

// tests
console.log(getHostnameFromRegex("https://dev59.com/FWoy5IYBdhLWcg3wg-Ym"));
console.log(getHostnameFromRegex("https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname"));


TL;DR

你应该使用URL.hostname。如果你需要处理大量的URL(性能是一个因素),可以考虑使用正则表达式。


38

解析URL可能会很棘手,因为您可能会有端口号和特殊字符。因此,我建议使用类似parseUri的工具来为您完成此操作。除非您正在解析数百个URL,否则我认为性能不会成为问题。


15
如果需要快速执行,请不要使用此方法。仅仅获取主机名,该方法比gilly3的方法慢了大约40-60倍。在jsperf上进行了测试:http://jsperf.com/hostname-from-url。 - cprcrack

21

如果您来到这个页面并正在寻找最佳的URL正则表达式,请尝试使用以下内容:

^(?:https?:)?(?:\/\/)?([^\/\?]+)

你可以像下面这样使用它,也可以不区分大小写地匹配 HTTPSHTTP

https://regex101.com/r/pX5dL9/1

const match = str.match(/^(?:https?:)?(?:\/\/)?([^\/\?]+)/i);
const hostname = match && match[1];

它可以适用于没有http://,有http,有https,只有//的URL,并且不会获取路径和查询路径。

祝你好运


虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅链接的答案可能会失效。- 来自审查 - Lawrence Aiello
1
已编辑并提交了正则表达式 :) - Luis Lopes
教人们使用正则表达式来解析URL就像教他们用AR-15开锁一样。 - Jason

20

我尝试使用了提供的解决方案,但选定的那个对我的目的来说有些过头了,而“创建一个元素”则会让我的内容出现问题。

它还没有准备好在URL中进行传输。我希望有人能够发现它有用。

function parseURL(url){
    parsed_url = {}

    if ( url == null || url.length == 0 )
        return parsed_url;

    protocol_i = url.indexOf('://');
    parsed_url.protocol = url.substr(0,protocol_i);

    remaining_url = url.substr(protocol_i + 3, url.length);
    domain_i = remaining_url.indexOf('/');
    domain_i = domain_i == -1 ? remaining_url.length - 1 : domain_i;
    parsed_url.domain = remaining_url.substr(0, domain_i);
    parsed_url.path = domain_i == -1 || domain_i + 1 == remaining_url.length ? null : remaining_url.substr(domain_i + 1, remaining_url.length);

    domain_parts = parsed_url.domain.split('.');
    switch ( domain_parts.length ){
        case 2:
          parsed_url.subdomain = null;
          parsed_url.host = domain_parts[0];
          parsed_url.tld = domain_parts[1];
          break;
        case 3:
          parsed_url.subdomain = domain_parts[0];
          parsed_url.host = domain_parts[1];
          parsed_url.tld = domain_parts[2];
          break;
        case 4:
          parsed_url.subdomain = domain_parts[0];
          parsed_url.host = domain_parts[1];
          parsed_url.tld = domain_parts[2] + '.' + domain_parts[3];
          break;
    }

    parsed_url.parent_domain = parsed_url.host + '.' + parsed_url.tld;

    return parsed_url;
}

运行这个:

parseURL('https://www.facebook.com/100003379429021_356001651189146');

结果:

Object {
    domain : "www.facebook.com",
    host : "facebook",
    path : "100003379429021_356001651189146",
    protocol : "https",
    subdomain : "www",
    tld : "com"
}

8

所有URL属性,无依赖,无JQuery,易于理解

此解决方案提供您的答案以及其他属性。无需JQuery或其他依赖项,复制并使用即可。

用法

getUrlParts("https://news.google.com/news/headlines/technology.html?ned=us&hl=en")

输出

{
  "origin": "https://news.google.com",
  "domain": "news.google.com",
  "subdomain": "news",
  "domainroot": "google.com",
  "domainpath": "news.google.com/news/headlines",
  "tld": ".com",
  "path": "news/headlines/technology.html",
  "query": "ned=us&hl=en",
  "protocol": "https",
  "port": 443,
  "parts": [
    "news",
    "google",
    "com"
  ],
  "segments": [
    "news",
    "headlines",
    "technology.html"
  ],
  "params": [
    {
      "key": "ned",
      "val": "us"
    },
    {
      "key": "hl",
      "val": "en"
    }
  ]
}

代码
该代码旨在易于理解而不是超快速。它可以轻松地每秒调用100次,因此非常适用于前端或少量服务器使用,但不适用于高吞吐量。

function getUrlParts(fullyQualifiedUrl) {
    var url = {},
        tempProtocol
    var a = document.createElement('a')
    // if doesn't start with something like https:// it's not a url, but try to work around that
    if (fullyQualifiedUrl.indexOf('://') == -1) {
        tempProtocol = 'https://'
        a.href = tempProtocol + fullyQualifiedUrl
    } else
        a.href = fullyQualifiedUrl
    var parts = a.hostname.split('.')
    url.origin = tempProtocol ? "" : a.origin
    url.domain = a.hostname
    url.subdomain = parts[0]
    url.domainroot = ''
    url.domainpath = ''
    url.tld = '.' + parts[parts.length - 1]
    url.path = a.pathname.substring(1)
    url.query = a.search.substr(1)
    url.protocol = tempProtocol ? "" : a.protocol.substr(0, a.protocol.length - 1)
    url.port = tempProtocol ? "" : a.port ? a.port : a.protocol === 'http:' ? 80 : a.protocol === 'https:' ? 443 : a.port
    url.parts = parts
    url.segments = a.pathname === '/' ? [] : a.pathname.split('/').slice(1)
    url.params = url.query === '' ? [] : url.query.split('&')
    for (var j = 0; j < url.params.length; j++) {
        var param = url.params[j];
        var keyval = param.split('=')
        url.params[j] = {
            'key': keyval[0],
            'val': keyval[1]
        }
    }
    // domainroot
    if (parts.length > 2) {
        url.domainroot = parts[parts.length - 2] + '.' + parts[parts.length - 1];
        // check for country code top level domain
        if (parts[parts.length - 1].length == 2 && parts[parts.length - 1].length == 2)
            url.domainroot = parts[parts.length - 3] + '.' + url.domainroot;
    }
    // domainpath (domain+path without filenames) 
    if (url.segments.length > 0) {
        var lastSegment = url.segments[url.segments.length - 1]
        var endsWithFile = lastSegment.indexOf('.') != -1
        if (endsWithFile) {
            var fileSegment = url.path.indexOf(lastSegment)
            var pathNoFile = url.path.substr(0, fileSegment - 1)
            url.domainpath = url.domain
            if (pathNoFile)
                url.domainpath = url.domainpath + '/' + pathNoFile
        } else
            url.domainpath = url.domain + '/' + url.path
    } else
        url.domainpath = url.domain
    return url
}

在一些相当简单的解析上失败了。请在此页面的控制台中尝试 getUrlParts('www.google.com') - Chamilyan
@Chamilyan 这不是一个URL,URL需要有协议。但是我已经更新了代码以处理更一般的情况,请收回你的踩。 - whitneyland
我没有给你点踩。但是如果我在最初的问题中没有特别要求 http://,我可能会这样做。 - Chamilyan
2
@Lee在这个输入中失败了:var url="https://mail.gggg.google.cn/link/link/link"; domainroot 应该是 google.com,但输出结果为:gggg.google.cn,而 gggg 是一个子域名(域名可以有多个子域名)。 - None

7

1
请返回与@Pavlo给出的答案相同的翻译文本,包括https://stackoverflow.com/questions/8498592/extract-hostname-name-from-string/35222901#comment58179334_35222901链接中的内容。 - Chamilyan

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