blob: 7a2e6ce746141c21b908a1209770a5e1c07b60af [file] [log] [blame]
Guido van Rossume7b146f2000-02-04 15:28:42 +00001"""Parse (absolute and relative) URLs.
2
3See RFC 1808: "Relative Uniform Resource Locators", by R. Fielding,
4UC Irvine, June 1995.
5"""
Guido van Rossum23cb2a81994-09-12 10:36:35 +00006
Fred Drakef606e8d2002-10-16 21:21:39 +00007__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
8 "urlsplit", "urlunsplit"]
Skip Montanaro40fc1602001-03-01 04:27:19 +00009
Guido van Rossum23cb2a81994-09-12 10:36:35 +000010# A classification of schemes ('' means apply by default)
Raymond Hettinger156c49a2004-05-07 05:50:35 +000011uses_relative = ['ftp', 'http', 'gopher', 'nntp', 'imap',
Georg Brandl89f35ac2006-01-20 17:24:23 +000012 'wais', 'file', 'https', 'shttp', 'mms',
13 'prospero', 'rtsp', 'rtspu', '', 'sftp']
Raymond Hettinger156c49a2004-05-07 05:50:35 +000014uses_netloc = ['ftp', 'http', 'gopher', 'nntp', 'telnet',
Georg Brandl89f35ac2006-01-20 17:24:23 +000015 'imap', 'wais', 'file', 'mms', 'https', 'shttp',
16 'snews', 'prospero', 'rtsp', 'rtspu', 'rsync', '',
17 'svn', 'svn+ssh', 'sftp']
Raymond Hettinger156c49a2004-05-07 05:50:35 +000018non_hierarchical = ['gopher', 'hdl', 'mailto', 'news',
Fred Drake23fd3d42006-04-01 06:11:07 +000019 'telnet', 'wais', 'imap', 'snews', 'sip', 'sips']
Raymond Hettinger156c49a2004-05-07 05:50:35 +000020uses_params = ['ftp', 'hdl', 'prospero', 'http', 'imap',
Fred Drake23fd3d42006-04-01 06:11:07 +000021 'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips',
Georg Brandl89f35ac2006-01-20 17:24:23 +000022 'mms', '', 'sftp']
Raymond Hettinger156c49a2004-05-07 05:50:35 +000023uses_query = ['http', 'wais', 'imap', 'https', 'shttp', 'mms',
Fred Drake23fd3d42006-04-01 06:11:07 +000024 'gopher', 'rtsp', 'rtspu', 'sip', 'sips', '']
Raymond Hettinger156c49a2004-05-07 05:50:35 +000025uses_fragment = ['ftp', 'hdl', 'http', 'gopher', 'news',
Georg Brandl89f35ac2006-01-20 17:24:23 +000026 'nntp', 'wais', 'https', 'shttp', 'snews',
27 'file', 'prospero', '']
Guido van Rossum23cb2a81994-09-12 10:36:35 +000028
29# Characters valid in scheme names
Guido van Rossumfad81f02000-12-19 16:48:13 +000030scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
31 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
32 '0123456789'
33 '+-.')
Guido van Rossum23cb2a81994-09-12 10:36:35 +000034
Guido van Rossum74495401997-07-14 19:08:15 +000035MAX_CACHE_SIZE = 20
Guido van Rossum3fd32ec1996-05-28 23:54:24 +000036_parse_cache = {}
37
38def clear_cache():
Tim Peterse1190062001-01-15 03:34:38 +000039 """Clear the parse cache."""
40 global _parse_cache
41 _parse_cache = {}
Guido van Rossum3fd32ec1996-05-28 23:54:24 +000042
43
Fred Drakead5177c2006-04-01 22:14:43 +000044class BaseResult(tuple):
45 """Base class for the parsed result objects.
46
47 This provides the attributes shared by the two derived result
48 objects as read-only properties. The derived classes are
49 responsible for checking the right number of arguments were
50 supplied to the constructor.
51
52 """
53
54 __slots__ = ()
55
56 # Attributes that access the basic components of the URL:
57
58 @property
59 def scheme(self):
60 return self[0]
61
62 @property
63 def netloc(self):
64 return self[1]
65
66 @property
67 def path(self):
68 return self[2]
69
70 @property
71 def query(self):
72 return self[-2]
73
74 @property
75 def fragment(self):
76 return self[-1]
77
78 # Additional attributes that provide access to parsed-out portions
79 # of the netloc:
80
81 @property
82 def username(self):
83 netloc = self.netloc
84 if "@" in netloc:
Guido van Rossumced4eb02008-01-05 01:21:57 +000085 userinfo = netloc.rsplit("@", 1)[0]
Fred Drakead5177c2006-04-01 22:14:43 +000086 if ":" in userinfo:
87 userinfo = userinfo.split(":", 1)[0]
88 return userinfo
89 return None
90
91 @property
92 def password(self):
93 netloc = self.netloc
94 if "@" in netloc:
Guido van Rossumced4eb02008-01-05 01:21:57 +000095 userinfo = netloc.rsplit("@", 1)[0]
Fred Drakead5177c2006-04-01 22:14:43 +000096 if ":" in userinfo:
97 return userinfo.split(":", 1)[1]
98 return None
99
100 @property
101 def hostname(self):
102 netloc = self.netloc
103 if "@" in netloc:
Guido van Rossumced4eb02008-01-05 01:21:57 +0000104 netloc = netloc.rsplit("@", 1)[1]
Fred Drakead5177c2006-04-01 22:14:43 +0000105 if ":" in netloc:
106 netloc = netloc.split(":", 1)[0]
107 return netloc.lower() or None
108
109 @property
110 def port(self):
111 netloc = self.netloc
112 if "@" in netloc:
Guido van Rossumced4eb02008-01-05 01:21:57 +0000113 netloc = netloc.rsplit("@", 1)[1]
Fred Drakead5177c2006-04-01 22:14:43 +0000114 if ":" in netloc:
115 port = netloc.split(":", 1)[1]
116 return int(port, 10)
117 return None
118
119
120class SplitResult(BaseResult):
121
122 __slots__ = ()
123
124 def __new__(cls, scheme, netloc, path, query, fragment):
125 return BaseResult.__new__(
126 cls, (scheme, netloc, path, query, fragment))
127
128 def geturl(self):
129 return urlunsplit(self)
130
131
132class ParseResult(BaseResult):
133
134 __slots__ = ()
135
136 def __new__(cls, scheme, netloc, path, params, query, fragment):
137 return BaseResult.__new__(
138 cls, (scheme, netloc, path, params, query, fragment))
139
140 @property
141 def params(self):
142 return self[3]
143
144 def geturl(self):
145 return urlunparse(self)
146
147
148def urlparse(url, scheme='', allow_fragments=True):
Tim Peterse1190062001-01-15 03:34:38 +0000149 """Parse a URL into 6 components:
150 <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
151 Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
152 Note that we don't break the components up in smaller bits
153 (e.g. netloc is a single string) and we don't expand % escapes."""
Fred Drake5751a222001-11-16 02:52:57 +0000154 tuple = urlsplit(url, scheme, allow_fragments)
155 scheme, netloc, url, query, fragment = tuple
156 if scheme in uses_params and ';' in url:
157 url, params = _splitparams(url)
158 else:
159 params = ''
Fred Drakead5177c2006-04-01 22:14:43 +0000160 return ParseResult(scheme, netloc, url, params, query, fragment)
Fred Drake5751a222001-11-16 02:52:57 +0000161
162def _splitparams(url):
163 if '/' in url:
164 i = url.find(';', url.rfind('/'))
165 if i < 0:
166 return url, ''
167 else:
168 i = url.find(';')
169 return url[:i], url[i+1:]
170
Johannes Gijsbers41e4faa2005-01-09 15:29:10 +0000171def _splitnetloc(url, start=0):
Guido van Rossumc6a04c22008-01-05 22:19:06 +0000172 delim = len(url) # position of end of domain part of url, default is end
173 for c in '/?#': # look for delimiters; the order is NOT important
174 wdelim = url.find(c, start) # find first of this delim
175 if wdelim >= 0: # if found
176 delim = min(delim, wdelim) # use earliest delim position
177 return url[start:delim], url[delim:] # return (domain, rest)
Johannes Gijsbers41e4faa2005-01-09 15:29:10 +0000178
Fred Drakead5177c2006-04-01 22:14:43 +0000179def urlsplit(url, scheme='', allow_fragments=True):
Fred Drake5751a222001-11-16 02:52:57 +0000180 """Parse a URL into 5 components:
181 <scheme>://<netloc>/<path>?<query>#<fragment>
182 Return a 5-tuple: (scheme, netloc, path, query, fragment).
183 Note that we don't break the components up in smaller bits
184 (e.g. netloc is a single string) and we don't expand % escapes."""
Fred Drakead5177c2006-04-01 22:14:43 +0000185 allow_fragments = bool(allow_fragments)
Alexandre Vassalotti2f9ca292007-12-13 17:58:23 +0000186 key = url, scheme, allow_fragments, type(url), type(scheme)
Tim Peterse1190062001-01-15 03:34:38 +0000187 cached = _parse_cache.get(key, None)
188 if cached:
189 return cached
190 if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
191 clear_cache()
Fred Drake5751a222001-11-16 02:52:57 +0000192 netloc = query = fragment = ''
Tim Peterse1190062001-01-15 03:34:38 +0000193 i = url.find(':')
194 if i > 0:
195 if url[:i] == 'http': # optimize the common case
196 scheme = url[:i].lower()
197 url = url[i+1:]
198 if url[:2] == '//':
Johannes Gijsbers41e4faa2005-01-09 15:29:10 +0000199 netloc, url = _splitnetloc(url, 2)
Fred Drake5751a222001-11-16 02:52:57 +0000200 if allow_fragments and '#' in url:
201 url, fragment = url.split('#', 1)
202 if '?' in url:
203 url, query = url.split('?', 1)
Fred Drakead5177c2006-04-01 22:14:43 +0000204 v = SplitResult(scheme, netloc, url, query, fragment)
205 _parse_cache[key] = v
206 return v
Tim Peterse1190062001-01-15 03:34:38 +0000207 for c in url[:i]:
208 if c not in scheme_chars:
209 break
210 else:
211 scheme, url = url[:i].lower(), url[i+1:]
Johannes Gijsbers41e4faa2005-01-09 15:29:10 +0000212 if scheme in uses_netloc and url[:2] == '//':
213 netloc, url = _splitnetloc(url, 2)
Fred Drake5751a222001-11-16 02:52:57 +0000214 if allow_fragments and scheme in uses_fragment and '#' in url:
215 url, fragment = url.split('#', 1)
216 if scheme in uses_query and '?' in url:
217 url, query = url.split('?', 1)
Fred Drakead5177c2006-04-01 22:14:43 +0000218 v = SplitResult(scheme, netloc, url, query, fragment)
219 _parse_cache[key] = v
220 return v
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000221
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000222def urlunparse((scheme, netloc, url, params, query, fragment)):
Tim Peterse1190062001-01-15 03:34:38 +0000223 """Put a parsed URL back together again. This may result in a
224 slightly different, but equivalent URL, if the URL that was parsed
225 originally had redundant delimiters, e.g. a ? with an empty query
226 (the draft states that these are equivalent)."""
Fred Drake5751a222001-11-16 02:52:57 +0000227 if params:
228 url = "%s;%s" % (url, params)
229 return urlunsplit((scheme, netloc, url, query, fragment))
230
231def urlunsplit((scheme, netloc, url, query, fragment)):
Guido van Rossumbbc05682002-10-14 19:59:54 +0000232 if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
Tim Peterse1190062001-01-15 03:34:38 +0000233 if url and url[:1] != '/': url = '/' + url
234 url = '//' + (netloc or '') + url
235 if scheme:
236 url = scheme + ':' + url
Tim Peterse1190062001-01-15 03:34:38 +0000237 if query:
238 url = url + '?' + query
239 if fragment:
240 url = url + '#' + fragment
241 return url
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000242
Fred Drakead5177c2006-04-01 22:14:43 +0000243def urljoin(base, url, allow_fragments=True):
Tim Peterse1190062001-01-15 03:34:38 +0000244 """Join a base URL and a possibly relative URL to form an absolute
245 interpretation of the latter."""
246 if not base:
247 return url
248 if not url:
249 return base
250 bscheme, bnetloc, bpath, bparams, bquery, bfragment = \
251 urlparse(base, '', allow_fragments)
252 scheme, netloc, path, params, query, fragment = \
253 urlparse(url, bscheme, allow_fragments)
254 if scheme != bscheme or scheme not in uses_relative:
255 return url
256 if scheme in uses_netloc:
257 if netloc:
258 return urlunparse((scheme, netloc, path,
259 params, query, fragment))
260 netloc = bnetloc
261 if path[:1] == '/':
262 return urlunparse((scheme, netloc, path,
263 params, query, fragment))
Brett Cannon8da2a522003-10-12 04:29:10 +0000264 if not (path or params or query):
Tim Peterse1190062001-01-15 03:34:38 +0000265 return urlunparse((scheme, netloc, bpath,
Brett Cannon8da2a522003-10-12 04:29:10 +0000266 bparams, bquery, fragment))
Tim Peterse1190062001-01-15 03:34:38 +0000267 segments = bpath.split('/')[:-1] + path.split('/')
268 # XXX The stuff below is bogus in various ways...
269 if segments[-1] == '.':
270 segments[-1] = ''
271 while '.' in segments:
272 segments.remove('.')
273 while 1:
274 i = 1
275 n = len(segments) - 1
276 while i < n:
277 if (segments[i] == '..'
278 and segments[i-1] not in ('', '..')):
279 del segments[i-1:i+1]
280 break
281 i = i+1
282 else:
283 break
284 if segments == ['', '..']:
285 segments[-1] = ''
286 elif len(segments) >= 2 and segments[-1] == '..':
287 segments[-2:] = ['']
288 return urlunparse((scheme, netloc, '/'.join(segments),
289 params, query, fragment))
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000290
Guido van Rossum3fd32ec1996-05-28 23:54:24 +0000291def urldefrag(url):
Tim Peterse1190062001-01-15 03:34:38 +0000292 """Removes any existing fragment from URL.
Guido van Rossum3fd32ec1996-05-28 23:54:24 +0000293
Tim Peterse1190062001-01-15 03:34:38 +0000294 Returns a tuple of the defragmented URL and the fragment. If
295 the URL contained no fragments, the second element is the
296 empty string.
297 """
Fred Drake5751a222001-11-16 02:52:57 +0000298 if '#' in url:
299 s, n, p, a, q, frag = urlparse(url)
300 defrag = urlunparse((s, n, p, a, q, ''))
301 return defrag, frag
302 else:
303 return url, ''
Guido van Rossum3fd32ec1996-05-28 23:54:24 +0000304
305
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000306test_input = """
307 http://a/b/c/d
308
309 g:h = <URL:g:h>
310 http:g = <URL:http://a/b/c/g>
311 http: = <URL:http://a/b/c/d>
312 g = <URL:http://a/b/c/g>
313 ./g = <URL:http://a/b/c/g>
314 g/ = <URL:http://a/b/c/g/>
315 /g = <URL:http://a/g>
316 //g = <URL:http://g>
317 ?y = <URL:http://a/b/c/d?y>
318 g?y = <URL:http://a/b/c/g?y>
319 g?y/./x = <URL:http://a/b/c/g?y/./x>
320 . = <URL:http://a/b/c/>
321 ./ = <URL:http://a/b/c/>
322 .. = <URL:http://a/b/>
323 ../ = <URL:http://a/b/>
324 ../g = <URL:http://a/b/g>
325 ../.. = <URL:http://a/>
326 ../../g = <URL:http://a/g>
327 ../../../g = <URL:http://a/../g>
328 ./../g = <URL:http://a/b/g>
329 ./g/. = <URL:http://a/b/c/g/>
330 /./g = <URL:http://a/./g>
331 g/./h = <URL:http://a/b/c/g/h>
332 g/../h = <URL:http://a/b/c/h>
333 http:g = <URL:http://a/b/c/g>
334 http: = <URL:http://a/b/c/d>
Andrew M. Kuchling5c355201999-01-06 22:13:09 +0000335 http:?y = <URL:http://a/b/c/d?y>
336 http:g?y = <URL:http://a/b/c/g?y>
337 http:g?y/./x = <URL:http://a/b/c/g?y/./x>
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000338"""
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000339
340def test():
Tim Peterse1190062001-01-15 03:34:38 +0000341 import sys
342 base = ''
343 if sys.argv[1:]:
344 fn = sys.argv[1]
345 if fn == '-':
346 fp = sys.stdin
347 else:
348 fp = open(fn)
349 else:
Raymond Hettingera6172712004-12-31 19:15:26 +0000350 try:
351 from cStringIO import StringIO
352 except ImportError:
353 from StringIO import StringIO
354 fp = StringIO(test_input)
Tim Peterse1190062001-01-15 03:34:38 +0000355 while 1:
356 line = fp.readline()
357 if not line: break
358 words = line.split()
359 if not words:
360 continue
361 url = words[0]
362 parts = urlparse(url)
363 print '%-10s : %s' % (url, parts)
364 abs = urljoin(base, url)
365 if not base:
366 base = abs
367 wrapped = '<URL:%s>' % abs
368 print '%-10s = %s' % (url, wrapped)
369 if len(words) == 3 and words[1] == '=':
370 if wrapped != words[2]:
371 print 'EXPECTED', words[2], '!!!!!!!!!!'
Guido van Rossum23cb2a81994-09-12 10:36:35 +0000372
373if __name__ == '__main__':
Tim Peterse1190062001-01-15 03:34:38 +0000374 test()