Come unire i componenti di un percorso quando si costruisce un URL in Python


103

Ad esempio, voglio unire un percorso di prefisso a percorsi di risorse come /js/foo.js.

Voglio che il percorso risultante sia relativo alla radice del server. Nell'esempio precedente, se il prefisso fosse "media", vorrei che il risultato fosse /media/js/foo.js.

os.path.join lo fa molto bene, ma il modo in cui unisce i percorsi dipende dal sistema operativo. In questo caso so che sto prendendo di mira il Web, non il file system locale.

Esiste un'alternativa migliore quando lavori con percorsi che sai verranno utilizzati negli URL? Os.path.join funzionerà abbastanza bene? Dovrei solo rotolare il mio?


1
os.path.joinnon funzionerà. Ma la semplice unione per /carattere dovrebbe funzionare in tutti i casi: /è il separatore di percorso standard in HTTP secondo le specifiche.
intgr

Risposte:


60

Dal momento che, dai commenti postati dall'OP, sembra che non voglia conservare "URL assoluti" nel join (che è uno dei compiti chiave di urlparse.urljoin;-), consiglio di evitarlo. os.path.joinSarebbe anche un male, esattamente per lo stesso motivo.

Quindi, userei qualcosa del tipo '/'.join(s.strip('/') for s in pieces)(se anche l'interlinea /deve essere ignorata - se il pezzo principale deve essere in un caso speciale, anche questo è ovviamente fattibile ;-).


1
Grazie. Non mi importava tanto di richiedere che la "/" iniziale nella seconda parte non potesse essere lì, ma richiedere la fine "/" nella prima parte mi fa sentire come se in questo caso d'uso urljoin non stesse facendo nulla per me. Vorrei almeno che join ("/ media", "js / foo.js") e join ("/ media /", "js / foo.js") funzionassero. Grazie per quella che sembra essere la risposta giusta: tira la tua.
amjoconn

Speravo che qualcosa avrebbe fatto il "/" spogliarsi e unirsi per me.
statueofmike

No, questo non funzionerà su Windows, dove os.path.join('http://media.com', 'content')tornerà il mondo http://media.com\content.
SeF

154

Puoi usare urllib.parse.urljoin:

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

Ma attenzione :

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

Il motivo per cui si ottengono risultati diversi /js/foo.jsed js/foo.jsè perché il primo inizia con una barra che significa che inizia già alla radice del sito Web.

Su Python 2, devi farlo

from urlparse import urljoin

Quindi ho la rimozione della "/" iniziale su /js/foo.js, ma sembra che sarebbe anche il caso di os.path.join. Richiedere il taglio dopo i media significa che devo comunque fare la maggior parte del lavoro da solo.
amjoconn

In particolare, una volta che ho che il prefisso deve finire in / e che il percorso di destinazione non può iniziare in /, potrei anche solo concatenarlo. In questo caso non sono sicuro che urljoin stia davvero aiutando?
amjoconn

3
@MedhatGayed Non è chiaro per me che urljoinrimuove mai "/". Se lo chiamo con urlparse.urljoin('/media/', '/js/foo.js')il valore restituito è "/js/foo.js". Ha rimosso tutti i media, non il duplicato "/". In urlparse.urljoin('/media//', 'js/foo.js')realtà, restituisce "/media//js/foo.js", quindi nessun duplicato viene rimosso.
amjoconn

8
urljoin ha un comportamento strano se stai unendo componenti che non finiscono in /, rimuove il primo componente alla sua base e poi unisce gli altri argomenti su. Non quello che mi sarei aspettato.
Pete

7
Purtroppo urljoinnon è per l'adesione agli URL. È per risolvere gli URL relativi come si trovano nei documenti HTML, ecc.
OrangeDog

46

Come dici tu, os.path.joinunisce i percorsi in base al sistema operativo corrente. posixpathè il modulo sottostante che viene utilizzato sui sistemi posix sotto lo spazio dei nomi os.path:

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

Quindi puoi semplicemente importare e utilizzare posixpath.joininvece per gli URL, che è disponibile e funzionerà su qualsiasi piattaforma .

Modifica: il suggerimento di @ Pete è buono, puoi alias l'importazione per una maggiore leggibilità

from posixpath import join as urljoin

Modifica: penso che questo sia reso più chiaro, o almeno mi ha aiutato a capire, se guardi nel sorgente di os.py(il codice qui è di Python 2.7.11, in più ho tagliato alcuni bit). Ci sono importazioni condizionali in os.pyquello che sceglie quale modulo di percorso usare nello spazio dei nomi os.path. Tutti i moduli sottostanti ( posixpath, ntpath, os2emxpath, riscospath) che possono essere importati in os.py, come alias path, ci sono e esistono per essere usato su tutti i sistemi. os.pysta solo scegliendo uno dei moduli da utilizzare nello spazio dei nomi os.pathin fase di esecuzione in base al sistema operativo corrente.

# 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 urljoinlo alias piacevolmente a qualcosa di facile da leggere.
Pete

29

Questo fa bene il lavoro:

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 funzione basejoin nel pacchetto urllib potrebbe essere quello che stai cercando.

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

Modifica: non l'ho notato prima, ma urllib.basejoin sembra mappare direttamente a urlparse.urljoin, rendendo quest'ultimo preferito.


9

Utilizzando furl, pip install furlsarà:

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

1
Se vuoi che il risultato sia una stringa puoi aggiungere .urlalla fine:furl.furl('/media/path/').add(path='js/foo.js').url
Eyal Levin

furl funziona meglio nell'unirsi all'URL rispetto a urlparse.urljoin in python 2 atleast (y)
Ciasto piekarz

È meglio farlo furl('/media/path/').add(path=furl('/js/foo.js').path).urlperché furl('/media/path/').add(path='/js/foo.js').urlè/media/path//js/foo.js
bartolo-otrit

5

So che questo è un po 'più di quanto richiesto dall'OP, tuttavia avevo i pezzi al seguente URL e stavo cercando un modo semplice per unirli:

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

Guardandosi intorno:

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

Quindi oltre al percorso di giunzione a cui è già stata data risposta nelle altre risposte, per ottenere quello che cercavo ho fatto quanto segue:

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

Secondo la documentazione ci vuole ESATTAMENTE una tupla di 5 parti.

Con il seguente formato di tupla:

schema 0 URL specificatore di schema stringa vuota

netloc 1 Stringa vuota della parte della posizione di rete

percorso 2 Percorso gerarchico stringa vuota

query 3 Stringa vuota del componente di query

frammento 4 Identificatore del frammento stringa vuota


5

Rune Kaagaard ha fornito una soluzione fantastica e compatta che ha funzionato per me, l'ho ampliata un po ':

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

Ciò consente di unire tutti gli argomenti indipendentemente dalle barre finali e finali, preservando l'ultima barra, se presente.


Puoi rendere quell'ultima riga un po 'più breve e più pitonica usando una comprensione dell'elenco, come:return "/".join([str(x).strip("/") for x in args]) + trailing_slash
Dan Coates

3

Per migliorare leggermente la risposta di Alex Martelli, quanto segue non solo pulirà le barre extra, ma preserverà anche le barre finali (finali), che a volte possono essere utili:

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

Tuttavia, non è così facile da leggere e non cancellerà più barre finali aggiuntive.


3

Ho trovato cose che non mi piacciono in tutte le soluzioni di cui sopra, quindi ho pensato alla mia. Questa versione assicura che le parti siano unite con una singola barra e lascia da sole le barre iniziali e finali. No pip install, nessuna urllib.parse.urljoinstranezza.

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

Usare 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'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.