在用 Python 构造 URL 时,如何连接路径的组件

例如,我想将前缀路径连接到/js/foo.js 这样的资源路径。

我希望得到的路径相对于服务器的根目录。在上面的例子中,如果前缀是“ media”,我希望结果是/media/js/foo.js。

Join 在这方面做得很好,但是它如何连接路径取决于操作系统。在这种情况下,我知道我的目标是网络,而不是本地文件系统。

当您使用您知道将在 URL 中使用的路径时,有没有最好的替代方案?OS Path 能好好工作吗?要我自己卷吗?

141205 次浏览

你可以使用 urllib.parse.urljoin:

>>> from urllib.parse import urljoin
>>> urljoin('/media/path/', 'js/foo.js')
'/media/path/js/foo.js'

但要当心:

>>> urljoin('/media/path', 'js/foo.js')
'/media/js/foo.js'
>>> urljoin('/media/path', '/js/foo.js')
'/js/foo.js'

/js/foo.jsjs/foo.js不同的结果是因为前者以一个斜杠开始,这意味着它已经从网站的根开始。

在 Python2上,必须这样做

from urlparse import urljoin

Urllib包中的 Basejoin函数可能就是您要寻找的。

basejoin = urljoin(base, url, allow_fragments=True)
Join a base URL and a possibly relative URL to form an absolute
interpretation of the latter.

编辑: 我之前没有注意到,但 urllib.basejoin 似乎直接映射到 urlparse.urljoin,因此更喜欢后者。

因为,从 OP 发布的评论来看,似乎他 没有想要在连接中保留“绝对 URL”(这是 urlparse.urljoin的关键工作之一; ——) ,所以我建议避免这样做。出于完全相同的原因,os.path.join也是不好的。

因此,我会使用类似于 '/'.join(s.strip('/') for s in pieces)的代码(如果主要的 /也必须被忽略——如果主要的代码必须是特殊情况下的,那当然也是可行的;)。

这种方法效果很好:

def urljoin(*args):
"""
Joins given arguments into an url. Trailing but not leading slashes are
stripped for each argument.
"""


return "/".join(map(lambda x: str(x).rstrip('/'), args))

如您所说,os.path.join基于当前操作系统连接路径。posixpath是用于 posx 系统名称空间 os.path下的底层模块:

>>> os.path.join is posixpath.join
True
>>> posixpath.join('/media/', 'js/foo.js')
'/media/js/foo.js'

因此,您可以只导入和使用 posixpath.join来代替 urls,这是可用的,并将在 任何平台上工作。

编辑: @Pete 的建议是个好建议,你可以把导入作为增加可读性的别名

from posixpath import join as urljoin

编辑: 我认为如果您查看 os.py的源代码(这里的代码来自 Python 2.7.11,另外我已经删减了一些比特) ,这样就更清楚了,或者至少帮助我理解了。os.py中有条件导入,它选择在名称空间 os.path中使用哪个路径模块。所有的基础模块(posixpathntpathos2emxpathriscospath) ,可以导入到 os.py,别名为 path,存在于所有系统上使用。os.py只是基于当前操作系统在运行时在名称空间 os.path中选择要使用的模块之一。

# os.py
import sys, errno


_names = sys.builtin_module_names


if 'posix' in _names:
# ...
from posix import *
# ...
import posixpath as path
# ...


elif 'nt' in _names:
# ...
from nt import *
# ...
import ntpath as path
# ...


elif 'os2' in _names:
# ...
from os2 import *
# ...
if sys.version.find('EMX GCC') == -1:
import ntpath as path
else:
import os2emxpath as path
from _emx_link import link
# ...


elif 'ce' in _names:
# ...
from ce import *
# ...
# We can use the standard Windows path.
import ntpath as path


elif 'riscos' in _names:
# ...
from riscos import *
# ...
import riscospath as path
# ...


else:
raise ImportError, 'no os specific module found'

我知道这比 OP 要求的要多一点,但是我已经有了下面这个网址,并且正在寻找一个简单的方法来加入它们:

>>> url = 'https://api.foo.com/orders/bartag?spamStatus=awaiting_spam&page=1&pageSize=250'

四处看看:

>>> split = urlparse.urlsplit(url)
>>> split
SplitResult(scheme='https', netloc='api.foo.com', path='/orders/bartag', query='spamStatus=awaiting_spam&page=1&pageSize=250', fragment='')
>>> type(split)
<class 'urlparse.SplitResult'>
>>> dir(split)
['__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__getstate__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_asdict', '_fields', '_make', '_replace', 'count', 'fragment', 'geturl', 'hostname', 'index', 'netloc', 'password', 'path', 'port', 'query', 'scheme', 'username']
>>> split[0]
'https'
>>> split = (split[:])
>>> type(split)
<type 'tuple'>

因此,除了已经在其他答案中得到回答的路径加入之外,为了得到我想要的,我做了以下几件事:

>>> split
('https', 'api.foo.com', '/orders/bartag', 'spamStatus=awaiting_spam&page=1&pageSize=250', '')
>>> unsplit = urlparse.urlunsplit(split)
>>> unsplit
'https://api.foo.com/orders/bartag?spamStatus=awaiting_spam&page=1&pageSize=250'

根据 文件,它正好需要一个5部分的元组。

元组格式如下:

方案0 URL 方案说明符空字符串

Netloc 1网络位置部分空字符串

分层路径空字符串

查询3查询组件空字符串

片段4片段标识符空字符串

