Comment joindre les composants d'un chemin lorsque vous construisez une URL en Python

103

Par exemple, je veux joindre un chemin de préfixe aux chemins de ressources comme /js/foo.js.

Je veux que le chemin résultant soit relatif à la racine du serveur. Dans l'exemple ci-dessus, si le préfixe était "media", je voudrais que le résultat soit /media/js/foo.js.

os.path.join le fait très bien, mais la façon dont il joint les chemins dépend du système d'exploitation. Dans ce cas, je sais que je cible le Web, pas le système de fichiers local.

Existe-t-il une meilleure alternative lorsque vous travaillez avec des chemins dont vous savez qu'ils seront utilisés dans les URL? Os.path.join fonctionnera-t-il assez bien? Dois-je simplement rouler le mien?

amjoconn
la source
1
os.path.joinne fonctionnera pas. Mais la simple jonction par le /caractère devrait fonctionner dans tous les cas - /est le séparateur de chemin standard dans HTTP selon la spécification.
intégré

Réponses:

60

Puisque, d'après les commentaires publiés par l'OP, il semble qu'il ne veuille pas conserver les "URL absolues" dans la jointure (qui est l'une des tâches clés de urlparse.urljoin;-), je recommanderais d'éviter cela. os.path.joinserait également mauvais, pour exactement la même raison.

Donc, j'utiliserais quelque chose comme '/'.join(s.strip('/') for s in pieces)(si le début /doit également être ignoré - si le morceau principal doit être avec une casse spéciale, c'est également faisable bien sûr ;-).

Alex Martelli
la source
1
Merci. Cela ne me dérangeait pas tellement d'exiger que le début '/' de la deuxième partie ne puisse pas être là, mais exiger le '/' de fin de la première partie me donne l'impression que dans ce cas d'utilisation, urljoin ne faisait rien pour moi. Je voudrais au moins que join ("/ media", "js / foo.js") et join ("/ media /", "js / foo.js") fonctionnent. Merci pour ce qui semble être la bonne réponse: roulez le vôtre.
amjoconn
J'espérais que quelque chose ferait le "/" décapage et rejoindre pour moi.
statueofmike
Non, cela ne fonctionnera pas sur Windows, où os.path.join('http://media.com', 'content')nous reviendrons http://media.com\content.
SeF
154

Vous pouvez utiliser urllib.parse.urljoin:

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

Mais attention :

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

La raison pour laquelle vous obtenez des résultats différents de /js/foo.jset js/foo.jsest que le premier commence par une barre oblique qui signifie qu'il commence déjà à la racine du site Web.

Sur Python 2, vous devez faire

from urlparse import urljoin
Ben James
la source
J'ai donc la suppression du premier "/" sur /js/foo.js, mais il semble que ce soit le cas avec os.path.join aussi. Exiger la barre oblique après les médias signifie que je dois de toute façon faire la plupart du travail moi-même.
amjoconn
Plus précisément, une fois que j'ai que le préfixe doit se terminer par / et que le chemin cible ne peut pas commencer dans / je pourrais tout aussi bien concaténer. Dans ce cas, je ne sais pas si urljoin aide vraiment?
amjoconn
3
@MedhatGayed Ce n'est pas clair pour moi qui urljoinsupprime jamais «/». Si je l'appelle avec urlparse.urljoin('/media/', '/js/foo.js')la valeur renvoyée est «/js/foo.js». Il a supprimé tous les médias, pas le double «/». En fait urlparse.urljoin('/media//', 'js/foo.js')retourne en fait '/media//js/foo.js', donc aucun dupliqué supprimé.
amjoconn
8
urljoin a un comportement étrange si vous joignez un composant qui ne se termine pas par / il supprime le premier composant à sa base et rejoint ensuite les autres arguments. Pas ce à quoi je m'attendais.
Pete
7
Malheureusement, ce urljoinn'est pas pour rejoindre des URL. C'est pour résoudre les URL relatives telles que trouvées dans les documents HTML, etc.
OrangeDog
46

Comme vous le dites, os.path.joinjoint les chemins en fonction du système d'exploitation actuel. posixpathest le module sous-jacent qui est utilisé sur les systèmes posix sous l'espace de noms os.path:

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

