Cara menggabungkan komponen jalur saat Anda membuat URL dengan Python

103

Misalnya, saya ingin bergabung dengan jalur awalan ke jalur sumber daya seperti /js/foo.js.

Saya ingin jalur yang dihasilkan relatif terhadap root server. Dalam contoh di atas, jika awalannya adalah "media", saya ingin hasilnya menjadi /media/js/foo.js.

os.path.join melakukan ini dengan sangat baik, tetapi cara bergabung dengan jalur bergantung pada OS. Dalam hal ini saya tahu saya menargetkan web, bukan sistem file lokal.

Apakah ada alternatif terbaik saat Anda bekerja dengan jalur yang Anda tahu akan digunakan di URL? Akankah os.path.join bekerja dengan cukup baik? Haruskah saya menggulung sendiri?

amjoconn
sumber
1
os.path.jointidak akan berfungsi. Tetapi hanya bergabung dengan /karakter harus berfungsi dalam semua kasus - /adalah pemisah jalur standar di HTTP sesuai spesifikasi.
intgr

Jawaban:

60

Karena, dari komentar yang diposting OP, sepertinya dia tidak ingin mempertahankan "URL absolut" saat bergabung (yang merupakan salah satu tugas utama urlparse.urljoin;-), saya sarankan untuk menghindari itu. os.path.joinjuga buruk, karena alasan yang persis sama.

Jadi, saya akan menggunakan sesuatu seperti '/'.join(s.strip('/') for s in pieces)(jika bagian depan /juga harus diabaikan - jika bagian utama harus menggunakan kasing khusus, tentu saja itu juga layak ;-).

Alex Martelli
sumber
1
Terima kasih. Saya tidak keberatan terlalu banyak mengharuskan '/' di bagian kedua tidak bisa ada di sana, tetapi membutuhkan tanda '/' di bagian pertama membuat saya merasa seolah-olah dalam kasus penggunaan ini urljoin tidak melakukan apa-apa untuk saya. Saya ingin setidaknya bergabung ("/ media", "js / foo.js") dan bergabung ("/ media /", "js / foo.js") untuk bekerja. Terima kasih atas jawaban yang tampaknya benar: putar jawaban Anda sendiri.
amjoconn
Saya berharap sesuatu akan melakukan stripping '/' dan bergabung untuk saya.
statueofmike
Tidak, ini tidak akan berfungsi di windows, di mana os.path.join('http://media.com', 'content')wourd kembali http://media.com\content.
SeF
154

Anda dapat menggunakan urllib.parse.urljoin:

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

Namun berhati-hatilah :

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

Alasan Anda mendapatkan hasil yang berbeda dari /js/foo.jsdan js/foo.jskarena yang pertama dimulai dengan garis miring yang menandakan bahwa hasil tersebut sudah dimulai di root situs web.

Di Python 2, Anda harus melakukannya

from urlparse import urljoin
Ben James
sumber
Jadi saya telah menghapus awalan "/" di /js/foo.js, tetapi tampaknya itu juga terjadi pada os.path.join. Membutuhkan garis miring setelah media berarti saya harus melakukan sebagian besar pekerjaan sendiri.
amjoconn
Khususnya setelah saya memiliki awalan yang harus diakhiri dengan / dan jalur target tidak dapat dimulai di / Saya mungkin juga hanya menggabungkan. Dalam hal ini saya tidak yakin apakah urljoin benar-benar membantu?
amjoconn
3
@MedhatGayed Tidak jelas bagi saya bahwa tidak urljoinpernah menghapus '/'. Jika saya menyebutnya dengan urlparse.urljoin('/media/', '/js/foo.js')nilai yang dikembalikan adalah '/js/foo.js'. Ini menghapus semua media, bukan duplikat '/'. Bahkan urlparse.urljoin('/media//', 'js/foo.js')sebenarnya mengembalikan '/media//js/foo.js', jadi tidak ada duplikat yang dihapus.
amjoconn
8
urljoin memiliki perilaku aneh jika Anda bergabung dengan komponen yang tidak diakhiri / itu strip komponen pertama ke basisnya dan kemudian bergabung dengan argumen lain. Tidak seperti yang saya harapkan.
Pete
7
Sayangnya urljoinbukan untuk bergabung dengan URL. Itu untuk menyelesaikan URL relatif seperti yang ditemukan dalam dokumen HTML, dll.
OrangeDog
46