为了稍微改进 Alex Martelli 的响应,下面的代码不仅可以清除额外的斜杠,还可以保留尾部(结束)斜杠,这有时候是很有用的:

>>> items = ["http://www.website.com", "/api", "v2/"]
>>> url = "/".join([(u.strip("/") if index + 1 < len(items) else u.lstrip("/")) for index, u in enumerate(items)])
>>> print(url)
http://www.website.com/api/v2/

但是它不太容易阅读,而且不会清除多个额外的尾部斜杠。

使用 furl,pip install furl将是:

 furl.furl('/media/path/').add(path='js/foo.js')

Rune Kaagaard 提供了一个非常好的紧凑的解决方案,我对它进行了一些扩展:

def urljoin(*args):
trailing_slash = '/' if args[-1].endswith('/') else ''
return "/".join(map(lambda x: str(x).strip('/'), args)) + trailing_slash

这允许将所有参数联接起来,而不管尾部的斜杠和结束斜杠,同时如果存在则保留最后一个斜杠。

使用 正则表达式(python 3)

>>> import re
>>> import furl
>>> p = re.compile(r'(\/)+')
>>> url = furl.furl('/media/path').add(path='/js/foo.js').url
>>> url
'/media/path/js/foo.js'
>>> p.sub(r"\1", url)
'/media/path/js/foo.js'
>>> url = furl.furl('/media/path').add(path='js/foo.js').url
>>> url
'/media/path/js/foo.js'
>>> p.sub(r"\1", url)
'/media/path/js/foo.js'
>>> url = furl.furl('/media/path/').add(path='js/foo.js').url
>>> url
'/media/path/js/foo.js'
>>> p.sub(r"\1", url)
'/media/path/js/foo.js'
>>> url = furl.furl('/media///path///').add(path='//js///foo.js').url
>>> url
'/media///path/////js///foo.js'
>>> p.sub(r"\1", url)
'/media/path/js/foo.js'

我发现上述所有的解决方案都有不好的地方,所以我想出了自己的解决方案。这个版本确保各个部分使用一个斜杠连接,而不使用前导斜杠和尾随斜杠。没有 pip install也没有 urllib.parse.urljoin怪异。

In [1]: from functools import reduce


In [2]: def join_slash(a, b):
...:     return a.rstrip('/') + '/' + b.lstrip('/')
...:


In [3]: def urljoin(*args):
...:     return reduce(join_slash, args) if args else ''
...:


In [4]: parts = ['https://foo-bar.quux.net', '/foo', 'bar', '/bat/', '/quux/']


In [5]: urljoin(*parts)
Out[5]: 'https://foo-bar.quux.net/foo/bar/bat/quux/'


In [6]: urljoin('https://quux.com/', '/path', 'to/file///', '//here/')
Out[6]: 'https://quux.com/path/to/file/here/'


In [7]: urljoin()
Out[7]: ''


In [8]: urljoin('//','beware', 'of/this///')
Out[8]: '/beware/of/this///'


In [9]: urljoin('/leading', 'and/', '/trailing/', 'slash/')
Out[9]: '/leading/and/trailing/slash/'

这样吧: 是 有点效率,有点简单。 只需加入网址路径的’2’部分:

def UrlJoin(a , b):
a, b = a.strip(), b.strip()
a = a if a.endswith('/') else a + '/'
b = b if not b.startswith('/') else b[1:]
return a + b

或: 更传统,但没有效率,如果加入只有两个网址部分的路径。

def UrlJoin(*parts):
return '/'.join([p.strip().strip('/') for p in parts])

测试案例:

>>> UrlJoin('https://example.com/', '/TestURL_1')
'https://example.com/TestURL_1'


>>> UrlJoin('https://example.com', 'TestURL_2')
'https://example.com/TestURL_2'

注意: 我在这里可能有些吹毛求疵,但这至少是一个很好的实践,而且可能更具可读性。

一句话:

from functools import reduce
reduce(lambda x,y: '{}/{}'.format(x,y), parts)

零件在哪里,例如「 https://api.somecompany.com/v1」、「天气」、「雨水」

还有一种具有独特特征的变体:

def urljoin(base:str, *parts:str) -> str:
for part in filter(None, parts):
base = '{}/{}'.format(base.rstrip('/'), part.lstrip('/'))
return base
  • 保留尾部斜线在基地或最后一部分
  • 空的部分被忽略
  • 对于每个非空零件,从底部移除尾随和从零件引导,并用单个 /连接
urljoin('http://a.com/api',  '')  -> 'http://a.com/api'
urljoin('http://a.com/api',  '/') -> 'http://a.com/api/'
urljoin('http://a.com/api/', '')  -> 'http://a.com/api/'
urljoin('http://a.com/api/', '/') -> 'http://a.com/api/'
urljoin('http://a.com/api/', '/a/', '/b', 'c', 'd/') -> 'http://a.com/api/a/b/c/d/'

好吧,这就是我所做的,因为我需要完全独立于预定义的根:

def url_join(base: str, *components: str, slash_left=True, slash_right=True) -> str:
"""Join two or more url components, inserting '/' as needed.
Optionally, a slash can be added to the left or right side of the URL.
"""
base = base.lstrip('/').rstrip('/')
components = [component.lstrip('/').rstrip('/') for component in components]
url = f"/{base}" if slash_left else base
for component in components:
url = f"{url}/{component}"
return f"{url}/" if slash_right else url


url_join("http://whoops.io", "foo/", "/bar", "foo", slash_left=False)
# "http://whoops.io/foo/bar/foo/"
url_join("foo", "bar")
# "/foo/bar/""