Ainsi, vous pouvez simplement importer et utiliser à la posixpath.joinplace des URL, qui sont disponibles et fonctionnent sur n'importe quelle plate-forme .

Edit: La suggestion de @ Pete est bonne, vous pouvez alias l'importation pour une meilleure lisibilité

from posixpath import join as urljoin

Edit: Je pense que cela est plus clair, ou du moins m'a aidé à comprendre, si vous regardez la source de os.py(le code ici est de Python 2.7.11, plus j'ai coupé quelques bits). Il y a des importations conditionnelles os.pyqui choisissent le module de chemin à utiliser dans l'espace de noms os.path. Tous les modules sous - jacents ( posixpath, ntpath, os2emxpath, riscospath) qui peuvent être importés dans os.py, comme aliasé path, sont là et existent pour être utilisé sur tous les systèmes. os.pychoisit simplement l'un des modules à utiliser dans l'espace os.pathde noms au moment de l'exécution en fonction du système d'exploitation actuel.

# 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
la source
4
from posixpath import join as urljoinl'alias joliment en quelque chose de facile à lire.
Pete
29

Cela fait bien le travail:

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
la source
9

La fonction basejoin dans le package urllib peut être ce que vous recherchez.

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: Je n'avais pas remarqué avant, mais urllib.basejoin semble mapper directement à urlparse.urljoin, ce qui rend ce dernier préféré.

mwcz
la source
9

En utilisant furl, pip install furlce sera:

 furl.furl('/media/path/').add(path='js/foo.js')
Vasili Pascal
la source
1
Si vous voulez que le résultat soit une chaîne, vous pouvez ajouter .urlà la fin:furl.furl('/media/path/').add(path='js/foo.js').url
Eyal Levin
furl fonctionne mieux pour joindre l'URL par rapport à urlparse.urljoin en python 2 atleast (y)
Ciasto piekarz
Il vaut mieux faire furl('/media/path/').add(path=furl('/js/foo.js').path).urlparce que furl('/media/path/').add(path='/js/foo.js').urlc'est/media/path//js/foo.js
bartolo-otrit
5

Je sais que c'est un peu plus que ce que l'OP demandait, mais j'avais les pièces à l'url suivante et je cherchais un moyen simple de les rejoindre:

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

Faire quelques recherches:

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

Donc, en plus du chemin de jonction auquel on a déjà répondu dans les autres réponses, pour obtenir ce que je cherchais, j'ai fait ce qui suit:

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

Selon la documentation, il faut EXACTEMENT un tuple en 5 parties.

Avec le format de tuple suivant:

schéma 0 Spécificateur de schéma d'URL chaîne vide

netloc 1 Partie d'emplacement réseau chaîne vide

chemin 2 Chemin hiérarchique chaîne vide

requête 3 Chaîne vide du composant de requête

fragment 4 Chaîne vide d'identifiant de fragment

Jmunsch
la source
5

Rune Kaagaard a fourni une solution géniale et compacte qui a fonctionné pour moi, je l'ai un peu développée:

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

Cela permet à tous les arguments d'être joints indépendamment des barres obliques de fin et de fin, tout en préservant la dernière barre oblique si elle est présente.

futuere
la source
Vous pouvez rendre cette dernière ligne un peu plus courte et plus pythonique en utilisant une compréhension de liste, comme:return "/".join([str(x).strip("/") for x in args]) + trailing_slash
Dan Coates
3

Pour améliorer légèrement la réponse d'Alex Martelli, ce qui suit permettra non seulement de nettoyer les barres obliques supplémentaires, mais également de préserver les barres obliques de fin (fin), ce qui peut parfois être utile:

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

Ce n'est pas aussi facile à lire cependant, et ne nettoiera pas plusieurs barres obliques supplémentaires.

Florent Thiery
la source
3

J'ai trouvé des choses qui ne me plaisaient pas dans toutes les solutions ci-dessus, alors j'ai proposé la mienne. Cette version s'assure que les pièces sont jointes avec une seule barre oblique et laisse seules les barres obliques de début et de fin. Non pip install, pas de urllib.parse.urljoinbizarrerie.

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
la source
0

Utilisation de furl et 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
la source