diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..28ec9fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.md linguist-language=Python diff --git a/Advanced-Features/context.md b/Advanced-Features/context.md index 0716bbc..6807d9c 100644 --- a/Advanced-Features/context.md +++ b/Advanced-Features/context.md @@ -49,7 +49,7 @@ Exiting context # 调用了 __exit__ 方法 - Point(3, 4) 生成了一个上下文管理器; - 调用上下文管理器的 `__enter__()` 方法,并将 `__enter__()` 方法的返回值赋给 as 字句中的变量 pt; - 执行**语句体**(指 with 语句包裹起来的代码块)内容,输出 distance; -- 不管执行过程中是否发生异常,都执行上下文管理器的 `__exit__()` 方法。`__exit__()` 方法负责执行『清理』工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 `__exit__(None, None, None) `;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 `__exit__(exc_type, exc_value, exc_traceback)`; +- 不管执行过程中是否发生异常,都执行上下文管理器的 `__exit__()` 方法。`__exit__()` 方法负责执行『清理』工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 `__exit__(None, None, None) `;如果执行过程中出现异常,则使用 `sys.exc_info` 得到的异常信息为参数调用 `__exit__(exc_type, exc_value, exc_traceback)`; - 出现异常时,如果 `__exit__(type, value, traceback)` 返回 False 或 None,则会重新抛出异常,让 with 之外的语句逻辑来处理异常;如果返回 True,则忽略异常,不再对异常进行处理; 上面的 with 语句执行过程没有出现异常,我们再来看出现异常的情形: diff --git a/Advanced-Features/generator.md b/Advanced-Features/generator.md index 14d916e..9e330da 100644 --- a/Advanced-Features/generator.md +++ b/Advanced-Features/generator.md @@ -132,7 +132,7 @@ for piece in read_in_chunks(f): 我们除了能对生成器进行迭代使它返回值外,还能: - 使用 `send()` 方法给它发送消息; -- 使用 `thow()` 方法给它发送异常; +- 使用 `throw()` 方法给它发送异常; - 使用 `close()` 方法关闭生成器; ## send() 方法 diff --git a/Class/class_and_object.md b/Class/class_and_object.md index 416f2bb..1f3e6b9 100644 --- a/Class/class_and_object.md +++ b/Class/class_and_object.md @@ -30,7 +30,7 @@ class Animal(object): 然后,在创建实例的时候,传入参数: ```python ->>> animal = Aniaml('dog1') # 传入参数 'dog1' +>>> animal = Animal('dog1') # 传入参数 'dog1' >>> animal.name # 访问对象的 name 属性 'dog1' ``` diff --git a/Class/property.md b/Class/property.md index aca7f08..5ea1c1b 100644 --- a/Class/property.md +++ b/Class/property.md @@ -56,7 +56,7 @@ class Exam(object): 90 >>> e.score = 200 >>> e.score -200 +100 ``` 在上面,我们给方法 score 加上了 `@property`,于是我们可以把 score 当成一个属性来用,此时,又会创建一个新的装饰器 `score.setter`,它可以把被装饰的方法变成属性来赋值。 diff --git a/Class/singleton.md b/Class/singleton.md new file mode 100644 index 0000000..b88cee6 --- /dev/null +++ b/Class/singleton.md @@ -0,0 +1,126 @@ +# 单例模式 + +**单例模式(Singleton Pattern)**是一种常用的软件设计模式,该模式的主要目的是确保**某一个类只有一个实例存在**。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。 + +比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。 + +在 Python 中,我们可以用多种方法来实现单例模式: + +- 使用模块 +- 使用 `__new__` +- 使用装饰器(decorator) +- 使用元类(metaclass) + +# 使用模块 + +其实,**Python 的模块就是天然的单例模式**,因为模块在第一次导入时,会生成 `.pyc` 文件,当第二次导入时,就会直接加载 `.pyc` 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做: + +```python +# mysingleton.py +class My_Singleton(object): + def foo(self): + pass + +my_singleton = My_Singleton() +``` + +将上面的代码保存在文件 `mysingleton.py` 中,然后这样使用: + +``` +from mysingleton import my_singleton + +my_singleton.foo() +``` + +# 使用 `__new__` + +为了使类只能出现一个实例,我们可以使用 `__new__` 来控制实例的创建过程,代码如下: + +```python +class Singleton(object): + _instance = None + def __new__(cls, *args, **kw): + if not cls._instance: + cls._instance = super(Singleton, cls).__new__(cls, *args, **kw) + return cls._instance + +class MyClass(Singleton): + a = 1 +``` + +在上面的代码中,我们将类的实例和一个类变量 `_instance` 关联起来,如果 `cls._instance` 为 None 则创建实例,否则直接返回 `cls._instance`。 + +执行情况如下: + +``` +>>> one = MyClass() +>>> two = MyClass() +>>> one == two +True +>>> one is two +True +>>> id(one), id(two) +(4303862608, 4303862608) +``` + +# 使用装饰器 + +我们知道,装饰器(decorator)可以动态地修改一个类或函数的功能。这里,我们也可以使用装饰器来装饰某个类,使其只能生成一个实例,代码如下: + +``` +from functools import wraps + +def singleton(cls): + instances = {} + @wraps(cls) + def getinstance(*args, **kw): + if cls not in instances: + instances[cls] = cls(*args, **kw) + return instances[cls] + return getinstance + +@singleton +class MyClass(object): + a = 1 +``` + +在上面,我们定义了一个装饰器 `singleton`,它返回了一个内部函数 `getinstance`,该函数会判断某个类是否在字典 `instances` 中,如果不存在,则会将 `cls` 作为 key,`cls(*args, **kw)` 作为 value 存到 `instances` 中,否则,直接返回 `instances[cls]`。 + +# 使用 metaclass + +元类(metaclass)可以控制类的创建过程,它主要做三件事: + +- 拦截类的创建 +- 修改类的定义 +- 返回修改后的类 + +使用元类实现单例模式的代码如下: + +```python +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +# Python2 +class MyClass(object): + __metaclass__ = Singleton + +# Python3 +# class MyClass(metaclass=Singleton): +# pass +``` + +# 小结 + +- Python 的模块是天然的单例模式,这在大部分情况下应该是够用的,当然,我们也可以使用装饰器、元类等方法 + +# 参考资料 + +- [Creating a singleton in Python - Stack Overflow](http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) +- [深入浅出单实例Singleton设计模式 | 酷 壳](http://coolshell.cn/articles/265.html) +- [design patterns - Python's use of __new__ and __init__? - Stack Overflow](http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init) + + diff --git a/Conclusion/README.md b/Conclusion/README.md index 9c5a300..6f7d00e 100644 --- a/Conclusion/README.md +++ b/Conclusion/README.md @@ -1,2 +1,10 @@ # 结束语 +到这里,虽然本书结束了,但对于 Python 的学习和实践还远远没结束,后面我也会持续更新本书。虽然 Python 的语法相比 C++ 等语言比较简洁,但想熟练运用,仍需在实际的项目中多多实践,而不只是停留在简单的概念学习中。 + +这里主要推荐 Python 相关的一些学习资源,同时也列出本书的主要参考资料。 + +- [资源推荐](./resource_recommendation.md) +- [参考资料](./reference_material.md) + + diff --git a/Conclusion/resource_recommendation.md b/Conclusion/resource_recommendation.md index 20b90bd..5c5beee 100644 --- a/Conclusion/resource_recommendation.md +++ b/Conclusion/resource_recommendation.md @@ -1,2 +1,53 @@ # 资源推荐 +这里列出了 Python 相关的一些资源,欢迎读者补充。 + +- [vinta/awesome-python: A curated list of awesome Python frameworks, libraries, software and resources](https://github.com/vinta/awesome-python) + + 包含了 Python 框架、Python 库和软件的 awesome 列表。 + +- [aosabook/500lines: 500 Lines or Less](https://github.com/aosabook/500lines) + + Python 神书,里面有若干个项目,每个项目都是由业内大神所写,每个项目代码在 500 行左右。 + +- [Python Module of the Week - PyMOTW 2](https://pymotw.com/2/) + + 自 2007 年以来,[Doug Hellmann](http://www.doughellmann.com/) 在他的博客上发表了颇受关注的「Python Module of the Week」系列,计划每周介绍一个 Python 标准库的使用。上面的链接是介绍 Python2 中的标准库,同样也有 Python3 的:[Python 3 Module of the Week — PyMOTW 3](https://pymotw.com/3/#python-3-module-of-the-week)。 + +- [Transforming Code into Beautiful, Idiomatic Python](https://gist.github.com/JeffPaine/6213790) + + 写出简洁的、优雅的 Python 代码。 + +- [jobbole/awesome-python-cn: Python资源大全中文版](https://github.com/jobbole/awesome-python-cn) + + Python 资源大全,包含:Web 框架、网络爬虫、模板引擎和数据库等,由[伯乐在线](https://github.com/jobbole)更新。 + +- [Pycoder's Weekly | A Weekly Python E-Mail Newsletter](http://pycoders.com/) + + 优秀的免费邮件 Python 新闻周刊。 + +- [Python 初学者的最佳学习资源](http://python.jobbole.com/82399/) + + 伯乐在线翻译的 Python 学习资源。 + +- [Full Stack Python](http://www.fullstackpython.com/) + + Python 资源汇总,从基础入门到各种 Web 开发框架,再到高级的 ORM,Docker 等等。 + +- [The Hitchhiker’s Guide to Python!](http://docs.python-guide.org/en/latest/) + + [Requests](https://github.com/kennethreitz/requests) 作者 kennethreitz 的一本开源书籍,介绍 Python 的最佳实践。 + +- [Welcome to Python for you and me](http://pymbook.readthedocs.io/en/latest/) + + 介绍 Python 的基本语法,特点等。 + +- [District Data Labs - How to Develop Quality Python Code](https://districtdatalabs.silvrback.com/how-to-develop-quality-python-code) + + 开发高质量的 Python 代码。 + +- [A "Best of the Best Practices" (BOBP) guide to developing in Python.](https://gist.github.com/sloria/7001839) + + Python 最佳实践。 + + diff --git a/Datatypes/README.md b/Datatypes/README.md index 44fd8c2..397145e 100644 --- a/Datatypes/README.md +++ b/Datatypes/README.md @@ -7,7 +7,7 @@ 所有序列类型都可以进行某些通用的操作,比如: - 索引(indexing) -- 分片(sliceing) +- 分片(slicing) - 迭代(iteration) - 加(adding) - 乘(multiplying) diff --git a/Datatypes/set.md b/Datatypes/set.md index 32d4fd6..4feb79a 100644 --- a/Datatypes/set.md +++ b/Datatypes/set.md @@ -56,7 +56,7 @@ set(['a', 'c', 'b', 4, 'd', 'e']) ## 删除元素 -`remove()` 方法可以删除集合中的元素。 +`remove()` 方法可以删除集合中的元素, 但是删除不存在的元素,会抛出 KeyError,可改用 `discard()`。 看看例子: @@ -67,10 +67,11 @@ set(['a', 'c', 'b', 'd']) >>> s.remove('a') # 删除元素 'a' >>> s set(['c', 'b', 'd']) ->>> s.remove('e') # 删除不存在的元素,会抛出 KeyErro +>>> s.remove('e') # 删除不存在的元素,会抛出 KeyError Traceback (most recent call last): File "", line 1, in KeyError: 'e' +>>> s.discard('e') # 删除不存在的元素, 不会抛出 KeyError ``` ## 交集/并集/差集 diff --git a/File-Directory/os.md b/File-Directory/os.md index aa1b7b6..75cac12 100644 --- a/File-Directory/os.md +++ b/File-Directory/os.md @@ -109,7 +109,7 @@ False - os.walk -os.walk 是遍历目录常用的模块,它返回一个包含 3 个元素的元祖:(dirpath, dirnames, filenames)。dirpath 是以 string 字符串形式返回该目录下所有的绝对路径;dirnames 是以列表 list 形式返回每一个绝对路径下的文件夹名字;filesnames 是以列表 list 形式返回该路径下所有文件名字。 +os.walk 是遍历目录常用的模块,它返回一个包含 3 个元素的元组:(dirpath, dirnames, filenames)。dirpath 是以 string 字符串形式返回该目录下所有的绝对路径;dirnames 是以列表 list 形式返回每一个绝对路径下的文件夹名字;filenames 是以列表 list 形式返回该路径下所有文件名字。 ```python >>> for root, dirs, files in os.walk('/Users/ethan/coding'): diff --git a/Functional/decorator.md b/Functional/decorator.md index ad388e8..d6c4309 100644 --- a/Functional/decorator.md +++ b/Functional/decorator.md @@ -313,7 +313,7 @@ def hello(): 'wrapped' ``` -为了消除这样的副作用,Python 中的 functool 包提供了一个 wraps 的装饰器: +为了消除这样的副作用,Python 中的 functools 包提供了一个 wraps 的装饰器: ```python from functools import wraps diff --git a/HTTP/README.md b/HTTP/README.md index ade5b64..d0feb90 100644 --- a/HTTP/README.md +++ b/HTTP/README.md @@ -1,2 +1,8 @@ # HTTP 服务 +本章主要介绍: + +- [HTTP 协议](./HTTP.md) +- [Requests 库的使用](./Requests.md) + + diff --git a/HTTP/Requests.md b/HTTP/Requests.md new file mode 100644 index 0000000..be2efc2 --- /dev/null +++ b/HTTP/Requests.md @@ -0,0 +1,566 @@ +# Requests 库的使用 + +Python 的标准库 urllib 提供了大部分 HTTP 功能,但使用起来较繁琐。通常,我们会使用另外一个优秀的第三方库:[Requests](https://github.com/kennethreitz/requests),它的标语是:**Requests: HTTP for Humans**。 + +Requests 提供了很多功能特性,几乎涵盖了当今 Web 服务的需求,比如: + +- 浏览器式的 SSL 验证 +- 身份认证 +- Keep-Alive & 连接池 +- 带持久 Cookie 的会话 +- 流下载 +- 文件分块上传 + +下面,我们将从以下几个方面介绍 Requests 库: + +- HTTP 请求 +- HTTP 响应 +- cookie +- 会话对象 +- 代理 +- 身份认证 + +# HTTP 请求 + +我们知道,一个 HTTP 请求由三部分构成: + +- **请求行**:包含请求方法(比如 GET, POST)、请求地址和 HTTP 协议版本 +- **请求头**:包含一系列的键值对 +- **请求正文(可选)** + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e61d5d47.png) + +Requests 提供了几乎所有 HTTP 动词的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH、DELETE,另外,它提供了 `headers` 参数让我们根据需求定制请求头。 + +使用 Requests 发送一个请求很方便,比如: + +``` +import requests + +r = requests.get("http://httpbin.org/get") +r = requests.post("http://httpbin.org/post") +r = requests.put("http://httpbin.org/put") +r = requests.delete("http://httpbin.org/delete") +r = requests.head("http://httpbin.org/get") +r = requests.options("http://httpbin.org/get") +``` + +下面,我们重点讲一下 GET 请求,POST 请求和定制请求头。 + +## GET 请求 + +使用 Requests 发送 GET 请求非常简单,如下: + +```python +import requests + +r = requests.get("http://httpbin.org/get") +``` + +在有些情况下,URL 会带参数,比如 `https://segmentfault.com/blogs?page=2`,这个 URL 有一个参数 page,值为 2。Requests 提供了 `params` 关键字参数,允许我们以一个字典来提供这些参数,比如: + +```python +import requests + +payload = {'page': '1', 'per_page': '10'} +r = requests.get("http://httpbin.org/get", params=payload) +``` + +通过打印该 URL,我们可以看到 URL 已被正确编码: + +```python +>>> print r.url +http://httpbin.org/get?per_page=10&page=1 +``` + +需要注意的是字典里值为 None 的键不会被添加到 URL 的查询字符串中。 + +## POST 请求 + +使用 Requests 发送 POST 请求也很简单,如下: + +```python +import requests + +r = requests.post("http://httpbin.org/post") +``` + +通常,我们在发送 POST 请求时还会附上数据,比如发送编码为表单形式的数据或编码为 JSON 形式的数据,这时,我们可以使用 Requests 提供的 `data` 参数。 + +- 发送编码为表单形式的数据 + +通过给 `data` 参数传递一个 `dict`,我们的数据字典在发出请求时会被自动编码为表单形式,比如: + +```python +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", data=payload) +``` + +看看返回的内容(省略了部分数据): + +```python +>>> print r.text +{ + ... + "form": { + "page": "1", + "per_page": "10" + }, + ... +} +``` + +- 发送编码为 JSON 形式的数据 + +如果给 `data` 参数传递一个 `string`,我们的数据会被直接发布出去,比如: + +```python +import json +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", data=json.dumps(payload)) +``` + +看看返回: + +```python +>>> print r.text +{ + "args": {}, + "data": "{\"per_page\": 10, \"page\": 1}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "27", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.9.1" + }, + "json": { + "page": 1, + "per_page": 10 + }, + "origin": "13.75.42.240", + "url": "http://httpbin.org/post" +} +``` + +在上面,我们自行对 `dict` 进行了编码,这种方式等价于使用 `json` 参数,而给它传递 `dict`,如下: + +```python +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", json=payload) +``` + +这种做法跟上面的做法是等价的,数据在发出时会被自动编码。 + +## 请求头 + +有时,我们需要为请求添加 HTTP 头部,我们可以**通过传递一个 `dict` 给 `headers` 参数来实现**。比如: + +```python +import requests + +url = 'http://httpbin.org/post' +payload = {'page': 1, 'per_page': 10} +headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} + +r = requests.post("http://httpbin.org/post", json=payload, headers=headers) +``` + +发送到服务器的请求的头部可以通过 `r.request.headers` 访问: + +``` +>>> print r.request.headers +{'Content-Length': '27', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Connection': 'keep-alive', 'Content-Type': 'application/json'} +``` + +服务器返回给我们的响应头部信息可以通过 `r.headers` 访问: + +``` +>>> print r.headers +{'Content-Length': '462', 'Server': 'nginx', 'Connection': 'close', 'Access-Control-Allow-Credentials': 'true', 'Date': 'Mon, 05 Dec 2016 15:41:05 GMT', 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'} +``` + +# HTTP 响应 + +HTTP 响应与 HTTP 请求相似,由三部分组成: + +- **状态行**:包含 HTTP 协议版本、状态码和状态描述,以空格分隔 +- **响应头**:包含一系列的键值对 +- **响应正文** + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e62d49d6.png) + +当我们使用 `requests.*` 发送请求时,Requests 做了两件事: + +- 构建一个 Request 对象,该对象会根据请求方法或相关参数发起 HTTP 请求 +- 一旦服务器返回响应,就会产生一个 Response 对象,该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象 + +对于响应状态码,我们可以访问响应对象的 `status_code` 属性: + +```python +import requests + +r = requests.get("http://httpbin.org/get") +print r.status_code + +# 输出 +200 +``` + +对于响应正文,我们可以通过多种方式读取,比如: + +- 普通响应,使用 `r.text` 获取 +- JSON 响应,使用 `r.json()` 获取 +- 二进制响应,使用 `r.content` 获取 +- 原始响应,使用 `r.raw` 获取 + +## 普通响应 + +我们可以使用 `r.text` 来读取 unicode 形式的响应,看看例子: + +```python +import requests + +r = requests.get("https://github.com/timeline.json") +print r.text +print r.encoding + +# 输出 +{"message":"Hello there, wayfaring stranger. If you’re reading this then you probably didn’t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.","documentation_url":"https://developer.github.com/v3/activity/events/#list-public-events"} +utf-8 +``` + +Requests 会自动解码来自服务器的内容,大多数 unicode 字符集都能被正确解码。 + +## JSON 响应 + +对于 JSON 响应的内容,我们可以使用 `json()` 方法把返回的数据解析成 Python 对象。 + +看看例子: + +```python +import requests + +r = requests.get("https://github.com/timeline.json") + +if r.status_code == 200: + print r.headers.get('content-type') + print r.json() + +# 输出 +application/json; charset=utf-8 +{u'documentation_url': u'https://developer.github.com/v3/activity/events/#list-public-events', u'message': u'Hello there, wayfaring stranger. If you\u2019re reading this then you probably didn\u2019t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.'} +``` + +如果 JSON 解码失败,`r.json()` 就会抛出异常,比如: + +```python +import requests + +r = requests.get("https://www.baidu.com") + +if r.status_code == 200: + print r.headers.get('content-type') + print r.json() + +# 输出 +text/html +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) + in () + 1 if r.status_code == 200: + 2 print r.headers.get('content-type') +----> 3 print r.json() + 4 +.... +.... +ValueError: No JSON object could be decoded +``` + +## 二进制响应 + +我们也可以以字节的方式访问响应正文,访问 `content` 属性可以获取二进制数据,比如用返回的二进制数据创建一张图片: + +```python +import requests + +url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' +r = requests.get(url) +image_data = r.content # 获取二进制数据 + +with open('/Users/Ethan/Downloads/redux.png', 'wb') as fout: + fout.write(image_data) +``` + +## 原始响应 + +在少数情况下,我们可能想获取来自服务器的原始套接字响应,这可以通过访问响应对象的 `raw` 属性来实现,但要确保在初始请求中设置了 `stream=True`,比如: + +```python +import requests + +url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' +r = requests.get(url, stream=True) +print r.raw +r.raw.read(10) + +# 输出 + +'\x89PNG\r\n\x1a\n\x00\x00' +``` + +# 重定向 + +默认情况下,除了 HEAD,Requests 会自动处理所有重定向。我们可以使用响应对象的 `history` 属性来追踪重定向,**Response.history** 是一个 Response 对象的列表,这个对象列表按照从最老到最近的请求进行排序。 + +比如,点击某些网站的链接,它会将页面重定向到其他网站: + +```python +>>> import requests + +>>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} +>>> r = requests.get('https://toutiao.io/k/c32y51', headers=headers) + +>>> r.status_code +200 + +>>> r.url # 发生了重定向,响应对象的 url,跟请求对象不一样 +u'http://www.jianshu.com/p/490441391db6?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' + +>>> r.history +[] + +>>> r.history[0].text +u'You are being redirected.' +``` + +可以看到,我们访问网址 `https://toutiao.io/k/c32y51` 被重定向到了下面的链接: + +```python +http://www.jianshu.com/p/490441391db6?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' +``` + +我们还看到 `r.history` 包含了一个 Response 对象列表,我们可以用它来追踪重定向。 + +如果请求方法是 GET、POST、PUT、OPTIONS、PATCH 或 DELETE,我们可以通过 `all_redirects` 参数禁止重定向: + +```python +>>> import requests + +>>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} +>>> r = requests.get('https://toutiao.io/k/c32y51', headers=headers, allow_redirects=False) +>>> r.url # 禁止重定向,响应对象的 url 跟请求对象一致 +u'https://toutiao.io/k/c32y51' +>>> r.history +[] +>>> r.text +u'You are being redirected.' +``` + +# Cookie + +- 如果某个响应包含一些 cookie,我们可以直接访问它们,比如: + +```python +>>> import requests + +>>> url = 'http://exmaple.com/some/cookie/setting/url' +>>> r = requests.get(url) + +>>> r.cookies['some_key'] +'some_value' +``` + +- 发送 cookies 到服务器,可以使用 `cookies` 参数: + +```python +>>> import requests + +>>> url = 'http://httpbin.org/cookies' +>>> cookies = dict(key1='value1') + +>>> r = requests.get(url, cookies=cookies) +>>> r.text +u'{\n "cookies": {\n "key1": "value1"\n }\n}\n' +>>> print r.text +{ + "cookies": { + "key1": "value1" + } +} +``` + +# 会话对象 + +我们知道,HTTP 协议是无状态的,这意味着每个请求都是独立的,如果后续的处理需要用到前面的信息,则数据需要重传,为了解决这个问题,我们可以使用 Cookie 或 Session 来存储某些特定的信息,比如用户名、密码等,这样,当用户在不同 Web 页面跳转或再次登录网站时,就不用重新输入用户名和密码(当然,如果 Cookie 被删除和 Session 过期则需要重新输入)。 + +Requests 提供了会话对象让我们能够跨请求保持某些参数,也可以在同一个 Session 实例发出的所有请求之间保持 Cookie。 + +下面,我们看看会话对象的使用。 + +下面是一个跨请求保持 Cookie 的例子: + +```python +>>> import requests +>>> s = requests.Session() +>>> s.get('http://httpbin.org/cookies/set/sessioncookie/123456789') + +>>> r = s.get("http://httpbin.org/cookies") +>>> print r.text +{ + "cookies": { + "sessioncookie": "123456789" + } +} +``` + +会话还可用来为请求方法提供缺省数据,通过设置会话对象的属性来实现: + +```python +import requests + +s = requests.Session() +s.auth = ('user', 'pass') +s.headers.update({'x-test': 'true'}) + +# x-test 和 x-test2 都会被发送 +s.get('http://httpbin.org/headers', headers={'x-test2': 'true'}) +``` + +# 代理 + +Requests 支持基本的 HTTP 代理 和 SOCKS 代理(2.10.0 新增功能)。 + +## HTTP 代理 + +如果需要使用 HTTP 代理,我们可以为任意请求方法提供 `proxies` 参数,如下: + +```python +import requests + +proxies = { + "http": "http://10.10.1.10:3128", + "https": "http://10.10.1.10:1080", +} + +requests.get("http://example.org", proxies=proxies) +``` + +我们也可以通过设置环境变量 `HTTP_PROXY` 和 `HTTPS_PROXY` 来配置代理: + +```python +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" + +$ python +>>> import requests +>>> requests.get("http://example.org") +``` + +## SOCKS 代理 + +Requests 自 2.10.0 版起,开始支持 SOCKS 协议的代理,如果要使用,我们还需安装一个第三方库: + +``` +$ pip install requests[socks] +``` + +SOCKS 代理的使用和 HTTP 代理类似: + +```python +import requests + +proxies = { + "http": "socks5://user:pass@host:port", + "https": "socks5://user:pass@host:port", +} + +requests.get("http://example.org", proxies=proxies) +``` + +# 身份认证 + +大部分 Web 服务都需要身份认证,并且有多种不同的认证类型,比如: + +- 基本身份认证 +- 摘要式身份认证 +- OAuth 认证 + +下面介绍一下基本身份认证和 OAuth 认证。 + +## 基本身份认证 + +基本身份认证(HTTP Basic Auth)是最简单的一种身份认证,一般需要身份认证的 Web 服务也都接受 HTTP Basic Auth,Requests 提供了非常简单的形式让我们使用 HTTP Basic Auth: + +```python +>>> from requests.auth import HTTPBasicAuth +>>> requests.get('https://api.github.com/user', auth=HTTPBasicAuth('user', 'pass')) +``` + +由于 HTTP Basic Auth 非常常见,Requests 提供了一种简写的形式: + +```python +requests.get('https://api.github.com/user', auth=('user', 'pass')) +``` + +## OAuth 2 认证 + +OAuth 是一种常见的 Web API 认证方式,目前的版本是 2.0。Requests 并不直接支持 OAuth 认证,而是要配合另外一个库一起使用,该库是 [requests-oauthlib](https://github.com/requests/requests-oauthlib)。 + +下面以 GitHub 为例,介绍一下 OAuth 2 认证。 + +```python +>>> # Credentials you get from registering a new application +>>> client_id = '' +>>> client_secret = '' + +>>> # OAuth endpoints given in the GitHub API documentation +>>> authorization_base_url = 'https://github.com/login/oauth/authorize' +>>> token_url = 'https://github.com/login/oauth/access_token' + +>>> from requests_oauthlib import OAuth2Session +>>> github = OAuth2Session(client_id) + +>>> # Redirect user to GitHub for authorization +>>> authorization_url, state = github.authorization_url(authorization_base_url) +>>> print 'Please go here and authorize,', authorization_url + +>>> # Get the authorization verifier code from the callback url +>>> redirect_response = raw_input('Paste the full redirect URL here:') + +>>> # Fetch the access token +>>> github.fetch_token(token_url, client_secret=client_secret, +>>> authorization_response=redirect_response) + +>>> # Fetch a protected resource, i.e. user profile +>>> r = github.get('https://api.github.com/user') +>>> print r.content +``` + +更多关于 OAuth 工作流程的信息,可以参考 [OAuth 官方网站](https://oauth.net/),关于 `requests-oauthlib` 库的使用,可以参考[官方文档](https://requests-oauthlib.readthedocs.io/en/latest/#)。 + + +# 小结 + +- 任何时候调用 requests.*() 你都在做两件主要的事情。其一,你在构建一个 Request 对象, 该对象将被发送到某个服务器请求或查询一些资源。其二,一旦 requests 得到一个从 服务器返回的响应就会产生一个 Response 对象。该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象。 + +# 参考资料 + +- [Requests: Requests 2.10.0 文档](http://docs.python-requests.org/zh_CN/latest/) +- [如何理解HTTP协议的 “无连接,无状态” 特点?](http://blog.csdn.net/tennysonsky/article/details/44562435) +- [理解OAuth 2.0 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) +- [作为客户端与HTTP服务交互 — python3-cookbook 2.0.0 文档](http://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p01_interact_with_http_services_as_client.html) + + diff --git a/PYTHONroadmap.png b/PYTHONroadmap.png new file mode 100644 index 0000000..8a2e097 Binary files /dev/null and b/PYTHONroadmap.png differ diff --git a/Process-Thread-Coroutine/process.md b/Process-Thread-Coroutine/process.md index b468711..8861568 100644 --- a/Process-Thread-Coroutine/process.md +++ b/Process-Thread-Coroutine/process.md @@ -62,7 +62,7 @@ I am child process (86646) and my parent is (86645). # 多进程 -Python 提供了一个 [multiprocessing][mp] 模块,利用它,我们可以来编写跨平台的多进程程序。 +Python 提供了一个 [multiprocessing][mp] 模块,利用它,我们可以来编写跨平台的多进程程序,但需要注意的是 multiprocessing 在 Windows 和 Linux 平台的不一致性:一样的代码在 Windows 和 Linux 下运行的结果可能不同。因为 Windows 的进程模型和 Linux 不一样,Windows 下没有 fork。 我们先来看一个简单的例子,该例子演示了在主进程中启动一个子进程,并等待其结束,代码如下: @@ -94,6 +94,64 @@ Run child process test (10075)... Process end. ``` +## multiprocessing 与平台有关 + +``` python +import random +import os +from multiprocessing import Process + +num = random.randint(0, 100) + +def show_num(): + print("pid:{}, num is {}".format(os.getpid(), num)) + +if __name__ == "__main__": + print("pid:{}, num is {}".format(os.getpid(), num)) + p = Process(target=show_num) + p.start() + p.join() +``` + +在 Windows 下运行以上代码,输出的结果如下(你得到不一样的结果也是对的): + +``` +pid:6504, num is 25 +pid:6880, num is 6 +``` + +我们发现,num 的值是不一样的! + +在 Linux 下运行以上代码,可以看到 num 的值是一样的: + +``` +pid:11747, num is 13 +pid:11748, num is 13 +``` + +这是因为根据不同的平台,multiprocessing支持三种启动进程的办法: +#### spawn +父进程会启动一个新的解释器,子进程只会继承run()所需的资源。 +不必要的文件描述符和句柄(一种指针)不会被继承。 +该方法和fork,forkserver相比,启动进程较慢。 + +可在Unix和Windows上使用。 Windows上的默认设置。 + +#### fork +父进程使用 os.fork() 来产生 Python 解释器分叉。 +子进程在开始时实际上与父进程相同,并且会继承父进程的所有资源。 +多线程的安全是有问题的。 + +只能在Unix上使用。Unix上的默认设置。 + +#### forkserver +程序启动并选择forkserver启动方法时,将启动一个服务器进程。 +之后每当需要一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。 +分叉服务器进程是单线程的,因此使用 os.fork() 是安全的。 +没有不必要的资源被继承。 + +可在Unix平台上使用,并支持通过Unix管道传递文件描述符。 + ## 使用进程池创建多个进程 在上面,我们只是创建了一个进程,如果要创建多个进程呢?Python 提供了**进程池**的方式,让我们批量创建子进程,让我们看一个简单的示例: @@ -140,7 +198,7 @@ All subprocesses done. # 进程间通信 -进程间的通信可以通过管道(Pipe),队列(Queue)等多种方式来实现。Python 的 multiprocessing 模块封装了底层的实现机制,让我们可以很容器地实现进程间的通信。 +进程间的通信可以通过管道(Pipe),队列(Queue)等多种方式来实现。Python 的 multiprocessing 模块封装了底层的实现机制,让我们可以很容易地实现进程间的通信。 下面以队列(Queue)为例,在父进程中创建两个子进程,一个往队列写数据,一个从对列读数据,代码如下: @@ -219,9 +277,7 @@ DONE - [编程中的进程、线程、协程、同步、异步、回调 · 浮生半日闲](https://wangdashuaihenshuai.github.io/2015/10/17/%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84%E8%BF%9B%E7%A8%8B%E3%80%81%E7%BA%BF%E7%A8%8B%E3%80%81%E5%8D%8F%E7%A8%8B%E3%80%81%E5%90%8C%E6%AD%A5%E3%80%81%E5%BC%82%E6%AD%A5%E3%80%81%E5%9B%9E%E8%B0%83/) - [python中多进程以及多线程编程的总结 - Codefly](http://sunms.codefly.top/2016/11/05/python%E4%B8%AD%E5%A4%9A%E8%BF%9B%E7%A8%8B%E4%BB%A5%E5%8F%8A%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%BC%96%E7%A8%8B%E7%9A%84%E6%80%BB%E7%BB%93/) - [multithreading - Python multiprocessing.Pool: when to use apply, apply_async or map? - Stack Overflow](http://stackoverflow.com/questions/8533318/python-multiprocessing-pool-when-to-use-apply-apply-async-or-map) - +- [multiprocessing --- 基于进程的并行¶](https://docs.python.org/zh-cn/3/library/multiprocessing.html) [mp]: https://docs.python.org/2/library/multiprocessing.html - - diff --git a/README.md b/README.md index e69de29..e33cca7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,89 @@ +![cover](https://i.loli.net/2020/03/23/9B1oWLeu6Q3Hbrz.png) + +Python 之旅 +=== + +![Version](https://img.shields.io/badge/version-1.0-brightgreen.svg) +[![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-brightgreen.svg)](https://raw.githubusercontent.com/ethan-funny/explore-python/master/LICENSE) + +## Python 简介 + +Python 诞生于 1989 年的圣诞期间,由 [Guido van Rossum](https://gvanrossum.github.io/) 开发而成,目前 Guido 仍然是 Python 的主要开发者,主导着 Python 的发展方向,Python 社区经常称呼他为『仁慈的独裁者』。 + +Python 是一门面向对象,解释型的高级程序设计语言,它的语法非常简洁、优雅,而这也是 Python 的一些设计哲学。Python 自带了很完善的库,涵盖了数据库,网络,文件处理,GUI 等方方面面,通过这些库,我们可以比较快速地解决一些棘手问题,也可以将其作为基础库,开发出一些高级库。 + +目前 Python 在大部分领域都占有一席之地,比如 Web 开发,机器学习,科学计算等。不少大型网站都是使用 Python 作为后台开发语言的,比如 [YouTube](https://www.youtube.com/)、[Pinterest](https://www.pinterest.com/)、国内的[豆瓣](https://www.douban.com/)和[知乎](http://www.zhihu.com/)等。 + +另外,有不少知名的机器学习库也是使用 Python 开发的,比如,[scikit-learn](http://scikit-learn.org/stable/) 是一个强大的机器学习库,[PyTorch](https://pytorch.org/) 是一个成熟的深度学习库。 + +当然了,Python 也有一些缺点。Python 经常被人们吐槽的一点就是:运行速度慢,和 C/C++ 相比非常慢。但是,除了像视频高清解码等**计算密集型任务**对运行速度有较高的要求外,在大部分时候,我们可能并不需要非常快的运行速度。比如,一个程序使用 C 来实现,运行时间只需 0.01 秒,而使用 Python 来实现,需要 0.1 秒,虽然 Python 的运行时间是 C 的 10 倍,显然很慢,但对我们而言,这压根不是问题。 + +## 关于本书 + +本书是我学习和使用 Python 的总结。在学习和使用 Python 的过程中,我作了不少笔记,并对一些笔记进行了加工和完善,发表在博客上。随着笔记的增加,我就萌生了写一本书的想法,希望能比较系统地总结相关知识,巩固自己的知识体系,而不是停留在『感觉好像懂了』的状态中。 + +有了想法之后,接下来就要开始写了。当然,从产生想法到付诸实践还是纠结了一段时间,毕竟,作笔记和写书很不一样啊。思想斗争过后,我下定决心要把它写出来。 + +首先,我参考一些相关的书籍,作了一个基础的思维导图,如下: + +![思维导图](https://i.loli.net/2020/03/23/uZN8aehmwl14XcG.png) +![Eng journy](PYTHONroadmap.png) + +接下来,就要开始写作了,这也是最艰难的一关。 + +我没有按照从头到尾的顺序写,而是从最感兴趣的知识点入手,比如函数式编程、类的使用等等。就这样,一点一点地写,实在不想写了,就先搁置一下,过两天继续写。 + +我在写作的过程中,给自己提了一个要求:**尽量深入浅出,条理清晰**。至于是否达到了,希望读者们多多批评指正,并给我提意见和建议。 + +本书的每章基本上都是独立的,读者可以挑选感兴趣的章节进行阅读。目前本书有 15 个章节: + +- 第 1 章:介绍一些基础知识,包括 Python 中的输入和输出,字符编码。 +- 第 2 章:介绍常用数据类型,比如字符串、列表和字典等。 +- 第 3 章:介绍函数的定义和函数参数魔法。 +- 第 4 章:介绍 Python 中的函数式编程,包括匿名函数、闭包和装饰器等。 +- 第 5 章:介绍 Python 中类的使用,包括类方法、静态方法、super 和元类的使用等。 +- 第 6 章:介绍 Python 中的高级特性,比如生成器,上下文管理器。 +- 第 7 章:介绍文件和目录操作,os 的使用。 +- 第 8 章:介绍使用 Python 处理进程、线程和协程。 +- 第 9 章:异常处理。 +- 第 10 章:单元测试。 +- 第 11 章:正则表达式,re 模块的使用。 +- 第 12 章:HTTP 服务,requests 模块的使用。 +- 第 13 章:一些标准模块的使用,比如 argparse、collections 和 datetime 等。 +- 第 14 章:一些第三方模块的使用。 +- 第 15 章:结束语。 + +本书的编码环境: + +- Python 版本以 2.7 为主,同时也会指出在 Python3 中的相应变化 +- 操作系统使用 macOS,代码结果,尤其是内存地址等由于运行环境的不同会存在差异 + +本书将会持续进行修订和更新,读者如果遇到问题,请及时向我反馈,我会在第一时间加以解决。 + + +## 声明 + +Creative Commons License + +本书由 [Ethan](https://github.com/ethan-funny) 编写,采用 [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh) 协议发布。 + +这意味着你可以在非商业性使用的前提下自由转载,但必须: + +1. 保持署名 +2. 不对本书进行修改 + +## 更新记录 + +| 时间 | 说明 | +| :---: | :---: | +| 2017-01-03 | 发布版本 v1.0 | +| 2019-02-09 | fix typo | + + +## 支持我 + +如果你觉得本书对你有所帮助,不妨请我喝杯咖啡,感谢支持! + + + + diff --git a/Regular-Expressions/re.md b/Regular-Expressions/re.md new file mode 100644 index 0000000..77a6832 --- /dev/null +++ b/Regular-Expressions/re.md @@ -0,0 +1,594 @@ +# re 模块 + +在 Python 中,我们可以使用内置的 re 模块来使用正则表达式。 + +有一点需要特别注意的是,正则表达式使用 `\` 对特殊字符进行转义,比如,为了匹配字符串 'python.org',我们需要使用正则表达式 `'python\.org'`,而 Python 的字符串本身也用 `\` 转义,所以上面的正则表达式在 Python 中应该写成 `'python\\.org'`,这会很容易陷入 `\` 的困扰中,因此,我们建议使用 Python 的原始字符串,只需加一个 r 前缀,上面的正则表达式可以写成: + +``` +r'python\.org' +``` + +re 模块提供了不少有用的函数,用以匹配字符串,比如: + +- compile 函数 +- match 函数 +- search 函数 +- findall 函数 +- finditer 函数 +- split 函数 +- sub 函数 +- subn 函数 + +re 模块的一般使用步骤如下: + +- 使用 compile 函数将正则表达式的字符串形式编译为一个 Pattern 对象 +- 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果(一个 Match 对象) +- 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作 + +# compile 函数 + +**compile 函数用于编译正则表达式,生成一个 Pattern 对象**,它的一般使用形式如下: + +``` +re.compile(pattern[, flag]) +``` + +其中,pattern 是一个字符串形式的正则表达式,flag 是一个可选参数,表示匹配模式,比如忽略大小写,多行模式等。 + +下面,让我们看看例子。 + +```python +import re + +# 将正则表达式编译成 Pattern 对象 +pattern = re.compile(r'\d+') +``` + +在上面,我们已将一个正则表达式编译成 Pattern 对象,接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了。Pattern 对象的一些常用方法主要有: + +- match 方法 +- search 方法 +- findall 方法 +- finditer 方法 +- split 方法 +- sub 方法 +- subn 方法 + +## match 方法 + +match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果。它的一般使用形式如下: + +``` +match(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。因此,**当你不指定 pos 和 endpos 时,match 方法默认匹配字符串的头部**。 + +当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。 + +看看例子。 + +```python +>>> import re +>>> pattern = re.compile(r'\d+') # 用于匹配至少一个数字 +>>> m = pattern.match('one12twothree34four') # 查找头部,没有匹配 +>>> print m +None +>>> m = pattern.match('one12twothree34four', 2, 10) # 从'e'的位置开始匹配,没有匹配 +>>> print m +None +>>> m = pattern.match('one12twothree34four', 3, 10) # 从'1'的位置开始匹配,正好匹配 +>>> print m # 返回一个 Match 对象 +<_sre.SRE_Match object at 0x10a42aac0> +>>> m.group(0) # 可省略 0 +'12' +>>> m.start(0) # 可省略 0 +3 +>>> m.end(0) # 可省略 0 +5 +>>> m.span(0) # 可省略 0 +(3, 5) +``` + +在上面,当匹配成功时返回一个 Match 对象,其中: + +- `group([group1, …])` 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 `group()` 或 `group(0)`; +- `start([group])` 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0; +- `end([group])` 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0; +- `span([group])` 方法返回 `(start(group), end(group))`。 + +再看看一个例子: + +```python +>>> import re +>>> pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I) # re.I 表示忽略大小写 +>>> m = pattern.match('Hello World Wide Web') +>>> print m # 匹配成功,返回一个 Match 对象 +<_sre.SRE_Match object at 0x10bea83e8> +>>> m.group(0) # 返回匹配成功的整个子串 +'Hello World' +>>> m.span(0) # 返回匹配成功的整个子串的索引 +(0, 11) +>>> m.group(1) # 返回第一个分组匹配成功的子串 +'Hello' +>>> m.span(1) # 返回第一个分组匹配成功的子串的索引 +(0, 5) +>>> m.group(2) # 返回第二个分组匹配成功的子串 +'World' +>>> m.span(2) # 返回第二个分组匹配成功的子串 +(6, 11) +>>> m.groups() # 等价于 (m.group(1), m.group(2), ...) +('Hello', 'World') +>>> m.group(3) # 不存在第三个分组 +Traceback (most recent call last): + File "", line 1, in +IndexError: no such group +``` + +## search 方法 + +search 方法用于查找字符串的任何位置,它也是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果,它的一般使用形式如下: + +``` +search(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。 + +当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。 + +让我们看看例子: + +```python +>>> import re +>>> pattern = re.compile('\d+') +>>> m = pattern.search('one12twothree34four') # 这里如果使用 match 方法则不匹配 +>>> m +<_sre.SRE_Match object at 0x10cc03ac0> +>>> m.group() +'12' +>>> m = pattern.search('one12twothree34four', 10, 30) # 指定字符串区间 +>>> m +<_sre.SRE_Match object at 0x10cc03b28> +>>> m.group() +'34' +>>> m.span() +(13, 15) +``` + +再来看一个例子: + +```python +# -*- coding: utf-8 -*- + +import re + +# 将正则表达式编译成 Pattern 对象 +pattern = re.compile(r'\d+') + +# 使用 search() 查找匹配的子串,不存在匹配的子串时将返回 None +# 这里使用 match() 无法成功匹配 +m = pattern.search('hello 123456 789') + +if m: + # 使用 Match 获得分组信息 + print 'matching string:',m.group() + print 'position:',m.span() +``` + +执行结果: + +``` +matching string: 123456 +position: (6, 12) +``` + +## findall 方法 + +上面的 match 和 search 方法都是一次匹配,只要找到了一个匹配的结果就返回。然而,在大多数时候,我们需要搜索整个字符串,获得所有匹配的结果。 + +findall 方法的使用形式如下: + +``` +findall(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。 + +findall 以列表形式返回全部能匹配的子串,如果没有匹配,则返回一个空列表。 + +看看例子: + +```python +import re + +pattern = re.compile(r'\d+') # 查找数字 +result1 = pattern.findall('hello 123456 789') +result2 = pattern.findall('one1two2three3four4', 0, 10) + +print result1 +print result2 +``` + +执行结果: + +``` +['123456', '789'] +['1', '2'] +``` + +## finditer 方法 + +finditer 方法的行为跟 findall 的行为类似,也是搜索整个字符串,获得所有匹配的结果。但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器。 + +看看例子: + +```python +# -*- coding: utf-8 -*- + +import re + +pattern = re.compile(r'\d+') + +result_iter1 = pattern.finditer('hello 123456 789') +result_iter2 = pattern.finditer('one1two2three3four4', 0, 10) + +print type(result_iter1) +print type(result_iter2) + +print 'result1...' +for m1 in result_iter1: # m1 是 Match 对象 + print 'matching string: {}, position: {}'.format(m1.group(), m1.span()) + +print 'result2...' +for m2 in result_iter2: + print 'matching string: {}, position: {}'.format(m2.group(), m2.span()) +``` + +执行结果: + +``` + + +result1... +matching string: 123456, position: (6, 12) +matching string: 789, position: (13, 16) +result2... +matching string: 1, position: (3, 4) +matching string: 2, position: (7, 8) +``` + +## split 方法 + +split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下: + +``` +split(string[, maxsplit]) +``` + +其中,maxsplit 用于指定最大分割次数,不指定将全部分割。 + +看看例子: + +```python +import re + +p = re.compile(r'[\s\,\;]+') +print p.split('a,b;; c d') +``` + +执行结果: + +``` +['a', 'b', 'c', 'd'] +``` + +## sub 方法 + +sub 方法用于替换。它的使用形式如下: + +``` +sub(repl, string[, count]) +``` + +其中,repl 可以是字符串也可以是一个函数: + +- 如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 `\id` 的形式来引用分组,但不能使用编号 0; +- 如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。 + +count 用于指定最多替换次数,不指定时全部替换。 + +看看例子: + +```python +import re + +p = re.compile(r'(\w+) (\w+)') +s = 'hello 123, hello 456' + +def func(m): + return 'hi' + ' ' + m.group(2) + +print p.sub(r'hello world', s) # 使用 'hello world' 替换 'hello 123' 和 'hello 456' +print p.sub(r'\2 \1', s) # 引用分组 +print p.sub(func, s) +print p.sub(func, s, 1) # 最多替换一次 +``` + +执行结果: + +``` +hello world, hello world +123 hello, 456 hello +hi 123, hi 456 +hi 123, hello 456 +``` + +## subn 方法 + +subn 方法跟 sub 方法的行为类似,也用于替换。它的使用形式如下: + +``` +subn(repl, string[, count]) +``` + +它返回一个元组: + +``` +(sub(repl, string[, count]), 替换次数) +``` + +元组有两个元素,第一个元素是使用 sub 方法的结果,第二个元素返回原字符串被替换的次数。 + +看看例子: + +``` +import re + +p = re.compile(r'(\w+) (\w+)') +s = 'hello 123, hello 456' + +def func(m): + return 'hi' + ' ' + m.group(2) + +print p.subn(r'hello world', s) +print p.subn(r'\2 \1', s) +print p.subn(func, s) +print p.subn(func, s, 1) +``` + +执行结果: + +``` +('hello world, hello world', 2) +('123 hello, 456 hello', 2) +('hi 123, hi 456', 2) +('hi 123, hello 456', 1) +``` + +# 其他函数 + +事实上,使用 compile 函数生成的 Pattern 对象的一系列方法跟 re 模块的多数函数是对应的,但在使用上有细微差别。 + +## match 函数 + +match 函数的使用形式如下: + +``` +re.match(pattern, string[, flags]): +``` + +其中,pattern 是正则表达式的字符串形式,比如 `\d+`, `[a-z]+`。 + +而 Pattern 对象的 match 方法使用形式是: + +``` +match(string[, pos[, endpos]]) +``` + +可以看到,match 函数不能指定字符串的区间,它只能搜索头部,看看例子: + +```python +import re + +m1 = re.match(r'\d+', 'One12twothree34four') +if m1: + print 'matching string:',m1.group() +else: + print 'm1 is:',m1 + +m2 = re.match(r'\d+', '12twothree34four') +if m2: + print 'matching string:', m2.group() +else: + print 'm2 is:',m2 +``` + +执行结果: + +``` +m1 is: None +matching string: 12 +``` + +## search 函数 + +search 函数的使用形式如下: + +``` +re.search(pattern, string[, flags]) +``` + +search 函数不能指定字符串的搜索区间,用法跟 Pattern 对象的 search 方法类似。 + +## findall 函数 + +findall 函数的使用形式如下: + +``` +re.findall(pattern, string[, flags]) +``` + +findall 函数不能指定字符串的搜索区间,用法跟 Pattern 对象的 findall 方法类似。 + +看看例子: + +```python +import re + +print re.findall(r'\d+', 'hello 12345 789') + +# 输出 +['12345', '789'] +``` + +## finditer 函数 + +finditer 函数的使用方法跟 Pattern 的 finditer 方法类似,形式如下: + +``` +re.finditer(pattern, string[, flags]) +``` + +## split 函数 + +split 函数的使用形式如下: + +``` +re.split(pattern, string[, maxsplit]) +``` + +## sub 函数 + +sub 函数的使用形式如下: + +``` +re.sub(pattern, repl, string[, count]) +``` + +## subn 函数 + +subn 函数的使用形式如下: + +``` +re.subn(pattern, repl, string[, count]) +``` + +# 到底用哪种方式 + +从上文可以看到,使用 re 模块有两种方式: + +- 使用 re.compile 函数生成一个 Pattern 对象,然后使用 Pattern 对象的一系列方法对文本进行匹配查找; +- 直接使用 re.match, re.search 和 re.findall 等函数直接对文本匹配查找; + +下面,我们用一个例子展示这两种方法。 + +先看第 1 种用法: + +```python +import re + +# 将正则表达式先编译成 Pattern 对象 +pattern = re.compile(r'\d+') + +print pattern.match('123, 123') +print pattern.search('234, 234') +print pattern.findall('345, 345') +``` + +再看第 2 种用法: + +```python +import re + +print re.match(r'\d+', '123, 123') +print re.search(r'\d+', '234, 234') +print re.findall(r'\d+', '345, 345') +``` + +如果一个正则表达式需要用到多次(比如上面的 `\d+`),在多种场合经常需要被用到,出于效率的考虑,我们应该预先编译该正则表达式,生成一个 Pattern 对象,再使用该对象的一系列方法对需要匹配的文件进行匹配;而如果直接使用 re.match, re.search 等函数,每次传入一个正则表达式,它都会被编译一次,效率就会大打折扣。 + +因此,我们推荐使用第 1 种用法。 + +# 匹配中文 + +在某些情况下,我们想匹配文本中的汉字,有一点需要注意的是,[中文的 unicode 编码范围](http://blog.oasisfeng.com/2006/10/19/full-cjk-unicode-range/) 主要在 `[\u4e00-\u9fa5]`,这里说主要是因为这个范围并不完整,比如没有包括全角(中文)标点,不过,在大部分情况下,应该是够用的。 + +假设现在想把字符串 `title = u'你好,hello,世界'` 中的中文提取出来,可以这么做: + +```python +# -*- coding: utf-8 -*- + +import re + +title = u'你好,hello,世界' +pattern = re.compile(ur'[\u4e00-\u9fa5]+') +result = pattern.findall(title) + +print result +``` + +注意到,我们在正则表达式前面加上了两个前缀 `ur`,其中 `r` 表示使用原始字符串,`u` 表示是 unicode 字符串。 + +执行结果: + +``` +[u'\u4f60\u597d', u'\u4e16\u754c'] +``` + +# 贪婪匹配 + +在 Python 中,正则匹配默认是**贪婪匹配**(在少数语言中可能是非贪婪),也就是**匹配尽可能多的字符**。 + +比如,我们想找出字符串中的所有 `div` 块: + +```python +import re + +content = 'aa
test1
bb
test2
cc' +pattern = re.compile(r'
.*
') +result = pattern.findall(content) + +print result +``` + +执行结果: + +``` +['
test1
bb
test2
'] +``` + +由于正则匹配是贪婪匹配,也就是尽可能多的匹配,因此,在成功匹配到第一个 `` 时,它还会向右尝试匹配,查看是否还有更长的可以成功匹配的子串。 + +如果我们想非贪婪匹配,可以加一个 `?`,如下: + +```python +import re + +content = 'aa
test1
bb
test2
cc' +pattern = re.compile(r'
.*?
') # 加上 ? +result = pattern.findall(content) + +print result +``` + +结果: + +``` +['
test1
', '
test2
'] +``` + +# 小结 + +- re 模块的一般使用步骤如下: + - 使用 compile 函数将正则表达式的字符串形式编译为一个 Pattern 对象; + - 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果(一个 Match 对象); + - 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作; +- Python 的正则匹配默认是贪婪匹配。 + +# 参考资料 + +- [Python正则表达式指南](http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html) +- [正则表达式 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832260566c26442c671fa489ebc6fe85badda25cd000) + + diff --git a/SUMMARY.md b/SUMMARY.md index 1b73d93..dc8cb78 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -53,6 +53,7 @@ * [argparse](Standard-Modules/argparse.md) * [base64](Standard-Modules/base64.md) * [collections](Standard-Modules/collections.md) + * [itertools](Standard-Modules/itertools.md) * [datetime](Standard-Modules/datetime.md) * [hashlib](Standard-Modules/hashlib.md) * [hmac](Standard-Modules/hmac.md) diff --git a/Standard-Modules/README.md b/Standard-Modules/README.md index f2c218e..375df38 100644 --- a/Standard-Modules/README.md +++ b/Standard-Modules/README.md @@ -1,2 +1,20 @@ -# 常用标准模块 +# 标准模块 + +前面我们介绍了 os 模块和 re 模块,本章再介绍 Python 常用的一些标准模块: + +- [argparse](./argparse.md) +- [base64](./base64.md) +- [collections](./collections.md) +- [datetime](./datetime.md) +- [hashlib](./hashlib.md) +- [hmac](./hmac.md) + +其中: + +- argparse 是用于创建命令行的库; +- base64 是用于 base64 编码和解码的库; +- collections 模块提供了 5 个高性能的数据类型,如 `Counter`,`OrderedDict` 等; +- datetime 是用于处理日期时间的模块; +- hashlib 模块提供了常见的摘要算法,比如 MD5,SHA1 等; +- hmac 模块提供了 HMAC 哈希算法; diff --git a/Standard-Modules/collections.md b/Standard-Modules/collections.md new file mode 100644 index 0000000..48ad146 --- /dev/null +++ b/Standard-Modules/collections.md @@ -0,0 +1,205 @@ +# collections + +我们知道,Python 的数据类型有 list, tuple, dict, str 等,**collections 模块**提供了额外 5 个高性能的数据类型: + +- `Counter`: 计数器 +- `OrderedDict`: 有序字典 +- `defaultdict`: 带有默认值的字典 +- `namedtuple`: 生成可以通过属性访问元素内容的 tuple 子类 +- `deque`: 双端队列,能够在队列两端添加或删除元素 + +# Counter + +`Counter` 是一个简单的计数器,可用于统计字符串、列表等的元素个数。 + +看看例子: + +```python +>>> from collections import Counter +>>> +>>> s = 'aaaabbbccd' +>>> c = Counter(s) # 创建了一个 Counter 对象 +>>> c +Counter({'a': 4, 'b': 3, 'c': 2, 'd': 1}) +>>> isinstance(c, dict) # c 其实也是一个字典对象 +True +>>> c.get('a') +4 +>>> c.most_common(2) # 获取出现次数最多的前两个元素 +[('a', 4), ('b', 3)] +``` + +在上面,我们使用 `Counter()` 创建了一个 `Counter` 对象 `c`,`Counter` 其实是 dict 的一个子类,我们可以使用 `get` 方法来获取某个元素的个数。`Counter` 对象有一个 `most_common` 方法,允许我们获取出现次数最多的前几个元素。 + +另外,两个 `Counter` 对象还可以做运算: + +```python +>>> from collections import Counter +>>> +>>> s1 = 'aaaabbbccd' +>>> c1 = Counter(s1) +>>> c1 +Counter({'a': 4, 'b': 3, 'c': 2, 'd': 1}) +>>> +>>> s2 = 'aaabbef' +>>> c2 = Counter(s2) +>>> c2 +Counter({'a': 3, 'b': 2, 'e': 1, 'f': 1}) +>>> +>>> c1 + c2 # 两个计数结果相加 +Counter({'a': 7, 'b': 5, 'c': 2, 'e': 1, 'd': 1, 'f': 1}) +>>> c1 - c2 # c2 相对于 c1 的差集 +Counter({'c': 2, 'a': 1, 'b': 1, 'd': 1}) +>>> c1 & c2 # c1 和 c2 的交集 +Counter({'a': 3, 'b': 2}) +>>> c1 | c2 # c1 和 c2 的并集 +Counter({'a': 4, 'b': 3, 'c': 2, 'e': 1, 'd': 1, 'f': 1}) +``` + +# OrderedDict + +Python 中的 dict 是无序的: + +```python +>>> dict([('a', 10), ('b', 20), ('c', 15)]) +{'a': 10, 'c': 15, 'b': 20} +``` + +有时,我们希望保持 key 的顺序,这时可以用 OrderedDict: + +```python +>>> from collections import OrderedDict +>>> OrderedDict([('a', 10), ('b', 20), ('c', 15)]) +OrderedDict([('a', 10), ('b', 20), ('c', 15)]) +``` + +# defaultdict + +在 Python 中使用 dict 时,如果访问了不存在的 key,会抛出 KeyError 异常,因此,在访问之前,我们经常需要对 key 作判断,比如: + +```python +>>> d = dict() +>>> s = 'aaabbc' +>>> for char in s: +... if char in d: +... d[char] += 1 +... else: +... d[char] = 1 +... +>>> d +{'a': 3, 'c': 1, 'b': 2} +``` + +使用 defaultdict,我们可以给字典中的 key 提供一个默认值。访问 defaultdict 中的 key,如果 key 存在,就返回 key 对应的 value,如果 key 不存在,就返回默认值。 + +```python +>>> from collections import defaultdict +>>> d = defaultdict(int) # 默认的 value 值是 0 +>>> s = 'aaabbc' +>>> for char in s: +... d[char] += 1 +... +>>> d +defaultdict(, {'a': 3, 'c': 1, 'b': 2}) +>>> d.get('a') +3 +>>> d['z'] +0 +``` + +使用 defaultdict 时,我们可以传入一个工厂方法来指定默认值,比如传入 int,表示默认值是 0,传入 list,表示默认是 `[]`: + +```python +>>> from collections import defaultdict +>>> +>>> d1 = defaultdict(int) +>>> d1['a'] +0 +>>> d2 = defaultdict(list) +>>> d2['b'] +[] +>>> d3 = defaultdict(str) +>>> d3['a'] +'' +``` + +我们还可以自定义默认值,通过 `lambda` 函数来实现: + +```python +>>> from collections import defaultdict +>>> +>>> d = defaultdict(lambda: 10) +>>> d['a'] +10 +``` + +# namedtuple + +我们经常用 tuple (元组) 来表示一个不可变对象,比如用一个 `(姓名, 学号, 年龄)` 的元组来表示一个学生: + +```python +>>> stu = ('ethan', '001', 20) +>>> stu[0] +'ethan' +``` + +这里使用 tuple 没什么问题,但可读性比较差,我们必须清楚索引代表的含义,比如索引 0 表示姓名,索引 1 表示学号。如果用类来定义,就可以通过设置属性 name, id, age 来表示,但就有些小题大作了。 + +我们可以通过 namedtuple 为元组的每个索引设置名称,然后通过「属性名」来访问: + +```python +>>> from collections import namedtuple +>>> Student = namedtuple('Student', ['name', 'id', 'age']) # 定义了一个 Student 元组 +>>> +>>> stu = Student('ethan', '001', 20) +>>> stu.name +'ethan' +>>> stu.id +'001' +``` + +# deque + +deque 是双端队列,允许我们在队列两端添加或删除元素。 + +```python +>>> from collections import deque + +>>> q = deque(['a', 'b', 'c', 'd']) +>>> q.append('e') # 添加到尾部 +>>> q +deque(['a', 'b', 'c', 'd', 'e']) +>>> q.appendleft('o') # 添加到头部 +>>> q +deque(['o', 'a', 'b', 'c', 'd', 'e']) +>>> q.pop() # 从尾部弹出元素 +'e' +>>> q +deque(['o', 'a', 'b', 'c', 'd']) +>>> q.popleft() # 从头部弹出元素 +'o' +>>> q +deque(['a', 'b', 'c', 'd']) +>>> q.extend('ef') # 在尾部 extend 元素 +>>> q +deque(['a', 'b', 'c', 'd', 'e', 'f']) +>>> q.extendleft('uv') # 在头部 extend 元素,注意顺序 +>>> q +deque(['v', 'u', 'a', 'b', 'c', 'd', 'e', 'f']) +>>> +>>> q.rotate(2) # 将尾部的两个元素移动到头部 +>>> q +deque(['e', 'f', 'v', 'u', 'a', 'b', 'c', 'd']) +>>> q.rotate(-2) # 将头部的两个元素移动到尾部 +>>> q +deque(['v', 'u', 'a', 'b', 'c', 'd', 'e', 'f']) +``` + +其中,`rotate` 方法用于旋转,如果旋转参数 n 大于 0,表示将队列右端的 n 个元素移动到左端,否则相反。 + + +# 参考资料 + +- [collections — High-performance container datatypes — Python 2.7.13 documentation](https://docs.python.org/2/library/collections.html#module-collections) + + diff --git a/Standard-Modules/datetime.md b/Standard-Modules/datetime.md new file mode 100644 index 0000000..b9793f6 --- /dev/null +++ b/Standard-Modules/datetime.md @@ -0,0 +1,153 @@ +# datetime + +Python 提供了两个标准库用于处理跟时间相关的问题,一个是 `time`,另一个是 `datetime`,`datetime` 对 `time` 进行了封装,提供了更多实用的函数。本文介绍 `datetime` 库的简单使用。 + +# 当前时间 + +获取当前时间可以使用 `now()` 或 `utcnow()` 方法,其中,`now()` 用于获取当地时间,而 `utcnow()` 用于获取 UTC 时间。 + +```python +>>> from datetime import datetime + +>>> datetime.now() # 返回一个 datetime 对象,这里是当地时间 +datetime.datetime(2016, 12, 10, 11, 32, 43, 806970) + +>>> datetime.utcnow() # 返回一个 datetime 对象,这里是 UTC 时间 +datetime.datetime(2016, 12, 10, 3, 32, 49, 999423) + +>>> datetime.now().year, datetime.now().month, datetime.now().day # 年月日 +(2016, 12, 10) + +>>> datetime.now().hour, datetime.now().minute, datetime.now().second # 时分秒 +(11, 35, 37) +``` + +# 时间格式化 + +有时,我们需要对时间做格式化处理,可以使用 `strftime()` 或 `strptime()` 方法,其中,`strftime` 用于对 `datetime` 对象进行格式化,`strptime` 用于对字符串对象进行格式化。 + +```python +>>> from datetime import datetime + +# 获取当前当地时间 +>>> now = datetime.now() +>>> now +datetime.datetime(2016, 12, 10, 11, 46, 24, 432168) + +# 对 datetime 对象进行格式化,转为字符串格式 +>>> now_str = now.strftime('%Y-%m-%d %H:%M:%S.%f') +>>> now_str +'2016-12-10 11:46:24.432168' + +# 对字符串对象进行格式化,转为 datetime 对象 +>>> datetime.strptime(now_str, '%Y-%m-%d %H:%M:%S.%f') +datetime.datetime(2016, 12, 10, 11, 46, 24, 432168) +``` + +# 时间戳 + +[Unix 时间戳](http://funhacks.net/2015/04/29/Unix-timestamp/)根据精度的不同,有 10 位(秒级),13 位(毫秒级),16 位(微妙级)和 19 位(纳秒级)。 + +要注意的是,由于每个时区都有自己的本地时间(北京在东八区),因此也产生了世界标准时间(UTC, Universal Time Coordinated)。所以,在将一个时间戳转换为普通时间(比如 2016-01-01 12:00:00)时,要注意是要本地时区的时间还是世界时间等。 + +- 获取当前当地时间戳 + +```python +>>> import time +>>> from datetime import datetime + +# 获取当前当地时间,返回一个 datetime 对象 +>>> now = datetime.now() +>>> now +datetime.datetime(2016, 12, 9, 11, 56, 47, 632778) + +# 13 位的毫秒时间戳 +>>> long(time.mktime(now.timetuple()) * 1000.0 + now.microsecond / 1000.0) +1481255807632L + +# 10 位的时间戳 +>>> int(time.mktime(now.timetuple())) +1481255807 +``` + +- 获取当前 UTC 时间戳 + +```python +>>> import calendar +>>> from datetime import datetime + +# 获取当前的 UTC 时间,返回 datetime 对象 +>>> utc_now = datetime.utcnow() +>>> utc_now +datetime.datetime(2016, 12, 9, 4, 0, 53, 356641) + +# 13 位的时间戳 +>>> long(calendar.timegm(utc_now.timetuple()) * 1000.0 + utc_now.microsecond / 1000.0) +1481256053356L + +# 10 位的时间戳 +>>> calendar.timegm(utc_now.timetuple()) +1481256053 +``` + +- 将时间戳转为字符串形式 + +```python +>>> from datetime import datetime + +# 13 位的毫秒时间戳 +>>> timestamp = 1456402864242 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%f') +'2016-02-25 20:21:04.242000' + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%f') +'2016-02-25 12:21:04.242000' + +# 10 位的时间戳 +>>> timestamp = 1456402864 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') +'2016-02-25 20:21:04' + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') +'2016-02-25 12:21:04' +``` + +- 将时间戳转为 datetime 形式 + +```python +>>> from datetime import datetime + +# 13 位的毫秒时间戳 +>>> timestamp = 1456402864242 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp / 1000.0) +datetime.datetime(2016, 2, 25, 20, 21, 4, 242000) + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp / 1000.0) +datetime.datetime(2016, 2, 25, 12, 21, 4, 242000) + +# 10 位的时间戳 +>>> timestamp = 1456402864 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp) +datetime.datetime(2016, 2, 25, 20, 21, 4) + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp) +datetime.datetime(2016, 2, 25, 12, 21, 4) +``` + +# 参考资料 + +- [python-datetime-time-conversions](http://www.saltycrane.com/blog/2008/11/python-datetime-time-conversions/) + + diff --git a/Standard-Modules/hashlib.md b/Standard-Modules/hashlib.md new file mode 100644 index 0000000..7b841d4 --- /dev/null +++ b/Standard-Modules/hashlib.md @@ -0,0 +1,57 @@ +# hashlib + +Python 内置的 hashlib 模块提供了常见的**摘要算法**(或称哈希算法,散列算法),如 MD5,SHA1, SHA256 等。**摘要算法的基本原理是:将数据(如一段文字)运算变为另一固定长度值**。 + +MD5 (Message-Digest Algorithm 5, 消息摘要算法),是一种被广泛使用的密码散列函数,可以产生出一个 128 位(16 字节)的散列值(hash value),用于确保信息传输完整一致。 + +SHA1 (Secure Hash Algorithm, 安全哈希算法) 是 SHA 家族的其中一个算法,它经常被用作数字签名。 + +# MD5 + +hashlib 模块提供了 `md5` 函数,我们可以很方便地使用它: + +```python +>>> import hashlib +>>> +>>> m = hashlib.md5('md5 test in Python!') +>>> m.digest() +'\xad\xc0\x99\x01\x12\xc7&\xb5~\xb0\xaf \x974\x11\xab' +>>> m.hexdigest() # 使用一个 32 位的 16 进制字符串表示 +'adc0990112c726b57eb0af20973411ab' +``` + +上面,我们是直接把数据传入 `md5()` 函数,我们也可以通过一次或多次使用 `update` 来实现: + +```python +>>> import hashlib +>>> m = hashlib.md5() +>>> m.update('md5 test ') +>>> m.update('in Python!') +>>> m.hexdigest() +'adc0990112c726b57eb0af20973411ab' +``` + +# SHA1 + +SHA1 的使用和 MD5 的使用类似: + +```python +>>> import hashlib +>>> +>>> sha1 = hashlib.sha1() +>>> sha1.update('md5 test ') +>>> sha1.update('in Python!') +>>> sha1.hexdigest() +'698a8b18d5f99a140520475c342af455183c58a3' +``` + +# 小结 + +- MD5 以前经常用来做用户密码的存储。2004年,它被证明无法防止碰撞,因此无法适用于安全性认证,但仍广泛应用于普通数据的错误检查领域。如果你需要储存用户密码,你应该去了解一下诸如 [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) 或 [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) 之类的算法。而 SHA1 则经常用作数字签名。 + +# 参考资料 + +- [MD5 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/MD5) +- [SHA家族 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F) + + diff --git a/Standard-Modules/hmac.md b/Standard-Modules/hmac.md new file mode 100644 index 0000000..36a2ab7 --- /dev/null +++ b/Standard-Modules/hmac.md @@ -0,0 +1,58 @@ +# hmac + +HMAC 是用于消息认证的加密哈希算法,全称是 keyed-Hash Message Authentication Code。**HMAC 利用哈希算法,以一个密钥和一个消息作为输入,生成一个加密串作为输出**。HMAC 可以有效防止类似 MD5 的彩虹表等攻击,比如将常见密码的 MD5 值存入数据库,可能被反向破解。 + +Python 的 `hmac` 模块提供了 HMAC 算法,它的使用形式是: + +```python +hmac.new(key[, msg[, digestmod]]) +``` + +其中,key 是一个密钥;msg 是消息,可选,如果给出 msg,则调用方法 `update(msg)`;digestmod 是 HMAC 对象使用的摘要构造函数或模块,默认为 `hashlib.md5` 构造函数。 + +HMAC 对象常用的方法有: + +- HMAC.update(msg) + +用字符串 msg 更新 HMAC 对象,重复的调用等同于一次调用所有参数的组合,即: + +``` +m.update(a); +m.update(b); +``` + +相当于 + +``` +m.update(a+b) +``` + +- HMAC.digest() + +返回目前传递给 `update()` 方法的字符串的摘要。此字符串长度将与给构造函数的摘要的 digest_size 相同。它可能包含非 ASCII 字符,包括 NULL 字节。 + +- HMAC.hexdigest() + +类似 digest(),但是返回的摘要的字符串的长度翻倍,且只包含十六进制数字。 + +现在,让我们看一个简单的例子: + +```python +>>> from datetime import datetime +>>> import hashlib +>>> import hmac + +>>> key = 'you-never-know' +>>> msg = datetime.utcnow().strftime('%Y-%m-%d') + +>>> m = hmac.new(key, msg, hashlib.sha1) +>>> signature = m.hexdigest() +>>> signature +'fdb2087a66a2f00afbc1884738467ba089782779' +``` + +# 参考资料 + +- [hmac — 用于消息认证的加密哈希算法](http://python.usyiyi.cn/python_278/library/hmac.html) + + diff --git a/Standard-Modules/itertools.md b/Standard-Modules/itertools.md new file mode 100644 index 0000000..a3eadf8 --- /dev/null +++ b/Standard-Modules/itertools.md @@ -0,0 +1,545 @@ +# itertools + +我们知道,迭代器的特点是:**惰性求值**(Lazy evaluation),即只有当迭代至某个值时,它才会被计算,这个特点使得迭代器特别适合于遍历大文件或无限集合等,因为我们不用一次性将它们存储在内存中。 + +Python 内置的 itertools 模块包含了一系列用来产生不同类型迭代器的函数或类,这些函数的返回都是一个迭代器,我们可以通过 for 循环来遍历取值,也可以使用 `next()` 来取值。 + +itertools 模块提供的迭代器函数有以下几种类型: + +- 无限迭代器:生成一个无限序列,比如自然数序列 `1, 2, 3, 4, ...`; +- 有限迭代器:接收一个或多个序列(sequence)作为参数,进行组合、分组和过滤等; +- 组合生成器:序列的排列、组合,求序列的笛卡儿积等; + +# 无限迭代器 + +itertools 模块提供了三个函数(事实上,它们是类)用于生成一个无限序列迭代器: + +- count(firstval=0, step=1) + + 创建一个从 firstval (默认值为 0) 开始,以 step (默认值为 1) 为步长的的无限整数迭代器 + +- cycle(iterable) + + 对 iterable 中的元素反复执行循环,返回迭代器 + +- repeat(object [,times] + + 反复生成 object,如果给定 times,则重复次数为 times,否则为无限 + +下面,让我们看看一些例子。 + +## count + +`count()` 接收两个参数,第一个参数指定开始值,默认为 0,第二个参数指定步长,默认为 1: + +```python +>>> import itertools +>>> +>>> nums = itertools.count() +>>> for i in nums: +... if i > 6: +... break +... print i +... +0 +1 +2 +3 +4 +5 +6 +>>> nums = itertools.count(10, 2) # 指定开始值和步长 +>>> for i in nums: +... if i > 20: +... break +... print i +... +10 +12 +14 +16 +18 +20 +``` + +## cycle + +`cycle()` 用于对 iterable 中的元素反复执行循环: + +```python +>>> import itertools +>>> +>>> cycle_strings = itertools.cycle('ABC') +>>> i = 1 +>>> for string in cycle_strings: +... if i == 10: +... break +... print i, string +... i += 1 +... +1 A +2 B +3 C +4 A +5 B +6 C +7 A +8 B +9 C +``` + +## repeat + +`repeat()` 用于反复生成一个 object: + +```python +>>> import itertools +>>> +>>> for item in itertools.repeat('hello world', 3): +... print item +... +hello world +hello world +hello world +>>> +>>> for item in itertools.repeat([1, 2, 3, 4], 3): +... print item +... +[1, 2, 3, 4] +[1, 2, 3, 4] +[1, 2, 3, 4] +``` + +# 有限迭代器 + +itertools 模块提供了多个函数(类),接收一个或多个迭代对象作为参数,对它们进行组合、分组和过滤等: + +- chain() +- compress() +- dropwhile() +- groupby() +- ifilter() +- ifilterfalse() +- islice() +- imap() +- starmap() +- tee() +- takewhile() +- izip() +- izip_longest() + +## chain + +`chain` 的使用形式如下: + +``` +chain(iterable1, iterable2, iterable3, ...) +``` + +`chain` 接收多个可迭代对象作为参数,将它们『连接』起来,作为一个新的迭代器返回。 + +```python +>>> from itertools import chain +>>> +>>> for item in chain([1, 2, 3], ['a', 'b', 'c']): +... print item +... +1 +2 +3 +a +b +c +``` + +`chain` 还有一个常见的用法: + +``` +chain.from_iterable(iterable) +``` + +接收一个可迭代对象作为参数,返回一个迭代器: + +```python +>>> from itertools import chain +>>> +>>> string = chain.from_iterable('ABCD') +>>> string.next() +'A' +``` + +## compress + +`compress` 的使用形式如下: + +``` +compress(data, selectors) +``` + +`compress` 可用于对数据进行筛选,当 selectors 的某个元素为 true 时,则保留 data 对应位置的元素,否则去除: + +```python +>>> from itertools import compress +>>> +>>> list(compress('ABCDEF', [1, 1, 0, 1, 0, 1])) +['A', 'B', 'D', 'F'] +>>> list(compress('ABCDEF', [1, 1, 0, 1])) +['A', 'B', 'D'] +>>> list(compress('ABCDEF', [True, False, True])) +['A', 'C'] +``` + +## dropwhile + +`dropwhile` 的使用形式如下: + +``` +dropwhile(predicate, iterable) +``` + +其中,predicate 是函数,iterable 是可迭代对象。对于 iterable 中的元素,如果 predicate(item) 为 true,则丢弃该元素,否则返回该项及所有后续项。 + +```python +>>> from itertools import dropwhile +>>> +>>> list(dropwhile(lambda x: x < 5, [1, 3, 6, 2, 1])) +[6, 2, 1] +>>> +>>> list(dropwhile(lambda x: x > 3, [2, 1, 6, 5, 4])) +[2, 1, 6, 5, 4] +``` + +## groupby + +`groupby` 用于对序列进行分组,它的使用形式如下: + +``` +groupby(iterable[, keyfunc]) +``` + +其中,iterable 是一个可迭代对象,keyfunc 是分组函数,用于对 iterable 的连续项进行分组,如果不指定,则默认对 iterable 中的连续相同项进行分组,返回一个 `(key, sub-iterator)` 的迭代器。 + +```python +>>> from itertools import groupby +>>> +>>> for key, value_iter in groupby('aaabbbaaccd'): +... print key, ':', list(value_iter) +... +a : ['a', 'a', 'a'] +b : ['b', 'b', 'b'] +a : ['a', 'a'] +c : ['c', 'c'] +d : ['d'] +>>> +>>> data = ['a', 'bb', 'ccc', 'dd', 'eee', 'f'] +>>> for key, value_iter in groupby(data, len): # 使用 len 函数作为分组函数 +... print key, ':', list(value_iter) +... +1 : ['a'] +2 : ['bb'] +3 : ['ccc'] +2 : ['dd'] +3 : ['eee'] +1 : ['f'] +>>> +>>> data = ['a', 'bb', 'cc', 'ddd', 'eee', 'f'] +>>> for key, value_iter in groupby(data, len): +... print key, ':', list(value_iter) +... +1 : ['a'] +2 : ['bb', 'cc'] +3 : ['ddd', 'eee'] +1 : ['f'] +``` + +## ifilter + +`ifilter` 的使用形式如下: + +``` +ifilter(function or None, sequence) +``` + +将 iterable 中 function(item) 为 True 的元素组成一个迭代器返回,如果 function 是 None,则返回 iterable 中所有计算为 True 的项。 + +```python +>>> from itertools import ifilter +>>> +>>> list(ifilter(lambda x: x < 6, range(10))) +[0, 1, 2, 3, 4, 5] +>>> +>>> list(ifilter(None, [0, 1, 2, 0, 3, 4])) +[1, 2, 3, 4] +``` + +## ifilterfalse + +`ifilterfalse` 的使用形式和 `ifilter` 类似,它将 iterable 中 function(item) 为 False 的元素组成一个迭代器返回,如果 function 是 None,则返回 iterable 中所有计算为 False 的项。 + +```python +>>> from itertools import ifilterfalse +>>> +>>> list(ifilterfalse(lambda x: x < 6, range(10))) +[6, 7, 8, 9] +>>> +>>> list(ifilter(None, [0, 1, 2, 0, 3, 4])) +[0, 0] +``` + +## islice + +`islice` 是切片选择,它的使用形式如下: + +``` +islice(iterable, [start,] stop [, step]) +``` + +其中,iterable 是可迭代对象,start 是开始索引,stop 是结束索引,step 是步长,start 和 step 可选。 + +```python +>>> from itertools import count, islice +>>> +>>> list(islice([10, 6, 2, 8, 1, 3, 9], 5)) +[10, 6, 2, 8, 1] +>>> +>>> list(islice(count(), 6)) +[0, 1, 2, 3, 4, 5] +>>> +>>> list(islice(count(), 3, 10)) +[3, 4, 5, 6, 7, 8, 9] +>>> list(islice(count(), 3, 10 ,2)) +[3, 5, 7, 9] +``` + +## imap + +`imap` 类似 `map` 操作,它的使用形式如下: + +``` +imap(func, iter1, iter2, iter3, ...) +``` + +`imap` 返回一个迭代器,元素为 `func(i1, i2, i3, ...)`,`i1`,`i2` 等分别来源于 `iter`, `iter2`。 + +```python +>>> from itertools import imap +>>> +>>> imap(str, [1, 2, 3, 4]) + +>>> +>>> list(imap(str, [1, 2, 3, 4])) +['1', '2', '3', '4'] +>>> +>>> list(imap(pow, [2, 3, 10], [4, 2, 3])) +[16, 9, 1000] +``` + +## tee + +`tee` 的使用形式如下: + +``` +tee(iterable [,n]) +``` + +`tee` 用于从 iterable 创建 n 个独立的迭代器,以元组的形式返回,n 的默认值是 2。 + +```python +>>> from itertools import tee +>>> +>>> tee('abcd') # n 默认为 2,创建两个独立的迭代器 +(, ) +>>> +>>> iter1, iter2 = tee('abcde') +>>> list(iter1) +['a', 'b', 'c', 'd', 'e'] +>>> list(iter2) +['a', 'b', 'c', 'd', 'e'] +>>> +>>> tee('abc', 3) # 创建三个独立的迭代器 +(, , ) +``` + +## takewhile + +`takewhile` 的使用形式如下: + +``` +takewhile(predicate, iterable) +``` + +其中,predicate 是函数,iterable 是可迭代对象。对于 iterable 中的元素,如果 predicate(item) 为 true,则保留该元素,只要 predicate(item) 为 false,则立即停止迭代。 + +```python +>>> from itertools import takewhile +>>> +>>> list(takewhile(lambda x: x < 5, [1, 3, 6, 2, 1])) +[1, 3] +>>> list(takewhile(lambda x: x > 3, [2, 1, 6, 5, 4])) +[] +``` + +## izip + +`izip` 用于将多个可迭代对象**对应位置**的元素作为一个元组,将所有元组『组成』一个迭代器,并返回。它的使用形式如下: + +``` +izip(iter1, iter2, ..., iterN) +``` + +如果某个可迭代对象不再生成值,则迭代停止。 + +```python +>>> from itertools import izip +>>> +>>> for item in izip('ABCD', 'xy'): +... print item +... +('A', 'x') +('B', 'y') +>>> for item in izip([1, 2, 3], ['a', 'b', 'c', 'd', 'e']): +... print item +... +(1, 'a') +(2, 'b') +(3, 'c') +``` + +## izip_longest + +`izip_longest` 跟 `izip` 类似,但迭代过程会持续到所有可迭代对象的元素都被迭代完。它的形式如下: + +``` +izip_longest(iter1, iter2, ..., iterN, [fillvalue=None]) +``` + +如果有指定 fillvalue,则会用其填充缺失的值,否则为 None。 + +```python +>>> from itertools import izip_longest +>>> +>>> for item in izip_longest('ABCD', 'xy'): +... print item +... +('A', 'x') +('B', 'y') +('C', None) +('D', None) +>>> +>>> for item in izip_longest('ABCD', 'xy', fillvalue='-'): +... print item +... +('A', 'x') +('B', 'y') +('C', '-') +('D', '-') +``` + +# 组合生成器 + +itertools 模块还提供了多个组合生成器函数,用于求序列的排列、组合等: + +- product +- permutations +- combinations +- combinations_with_replacement + +## product + +`product` 用于求多个可迭代对象的笛卡尔积,它跟嵌套的 for 循环等价。它的一般使用形式如下: + +``` +product(iter1, iter2, ... iterN, [repeat=1]) +``` + +其中,repeat 是一个关键字参数,用于指定重复生成序列的次数, + + +```python +>>> from itertools import product +>>> +>>> for item in product('ABCD', 'xy'): +... print item +... +('A', 'x') +('A', 'y') +('B', 'x') +('B', 'y') +('C', 'x') +('C', 'y') +('D', 'x') +('D', 'y') +>>> +>>> list(product('ab', range(3))) +[('a', 0), ('a', 1), ('a', 2), ('b', 0), ('b', 1), ('b', 2)] +>>> +>>> list(product((0,1), (0,1), (0,1))) +[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)] +>>> +>>> list(product('ABC', repeat=2)) +[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] +>>> +``` + +## permutations + +`permutations` 用于生成一个排列,它的一般使用形式如下: + +``` +permutations(iterable[, r]) +``` + +其中,r 指定生成排列的元素的长度,如果不指定,则默认为可迭代对象的元素长度。 + +```python +>>> from itertools import permutations +>>> +>>> permutations('ABC', 2) + +>>> +>>> list(permutations('ABC', 2)) +[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] +>>> +>>> list(permutations('ABC')) +[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')] +>>> +``` + +## combinations + +`combinations` 用于求序列的组合,它的使用形式如下: + +``` +combinations(iterable, r) +``` + +其中,r 指定生成组合的元素的长度。 + +```python +>>> from itertools import combinations +>>> +>>> list(combinations('ABC', 2)) +[('A', 'B'), ('A', 'C'), ('B', 'C')] +``` + +## combinations_with_replacement + +`combinations_with_replacement` 和 `combinations` 类似,但它生成的组合包含自身元素。 + +```python +>>> from itertools import combinations_with_replacement +>>> +>>> list(combinations_with_replacement('ABC', 2)) +[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] +``` + +# 小结 + +- itertools 模块提供了很多用于产生多种类型迭代器的函数,它们的返回值不是 list,而是迭代器。 + +# 参考链接 + +- [itertools — Functions creating iterators for efficient looping](https://docs.python.org/2/library/itertools.html) +- [itertools – Iterator functions for efficient looping - Python Module of the Week](https://pymotw.com/2/itertools/) + + diff --git a/Third-Party-Modules/README.md b/Third-Party-Modules/README.md index 6ea6395..db1ae0b 100644 --- a/Third-Party-Modules/README.md +++ b/Third-Party-Modules/README.md @@ -1,2 +1,13 @@ # 第三方模块 +在前面,我们介绍了一个优秀的第三方库 -- requests,本章再介绍两个第三方库: + +- [celery](./celery.md) +- [click](./click.md) + +其中: + +- celery 是一个强大的分布式任务队列,通常用于实现异步任务; +- click 是快速创建命令行的神器; + + diff --git a/Third-Party-Modules/celery.md b/Third-Party-Modules/celery.md new file mode 100644 index 0000000..cb52fcb --- /dev/null +++ b/Third-Party-Modules/celery.md @@ -0,0 +1,406 @@ +# Celery + +在程序的运行过程中,我们经常会碰到一些耗时耗资源的操作,为了避免它们阻塞主程序的运行,我们经常会采用多线程或异步任务。比如,在 Web 开发中,对新用户的注册,我们通常会给他发一封激活邮件,而发邮件是个 IO 阻塞式任务,如果直接把它放到应用当中,就需要等邮件发出去之后才能进行下一步操作,此时用户只能等待再等待。更好的方式是在业务逻辑中触发一个发邮件的异步任务,而主程序可以继续往下运行。 + +[Celery][0] 是一个强大的分布式任务队列,它可以让任务的执行完全脱离主程序,甚至可以被分配到其他主机上运行。我们通常使用它来实现异步任务(async task)和定时任务(crontab)。它的架构组成如下图: + +![Celery_framework](https://ooo.0o0.ooo/2016/12/10/584bbf78e1783.png) + +可以看到,Celery 主要包含以下几个模块: + +- 任务模块 + + 包含异步任务和定时任务。其中,**异步任务通常在业务逻辑中被触发并发往任务队列,而定时任务由 Celery Beat 进程周期性地将任务发往任务队列**。 + +- 消息中间件 Broker + + Broker,即为任务调度队列,**接收任务生产者发来的消息(即任务),将任务存入队列**。Celery 本身不提供队列服务,官方推荐使用 RabbitMQ 和 Redis 等。 + +- 任务执行单元 Worker + + Worker 是执行任务的处理单元,**它实时监控消息队列,获取队列中调度的任务,并执行它**。 + +- 任务结果存储 Backend + + Backend 用于**存储任务的执行结果**,以供查询。同消息中间件一样,存储也可使用 RabbitMQ, Redis 和 MongoDB 等。 + +# 异步任务 + +使用 Celery 实现异步任务主要包含三个步骤: + +1. 创建一个 Celery 实例 +2. 启动 Celery Worker +3. 应用程序调用异步任务 + +## 快速入门 + +为了简单起见,对于 Broker 和 Backend,这里都使用 redis。在运行下面的例子之前,请确保 redis 已正确安装,并开启 redis 服务,当然,celery 也是要安装的。可以使用下面的命令来安装 celery 及相关依赖: + +``` +$ pip install 'celery[redis]' +``` + +### 创建 Celery 实例 + +将下面的代码保存为文件 `tasks.py`: + +```python +# -*- coding: utf-8 -*- + +import time +from celery import Celery + +broker = 'redis://127.0.0.1:6379' +backend = 'redis://127.0.0.1:6379/0' + +app = Celery('my_task', broker=broker, backend=backend) + +@app.task +def add(x, y): + time.sleep(5) # 模拟耗时操作 + return x + y +``` + +上面的代码做了几件事: + +- 创建了一个 Celery 实例 app,名称为 `my_task`; +- 指定消息中间件用 redis,URL 为 `redis://127.0.0.1:6379`; +- 指定存储用 redis,URL 为 `redis://127.0.0.1:6379/0`; +- 创建了一个 Celery 任务 `add`,当函数被 `@app.task` 装饰后,就成为可被 Celery 调度的任务; + +### 启动 Celery Worker + +在当前目录,使用如下方式启动 Celery Worker: + +``` +$ celery worker -A tasks --loglevel=info +``` + +其中: + +- 参数 `-A` 指定了 Celery 实例的位置,本例是在 `tasks.py` 中,Celery 会自动在该文件中寻找 Celery 对象实例,当然,我们也可以自己指定,在本例,使用 `-A tasks.app`; +- 参数 `--loglevel` 指定了日志级别,默认为 warning,也可以使用 `-l info` 来表示; + +在生产环境中,我们通常会使用 [Supervisor](http://supervisord.org/) 来控制 Celery Worker 进程。 + +启动成功后,控制台会显示如下输出: + +![celery](https://ooo.0o0.ooo/2016/12/10/584b7da9f2c17.png) + +### 调用任务 + +现在,我们可以在应用程序中使用 `delay()` 或 `apply_async()` 方法来调用任务。 + +在当前目录打开 Python 控制台,输入以下代码: + +```python +>>> from tasks import add +>>> add.delay(2, 8) + +``` + +在上面,我们从 `tasks.py` 文件中导入了 `add` 任务对象,然后使用 `delay()` 方法将任务发送到消息中间件(Broker),Celery Worker 进程监控到该任务后,就会进行执行。我们将窗口切换到 Worker 的启动窗口,会看到多了两条日志: + +``` +[2016-12-10 12:00:50,376: INFO/MainProcess] Received task: tasks.add[2272ddce-8be5-493f-b5ff-35a0d9fe600f] +[2016-12-10 12:00:55,385: INFO/PoolWorker-4] Task tasks.add[2272ddce-8be5-493f-b5ff-35a0d9fe600f] succeeded in 5.00642602402s: 10 +``` + +这说明任务已经被调度并执行成功。 + +另外,我们如果想获取执行后的结果,可以这样做: + +```python +>>> result = add.delay(2, 6) +>>> result.ready() # 使用 ready() 判断任务是否执行完毕 +False +>>> result.ready() +False +>>> result.ready() +True +>>> result.get() # 使用 get() 获取任务结果 +8 +``` + +在上面,我们是在 Python 的环境中调用任务。事实上,我们通常在应用程序中调用任务。比如,将下面的代码保存为 `client.py`: + +```python +# -*- coding: utf-8 -*- + +from tasks import add + +# 异步任务 +add.delay(2, 8) + +print 'hello world' +``` + +运行命令 `$ python client.py`,可以看到,虽然任务函数 `add` 需要等待 5 秒才返回执行结果,但由于它是一个异步任务,不会阻塞当前的主程序,因此主程序会往下执行 `print` 语句,打印出结果。 + +## 使用配置 + +在上面的例子中,我们直接把 Broker 和 Backend 的配置写在了程序当中,更好的做法是将配置项统一写入到一个配置文件中,通常我们将该文件命名为 `celeryconfig.py`。Celery 的配置比较多,可以在[官方文档][1]查询每个配置项的含义。 + +下面,我们再看一个例子。项目结构如下: + +``` +celery_demo # 项目根目录 + ├── celery_app # 存放 celery 相关文件 + │   ├── __init__.py + │   ├── celeryconfig.py # 配置文件 + │   ├── task1.py # 任务文件 1 + │   └── task2.py # 任务文件 2 + └── client.py # 应用程序 +``` + +`__init__.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery import Celery + +app = Celery('demo') # 创建 Celery 实例 +app.config_from_object('celery_app.celeryconfig') # 通过 Celery 实例加载配置模块 +``` + +`celeryconfig.py` 代码如下: + +```python +BROKER_URL = 'redis://127.0.0.1:6379' # 指定 Broker +CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' # 指定 Backend + +CELERY_TIMEZONE='Asia/Shanghai' # 指定时区,默认是 UTC +# CELERY_TIMEZONE='UTC' + +CELERY_IMPORTS = ( # 指定导入的任务模块 + 'celery_app.task1', + 'celery_app.task2' +) +``` + +`task1.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def add(x, y): + time.sleep(2) + return x + y +``` + +`task2.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def multiply(x, y): + time.sleep(2) + return x * y +``` + +`client.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery_app import task1 +from celery_app import task2 + +task1.add.apply_async(args=[2, 8]) # 也可用 task1.add.delay(2, 8) +task2.multiply.apply_async(args=[3, 7]) # 也可用 task2.multiply.delay(3, 7) + +print 'hello world' +``` + +现在,让我们启动 Celery Worker 进程,在项目的根目录下执行下面命令: + +``` +celery_demo $ celery -A celery_app worker --loglevel=info +``` + +接着,运行 `$ python client.py`,它会发送两个异步任务到 Broker,在 Worker 的窗口我们可以看到如下输出: + +``` +[2016-12-10 13:51:58,939: INFO/MainProcess] Received task: celery_app.task1.add[9ccffad0-aca4-4875-84ce-0ccfce5a83aa] +[2016-12-10 13:51:58,941: INFO/MainProcess] Received task: celery_app.task2.multiply[64b1f889-c892-4333-bd1d-ac667e677a8a] +[2016-12-10 13:52:00,948: INFO/PoolWorker-3] Task celery_app.task1.add[9ccffad0-aca4-4875-84ce-0ccfce5a83aa] succeeded in 2.00600231002s: 10 +[2016-12-10 13:52:00,949: INFO/PoolWorker-4] Task celery_app.task2.multiply[64b1f889-c892-4333-bd1d-ac667e677a8a] succeeded in 2.00601326401s: 21 +``` + +## delay 和 apply_async + +在前面的例子中,我们使用 `delay()` 或 `apply_async()` 方法来调用任务。事实上,`delay` 方法封装了 `apply_async`,如下: + +```python +def delay(self, *partial_args, **partial_kwargs): + """Shortcut to :meth:`apply_async` using star arguments.""" + return self.apply_async(partial_args, partial_kwargs) +``` + +也就是说,`delay` 是使用 `apply_async` 的快捷方式。`apply_async` 支持更多的参数,它的一般形式如下: + +```python +apply_async(args=(), kwargs={}, route_name=None, **options) +``` + +apply_async 常用的参数如下: + +- countdown:指定多少秒后执行任务 + +``` +task1.apply_async(args=(2, 3), countdown=5) # 5 秒后执行任务 +``` + +- eta (estimated time of arrival):指定任务被调度的具体时间,参数类型是 datetime + +```python +from datetime import datetime, timedelta + +# 当前 UTC 时间再加 10 秒后执行任务 +task1.multiply.apply_async(args=[3, 7], eta=datetime.utcnow() + timedelta(seconds=10)) +``` + +- expires:任务过期时间,参数类型可以是 int,也可以是 datetime + +```python +task1.multiply.apply_async(args=[3, 7], expires=10) # 10 秒后过期 +``` + +更多的参数列表可以在[官方文档](http://docs.celeryproject.org/en/latest/reference/celery.app.task.html#celery.app.task.Task.apply_async)中查看。 + +# 定时任务 + +Celery 除了可以执行**异步任务**,也支持执行**周期性任务(Periodic Tasks)**,或者说定时任务。Celery Beat 进程通过读取配置文件的内容,周期性地将定时任务发往任务队列。 + +让我们看看例子,项目结构如下: + +``` +celery_demo # 项目根目录 + ├── celery_app # 存放 celery 相关文件 +    ├── __init__.py +    ├── celeryconfig.py # 配置文件 +    ├── task1.py # 任务文件 +    └── task2.py # 任务文件 +``` + +`__init__.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery import Celery + +app = Celery('demo') +app.config_from_object('celery_app.celeryconfig') +``` + +`celeryconfig.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from datetime import timedelta +from celery.schedules import crontab + +# Broker and Backend +BROKER_URL = 'redis://127.0.0.1:6379' +CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' + +# Timezone +CELERY_TIMEZONE='Asia/Shanghai' # 指定时区,不指定默认为 'UTC' +# CELERY_TIMEZONE='UTC' + +# import +CELERY_IMPORTS = ( + 'celery_app.task1', + 'celery_app.task2' +) + +# schedules +CELERYBEAT_SCHEDULE = { + 'add-every-30-seconds': { + 'task': 'celery_app.task1.add', + 'schedule': timedelta(seconds=30), # 每 30 秒执行一次 + 'args': (5, 8) # 任务函数参数 + }, + 'multiply-at-some-time': { + 'task': 'celery_app.task2.multiply', + 'schedule': crontab(hour=9, minute=50), # 每天早上 9 点 50 分执行一次 + 'args': (3, 7) # 任务函数参数 + } +} +``` + +`task1.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def add(x, y): + time.sleep(2) + return x + y +``` + +`task2.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def multiply(x, y): + time.sleep(2) + return x * y +``` + +现在,让我们启动 Celery Worker 进程,在项目的根目录下执行下面命令: + +``` +celery_demo $ celery -A celery_app worker --loglevel=info +``` + +接着,启动 Celery Beat 进程,定时将任务发送到 Broker,在项目根目录下执行下面命令: + +``` +celery_demo $ celery beat -A celery_app +celery beat v4.0.1 (latentcall) is starting. +__ - ... __ - _ +LocalTime -> 2016-12-11 09:48:16 +Configuration -> + . broker -> redis://127.0.0.1:6379// + . loader -> celery.loaders.app.AppLoader + . scheduler -> celery.beat.PersistentScheduler + . db -> celerybeat-schedule + . logfile -> [stderr]@%WARNING + . maxinterval -> 5.00 minutes (300s) +``` + +之后,在 Worker 窗口我们可以看到,任务 `task1` 每 30 秒执行一次,而 `task2` 每天早上 9 点 50 分执行一次。 + +在上面,我们用两个命令启动了 Worker 进程和 Beat 进程,我们也可以将它们放在一个命令中: + +``` +$ celery -B -A celery_app worker --loglevel=info +``` + +Celery 周期性任务也有多个配置项,可参考[官方文档](http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html)。 + +# 参考资料 + +- [Celery - Distributed Task Queue — Celery 4.0.1 documentation](http://docs.celeryproject.org/en/latest/index.html) +- [使用Celery - Python之美](https://zhuanlan.zhihu.com/p/22304455) +- [分布式任务队列Celery的介绍 – 思诚之道](http://www.bjhee.com/celery.html) +- [异步任务神器 Celery 简明笔记](http://www.jianshu.com/p/1840035cb510) + + +[0]: https://github.com/celery/celery +[1]: http://docs.celeryproject.org/en/latest/userguide/configuration.html + + diff --git a/Third-Party-Modules/click.md b/Third-Party-Modules/click.md new file mode 100644 index 0000000..0318fd6 --- /dev/null +++ b/Third-Party-Modules/click.md @@ -0,0 +1,426 @@ +# Click + +[Click][0] 是 [Flask][1] 的开发团队 [Pallets][2] 的另一款开源项目,它是用于快速创建命令行的第三方模块。我们知道,Python 内置了一个 [Argparse][3] 的标准库用于创建命令行,但使用起来有些繁琐,[Click][0] 相比于 [Argparse][3],就好比 [requests][4] 相比于 [urllib][5]。 + +# 快速使用 + +Click 的使用大致有两个步骤: + +1. 使用 `@click.command()` 装饰一个函数,使之成为命令行接口; +2. 使用 `@click.option()` 等装饰函数,为其添加命令行选项等。 + +它的一种典型使用形式如下: + +```python +import click + +@click.command() +@click.option('--param', default=default_value, help='description') +def func(param): + pass +``` + +下面,让我们看一下[官方文档][6]的入门例子: + +```python +import click + +@click.command() +@click.option('--count', default=1, help='Number of greetings.') +@click.option('--name', prompt='Your name', help='The person to greet.') +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for x in range(count): + click.echo('Hello %s!' % name) + +if __name__ == '__main__': + hello() +``` + +在上面的例子中,函数 `hello` 有两个参数:count 和 name,它们的值从命令行中获取。 + +- `@click.command()` 使函数 `hello` 成为命令行接口; +- `@click.option` 的第一个参数指定了命令行选项的名称,不难猜到,count 的默认值是 1,name 的值从输入获取; +- 使用 `click.echo` 进行输出是为了获得更好的兼容性,因为 `print` 在 Python2 和 Python3 的用法有些差别。 + +看看执行情况: + +``` +$ python hello.py +Your name: Ethan # 这里会显示 'Your name: '(对应代码中的 prompt),接受用户输入 +Hello Ethan! + +$ python hello.py --help # click 帮我们自动生成了 `--help` 用法 +Usage: hello.py [OPTIONS] + + Simple program that greets NAME for a total of COUNT times. + +Options: + --count INTEGER Number of greetings. + --name TEXT The person to greet. + --help Show this message and exit. + +$ python hello.py --count 3 --name Ethan # 指定 count 和 name 的值 +Hello Ethan! +Hello Ethan! +Hello Ethan! + +$ python hello.py --count=3 --name=Ethan # 也可以使用 `=`,和上面等价 +Hello Ethan! +Hello Ethan! +Hello Ethan! + +$ python hello.py --name=Ethan # 没有指定 count,默认值是 1 +Hello Ethan! +``` + +# click.option + +option 最基本的用法就是通过指定命令行选项的名称,从命令行读取参数值,再将其传递给函数。在上面的例子,我们看到,除了设置命令行选项的名称,我们还会指定默认值,help 说明等,option 常用的设置参数如下: + +- default: 设置命令行参数的默认值 +- help: 参数说明 +- type: 参数类型,可以是 string, int, float 等 +- prompt: 当在命令行中没有输入相应的参数时,会根据 prompt 提示用户输入 +- nargs: 指定命令行参数接收的值的个数 + +下面,我们再看看相关的例子。 + +## 指定 type + +我们可以使用 `type` 来指定参数类型: + +```python +import click + +@click.command() +@click.option('--rate', type=float, help='rate') # 指定 rate 是 float 类型 +def show(rate): + click.echo('rate: %s' % rate) + +if __name__ == '__main__': + show() +``` + +执行情况: + +``` +$ python click_type.py --rate 1 +rate: 1.0 +$ python click_type.py --rate 0.66 +rate: 0.66 +``` + +## 可选值 + +在某些情况下,一个参数的值只能是某些可选的值,如果用户输入了其他值,我们应该提示用户输入正确的值。在这种情况下,我们可以通过 `click.Choice()` 来限定: + +```python +import click + +@click.command() +@click.option('--gender', type=click.Choice(['man', 'woman'])) # 限定值 +def choose(gender): + click.echo('gender: %s' % gender) + +if __name__ == '__main__': + choose() +``` + +执行情况: + +``` +$ python click_choice.py --gender boy +Usage: click_choice.py [OPTIONS] + +Error: Invalid value for "--gender": invalid choice: boy. (choose from man, woman) + +$ python click_choice.py --gender man +gender: man +``` + +## 多值参数 + +有时,一个参数需要接收多个值。**option 支持设置固定长度的参数值**,通过 `nargs` 指定。 + +看看例子就明白了: + +```python +import click + +@click.command() +@click.option('--center', nargs=2, type=float, help='center of the circle') +@click.option('--radius', type=float, help='radius of the circle') +def circle(center, radius): + click.echo('center: %s, radius: %s' % (center, radius)) + +if __name__ == '__main__': + circle() +``` + +在上面的例子中,option 指定了两个参数:center 和 radius,其中,center 表示二维平面上一个圆的圆心坐标,接收两个值,以元组的形式将值传递给函数,而 radius 表示圆的半径。 + +执行情况: + +``` +$ python click_multi_values.py --center 3 4 --radius 10 +center: (3.0, 4.0), radius: 10.0 + +$ python click_multi_values.py --center 3 4 5 --radius 10 +Usage: click_multi_values.py [OPTIONS] + +Error: Got unexpected extra argument (5) +``` + +## 输入密码 + +有时,在输入密码的时候,我们希望能隐藏显示。option 提供了两个参数来设置密码的输入:hide_input 和 confirmation_promt,其中,hide_input 用于隐藏输入,confirmation_promt 用于重复输入。 + +看看例子: + +```python +import click + +@click.command() +@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) +def input_password(password): + click.echo('password: %s' % password) + + +if __name__ == '__main__': + input_password() +``` + +执行情况: + +``` +$ python click_password.py +Password: # 不会显示密码 +Repeat for confirmation: # 重复一遍 +password: 666666 +``` + +由于上面的写法有点繁琐,click 也提供了一种快捷的方式,通过使用 `@click.password_option()`,上面的代码可以简写成: + +```python +import click + +@click.command() +@click.password_option() +def input_password(password): + click.echo('password: %s' % password) + +if __name__ == '__main__': + input_password() +``` + +## 改变命令行程序的执行 + +有些参数会改变命令行程序的执行,比如在终端输入 `python` 是进入 python 控制台,而输入 `python --version` 是打印 python 版本。Click 提供 eager 标识对参数名进行标识,如果输入该参数,则会拦截既定的命令行执行流程,跳转去执行一个回调函数。 + +让我们看看例子: + +```python +import click + +def print_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + click.echo('Version 1.0') + ctx.exit() + +@click.command() +@click.option('--version', is_flag=True, callback=print_version, + expose_value=False, is_eager=True) +@click.option('--name', default='Ethan', help='name') +def hello(name): + click.echo('Hello %s!' % name) + +if __name__ == '__main__': + hello() +``` + +其中: + +- `is_eager=True` 表明该命令行选项优先级高于其他选项; +- `expose_value=False` 表示如果没有输入该命令行选项,会执行既定的命令行流程; +- `callback` 指定了输入该命令行选项时,要跳转执行的函数; + +执行情况: + +``` +$ python click_eager.py +Hello Ethan! + +$ python click_eager.py --version # 拦截既定的命令行执行流程 +Version 1.0 + +$ python click_eager.py --name Michael +Hello Michael! + +$ python click_eager.py --version --name Ethan # 忽略 name 选项 +Version 1.0 +``` + +# click.argument + +我们除了使用 `@click.option` 来添加**可选参数**,还会经常使用 `@click.argument` 来添加**固定参数**。它的使用和 option 类似,但支持的功能比 option 少。 + +## 入门使用 + +下面是一个简单的例子: + +```python +import click + +@click.command() +@click.argument('coordinates') +def show(coordinates): + click.echo('coordinates: %s' % coordinates) + +if __name__ == '__main__': + show() +``` + +看看执行情况: + +```click_argument +$ python click_argument.py # 错误,缺少参数 coordinates +Usage: click_argument.py [OPTIONS] COORDINATES + +Error: Missing argument "coordinates". + +$ python click_argument.py --help # argument 指定的参数在 help 中没有显示 +Usage: click_argument.py [OPTIONS] COORDINATES + +Options: + --help Show this message and exit. + +$ python click_argument.py --coordinates 10 # 错误用法,这是 option 参数的用法 +Error: no such option: --coordinates + +$ python click_argument.py 10 # 正确,直接输入值即可 +coordinates: 10 +``` + +## 多个 argument + +我们再来看看多个 argument 的例子: + +```python +import click + +@click.command() +@click.argument('x') +@click.argument('y') +@click.argument('z') +def show(x, y, z): + click.echo('x: %s, y: %s, z:%s' % (x, y, z)) + +if __name__ == '__main__': + show() +``` + +执行情况: + +``` +$ python click_argument.py 10 20 30 +x: 10, y: 20, z:30 + +$ python click_argument.py 10 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Missing argument "y". + +$ python click_argument.py 10 20 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Missing argument "z". + +$ python click_argument.py 10 20 30 40 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Got unexpected extra argument (40) +``` + +## 不定参数 + +argument 还有另外一种常见的用法,就是接收不定量的参数,让我们看看例子: + +```python +import click + +@click.command() +@click.argument('src', nargs=-1) +@click.argument('dst', nargs=1) +def move(src, dst): + click.echo('move %s to %s' % (src, dst)) + +if __name__ == '__main__': + move() +``` + +其中,`nargs=-1` 表明参数 `src` 接收不定量的参数值,参数值会以 tuple 的形式传入函数。如果 `nargs` 大于等于 1,表示接收 `nargs` 个参数值,上面的例子中,`dst` 接收一个参数值。 + +让我们看看执行情况: + +``` +$ python click_argument.py file1 trash # src=('file1',) dst='trash' +move (u'file1',) to trash + +$ python click_argument.py file1 file2 file3 trash # src=('file1', 'file2', 'file3') dst='trash' +move (u'file1', u'file2', u'file3') to trash +``` + +# 彩色输出 + +在前面的例子中,我们使用 `click.echo` 进行输出,如果配合 [colorama][7] 这个模块,我们可以使用 `click.secho` 进行彩色输出,在使用之前,使用 pip 安装 colorama: + +``` +$ pip install colorama +``` + +看看例子: + +```python +import click + +@click.command() +@click.option('--name', help='The person to greet.') +def hello(name): + click.secho('Hello %s!' % name, fg='red', underline=True) + click.secho('Hello %s!' % name, fg='yellow', bg='black') + +if __name__ == '__main__': + hello() +``` + +其中: + +- `fg` 表示前景颜色(即字体颜色),可选值有:BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE 等; +- `bg` 表示背景颜色,可选值有:BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE 等; +- `underline` 表示下划线,可选的样式还有:`dim=True`,`bold=True` 等; + +# 小结 + +- 使用 `click.command()` 装饰一个函数,使其成为命令行接口。 +- 使用 `click.option()` 添加可选参数,支持设置固定长度的参数值。 +- 使用 `click.argument()` 添加固定参数,支持设置不定长度的参数值。 + +# 参考资料 + +- [Click Documentation (6.0)](http://click.pocoo.org/6/) +- [Python Click 学习笔记 | I sudo X](https://isudox.com/2016/09/03/learning-python-package-click/) +- [click模块 - cdwanze](http://cdwanze.github.io/%E7%94%B5%E8%84%91/python/%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A8%A1%E5%9D%97/click%E6%A8%A1%E5%9D%97.html) + + +[0]: https://github.com/pallets/click +[1]: https://github.com/pallets/flask +[2]: https://github.com/pallets +[3]: https://docs.python.org/2/howto/argparse.html +[4]: https://github.com/kennethreitz/requests +[5]: https://docs.python.org/2/library/urllib.html +[6]: http://click.pocoo.org/6/ +[7]: https://pypi.python.org/pypi/colorama + + diff --git a/book.json b/book.json index c2fa436..0520c60 100644 --- a/book.json +++ b/book.json @@ -12,8 +12,7 @@ "weibo": null }, "sidebar": { - "Blog": "https://funhacks.net", - "Github": "https://github.com/ethan-funny/explore-python/" + "书籍源码": "https://github.com/ethan-funny/explore-python/" } }, "output": null, diff --git a/config.json b/config.json new file mode 100644 index 0000000..7b54f47 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "name": "Python 之旅", + "introduction":"Explore Python,Python 学习之旅。", + "path": { + "toc": "SUMMARY.md" + } +} diff --git a/cover.png b/cover.png new file mode 100644 index 0000000..6b46fca Binary files /dev/null and b/cover.png differ diff --git a/cover/background.jpg b/cover/background.jpg new file mode 100644 index 0000000..bdf1798 Binary files /dev/null and b/cover/background.jpg differ diff --git a/cover/cover.png b/cover/cover.png new file mode 100644 index 0000000..6b46fca Binary files /dev/null and b/cover/cover.png differ diff --git a/cover/logo.png b/cover/logo.png new file mode 100644 index 0000000..608a4c0 Binary files /dev/null and b/cover/logo.png differ