Como juntar componentes de um caminho ao construir um URL em Python

103

Por exemplo, quero juntar um caminho de prefixo para caminhos de recursos como /js/foo.js.

Quero que o caminho resultante seja relativo à raiz do servidor. No exemplo acima, se o prefixo fosse "media", gostaria que o resultado fosse /media/js/foo.js.

os.path.join faz isso muito bem, mas como ele une caminhos depende do sistema operacional. Nesse caso, sei que estou visando a web, não o sistema de arquivos local.

Existe uma alternativa melhor quando você está trabalhando com caminhos que sabe que serão usados ​​em URLs? O os.path.join funcionará bem o suficiente? Devo apenas lançar o meu próprio?

Amjoconn
fonte
1
os.path.joinnão funciona. Mas simplesmente juntar pelo /caractere deve funcionar em todos os casos - /é o separador de caminho padrão em HTTP de acordo com a especificação.
intgr

Respostas:

60

Visto que, a partir dos comentários que o OP postou, parece que ele não deseja preservar "URLs absolutos" no join (que é um dos principais trabalhos de urlparse.urljoin;-), eu recomendo evitar isso. os.path.jointambém seria ruim, exatamente pelo mesmo motivo.

Então, eu usaria algo como '/'.join(s.strip('/') for s in pieces)(se o condutor /também deve ser ignorado - se o condutor deve ter uma caixa especial, isso também é viável, é claro ;-).

Alex Martelli
fonte
1
Obrigado. Não me importei tanto em exigir que o '/' inicial na segunda parte não pudesse estar lá, mas exigir o '/' final na primeira parte me fez sentir como se neste caso de uso o urljoin não estivesse fazendo nada para mim. Eu gostaria que pelo menos join ("/ media", "js / foo.js") e join ("/ media /", "js / foo.js") funcionassem. Obrigado pelo que parece ser a resposta certa: faça o seu próprio.
amjoconn
Eu esperava que algo fizesse a '/' remoção e união para mim.
estátua de Mike de
Não, isso não vai funcionar no windows, onde os.path.join('http://media.com', 'content')wourd return http://media.com\content.
SeF,
154

Você pode usar urllib.parse.urljoin:

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

Mas cuidado :

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

A razão pela qual você obtém resultados diferentes de /js/foo.jse js/foo.jsé porque o primeiro começa com uma barra que significa que já começa na raiz do site.

No Python 2, você deve fazer

from urlparse import urljoin
Ben James
fonte
Portanto, retirei o "/" inicial em /js/foo.js, mas parece que seria o caso com os.path.join também. Exigir a barra após a mídia significa que eu mesmo tenho que fazer a maior parte do trabalho.
amjoconn
Especificamente, depois de saber que o prefixo deve terminar em / e que o caminho de destino não pode começar em /, posso simplesmente concatenar. Nesse caso, não tenho certeza se o urljoin está realmente ajudando.
amjoconn
3
@MedhatGayed Não está claro para mim urljoinse remove '/'. Se eu chamá-lo com urlparse.urljoin('/media/', '/js/foo.js')o valor retornado é '/js/foo.js'. Ele removeu todas as mídias, não a duplicata '/'. Na urlparse.urljoin('/media//', 'js/foo.js')verdade, retorna '/media//js/foo.js', então nenhuma duplicata removida.
amjoconn
8
urljoin tem um comportamento estranho se você estiver juntando componentes que não terminam em / ele retira o primeiro componente de sua base e depois junta os outros argumentos. Não é o que eu esperava.
Pete
7
Infelizmente, urljoinnão é para juntar URLs. É para resolver URLs relativos como encontrados em documentos HTML, etc.
OrangeDog
46

Como você disse, os.path.joinjunta caminhos com base no sistema operacional atual. posixpathé o módulo subjacente que é usado em sistemas posix sob o namespace os.path:

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

Assim, você pode apenas importar e usar posixpath.joinpara urls, que estão disponíveis e funcionarão em qualquer plataforma .

Editar: a sugestão de @Pete é boa, você pode criar um alias para a importação para maior legibilidade

from posixpath import join as urljoin

Edit: Eu acho que isso fica mais claro, ou pelo menos me ajudou a entender, se você olhar para a fonte de os.py(o código aqui é do Python 2.7.11, além disso, cortei alguns bits). Há importações condicionais os.pyque selecionam qual módulo de caminho usar no namespace os.path. Todos os módulos subjacentes ( posixpath, ntpath, os2emxpath, riscospath) que podem ser importados em os.py, alias como path, estão lá e existem para ser utilizado em todos os sistemas. os.pyé apenas escolher um dos módulos para usar no namespace os.pathem tempo de execução com base no sistema operacional atual.

# 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'
GP89
fonte
4
from posixpath import join as urljoinagradavelmente aliases para algo fácil de ler.
Pete
29

Isso faz o trabalho muito bem:

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))
Rune Kaagaard
fonte
9

A função basejoin no pacote urllib pode ser o que você está procurando.

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

Edit: Eu não percebi antes, mas urllib.basejoin parece mapear diretamente para urlparse.urljoin, tornando o último preferido.

mwcz
fonte
9

Usando furl, pip install furlserá:

 furl.furl('/media/path/').add(path='js/foo.js')
Vasili Pascal
fonte
1
Se quiser que o resultado seja uma string, você pode adicionar .urlno final:furl.furl('/media/path/').add(path='js/foo.js').url
Eyal Levin
furl funciona melhor na junção de URL em comparação com urlparse.urljoin em python 2 atleast (y)
Ciasto piekarz
É melhor fazer furl('/media/path/').add(path=furl('/js/foo.js').path).urlporque furl('/media/path/').add(path='/js/foo.js').urlé/media/path//js/foo.js
bartolo-otrit
5

Eu sei que isso é um pouco mais do que o OP pediu, mas eu tinha as peças para a seguinte url e estava procurando uma maneira simples de juntá-las:

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

Olhando ao redor:

>>> 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'>

Portanto, além do caminho de junção que já foi respondido nas outras respostas, Para conseguir o que procurava fiz o seguinte:

>>> 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'

De acordo com a documentação , leva EXATAMENTE uma tupla de 5 partes.

Com o seguinte formato de tupla:

esquema 0 URL esquema especificador string vazia

netloc 1 string vazia da parte da localização da rede

caminho 2 string vazia do caminho hierárquico

consulta 3 String vazia do componente de consulta

fragmento 4 Identificador de fragmento string vazio

jmunsch
fonte
5

Rune Kaagaard forneceu uma solução excelente e compacta que funcionou para mim, eu a expandi um pouco:

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

Isso permite que todos os argumentos sejam unidos, independentemente das barras finais e finais, preservando a última barra, se houver.

futuere
fonte
Você pode deixar a última linha um pouco mais curta e mais pitônica usando uma compreensão de lista, como:return "/".join([str(x).strip("/") for x in args]) + trailing_slash
Dan Coates
3

Para melhorar um pouco a resposta de Alex Martelli, o seguinte não apenas limpará barras extras, mas também preservará barras finais (finais), que às vezes podem ser úteis:

>>> 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/

No entanto, não é tão fácil de ler e não limpará várias barras extras.

Florent Thiery
fonte
3

Descobri coisas que não gostei em todas as soluções acima, então criei a minha própria. Esta versão garante que as peças sejam unidas com uma única barra e deixa apenas as barras iniciais e finais. Não pip install, sem urllib.parse.urljoinestranheza.

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/'
cbare
fonte
0

Usando furl e regex (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'
Guillaume Cisco
fonte