规范化/归一化URL是什么意思?

12
我正在寻找一种在Python中规范化URL的库函数,即删除路径中的"./"或"../"部分,添加默认端口或转义特殊字符等。结果应该是一个字符串,两个指向相同网页的URL应该返回相同的结果。例如,http://google.comhttp://google.com:80/a/../应返回相同的结果。
我更喜欢Python 3,并已查看urllib模块。它提供了将URL拆分为各个部分的函数,但没有用于规范化的函数。Java有URI.normalize()函数可以执行类似的操作(虽然它不考虑默认端口80等于未给出端口),但是在Python中是否有类似的函数呢?

顺便提一下,像 http://google.com/ 这样的资源与 http://google.com:80/a/../ 不同。也就是说,如果 /a 不存在,那么第二个路径将失败。通过“规范化”它,你会失去这种特殊情况,并在开始时使用无效的 URI 得到一个有效的 URI... - Alexis Wilke
1
那不正确,在浏览器中是这样的。即使a不存在,你也可以写http://google.com:80/a/../,它会跳转到http://google.com:80/。这是因为浏览器在发送请求之前进行了初始解析。在服务器端,不同的服务器有不同的行为。 - Gil Cohen
7个回答

4

这是我目前使用的方法。你可以通过pip安装urlnorm。

注意,我对查询参数进行了排序,我发现这很重要。

from urlparse import urlsplit, urlunsplit, parse_qsl
from urllib import urlencode
import urlnorm

def canonizeurl(url):
    split = urlsplit(urlnorm.norm(url))
    path = split[2].split(' ')[0]

    while path.startswith('/..'):
        path = path[3:]

    while path.endswith('%20'):
        path = path[:-3]

    qs = urlencode(sorted(parse_qsl(split.query)))
    return urlunsplit((split.scheme, split.netloc, path, qs, ''))

好的,删除无效的父目录。 - hoju
你需要将 split[2].split(' ')[0] 替换为 urllib.parse.quote(split[2]) - 在某些情况下,URL中有空格是完全正常的,事实上是必须的。 此外,urlnorm仅适用于Python 2.x。 - Fake Name
此外,您正在丢弃片段,而在某些不寻常的情况下,片段实际上可以是必需的URL组件。 是的,有一定数量的网页,其中blah.com/#watblah.com/完全不同。 它通常使用JavaScript完成,并且非常麻烦,但它确实存在。 - Fake Name
@FakeName写道:“在URL中有空格是完全正常的,事实上是必需的。”不,那是绝对错误的。URL中永远不允许有空格。请阅读规范:https://tools.ietf.org/html/rfc2396 一些浏览器会误导地显示空格,但实际上它们是百分号编码的。 - DavidBooth

3
这个怎么样:
In [1]: from urllib.parse import urljoin

In [2]: urljoin('http://example.com/a/b/c/../', '.')
Out[2]: 'http://example.com/a/b/'

这个问题的答案启发,它没有规范化端口,但应该很容易编写一个函数来做到。


我没有urllib.parse,但我有urlparse - Sergey Orshanskiy
4
urllib.parse 是 Python 3 中的位置 - 最初的问题是关于 Python 3 的。 - Thomas K
这在任何不以'/'结尾的情况下都会失败。 - Jussi Kukkonen
2
这将删除最后一个组件,无论如何。这不是规范化!例如,http://example.com/a/../b/c 变成了 http://example.com/b/,省略了 c 组件。 - Martijn Pieters

2

旧的(已弃用)答案

[不再维护] urltools 模块可以规范多个斜杠、... 组件,而不会在 http:// 中搞乱双斜杠。

一旦你执行了 pip install urltools (这不再起作用,因为作者重命名了代码库) 使用方法如下:

print urltools.normalize('http://example.com:80/a////b/../c')
>>> 'http://example.com/a/c'

虽然该模块不再支持pip安装,但它是单个文件,因此您可以重复使用其中的部分内容。

针对Python3更新的答案

