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

Por ejemplo, quiero unir una ruta de prefijo a las 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”, quisiera que el resultado fuera /media/js/foo.js.

os.path.join hace esto realmente bien, pero la forma en que se une a 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 usarán en las URL? ¿Os.path.join funcionará lo suficientemente bien? ¿Debo rodar mi propia?

Dado que, a partir de los comentarios publicados por 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.join también sería malo, por exactamente la misma razón.

Por lo tanto, usaría algo como '/'.join(s.strip('/') for s in pieces) (si el encabezado / debe también ser ignorado – si la pieza líder debe tener una caja especial, eso también es factible por supuesto ;-).

Python2

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

Pero cuidado ,

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

tanto como

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

Python3

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

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

Como usted dice, os.path.join une rutas basadas en el sistema operativo actual. posixpath es el módulo subyacente que se usa en 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' 

Así que solo puede importar y usar posixpath.join lugar de las urls, que está disponible y funcionará en cualquier plataforma .

Edición: la sugerencia de @Pete es una buena idea, puedes hacer un alias de la importación para una mayor legibilidad

 from posixpath import join as urljoin 

Edición: creo que esto se hace más claro, o al menos me ayudó a entender, si analiza la fuente de os.py (el código aquí es de Python 2.7.11, además de que he recortado algunos bits). Hay importaciones condicionales en os.py que seleccionan qué módulo de ruta usar en el espacio de nombres os.path . Todos los módulos subyacentes ( posixpath , ntpath , os2emxpath , riscospath ) que pueden importarse en os.py , con alias como path , están allí y existen para ser utilizados en todos los sistemas. os.py está seleccionando uno de los módulos para usar en el espacio de nombres os.path en el 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' 

Esto hace el trabajo muy bien:

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

La función basejoin en el paquete urllib puede 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. 

Edit: no lo noté antes, pero urllib.basejoin parece asignarse directamente a urlparse.urljoin, haciendo que este último prefiera.

Usando furl, pip install furl será:

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

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

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

Mirando a su 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)  >>> 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)  

Entonces, además de la unión de ruta que ya ha sido respondida 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' 

De acuerdo con 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 la 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

Para mejorar ligeramente sobre la respuesta de Alex Martelli, lo siguiente no solo limpiará barras adicionales, sino que también conservará 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 diagonales adicionales.

Rune Kaagaard proporcionó una solución excelente y compacta que funcionó para mí, lo 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 diagonales al final y al final, al tiempo que se conserva la última barra diagonal si está presente.