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?


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 ;-).


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

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'

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

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.


9

Usando furl, pip install furlserá:

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

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


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.


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.


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

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'
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.