对于Python3,请考虑使用urllib.parse.urljoin来自urllib.parse模块。
from urllib.parse import urljoin

urljoin('https://dev59.com/02kv5IYBdhLWcg3wdQhT', '../dinsdale')
# Out[17]: 'https://stackoverflow.com/questions/dinsdale'

urltools似乎已经离开了这个星球。没有github,没有pypi,也没有任何缓存副本。如果有人知道发生了什么,请告诉我。 - Ganesh Kathiresan
@GaneshKathiresan:看起来作者决定重命名仓库,已更新。 - ccpizza

1

现在有一个专门解决这个问题的库url-normalize

它不仅可以按照文档中所述对路径进行规范化:

URI规范化功能:

  1. 处理IDN域名。
  2. 始终以小写字符提供URI方案。
  3. 始终以小写字符提供主机(如果有)。
  4. 仅在必要时执行百分比编码。
  5. 在百分比编码时始终使用大写A到F字符。
  6. 防止点段出现在非相对URI路径中。
  7. 对于定义默认授权的方案,如果需要默认值,则使用空授权。
  8. 对于将空路径定义为与“ /”路径等效的方案,请使用“ /”。
  9. 对于定义端口的方案,如果需要默认值,则使用空端口
  10. URI的所有部分都必须从Unicode字符串中的utf-8编码NFC

以下是一个例子:

from url_normalize import url_normalize

url = 'http://google.com:80/a/../'
print(url_normalize(url))

这是什么意思:

http://google.com/

不错!唯一不好的是如果没有提供协议,它会返回HTTPS而不是HTTP。例如,example.com 变成了 "https:////example.com"。 - Gil Cohen
1
@GilCohen,现在HTTPS是推荐使用的做法,因此这种行为可能是有意的。例如,请参阅W3C声明美国政府政策 - DavidBooth
1
理论上你可能是正确的,但在实践中,当协议未指定时,浏览器会首先尝试HTTP。 - Gil Cohen

0
这里有一个选项,不需要从Pip导入第三方库,也不需要滥用urljoin:只需在URL的路径部分使用posixpath.normpath
下面是一个交互会话的示例:
>>> import urllib.parse
>>> import posixpath
>>> 
>>> u = 'http://google.com/a/../b'
>>> parsed = urllib.parse.urlparse(u)
>>> normalized = posixpath.normpath(parsed.path)
>>> r = parsed._replace(path=normalized)
>>> r.geturl()
'http://google.com/b'

请注意,我们使用posixpath而不是os.path;这样可以在Windows上正常工作。

0

良好的开端之后,我编写了一个适用于Web中常见情况的方法。

def urlnorm(base, link=''):
  '''Normalizes an URL or a link relative to a base url. URLs that point to the same resource will return the same string.'''
  new = urlparse(urljoin(base, url).lower())
  return urlunsplit((
    new.scheme,
    (new.port == None) and (new.hostname + ":80") or new.netloc,
    new.path,
    new.query,
    ''))

https怎么样? - throws_exceptions_at_you

0
我使用了@Antony上面的答案,并使用了url-normalize库,但它有一个尚未修复的错误:当发送没有方案的URL时,它会意外地将其设置为HTTPS。 我编写了一个函数来包装和修复它,将其设置为HTTP。
from url_normalize import url_normalize
from urllib.parse import urlparse


def parse_url(url):
    return_val = url_normalize(url)
    wrong_default_prefix = "https://"
    new_default_prefix = "http://"
    # If the URL came with no scheme and the normalize function mistakenly 
    # set it to the HTTPS protocol, then fix it and set it to HTTP
    if urlparse(url).scheme.strip() == '' and return_val.startswith(wrong_default_prefix):
        return_val = new_default_prefix + return_val[len(wrong_default_prefix):]
    return return_val

那个库的作者说这是一个错误吗?在我看来,这似乎是有意为之的,因为出于安全考虑,应该优先选择https而不是http。 - DavidBooth
好的,这不应该发生,因为浏览器不会这样表现。 - Gil Cohen

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