URL 正则:该匹配什么,什么该交给解析器
在文本中查找链接和校验一个 URL 是两件不同的事。本文为两者各给出一个正则,讲解让坏 URL 混过去的陷阱(末尾标点、未转义的点、缺失的锚点),以及何时该把工作交给 new URL() 或 urllib。

· 阅读约 5 分钟 · 更新于
/^https?://\S+$/
抓住
- https://a.io
- http://x.com/p
- https://b.dev?q=1
放过
- ftp://x.com
- www.x.com
- http:/x.com
“URL 正则”通常指以下两件事之一:
- 在一段文本中查找 URL,比如聊天应用把链接变成可点击的内容。
- 校验单个输入,比如个人资料表单里的“网站”字段。
两者需要不同的正则,而第二件事主要应该交给 URL 解析器。本文两者都会讲,还会介绍各自容易踩的陷阱。每个示例都可以交互,修改正则就能看到变化。
01在文本中查找 URL
最快的写法是一个协议,后面跟着任何非空白字符:
/https?://\S+/应当匹配3/3
- Docs: https://regexhunter.com/guide.
- (see https://example.com/a)
- <https://example.com>
不应匹配2/2
- see www.example.com
- ftp://files.example.com
\S+ 会一直抓到下一个空格为止,而在真实的文字里,URL 后面常常紧跟着标点。把它做成链接,读者点开就是 404。
02去掉末尾的标点
解决办法是规定最后一个字符可以是什么。主体部分可以是除空白、尖括号和引号之外的任何字符,而最后一个字符还不能是常见的标点:
/https?://[^\s<>"]*[^\s<>".,;:!?)]/- Docs: https://regexhunter.com/guide.
- (see https://example.com/a)
- <https://example.com>
- https://example.com/search?q=regex&page=2
这之所以有效,是因为贪婪的 [^\s<>"]* 会一路匹配到 URL 末尾,然后一次退回一个字符,直到最后的字符类能够匹配。(我们关于贪婪与懒惰量词的文章详细讲解了这个回退过程。)
这里没有完美的规则。像 https://en.wikipedia.org/wiki/Regex_(disambiguation) 这样的维基百科链接确实以 ) 结尾,而这个正则会把它切掉。链接识别库的做法是检查括号是否配对,而这已经超出了一个正则应该承担的逻辑量。
注意,www.example.com 没有被找到,因为它没有协议。如果你也想匹配不带协议的 www. 链接,可以把正则开头改成 (https?://|www\.)。
03校验 URL:各个组成部分
一个网页 URL 最多有六个部分。以 https://regexhunter.com:443/blog/regex-urls?ref=home#top 为例:
| 部分 | 示例 | 必需? |
|---|---|---|
| 协议 | https |
是 |
| 主机 | regexhunter.com |
是 |
| 端口 | :443 |
否 |
| 路径 | /blog/regex-urls |
否 |
| 查询 | ?ref=home |
否 |
| 片段 | #top |
否 |
检查每个部分形状的正则如下:
/^(https?)://([^/:?#\s]+)(:\d+)?(/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/i应当匹配4/4
- https://example.com
- HTTPS://EXAMPLE.COM
- http://localhost:3000/api?x=1
- https://regexhunter.com/blog/regex-urls#top
不应匹配5/5
- ftp://example.com
- https://
- https://exa mple.com
- https://example.com:port
- example.com
逐部分来读:
^(https?)://协议,http 或 https,以及://([^/:?#\s]+)主机:直到下一个/、:、?或#之前的所有内容(:\d+)?可选的端口,只能是数字(/[^?#\s]*)?可选的路径,以/开头(\?[^#\s]*)?可选的查询,以?开头(#\S*)?$可选的片段,然后是结尾
作为形状检查,它很不错。但它完全不管主机是否合理。https://999.999.999.999 能通过,https://- 也能。你可以继续为主机名、IP 地址、IPv6 方括号、用户名和密码、国际化域名添加规则。确实有人这么做过,结果长达几百个字符。
04让解析器来解析
每种主流语言都自带一个 URL 解析器,它早就了解这些规则。在 JavaScript(浏览器和 Node)中,URL 构造函数遇到无法解析的内容就会抛出异常:
function isHttpUrl(s) {
try {
const u = new URL(s);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
}
new URL("https://exa mple.com")、new URL("https://999.999.999.999") 和 new URL("example.com") 都会抛出异常。协议检查很重要:new URL("javascript:alert(1)") 能顺利解析,而你肯定不希望链接里出现它。
各个部分也顺带就能拿到:u.hostname、u.port、u.pathname、u.search、u.hash,以及用 u.searchParams.get("x") 读取单个查询值。返回的 URL 还经过了规范化:HTTPS://Example.COM:443/a/../b 会变成 https://example.com/b。
在 Python 中,urllib.parse.urlsplit 会把 URL 拆成同样的几个部分:
from urllib.parse import urlsplit
u = urlsplit("http://localhost:3000/api?x=1#top")
(u.scheme, u.hostname, u.port, u.path, u.query, u.fragment)
# ('http', 'localhost', 3000, '/api', 'x=1', 'top')
但 urlsplit 只负责拆分,不负责校验,它很少报错。urlsplit("example.com") 返回空的协议和主机,并把 example.com 当作路径;urlsplit("https://exa mple.com") 则若无其事地把 exa mple.com 报告为主机。所以要检查 u.scheme in ("http", "https") 以及 u.netloc 不为空,需要的话再加上你自己的主机规则。
05陷阱:未转义的点
在正则中,. 表示“任意字符”。在域名里忘了加反斜杠,你就放行了仿冒域名:
要写成 www\.example\.com。正则里每个字面意义上的点都需要反斜杠。
06陷阱:缺失的结尾
这一点对重定向和允许列表检查很重要,比如“只把用户送回我们自己网站上的 URL”:
主机必须在 .com 之后立即结束。要求后面紧跟一个能结束主机的字符,或者字符串结尾:
/^https://example\.com([/?#:]|$)/应当匹配3/3
- https://example.com
- https://example.com/login
- https://example.com?ref=mail
不应匹配3/3
- https://example.com.evil.io
- https://example.company.io
- https://evil.io/?next=https://example.com
对安全检查来说更好的做法是:用 new URL() 或 urlsplit 解析,然后用 === 或 == 比较主机名。精确的字符串比较没有任何容易遗漏的边界情况。
07试一试
每个怪物都是一个网址。无辜者要么协议不对,要么 :// 写坏了,要么有拼写错误,要么根本没有协议:
抓住 http 和 https 地址,放过其余的。
目标 · 抓住 0/4
- https://regexhunter.com目标
- http://localhost:3000目标
- https://a.io/x?y=1目标
- http://example.org/目标
无辜词 · 误抓 0
- ftp://example.org无辜
- https:/broken.com无辜
- example.com无辜
- mailto:ana@site.com无辜
- htps://typo.io无辜
输入正则——棋盘会实时更新。
08检查清单
- 在文本中查找 URL:用
https?://[^\s<>"]*[^\s<>".,;:!?)],把末尾的标点排除在外。 - 校验单个 URL:在 JavaScript 中用
new URL()并检查协议,在 Python 中用urlsplit并检查协议和主机。 - 转义每个字面意义上的点:
example\.com。 - 在允许列表中,要同时锚定主机的开头和结尾,更好的做法是比较解析后的主机名。
- 如果可能出现大写字母,加上
i修饰符。HTTPS://EXAMPLE.COM是合法的 URL。