Seperti yang Anda katakan, os.path.joinbergabung dengan jalur berdasarkan os saat ini. posixpathadalah modul dasar yang digunakan pada sistem posix di bawah namespace os.path:

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

Jadi Anda bisa mengimpor dan menggunakan posixpath.joinurl, yang tersedia dan akan berfungsi di platform apa pun .

Edit: Saran @ Pete bagus, Anda bisa alias impor untuk meningkatkan keterbacaan

from posixpath import join as urljoin

Sunting: Saya pikir ini dibuat lebih jelas, atau setidaknya membantu saya memahami, jika Anda melihat ke dalam sumber os.py(kode di sini adalah dari Python 2.7.11, ditambah saya telah memangkas beberapa bit). Ada impor bersyarat os.pyyang memilih modul jalur mana yang akan digunakan di namespace os.path. Semua modul yang mendasari ( posixpath, ntpath, os2emxpath, riscospath) yang dapat diimpor dalam os.py, alias seperti path, ada dan eksis untuk digunakan pada semua sistem. os.pyhanya memilih salah satu modul untuk digunakan di namespace os.pathpada waktu proses berdasarkan OS saat ini.

# 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
sumber
4
from posixpath import join as urljoindengan baik menyamakannya dengan sesuatu yang mudah dibaca.
Pete
29

Ini melakukan pekerjaan dengan baik:

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
sumber
9

Fungsi basejoin dalam paket urllib mungkin yang Anda cari.

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

Sunting: Saya tidak memperhatikan sebelumnya, tetapi urllib.basejoin tampaknya memetakan langsung ke urlparse.urljoin, membuat yang terakhir lebih disukai.

mwcz.dll
sumber
9

Menggunakan furl, pip install furlitu akan menjadi:

 furl.furl('/media/path/').add(path='js/foo.js')
Vasili Pascal
sumber
1
Jika Anda ingin hasilnya menjadi string, Anda dapat menambahkan .urldi akhir:furl.furl('/media/path/').add(path='js/foo.js').url
Eyal Levin
furl bekerja lebih baik dalam menggabungkan URL dibandingkan dengan urlparse.urljoin di python 2 atleast (y)
Ciasto piekarz
Ini lebih baik untuk melakukan furl('/media/path/').add(path=furl('/js/foo.js').path).urlkarena furl('/media/path/').add(path='/js/foo.js').urladalah/media/path//js/foo.js
bartolo-otrit
5

Saya tahu ini sedikit lebih dari yang diminta OP, Namun saya memiliki potongan ke url berikut, dan sedang mencari cara sederhana untuk bergabung dengan mereka:

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

Melakukan beberapa melihat sekeliling:

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

Jadi selain penggabungan jalur yang telah dijawab di jawaban lain, Untuk mendapatkan apa yang saya cari saya lakukan sebagai berikut:

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

Menurut dokumentasi yang dibutuhkan PERSIS 5 bagian tupel.

Dengan format tuple berikut:

skema 0 Penentu skema URL string kosong

netloc 1 Bagian lokasi jaringan string kosong

jalur 2 Jalur hierarki string kosong

query 3 Query komponen string kosong

fragmen 4 string kosong pengidentifikasi Fragmen

jmunsch
sumber
5

Rune Kaagaard memberikan solusi hebat dan ringkas yang berhasil untuk saya, saya mengembangkannya sedikit:

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

Ini memungkinkan semua argumen untuk digabungkan, terlepas dari garis miring di akhir dan di akhir sambil mempertahankan garis miring terakhir jika ada.

futuere
sumber
Anda dapat membuat baris terakhir itu sedikit lebih pendek dan lebih Pythonic dengan menggunakan pemahaman daftar, seperti:return "/".join([str(x).strip("/") for x in args]) + trailing_slash
Dan Coates
3

Untuk sedikit meningkatkan respons Alex Martelli, berikut ini tidak hanya akan membersihkan garis miring ekstra tetapi juga mempertahankan garis miring (akhir) garis miring, yang terkadang berguna:

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

Ini tidak semudah membaca, dan tidak akan membersihkan beberapa garis miring tambahan.

Florent Thiery
sumber
3

Saya menemukan hal-hal yang tidak disukai tentang semua solusi di atas, jadi saya datang dengan solusi saya sendiri. Versi ini memastikan bagian-bagian digabungkan dengan satu garis miring dan meninggalkan garis miring di depan dan di belakang saja. Tidak pip install, tidak ada urllib.parse.urljoinkeanehan.

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

Menggunakan furl dan 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
sumber