urlparse 源码阅读

Table of content:


写这篇文章的初衷是在在使用 urljoin 的过程中,有几次的结果是我始料未及的,比如

1
2
3
4
5
6
7
from urlparse import urljoin

urljoin("//xxx.test.com/api/v1/", '/path/a') # http://xxx.test.com/path/a
urljoin('http://xxx.test.com/api/v1', '/path/a') # http://xxx.test.com/path/a

urljoin('http://xxx.test.com/api/v1', 'path/a') # http://xxx.test.com/api/path/a
urljoin('http://xxx.test.com/api/v1/', 'path/a') # http://xxx.test.com/api/v1/path/a

那么 urljoin 是怎么做的,urlparse 里还有什么其他的 function, 这里面涉及到的类有哪些?因为代码很简单,这篇文章只是一些逻辑分析。

关于 URL 结构

1
2
<scheme>://<netloc>/<path>;<params>?<query>#<fragment>
<scheme>://<username>:<password>@<host>:<port>/<path>;<parameters>?<query>#<fragment>
  • scheme, URL 使用的协议, 比如 HTTP, HTTPS, FTP
  • netloc, Network Location Part, 在 RFC 里面提到的是在 // 后面正式引入的才是 netloc, 比如 urlparse('www.cwi.nl/%7Eguido/Python.html') parse 出来的 netloc 是 '' 而 path 是 'www.cwi.nl/%7Eguido/Python.html'
  • username, password, host, port, path 对照上面的结构其实很清晰了。
  • parameters, 用 ; 分隔出来的 kv,使用的场景非常少, 在 RFC 里面也没有太看出和 query 的使用区别是什么。
  • query, 用 ? 分隔出来的 kv,
  • fragment, 用 # 分隔出来的锚点

主要的类

在 urlparse 中有两个比较关键的类 ParseResult, 和 ResultMixin

ParseResult

1
2
3
4
5
6
7
class ParseResult(namedtuple('ParseResult', 'scheme netloc path params query fragment'), ResultMixin):

__slots__ = ()

def geturl(self):
return urlunparse(self)

Python 在各个实例中名为 __dict__ 里存储实例属性,为了使用底层的散列表提高访问速度,字典会消耗大量的内存。如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量的内存,方法是让解释器在元祖中存储实例属性,而不用字典。

注:

  • 继承自超类的 __slots__ 属性没有效果,Python 只会使用各个类中定义的 __slots__ 属性
  • 实例只能拥有 __slots__ 中列出的属性

ResultMixin

ResultMixin 主要还是在把 netloc 里面的 username,password,host,port 的部分摘取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ResultMixin(object):
"""Shared methods for the parsed result objects."""

@property
def username(self):
netloc = self.netloc
if "@" in netloc:
userinfo = netloc.rsplit("@", 1)[0]
if ":" in userinfo:
userinfo = userinfo.split(":", 1)[0]
return userinfo
return None

@property
def password(self):
netloc = self.netloc
if "@" in netloc:
userinfo = netloc.rsplit("@", 1)[0]
if ":" in userinfo:
return userinfo.split(":", 1)[1]
return None

@property
def hostname(self):
netloc = self.netloc.split('@')[-1]
if '[' in netloc and ']' in netloc:
return netloc.split(']')[0][1:].lower()
elif ':' in netloc:
return netloc.split(':')[0].lower()
elif netloc == '':
return None
else:
return netloc.lower()

@property
def port(self):
netloc = self.netloc.split('@')[-1].split(']')[-1]
if ':' in netloc:
port = netloc.split(':')[1]
if port:
port = int(port, 10)
# verify legal port
if (0 <= port <= 65535):
return port
return None

这里比较有趣的是 hostname 是全部被处理成小写了。如果这里对大小写敏感,比如是 Mysql 或者 Redis 连接串时,可能就要注意了。

functions

主要的 function 有 urljoin、urlsplit、urlunsplit、urlparse, 这里目前只涉及到 urljoin

urljoin

在 urljoin 的过程中:

  • urlsplit 会将传入的两个 URL parse 为 ParseResult 对象
1
2
3
4
bscheme, bnetloc, bpath, bparams, bquery, bfragment = \
urlparse(base, '', allow_fragments)
scheme, netloc, path, params, query, fragment = \
urlparse(url, bscheme, allow_fragments)
  • 最近再根据不同的 scheme, 是否有 query,parameter,fragment 等情况将 parse 好的 URL unparse 回去, 返回完整的 URL
值得注意的点
  • 后一个 url 如果是以 / 开头, 会直接返回后者的 path
1
urljoin('http://xxx.test.com/api/v1', '/path/a')  # http://xxx.test.com/path/a
  • 如果后一个 url 不是以 / 开头,就会对两个 path 进行一些合并, 比如
1
urljoin('http://xxx.test.com/api/v1', 'path/a')  # http://xxx.test.com/api/path/a

Reference

关于头图

  • 拍摄自韩国首尔
防御式编程 EAFP 和 LBYL 的一些思考
youtube-dl download 的设计