Cómo unir componentes de una ruta cuando está construyendo una URL en Python

103

Por ejemplo, quiero unir una ruta de prefijo a rutas de recursos como /js/foo.js.

Quiero que la ruta resultante sea relativa a la raíz del servidor. En el ejemplo anterior, si el prefijo era "media", me gustaría que el resultado fuera /media/js/foo.js.

os.path.join hace esto muy bien, pero la forma en que une las rutas depende del sistema operativo. En este caso, sé que estoy apuntando a la web, no al sistema de archivos local.

¿Existe una mejor alternativa cuando trabaja con rutas que sabe que se utilizarán en las URL? ¿Os.path.join funcionará lo suficientemente bien? ¿Debería enrollar el mío?

amjoconn
fuente
1
os.path.joinno trabajará. Pero simplemente unirse por el /carácter debería funcionar en todos los casos: /es el separador de ruta estándar en HTTP según la especificación.
intgr

Respuestas:

60

Dado que, a partir de los comentarios que publicó el OP, parece que no quiere conservar "URL absolutas" en la unión (que es uno de los trabajos clave de urlparse.urljoin;-), recomiendo evitarlo. os.path.jointambién sería malo, exactamente por la misma razón.

Entonces, usaría algo como '/'.join(s.strip('/') for s in pieces)(si el encabezado /también debe ignorarse, si el encabezado debe tener una carcasa especial, eso también es factible, por supuesto ;-).

Alex Martelli
fuente
1
Gracias. No me importó tanto requerir que el '/' inicial en la segunda parte no pudiera estar allí, pero requerir el '/' final en la primera parte me hace sentir como si en este caso de uso urljoin no estuviera haciendo nada para mi. Me gustaría al menos unirme ("/ media", "js / foo.js") y unirme ("/ media /", "js / foo.js") para trabajar. Gracias por lo que parece ser la respuesta correcta: enrolle el suyo.
amjoconn
Esperaba que algo hiciera el '/' desnudarse y unirse por mí.
statueofmike
No, esto no va a funcionar en Windows, donde os.path.join('http://media.com', 'content')volverá http://media.com\content.
SeF
154

Puede utilizar urllib.parse.urljoin:

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

Pero cuidado :

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

La razón por la que obtienes resultados diferentes de /js/foo.jsy js/foo.jses porque el primero comienza con una barra que significa que ya comienza en la raíz del sitio web.

En Python 2, tienes que hacer

from urlparse import urljoin
Ben James
fuente
Así que tengo la tira de la "/" inicial en /js/foo.js, pero parece que ese también sería el caso con os.path.join. Requerir la barra después de los medios significa que tengo que hacer la mayor parte del trabajo yo mismo de todos modos.
amjoconn
Específicamente, una vez que tengo que el prefijo debe terminar en / y que la ruta de destino no puede comenzar en /, también podría concatenar. En este caso, no estoy seguro de si urljoin realmente está ayudando.
amjoconn
3
@MedhatGayed No tengo claro que urljoinalguna vez elimine '/'. Si lo llamo con urlparse.urljoin('/media/', '/js/foo.js')el valor devuelto es '/js/foo.js'. Eliminó todos los medios, no el duplicado '/'. De hecho, en urlparse.urljoin('/media//', 'js/foo.js')realidad devuelve '/media//js/foo.js', por lo que no se eliminan los duplicados.
amjoconn
8
urljoin tiene un comportamiento extraño si está uniendo componentes que no terminan en / quita el primer componente a su base y luego une los otros argumentos. No es lo que esperaba.
Pete
7
Lamentablemente, urljoinno es para unirse a URL. Es para resolver URL relativas como se encuentran en documentos HTML, etc.
OrangeDog
46

Como dices, os.path.joinune rutas basadas en el sistema operativo actual. posixpathes el módulo subyacente que se utiliza en los sistemas posix bajo el espacio de nombres os.path:

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

Por lo tanto, puede importar y usar posixpath.joinen su lugar las URL, que están disponibles y funcionarán en cualquier plataforma .

Editar: la sugerencia de @ Pete es buena, puede alias de la importación para una mayor legibilidad

from posixpath import join as urljoin

Editar: Creo que esto queda más claro, o al menos me ayudó a entender, si miras la fuente de os.py(el código aquí es de Python 2.7.11, además he recortado algunos bits). Hay importaciones condicionales os.pyque eligen qué módulo de ruta usar en el espacio de nombres os.path. Todos los módulos subyacentes ( posixpath, ntpath, os2emxpath, riscospath) que pueden ser importados en os.py, alias como path, hay y existen para ser utilizado en todos los sistemas. os.pyes simplemente elegir uno de los módulos para usar en el espacio os.pathde nombres en tiempo de ejecución según el sistema operativo actual.

# 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
fuente
4
from posixpath import join as urljoinmuy bien lo alias a algo fácil de leer.
Pete
29

Esto hace bien el trabajo:

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))
Runa Kaagaard
fuente
9

La función basejoin en el paquete urllib podría ser lo que está buscando.

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

Editar: no me di cuenta antes, pero urllib.basejoin parece mapear directamente a urlparse.urljoin, lo que hace que este último sea el preferido.

mwcz
fuente
9

Usando furl, pip install furlserá:

 furl.furl('/media/path/').add(path='js/foo.js')
Vasili Pascal
fuente
1
Si desea que el resultado sea una cadena, puede agregar .urlal final:furl.furl('/media/path/').add(path='js/foo.js').url
Eyal Levin
furl funciona mejor para unirse a la URL en comparación con urlparse.urljoin en python 2 al menos (y)
Ciasto piekarz
Es mejor hacerlo furl('/media/path/').add(path=furl('/js/foo.js').path).urlporque furl('/media/path/').add(path='/js/foo.js').urles/media/path//js/foo.js
bartolo-otrit
5

Sé que esto es un poco más de lo que pidió el OP, sin embargo, tenía las piezas para la siguiente URL y estaba buscando una forma sencilla de unirme a ellas:

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

Mirando alrededor:

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

Entonces, además de la ruta de unión que ya se ha respondido en las otras respuestas, para obtener lo que estaba buscando, hice lo siguiente:

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

Según la documentación, se necesita EXACTAMENTE una tupla de 5 partes.

Con el siguiente formato de tupla:

esquema 0 URL especificador de esquema cadena vacía

netloc 1 Parte de ubicación de red cadena vacía

ruta 2 Ruta jerárquica cadena vacía

consulta 3 Componente de consulta cadena vacía

fragmento 4 Identificador de fragmento cadena vacía

jmunsch
fuente
5

Rune Kaagaard proporcionó una solución excelente y compacta que funcionó para mí, la amplié un poco:

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

Esto permite que todos los argumentos se unan independientemente de las barras al final y al final mientras se conserva la última barra si está presente.

Futuere
fuente
Puede hacer esa última línea un poco más corta y más Pythonic usando una lista de comprensión, como:return "/".join([str(x).strip("/") for x in args]) + trailing_slash
Dan Coates
3

Para mejorar ligeramente la respuesta de Alex Martelli, lo siguiente no solo limpiará las barras adicionales, sino que también conservará las barras finales (finales), que a veces pueden ser útiles:

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

Sin embargo, no es tan fácil de leer y no limpiará varias barras finales adicionales.

Florent Thiery
fuente
3

Encontré cosas que no me gustaron de todas las soluciones anteriores, así que se me ocurrió la mía. Esta versión asegura que las partes se unan con una sola barra y deja las barras al principio y al final solas. No pip install, sin urllib.parse.urljoinrarezas.

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
fuente
0

Usando furl y 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
fuente