├── .gitignore ├── README.md ├── _config.yml ├── docs ├── content.md ├── database │ ├── MongoDB.md │ ├── MySQL.md │ └── attention.md ├── index.md ├── index.rst ├── introduce_spider │ ├── introduce_spider.md │ └── spider_interview.md ├── network │ ├── HTTP.md │ ├── TCP.md │ └── session.md ├── requests │ ├── basic_useage.md │ ├── request_attention.md │ └── request_source_code.md ├── thread │ └── Thread_process_coroutine.md └── xpath │ ├── attention.md │ ├── lxml.md │ └── scrapy_selector.md └── mkdocs.yml /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | # Byte-compiled / optimized / DLL files 3 | .scrapy/ 4 | __pycache__/ 5 | logs/ 6 | images/ 7 | conf/is_debug.py 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # JetBrains 109 | .idea/ 110 | 111 | # macOS 112 | .DS_Store 113 | 114 | # swap 115 | *.swp 116 | *.kate-swp 117 | 118 | 119 | *.log.* 120 | *.pyc 121 | 122 | Pipfile 123 | !/qq_news/qq_news/qq_news/main.py 124 | /_config.yml 125 | 126 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 数据采集从入门到放弃 2 | 3 | ## 内容介绍 4 | 5 | 本书会介绍我目前所知的所有关于爬虫的东西,更像是我的技能清单,仔细把其中所有的内容过一遍,目标是传播知识。 6 | 7 | 在想阅读:[数据采集从入门到放弃](https://zhangslob.github.io/docs/) 8 | 9 | 大概会分为这么几个大方向: 10 | 11 | 1. 爬虫介绍、就业情况、爬虫工程师面试题 12 | 2. HTTP协议介绍 13 | 3. Requests使用 14 | 4. 解析器Xpath介绍 15 | 5. MongoDB与MySQL 16 | 6. 多线程爬虫 17 | 7. Scrapy介绍 18 | 8. Scrapy-redis介绍 19 | 9. 使用docker部署 20 | 10. 使用nomad管理docker集群 21 | 11. 使用EFK查询docker日志 22 | 23 | 可能还会增加一些别的,主要是看心情。如: 24 | 25 | 1. 简单验证码处理(这个我也在学) 26 | 2. IOS逆向 27 | 3. Chrome断点调试和加密分析 28 | 4. Docker使用 29 | 5. Selenium与Appnium、pyppeteer 30 | 6. 布隆过滤器 31 | 7. Charles、mitmproxy抓包 32 | 8. 全站爬取思路 33 | 9. Flask开发 34 | 10. Spark相关 35 | 11. 其他语言如Go、JAVA爬虫 36 | 37 | 38 | 39 | 这其中的每一点都需要花很多时间去研究,希望我们一起进步。 40 | 41 | ![](https://ws3.sinaimg.cn/large/006tKfTcly1g077kx4c26j308c04oaa8.jpg) 42 | 43 | 44 | 45 | 我不会讲Python基础语法那些,建议去[BeginnersGuide](https://wiki.python.org/moin/BeginnersGuide/Programmers) 和 [documentation](https://docs.python.org/3/) 看。 46 | 47 | 48 | 49 | ## 开发环境 50 | 51 | - Python3系列 52 | - 建议macOS或Linux系统 53 | - PyCharm开发 54 | 55 | 56 | 57 | ## 说说标题 58 | 59 | 先解释下标题,为什么是入门到放弃。 60 | 61 | 首先这并不是一句调侃的话,而是我现在的内心感受。我做爬虫快两年了,是从运营转过来的。我觉得我对爬虫有这三个阶段: 62 | 63 | 1. **喜欢** 。刚开始还没有真正接触到真实企业需求时,由于知乎的渲染(你懂得),我对爬虫真的超级感兴趣,打开的每个新网站都想去试试如何爬取,有什么反爬没。这个阶段持续到开始做实际项目,就慢慢地转变为下个阶段。这里我想说下,肯定有别人和我一样对爬虫保持有很高的热情,喜欢去爬取一些网站的数据,有一个关键点就是数据的问题。很多时候数据不完整,或者数据不持久,没有持续的数据分析,你爬取的数据就是没有价值的,这是我做了几个长期项目的感受。 64 | 2. **无感** 。爱好变为职业是一个很痛苦的事情,之前做运营时超级羡慕爬虫工程师们,感觉他们好幸福。当自己真正开始做了,刚开始还是挺好的,过一年心态就会发生变化,原因很多,这个有时间再慢慢说吧。这首歌就是红玫瑰:*得不到的永远在骚动,被偏爱的都有恃无恐*,自行体会吧。 65 | 3. **放弃** 。阶段二与阶段三是同时会有的感受,因为对爬虫没有之前那么多兴趣,就会慢慢的想开始去做别的事情。我的博客中的描述是“数据采集、数据处理、机器学习”,数据采集知识第一步,数据处理、机器学习才是重点(高薪职业),是未来有前景的方向。所以我才会去学Spark,去学Scala,也是希望在未来的某个时候可以转行去真正接触“数据”,研究数据。 66 | 67 | ## 个人介绍 68 | 69 | 我叫小歪,公众号:**Python爬虫与算法进阶** ,知乎上也叫小歪。 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/content.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | 1. 爬虫介绍、就业情况 4 | 2. HTTP协议介绍 5 | 3. Requests使用 6 | 4. 解析器Xpath介绍 7 | 5. MongoDB与MySQL 8 | 6. 多线程爬虫 9 | 7. Scrapy介绍 10 | 8. Scrapy-redis介绍 11 | 12 | ## 介绍 13 | 14 | ## 第一章:数据采集概况 15 | 16 | 1. 什么是数据采集 17 | 2. 如何学习数据采集 18 | 3. 数据采集前景讨论 19 | 4. 爬虫工程师面试题 20 | 21 | ## 第二章:HTTP协议介绍 22 | 23 | 1. HTTP协议(面试高频) 24 | 2. HTTPS协议 25 | 3. Cookie状态管理 26 | 4. HTTP报文 27 | 28 | ## 第三章:Requests使用 29 | 30 | 1. 基础用法 31 | 2. 核心API 32 | 3. 注意事项 33 | 34 | ## 第四章:Xpath介绍 35 | 36 | 1. lxml使用 37 | 2. scrapy的selector 38 | 3. 注意事项 39 | 40 | ## 第五章:MongoDB与MySQL 41 | 42 | 1. MongoDB使用 43 | 2. MySQL使用 44 | 3. 注意事项 45 | 46 | ## 第六章:多线程爬虫 47 | 48 | 1. 线程、进程、协程(面试高频) 49 | 2. 多线程爬虫 50 | 3. 注意事项 51 | -------------------------------------------------------------------------------- /docs/database/MongoDB.md: -------------------------------------------------------------------------------- 1 | # MongoDB使用 2 | 3 | MongoDB在爬虫中是最常见的数据库选择,因为够灵活多变,简单好用。 4 | 5 | # 环境安装 6 | 7 | 使用docker一键安装:`docker run --name some-mongo -d mongo` 8 | 9 | `pip3 install pymongo` 10 | 11 | # Python操作MongoDB 12 | 13 | ## 连接 14 | 15 | 16 | ```python 17 | import pymongo 18 | 19 | client = pymongo.MongoClient() 20 | ``` 21 | 22 | 推荐使用`MongoDB_URL`传递MongoDB地址,`MongoDB_URL`格式:`mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] 23 | 24 | 例如连接本地的`test`数据库:`mongodb://localhost/test` 25 | 例如连接本地需要验证的`test`数据库:`mongodb://admin:123456@localhost/test` 26 | 27 | 获取数据库的方法如下: 28 | ```python 29 | import pymongo 30 | 31 | client = pymongo.MongoClient('mongodb://localhost/test') 32 | db = client.get_database() 33 | ``` 34 | 35 | ## 创建索引 36 | 37 | ```python 38 | import pymongo 39 | 40 | client = pymongo.MongoClient('mongodb://localhost/test') 41 | db = client.get_database() 42 | 43 | collection = db['test_collection'] 44 | collection.create_index('index') 45 | collection.create_index('id', unique=True) 46 | ``` 47 | 48 | 我们这里连接到本地MongoDB的test数据库下的test_collection,然后在test_collection这张表上面创建了两个索引: 49 | 50 | 1. index:普通索引 51 | 2. id:唯一索引,不允许重复 52 | 53 | > 不要建太多无用索引,也不要重复建索引 54 | 55 | ## 插入数据 56 | 57 | 单条数据插入: 58 | ```python 59 | import pymongo 60 | 61 | client = pymongo.MongoClient('mongodb://localhost/test') 62 | db = client.get_database() 63 | 64 | collection = db['test_collection'] 65 | # 不要再创建索引了 66 | 67 | data = {'name': "zhang", 'age': 18} 68 | collection.insert_one(data) 69 | ``` 70 | 71 | 然后就可以查看了,这里使用的是Robot 3t 72 | 73 | ![](https://i.loli.net/2019/06/13/5d01f286151c437037.png) 74 | 75 | 插入多条。如果有很多数据插入,可以选择此种方法,效率会比单条插入高,对数据库压力也会更小。 76 | 77 | ```python 78 | import pymongo 79 | 80 | client = pymongo.MongoClient('mongodb://localhost/test') 81 | db = client.get_database() 82 | 83 | collection = db['test_collection'] 84 | 85 | data = [ 86 | {"name": "Taobao", "alexa": "100", "url": "https://www.taobao.com"}, 87 | {"name": "QQ", "alexa": "101", "url": "https://www.qq.com"}, 88 | {"name": "Facebook", "alexa": "10", "url": "https://www.facebook.com"}, 89 | {"name": "知乎", "alexa": "103", "url": "https://www.zhihu.com"}, 90 | {"name": "Github", "alexa": "109", "url": "https://www.github.com"} 91 | ] 92 | collection.insert_many(data, ordered=False) 93 | ``` 94 | 95 | 如果我们直接运行是会报错的,因为我们上面有创建唯一索引,而且文档中都没有`id`这个字段,所以我们需要先删除索引`id`,然后再插入数据。 96 | 97 | ![](https://i.loli.net/2019/06/13/5d01f426cce8f41482.png) 98 | 99 | ## 查询数据 100 | 101 | 这里就包含很多知识了,MongoDB查询方法真的有很多,[看文档](https://docs.mongodb.com/manual/tutorial/query-documents/) 102 | 103 | ## 错误判断 104 | 105 | 上面我们说到,我们有建了唯一索引,比如是`id`,如果插入重复的`id`,是会报错的,我们可以捕获这个异常。 106 | 107 | ```python 108 | try: 109 | collection.insert_one(data) 110 | except pymongo.errors.DuplicateKeyError: 111 | print('重复插入数据:{}'.format(data)) 112 | ``` 113 | 114 | 115 | # 作业 116 | 117 | 阅读理解这些文章:[MongoDB 标签](https://zhangslob.github.io/tags/MongoDB/) 118 | 119 | -------------------------------------------------------------------------------- /docs/database/MySQL.md: -------------------------------------------------------------------------------- 1 | # MySQL使用 2 | 3 | MySQL是最常见的关系型数据库,应用很广泛。 4 | 5 | ## 环境安装 6 | 7 | docker: `docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql` 8 | 9 | `pip3 install PyMySQL` 10 | 11 | ## 连接 12 | 13 | 先创建一张表 14 | 15 | ```sql 16 | CREATE TABLE `users` ( 17 | `id` int(11) NOT NULL AUTO_INCREMENT, 18 | `email` varchar(255) COLLATE utf8_bin NOT NULL, 19 | `password` varchar(255) COLLATE utf8_bin NOT NULL, 20 | PRIMARY KEY (`id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin 22 | AUTO_INCREMENT=1 ; 23 | ``` 24 | 25 | ```python 26 | import pymysql.cursors 27 | 28 | # Connect to the database 29 | connection = pymysql.connect(host='localhost', 30 | user='user', 31 | password='passwd', 32 | db='db', 33 | charset='utf8mb4', 34 | cursorclass=pymysql.cursors.DictCursor) 35 | ``` 36 | 37 | ## 创建cursor、执行SQL 38 | 39 | ```python 40 | import pymysql.cursors 41 | 42 | # Connect to the database 43 | connection = pymysql.connect(host='localhost', 44 | user='user', 45 | password='passwd', 46 | db='db', 47 | charset='utf8mb4', 48 | cursorclass=pymysql.cursors.DictCursor) 49 | 50 | 51 | try: 52 | with connection.cursor() as cursor: 53 | # Create a new record 54 | sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)" 55 | cursor.execute(sql, ('webmaster@python.org', 'very-secret')) 56 | 57 | # connection is not autocommit by default. So you must commit to save 58 | # your changes. 59 | connection.commit() 60 | 61 | with connection.cursor() as cursor: 62 | # Read a single record 63 | sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s" 64 | cursor.execute(sql, ('webmaster@python.org',)) 65 | result = cursor.fetchone() 66 | print(result) 67 | finally: 68 | connection.close() 69 | ``` 70 | 71 | 将会输出:`{'password': 'very-secret', 'id': 1}` 72 | 73 | 74 | ## 查询数据 75 | 76 | ```python 77 | # 执行查询 SQL 78 | cursor.execute('SELECT * FROM `users`') 79 | # 获取单条数据 80 | cursor.fetchone() 81 | # 获取前N条数据 82 | cursor.fetchmany(3) 83 | # 获取所有数据 84 | cursor.fetchall() 85 | ``` 86 | 87 | ## 设置游标类型 88 | 89 | 查询时,默认返回的数据类型为元组,可以自定义设置返回类型。支持5种游标类型: 90 | 91 | - Cursor: 默认,元组类型 92 | - DictCursor: 字典类型 93 | - DictCursorMixin: 支持自定义的游标类型,需先自定义才可使用 94 | - SSCursor: 无缓冲元组类型 95 | - SSDictCursor: 无缓冲字典类型 96 | 97 | 无缓冲游标类型,适用于数据量很大,一次性返回太慢,或者服务端带宽较小时。 98 | 99 | 创建方法.创建连接时,通过 cursorclass 参数指定类型: 100 | 101 | ```python 102 | connection = pymysql.connect(host='localhost', 103 | user='root', 104 | password='root', 105 | db='demo', 106 | charset='utf8', 107 | cursorclass=pymysql.cursors.DictCursor) 108 | ``` 109 | 110 | 也可以在创建游标时指定类型: 111 | 112 | `cursor = connection.cursor(cursor=pymysql.cursors.DictCursor)` 113 | 114 | -------------------------------------------------------------------------------- /docs/database/attention.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangslob/docs/c934259f4cb10c15825ee7bdde7df9e95c671a7b/docs/database/attention.md -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 数据采集从入门到放弃 2 | ==================== 3 | 4 | ### [Github地址](https://github.com/zhangslob/docs) 5 | 6 | 7 | 8 | ## 内容介绍 9 | 10 | 本书会介绍我目前所知的所有关于爬虫的东西,更像是我的技能清单,仔细把其中所有的内容过一遍,目标是传播知识。 11 | 12 | 大概会分为这么几个大方向(入门): 13 | 14 | 1. 爬虫介绍、就业情况、爬虫工程师面试题 15 | 2. HTTP协议介绍 16 | 3. Requests使用 17 | 4. 解析器Xpath介绍 18 | 5. MongoDB与MySQL 19 | 6. 多线程爬虫 20 | 7. Scrapy介绍 21 | 8. Scrapy-redis介绍 22 | 9. 使用docker部署 23 | 10. 使用nomad管理docker集群 24 | 11. 使用EFK查询docker日志 25 | 26 | 可能还会增加一些别的(放弃),如: 27 | 28 | 1. 简单验证码处理(这个我也在学) 29 | 2. IOS逆向 30 | 3. Chrome断点调试和加密分析 31 | 4. Docker使用 32 | 5. Selenium与Appium、pyppeteer 33 | 6. 布隆过滤器 34 | 7. Charles、mitmproxy抓包 35 | 8. 全站爬取思路 36 | 9. Flask开发 37 | 10. Spark相关 38 | 11. 其他语言如Go、JAVA爬虫 39 | 40 | 41 | 42 | 这其中的每一点都需要花很多时间去研究,希望我们一起进步。 43 | 44 | ![](https://camo.githubusercontent.com/e738c0e672e83d6dbcb0e2dd482535d4defc8ab6/68747470733a2f2f7773332e73696e61696d672e636e2f6c617267652f303036744b6654636c7931673037376b78346332366a3330386330346f6161382e6a7067) 45 | 46 | 47 | 48 | 我不会讲Python基础语法那些,建议去[BeginnersGuide](https://wiki.python.org/moin/BeginnersGuide/Programmers) 和 [documentation](https://docs.python.org/3/) 看。 49 | 50 | 51 | 52 | ## 开发环境 53 | 54 | - Python3系列 55 | - 建议macOS或Linux系统 56 | - PyCharm开发 57 | 58 | 59 | 60 | 说说标题 61 | ---- 62 | 63 | 先解释下标题,为什么是入门到放弃。 64 | 65 | 首先这并不是一句调侃的话,而是我现在的内心感受。我做爬虫快两年了,是从运营转过来的。我觉得我对爬虫有这三个阶段: 66 | 67 | 1. **喜欢** 。刚开始还没有真正接触到真实企业需求时,由于知乎的渲染(你懂得),我对爬虫真的超级感兴趣,打开的每个新网站都想去试试如何爬取,有什么反爬没。这个阶段持续到开始做实际项目,就慢慢地转变为下个阶段。这里我想说下,肯定有别人和我一样对爬虫保持有很高的热情,喜欢去爬取一些网站的数据,有一个关键点就是数据的问题。很多时候数据不完整,或者数据不持久,没有持续的数据分析,你爬取的数据就是没有价值的,这是我做了几个长期项目的感受。 68 | 2. **无感** 。爱好变为职业是一个很痛苦的事情,之前做运营时超级羡慕爬虫工程师们,感觉他们好幸福。当自己真正开始做了,刚开始还是挺好的,过一年心态就会发生变化,原因很多,这个有时间再慢慢说吧。这首歌就是红玫瑰:*得不到的永远在骚动,被偏爱的都有恃无恐*,自行体会吧。 69 | 3. **放弃** 。阶段二与阶段三是同时会有的感受,因为对爬虫没有之前那么多兴趣,就会慢慢的想开始去做别的事情。我的博客中的描述是“数据采集、数据处理、机器学习”,数据采集只是第一步,数据处理、机器学习才是重点(高薪职业),是未来有前景的方向。所以我才会去学Spark,去学Scala,也是希望在未来的某个时候可以转行去真正接触“数据”,研究数据。 70 | 71 | 个人介绍 72 | -------- 73 | 74 | 我叫小歪,公众号:**Python爬虫与算法进阶** ,知乎上也叫小歪。 -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 数据采集从入门到放弃 2 | ==================== 3 | 4 | 内容介绍 5 | -------- 6 | 7 | 本书会介绍我目前所知的所有关于爬虫的东西,更像是我的技能清单,仔细把其中所有的内容过一遍,目标是传播知识。 8 | 9 | 大概会分为这么几个大方向(入门): 10 | 11 | 1. 爬虫介绍、就业情况 12 | 2. HTTP协议介绍 13 | 3. Requests使用 14 | 4. 解析器Xpath介绍 15 | 5. MongoDB与MySQL 16 | 6. 多线程爬虫 17 | 7. Scrapy介绍 18 | 8. Scrapy-redis介绍 19 | 20 | 可能还会增加一些别的(放弃),如: 21 | 22 | 1. 简单验证码处理(这个我也在学) 23 | 2. IOS逆向 24 | 3. Chrome断点调试和加密分析 25 | 4. Docker使用 26 | 5. Selenium与Appium、pyppeteer 27 | 6. 布隆过滤器 28 | 7. Charles、mitmproxy抓包 29 | 8. 全站爬取思路 30 | 9. Flask开发 31 | 10. Spark相关 32 | 11. 其他语言如Go、JAVA爬虫 33 | 34 | 这其中的每一点都需要花很多时间去研究,希望我们一起进步。 35 | 36 | .. figure:: https://ws3.sinaimg.cn/large/006tKfTcly1g077kx4c26j308c04oaa8.jpg 37 | :alt: 38 | 39 | 我不会讲Python基础语法那些,建议去\ `BeginnersGuide `__ 40 | 和 `documentation `__ 看。 41 | 42 | 开发环境 43 | -------- 44 | 45 | - Python3系列 46 | - 建议macOS或Linux系统 47 | - PyCharm开发 48 | 49 | 说说标题 50 | -------- 51 | 52 | 先解释下标题,为什么是入门到放弃。 53 | 54 | 首先这并不是一句调侃的话,而是我现在的内心感受。我做爬虫快两年了,是从运营转过来的。我觉得我对爬虫有这三个阶段: 55 | 56 | 1. **喜欢** 57 | 。刚开始还没有真正接触到真实企业需求时,由于知乎的渲染(你懂得),我对爬虫真的超级感兴趣,打开的每个新网站都想去试试如何爬取,有什么反爬没。这个阶段持续到开始做实际项目,就慢慢地转变为下个阶段。这里我想说下,肯定有别人和我一样对爬虫保持有很高的热情,喜欢去爬取一些网站的数据,有一个关键点就是数据的问题。很多时候数据不完整,或者数据不持久,没有持续的数据分析,你爬取的数据就是没有价值的,这是我做了几个长期项目的感受。 58 | 2. **无感** 59 | 。爱好变为职业是一个很痛苦的事情,之前做运营时超级羡慕爬虫工程师们,感觉他们好幸福。当自己真正开始做了,刚开始还是挺好的,过一年心态就会发生变化,原因很多,这个有时间再慢慢说吧。这首歌就是红玫瑰:\ *得不到的永远在骚动,被偏爱的都有恃无恐*\ ,自行体会吧。 60 | 3. **放弃** 61 | 。阶段二与阶段三是同时会有的感受,因为对爬虫没有之前那么多兴趣,就会慢慢的想开始去做别的事情。我的博客中的描述是“数据采集、数据处理、机器学习”,数据采集只是第一步,数据处理、机器学习才是重点(高薪职业),是未来有前景的方向。所以我才会去学Spark,去学Scala,也是希望在未来的某个时候可以转行去真正接触“数据”,研究数据。 62 | 63 | 个人介绍 64 | -------- 65 | 66 | 我叫小歪,公众号:\ **Python爬虫与算法进阶** ,知乎上也叫小歪。 67 | -------------------------------------------------------------------------------- /docs/introduce_spider/introduce_spider.md: -------------------------------------------------------------------------------- 1 | # 数据采集介绍 2 | 3 | ## 什么是数据采集 4 | 5 | ### 定义 6 | 7 | 就我个人而说,更喜欢说数据采集而不是”爬虫“。其实更标准的叫法是网络爬虫,在wiki上是这样定义的: 8 | 9 | > **网络爬虫**(英语:web crawler),也叫网络蜘蛛(spider),是一种用来自动浏览[万维网](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91)的[网络机器人](https://zh.wikipedia.org/w/index.php?title=%E7%BD%91%E7%BB%9C%E6%9C%BA%E5%99%A8%E4%BA%BA&action=edit&redlink=1)。其目的一般为编纂[网络索引](https://zh.wikipedia.org/w/index.php?title=%E7%BD%91%E7%BB%9C%E7%B4%A2%E5%BC%95&action=edit&redlink=1)。 10 | 11 | 就比如百度、谷歌,都是网络爬虫,把互联网上所有的数据采集下来,保存到自己的数据库中,并根据各种各种规则建立排名和索引,向用户提供搜索服务。大体上就是这么个意思。 12 | 13 | ### 分类 14 | 15 | 一般会分为两种:通用爬虫与垂直爬虫 16 | 17 | 1. 通用爬虫。类似百度、谷歌这样的爬虫,抓取对象是整个互联网,对于网页没有固定的抽取规则。 对于所有网页都是一套通用的处理方法。这里可以看看我之前分享的代码 [用Golang写爬虫(一)](https://github.com/zhangslob/awesome_crawl/tree/master/Golang_basic_spider) 18 | 2. 垂直爬虫。这类爬虫主要针对一些特定对象、网站,有一台指定的爬取路径、数据抽取规则。对于我们大多数人来说,写的爬虫都是垂直爬虫,具体的某个网站的某个数据,比如淘宝的手机、当当上的书籍等等。 19 | 20 | ### 策略 21 | 22 | 网络爬虫始于一张被称作种子的统一资源地址(URL)列表。当网络爬虫访问这些URL时,它们会甄别出页面上所有的超链接,并将它们写入一张“待访列表”,即将要访问的链接。然后再次访问这些URL,提取,循环往复。 23 | 24 | 常见的爬虫爬取策略有DFS和BFS,拿一张图来说明 25 | 26 | ![](https://ws2.sinaimg.cn/large/006tKfTcly1g0akaj0xfzj30b409bwer.jpg) 27 | 28 | 1. 深度优先策略(DFS) 深度优先策略是指爬虫从某个URL开始,一个链接一个链接的爬取下去,直到处理完了某个链接所在的所有线路,才切换到其它的线路。 此时抓取顺序为:A -> B -> C -> D -> E -> F -> G -> H -> I -> J 29 | 2. 广度优先策略(BFS) 宽度优先遍历策略的基本思路是,将新下载网页中发现的链接直接插入待抓取URL队列的末尾。也就是指网络爬虫会先抓取起始网页中链接的所有网页,然后再选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。 此时抓取顺序为:A -> B -> E -> G -> H -> I -> C -> F -> J -> D 30 | 31 | > 和算法中的保持一致 32 | 33 | 34 | 35 | ## 如何学习数据采集 36 | 37 | 在知乎上一直都有各种各样的入门文章,鱼龙混杂吧。具体的学习可以看看崔庆才的书,写的挺好的。再有把这个教程里**入门**章节看完,编写一般的爬虫是没问题的,想要去继续深入,可以去看看**放弃**章节。 38 | 39 | 其实这些方向都是差不多的,最主要还是看自己花多少时间去学习。我下班后回到家,每天都会写代码或者学新技术,一般到12点。 40 | 41 | ## 数据采集前景讨论 42 | 43 | 我们来看看一些数据吧。 44 | 45 | ![数据采集职位数量](https://ws2.sinaimg.cn/large/006tKfTcly1g0am1fqxpvj30sf0bvgma.jpg) 46 | 47 | 这是从某网站上抓取的(全国所有职位),从2018年6月到2018年12月,关键词是”爬虫“、”数据采集“的职位数量,多的就不用说了吧,职位明显是在减少。我们再来看看搜索”前端“的结果是什么。 48 | 49 | ![前端开发职位数量](https://ws4.sinaimg.cn/large/006tKfTcly1g0an3auyn2j30t40bsq3p.jpg) 50 | 51 | 52 | 53 | 注意看纵坐标的数量,根本就不在一个级别。但是注意看整体趋势,都是在下降,估计和现在的互联网寒冬有关。 54 | 55 | 数据采集职位数量上来看要比前端方向少很多,因为每家公司都会需要前端、但并非一定要数据采集,这里没有说后端,主要是后端包含太广,Python、JAVA、GO、PHP等等都可以做,所以统计数量可能不好算。 56 | 57 | 如果你真的喜欢数据采集,那么就努力提升自己的技能,这才是最重要的。 58 | 59 | 关于爬虫学到什么程度可以找工作,可以看看我之前写的,[爬虫学到什么程度可以去找工作](https://mp.weixin.qq.com/s?__biz=MzIwNjUxMTQyMA==&mid=2247484172&idx=1&sn=ba80e3ba10d523af73d67a25dd916e6b&chksm=9721cf5fa0564649aab3566ee27e22f3e9231f423a426f2f6f517385c5812e21d64dcb4415f1#rd) 60 | 61 | --- 62 | 63 | 如果有任何疑问,请[在此交流](https://github.com/zhangslob/docs/issues/1)。 64 | 65 | -------------------------------------------------------------------------------- /docs/introduce_spider/spider_interview.md: -------------------------------------------------------------------------------- 1 | # 爬虫工程师面试题 2 | 3 | 4 | 5 | ## 拼多多爬虫工程师面试题 6 | 7 | ### 电话面: 8 | 9 | - http协议、tcp协议(几次握手) 10 | - top命令 11 | - Linux/Mac 下虚拟内存(Swap) 12 | - 线程、进程、协程 13 | - Async 相关、事件驱动相关 14 | - 阻塞、非阻塞 15 | - Python GIL 16 | - 布隆过滤器原理:如何实现、一般要几次哈希函数 17 | 18 | 给我留下了一个作业:抓取天猫超市上某些商品的可以配送省份信息。(当时做这个也花了很久,主要是需要解决PC端的登陆问题,后来通过h5接口) 19 | 20 | ### 现场面(3小时): 21 | 22 | ### 一面(技术): 23 | 24 | 一面是之前电话面的,主要问了之前布置的作业相关 25 | 26 | 1. 问了下之前留给我的作业,各种详细的细节,每一步怎么做的,遇到了哪些问题,自己是怎么解决的 27 | 2. 说了下淘宝登陆的两种方法,自己写的一些中间件 28 | 3. 还问了些之前的项目细节,爬虫资源配置怎么做的 29 | 4. 就我简历上的东西问了下底层的东西:线程与进程,协程用的Linux底层的是什么技术,事件驱动,MySQL的索引底层是什么,查询怎么做的等等。(这些问题都不知道) 30 | 5. 验证码如何处理,TensorFlow训练成功率多少 31 | 6. redis快的原因是什么,底层原因,你平常用到了哪些数据结构 32 | 7. 逆向是怎么学的,该怎么做,做过的项目,解决的签名 33 | 8. 爬虫失败之后的重试是如何处理的,有没有针对具体的失败去做针对性的报警和重试 34 | 35 | ### 二面(技术): 36 | 37 | 二面感觉是做爬虫的,挑了两个项目问其中的细节。说项目遇到了两个问题,我是怎么解决的,当时解释这个项目时花了很久。 38 | 39 | 还问了有没有阅读国内爬虫大厂、搜索引擎等开源的爬虫框架、思想之类的。这个好像真没了解,也没看到有谁会写这个。 40 | 41 | ### 三面(小组长): 42 | 43 | 三面人感觉很和善,爱笑。后来通过HR了解到是这个团队的组长,从腾讯微信挖来的。 44 | 45 | - 他主要问了一个问题,给我一个数据抓取场景,让我设计一套数据抓取流程。我按着自己的思路画了出来,然后他会提出一些新需求,我按照他的意思在原有的爬虫系统上改进。 46 | - 之后提的一个新需求我并没有回答好,但是他当时说:没事,已经回答的很好了。我心里想,leader很nice。 47 | - 最后他介绍了他们团队目前正在做的事情,爬虫框架其实已经搭起来了,只是会有些粗糙,还需要有人来改进 48 | 49 | ### 四面(HR面): 50 | 51 | 1. 问了些基本情况,期望薪资,在上家公司什么情况 52 | 2. 说了下拼多多的福利啊什么的,包三餐,新员工可以住宿舍一年 53 | 3. 说了下加班时间,11116,节假日也会加班 54 | 4. 问现在有没有面其他公司,说了Airbnb 55 | 56 | 到这里已经面试完毕了,从1点到4点,整个人累到虚脱,5点回杭州的票,然后马上就赶回去了。有点想吐槽,拼多多的工位真的很挤,显示屏幕也小。 57 | 58 | 后天另一位HR联系我,需要核实信息,然后就准备了一些材料给他,说技术面已经通过了。 59 | 60 | **过了大约一周的时间,正式收到offer,预计7月1日入职** 61 | 62 | 👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏 63 | 64 | ## 网易爬虫工程师面试 65 | 66 | 网易的面试是小黑鱼这边的leader内推的,但是HR直接打电话约的周一面试,没有电话面。 67 | 68 | ### 一面(技术): 69 | 70 | 一面与之前小黑鱼的同事认识,他也听过我的名字(从前同事)。问的东西比较广泛。 71 | 72 | 1. 问项目,之前公司的业务 73 | 2. 在小黑鱼这边做的事情,框架是怎么做的,实现了什么功能 74 | 3. scrapy会用到哪些中间件,各个中间件有什么功能,我说其中很多的中间件我都没用到 (ㄒoㄒ) 75 | 4. Python的基础语法知识,有些我也不知道,靠语感 76 | 5. 问了些websocket项目,是怎么找到协议的 77 | 6. 验证码如何处理,TensorFlow训练成功率多少 78 | 7. wireshark报文如何查看 79 | 8. 逆向是怎么学的,该怎么做,做过的项目,解决的签名 80 | 9. 给我出了一道题目,关于JS混淆的。刚开始没有头目,后来他告诉我要怎么做,才慢慢找到方法。 81 | 82 | ### 二面(技术): 83 | 84 | 二面应该是leader,问题很直接。 85 | 86 | 1. 用到了哪些linux命令,全写下来。我刚开始写了几个,他说太少了,让我再写几个,最后写了十几个 87 | 2. 就我上面写到的命令问了下具体细节,比如crontab命令报错了可能有哪些原因 88 | 3. 出了一道算法题,最终就是写排序算法 89 | 4. 问了下我的基本情况,在小黑鱼这边怎么样 90 | 91 | ### 三面(HR面): 92 | 93 | 两个HR,一男一女,问题问的我很崩溃。说这周给结果。 94 | 95 | 网易面试是从下午2点到5点,3小时,中间都没口水喝。 96 | 97 | --- 98 | 99 | 想要与我交流爬虫面试技巧,请到这里:[爬虫面试交流]() -------------------------------------------------------------------------------- /docs/network/HTTP.md: -------------------------------------------------------------------------------- 1 | # HTTP协议 2 | 3 | ## HTTP的特性 4 | 5 | - HTTP构建于TCP/IP协议之上,默认端口号是80 6 | - HTTP是**无连接无状态**的 7 | 8 | ## HTTP报文 9 | 10 | ### 请求报文 11 | 12 | HTTP 协议是以 ASCII 码传输,建立在 TCP/IP 协议之上的应用层规范。规范把 HTTP 请求分为三个部分:状态行、请求头、消息主体。类似于下面这样: 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | HTTP定义了与服务器交互的不同方法,最基本的方法有4种,分别是`GET`,`POST`,`PUT`,`DELETE`。`URL`全称是资源描述符,我们可以这样认为:一个`URL`地址,它用于描述一个网络上的资源,而 HTTP 中的`GET`,`POST`,`PUT`,`DELETE`就对应着对这个资源的查,增,改,删4个操作(CRUD)。 22 | 23 | 1. GET用于信息获取,GET请求报文示例: 24 | 25 | ```http 26 | GET /books/?sex=man&name=Professional HTTP/1.1 27 | Host: www.example.com 28 | User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) 29 | Gecko/20050225 Firefox/1.0.1 30 | Connection: Keep-Alive 31 | ``` 32 | 33 | 2. POST表示可能修改变服务器上的资源的请求。 34 | 35 | ```http 36 | POST / HTTP/1.1 37 | Host: www.example.com 38 | User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) 39 | Gecko/20050225 Firefox/1.0.1 40 | Content-Type: application/x-www-form-urlencoded 41 | Content-Length: 40 42 | Connection: Keep-Alive 43 | 44 | sex=man&name=Professional 45 | ``` 46 | 47 | 3. 举例,Chrome随便打开一个网页,就会看到HTTP报文,我们看到的被浏览器简化的及外国,我们点击`view source`即可查看原始报文: 48 | 49 | ![](https://ws3.sinaimg.cn/large/006tKfTcly1g0l4tfmbyaj30ku06d0tn.jpg) 50 | 51 | ![](https://ws4.sinaimg.cn/large/006tKfTcly1g0l4tyqotbj30kr064gml.jpg) 52 | 53 | ### 响应报文 54 | 55 | HTTP 响应与 HTTP 请求相似,HTTP响应也由3个部分构成,分别是: 56 | 57 | - 状态行 58 | - 响应头(Response Header) 59 | - 响应正文 60 | 61 | 状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。 62 | 63 | 常见的状态码有如下几种: 64 | 65 | - `200 OK` 客户端请求成功 66 | - `301 Moved Permanently` 请求永久重定向 67 | - `302 Moved Temporarily` 请求临时重定向 68 | - `304 Not Modified` 文件未修改,可以直接使用缓存的文件。 69 | - `400 Bad Request` 由于客户端请求有语法错误,不能被服务器所理解。 70 | - `401 Unauthorized` 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用 71 | - `403 Forbidden` 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因 72 | - `404 Not Found` 请求的资源不存在,例如,输入了错误的URL 73 | - `500 Internal Server Error` 服务器发生不可预期的错误,导致无法完成客户端的请求。 74 | - `503 Service Unavailable` 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。 75 | 76 | 下面是一个HTTP响应的例子: 77 | 78 | ```http 79 | HTTP/1.1 200 OK 80 | 81 | Server:Apache Tomcat/5.0.12 82 | Date:Mon,6Oct2003 13:23:42 GMT 83 | Content-Length:112 84 | 85 | ... 86 | ``` 87 | 88 | 同样的可以在Chrome中查看原始报文和简化过的。 89 | 90 | ![](https://ws2.sinaimg.cn/large/006tKfTcly1g0l4w15tczj30l10f5aeb.jpg) 91 | 92 | ## 会话跟踪 93 | 94 | 1. 什么是会话?客户端打开与服务器的连接发出请求到服务器响应客户端请求的全过程称之为会话。 95 | 96 | 2. 什么是会话跟踪?。会话跟踪指的是对同一个用户对服务器的连续的请求和接受响应的监视。 97 | 98 | 3. 为什么需要会话跟踪?浏览器与服务器之间的通信是通过HTTP协议进行通信的,而HTTP协议是”无状态”的协议,它不能保存客户的信息,即一次响应完成之后连接就断开了,下一次的请求需要重新连接,这样就需要判断是否是同一个用户,所以才有会话跟踪技术来实现这种要求。 99 | 100 | 101 | 102 | 会话跟踪常用的方法: 103 | 104 | 1. **URL重写**。URL(统一资源定位符)是Web上特定页面的地址,URL重写的技术就是在URL结尾添加一个附加数据以标识该会话,把会话ID通过URL的信息传递过去,以便在服务器端进行识别不同的用户。 105 | 2. **隐藏表单域**。将会话ID添加到HTML表单元素中提交到服务器,此表单元素并不在客户端显示 106 | 3. **Cookie**。Cookie是Web服务器发送给客户端的一小段信息,客户端请求时可以读取该信息发送到服务器端,进而进行用户的识别。对于客户端的每次请求,服务器都会将Cookie发送到客户端,在客户端可以进行保存,以便下次使用。客户端可以采用两种方式来保存这个Cookie对象,一种方式是保存在客户端内存中,称为临时Cookie,浏览器关闭后这个Cookie对象将消失。另外一种方式是保存在客户机的磁盘上,称为永久Cookie。以后客户端只要访问该网站,就会将这个Cookie再次发送到服务器上,前提是这个Cookie在有效期内,这样就实现了对客户的跟踪。Cookie是可以被禁止的。 107 | 4. **Session**。每一个用户都有一个不同的session,各个用户之间是不能共享的,是每个用户所独享的,在session中可以存放信息。在服务器端会创建一个session对象,产生一个sessionID来标识这个session对象,然后将这个sessionID放入到Cookie中发送到客户端,下一次访问时,sessionID会发送到服务器,在服务器端进行识别不同的用户。Session的实现依赖于Cookie,如果Cookie被禁用,那么session也将失效。(如果你熟悉requests,那么你肯定用到requests.session,和这里讲到的Session一致,常常用于需要登录才能采集的网站) 108 | 109 | ## 跨站攻击 110 | 111 | CSRF(Cross-site request forgery,跨站请求伪造) 112 | 113 | CSRF(XSRF) 顾名思义,是伪造请求,冒充用户在站内的正常操作。 114 | 115 | 例如,一论坛网站的发贴是通过 GET 请求访问,点击发贴之后 JS 把发贴内容拼接成目标 URL 并访问: 116 | 117 | ``` 118 | http://example.com/bbs/create_post.php?title=标题&content=内容 119 | ``` 120 | 121 | 那么,我们只需要在论坛中发一帖,包含一链接: 122 | 123 | ``` 124 | http://example.com/bbs/create_post.php?title=我是脑残&content=哈哈 125 | ``` 126 | 127 | 只要有用户点击了这个链接,那么他们的帐户就会在不知情的情况下发布了这一帖子。可能这只是个恶作剧,但是既然发贴的请求可以伪造,那么删帖、转帐、改密码、发邮件全都可以伪造。 128 | 129 | **如何防范 CSRF 攻击**?可以注意以下几点: 130 | 131 | - 关键操作只接受POST请求 132 | 133 | - 验证码。CSRF攻击的过程,往往是在用户不知情的情况下构造网络请求。所以如果使用验证码,那么每次操作都需要用户进行互动,从而简单有效的防御了CSRF攻击。但是如果你在一个网站作出任何举动都要输入验证码会严重影响用户体验,所以验证码一般只出现在特殊操作里面,或者在注册时候使用。 134 | 135 | - 检测 Referer。常见的互联网页面与页面之间是存在联系的,比如你在`www.baidu.com`应该是找不到通往`www.google.com`的链接的,再比如你在论坛留言,那么不管你留言后重定向到哪里去了,之前的那个网址一定会包含留言的输入框,这个之前的网址就会保留在新页面头文件的 `Referer` 中。通过检查`Referer`的值,我们就可以判断这个请求是合法的还是非法的,但是问题出在服务器不是任何时候都能接受到`Referer`的值,所以 Referer Check 一般用于监控 CSRF 攻击的发生,而不用来抵御攻击。 136 | 137 | - Token。目前主流的做法是使用 Token 抵御 CSRF 攻击。Token 使用原则:Token 要足够随机————只有这样才算不可预测;Token 是一次性的,即每次请求成功后要更新Token————这样可以增加攻击难度,增加预测难度;Token 要注意保密性————敏感操作使用 post,防止 Token 出现在 URL 中。**注意**:过滤用户输入的内容**不能**阻挡 csrf,我们需要做的是过滤请求的**来源**。 138 | 139 | 140 | - XSS(Cross Site Scripting,跨站脚本攻击)XSS 全称“跨站脚本”,是注入攻击的一种。其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本。运行预期之外的脚本带来的后果有很多中,可能只是简单的恶作剧——一个关不掉的窗口: 141 | 142 | ```js 143 | while (true) { 144 | alert("你关不掉我~"); 145 | } 146 | ``` 147 | 148 | 看到上面说的这些,你是不是非常熟悉,这和我们每天做的反爬虫很相似嘛。验证码、Referer、Token,每一点都需要注意,更细的我们以后再说。 149 | 150 | ## 实践 151 | 152 | 打开[知乎首页](https://www.zhihu.com/),下拉,会出现一个叫`session_token`的参数,去探索,这个token带边什么意思,有什么作用,爬虫会需要它吗? 153 | 154 | ![](https://ws4.sinaimg.cn/large/006tKfTcly1g0l5muvz17j31ha0qw15k.jpg) 155 | 156 | ### 参考资料 157 | 158 | - [浅谈HTTP中Get与Post的区别](http://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html) 159 | - [http请求与http响应详细解析](http://www.cnblogs.com/loveyakamoz/archive/2011/07/22/2113614.html) 160 | - [HTTP 条件 Get (Conditional Get)](http://blog.csdn.net/luoleicn/article/details/5289496) 161 | - [HTTP中的长连接与短连接](http://www.cnblogs.com/cswuyg/p/3653263.html) 162 | - [HTTP Keep-Alive模式](http://www.cnblogs.com/skynet/archive/2010/12/11/1903347.html) 163 | - [分块传输编码](https://zh.wikipedia.org/zh-cn/%E5%88%86%E5%9D%97%E4%BC%A0%E8%BE%93%E7%BC%96%E7%A0%81) 164 | - [HTTP 管线化(HTTP pipelining)](http://blog.csdn.net/dongzhiquan/article/details/6114040) 165 | - [HTTP协议及其POST与GET操作差异 & C#中如何使用POST、GET等](http://www.cnblogs.com/skynet/archive/2010/05/18/1738301.html) 166 | - [四种常见的 POST 提交数据方式](https://www.cnblogs.com/softidea/p/5745369.html) 167 | - [会话跟踪](http://blog.163.com/chfyljt@126/blog/static/11758032520127302714624/) 168 | - [总结 XSS 与 CSRF 两种跨站攻击](https://blog.tonyseek.com/post/introduce-to-xss-and-csrf/) 169 | - [CSRF简单介绍与利用方法](http://drops.wooyun.org/papers/155) 170 | - [XSS攻击及防御](http://blog.csdn.net/ghsau/article/details/17027893) 171 | - [百度百科:HTTP](http://baike.baidu.com/view/9472.htm) 172 | - [ HTTP的特性](https://hit-alibaba.github.io/interview/basic/network/HTTP.html) 173 | 174 | --- 175 | 176 | 如果有任何疑问,请[在此交流](https://github.com/zhangslob/docs/issues/2)。 -------------------------------------------------------------------------------- /docs/network/TCP.md: -------------------------------------------------------------------------------- 1 | # TCP 协议 2 | 3 | ## TCP 的特性 4 | 5 | - TCP 提供一种**面向连接的、可靠的**字节流服务 6 | - 在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP 7 | - TCP 使用校验和,确认和重传机制来保证可靠传输 8 | - TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复 9 | - TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制 10 | 11 | **注意**:TCP 并不能保证数据一定会被对方接收到,因为这是不可能的。TCP 能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此准确说 TCP 也不是 100% 可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。 12 | 13 | ## 三次握手与四次挥手 14 | 15 | 所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。 16 | 17 | 三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 `connect()` 时。将触发三次握手。 18 | 19 | - 第一次握手(SYN=1, seq=x):客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。发送完毕后,客户端进入 `SYN_SEND` 状态。 20 | - 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 `SYN_RCVD` 状态。 21 | - 第三次握手(ACK=1,ACKnum=y+1):客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1。发送完毕后,客户端进入 `ESTABLISHED` 状态,当服务器端接收到这个包时,也进入 `ESTABLISHED`状态,TCP 握手结束。 22 | 23 | 三次握手的过程的示意图如下: 24 | 25 | ![three-way-handshake](https://raw.githubusercontent.com/HIT-Alibaba/interview/master/img/tcp-connection-made-three-way-handshake.png) 26 | 27 | TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 `close()` 操作即可产生挥手操作。 28 | 29 | - 第一次挥手(FIN=1,seq=x):假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 `FIN_WAIT_1` 状态。 30 | - 第二次挥手(ACK=1,ACKnum=x+1):服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 `CLOSE_WAIT` 状态,客户端接收到这个确认包之后,进入 `FIN_WAIT_2` 状态,等待服务器端关闭连接。 31 | - 第三次挥手(FIN=1,seq=y):服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。发送完毕后,服务器端进入 `LAST_ACK` 状态,等待来自客户端的最后一个ACK。 32 | - 第四次挥手(ACK=1,ACKnum=y+1):客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 `TIME_WAIT`状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入 `CLOSED` 状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 `CLOSED` 状态。 33 | 34 | 四次挥手的示意图如下: 35 | 36 | ![four-way-handshake](https://raw.githubusercontent.com/HIT-Alibaba/interview/master/img/tcp-connection-closed-four-way-handshake.png) 37 | 38 | ## SYN攻击 39 | 40 | ### 什么是 SYN 攻击(SYN Flood)? 41 | 42 | 在三次握手过程中,服务器发送 SYN-ACK 之后,收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态. 43 | 44 | SYN 攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。 45 | 46 | SYN 攻击是一种典型的 DoS/DDoS 攻击。 47 | 48 | ### 如何检测 SYN 攻击? 49 | 50 | 检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。 51 | 52 | ### 如何防御 SYN 攻击? 53 | 54 | SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害,常见的防御 SYN 攻击的方法有如下几种: 55 | 56 | - 缩短超时(SYN Timeout)时间 57 | - 增加最大半连接数 58 | - 过滤网关防护 59 | - SYN cookies技术 60 | 61 | ## TCP KeepAlive 62 | 63 | TCP 的连接,实际上是一种纯软件层面的概念,在物理层面并没有“连接”这种概念。TCP 通信双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会。在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启等各种意外,当这些意外发生之后,这些 TCP 连接并未来得及正常释放,在软件层面上,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的 KeepAlive 机制实现来实现。主流的操作系统基本都在内核里支持了这个特性。 64 | 65 | TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。 66 | 67 | [TCP-Keepalive-HOWTO](http://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/) 有对 TCP KeepAlive 特性的详细介绍,有兴趣的同学可以参考。这里主要说一下,TCP KeepAlive 的局限。首先 TCP KeepAlive 监测的方式是发送一个 probe 包,会给网络带来额外的流量,另外 TCP KeepAlive 只能在内核层级监测连接的存活与否,而连接的存活不一定代表服务的可用。例如当一个服务器 CPU 进程服务器占用达到 100%,已经卡死不能响应请求了,此时 TCP KeepAlive 依然会认为连接是存活的。因此 TCP KeepAlive 对于应用层程序的价值是相对较小的。需要做连接保活的应用层程序,例如 QQ,往往会在应用层实现自己的心跳功能。 68 | 69 | ### 参考资料 70 | 71 | - 计算机网络:自顶向下方法 72 | - [TCP三次握手及四次挥手详细图解](http://www.cnblogs.com/hnrainll/archive/2011/10/14/2212415.html) 73 | - [TCP协议三次握手过程分析](http://www.cnblogs.com/rootq/articles/1377355.html) 74 | - [TCP协议中的三次握手和四次挥手(图解)](http://blog.csdn.net/whuslei/article/details/6667471) 75 | - [百度百科:SYN攻击](http://baike.baidu.com/subview/32754/8048820.htm) 76 | - [TCP-Keepalive-HOWTO](http://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/) 77 | - [ TCP 的特性](https://hit-alibaba.github.io/interview/basic/network/TCP.html) 78 | 79 | --- 80 | 81 | 如果有任何疑问,请[在此交流](https://github.com/zhangslob/docs/issues/2)。 -------------------------------------------------------------------------------- /docs/network/session.md: -------------------------------------------------------------------------------- 1 | 这一节会讲讲如何模拟登陆。 2 | 3 | # 场景 4 | 5 | 模拟登陆是爬虫中非常常见的场景,网络上的数据有些是打开就看得到的,比如腾讯新闻、拉勾网等,有些是需要登录才可见的,比如上一节说的知乎首页,点评评论翻页等等。所以模拟登录时非常常见的一种数据获取手段。 6 | 7 | 模拟登陆一般有两种方法: 8 | 9 | 1. 手动 10 | 2. 自动 11 | 12 | # 手动 13 | 14 | 比如模拟登陆知乎,我们可以先打开知乎首页,然后输入我们自己的账号密码,打开控制台,将其中的cookies复制下来,完成业务操作。 15 | 16 | ![](https://ws3.sinaimg.cn/large/006tKfTcly1g0l98w4dh9j31gl0u0qbq.jpg) 17 | 18 | 我们把cookies运用在爬虫中就可以抓取知乎网站对“我”这个用户所推送的个性信息,在requests中我们可以这样应用。 19 | 20 | ```python 21 | >>> url = 'http://httpbin.org/cookies' 22 | >>> cookies = dict(cookies_are='working') 23 | 24 | >>> r = requests.get(url, cookies=cookies) 25 | >>> r.text 26 | '{"cookies": {"cookies_are": "working"}}' 27 | ``` 28 | 29 | 例子: 30 | 31 | ```python 32 | import requests 33 | 34 | cookies = { 35 | 'BAIDUID': '7B7D88053B10EB435DB1E212DBF145BF:FG=1', 36 | 'BIDUPSID': '7B7D88053B10EB435DB1E212DBF145BF', 37 | 'PSTM': '1551098874', 38 | 'BDRCVFR[pNjdDcNFITf]': 'mk3SLVN4HKm', 39 | 'delPer': '0', 40 | 'BD_CK_SAM': '1', 41 | 'BD_UPN': '123253', 42 | 'BD_HOME': '1', 43 | 'locale': 'zh', 44 | 'H_PS_PSSID': '1444_21092_18559_26350_28415', 45 | 'BDRCVFR[feWj1Vr5u3D]': 'I67x6TjHwwYf0', 46 | 'PSINO': '1', 47 | 'H_PS_645EC': 'b710sJmEk7esA9h2dxosAL9oFPrYrIk%2FdoimlGSz7DCKfuSVp27CVuygqGuvEqwhznOf', 48 | } 49 | 50 | headers = { 51 | 'Connection': 'keep-alive', 52 | 'Upgrade-Insecure-Requests': '1', 53 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36', 54 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 55 | 'Accept-Encoding': 'gzip, deflate, br', 56 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7', 57 | } 58 | 59 | params = ( 60 | ('wd', 'python'), 61 | ) 62 | 63 | response = requests.get('https://www.baidu.com/s', headers=headers, params=params, cookies=cookies) 64 | ``` 65 | 66 | cookies可以单独拆出来当做一个字典,也可以存放在headers中,如下: 67 | 68 | ```python 69 | import requests 70 | 71 | url = "https://www.baidu.com/s" 72 | 73 | querystring = {"wd":"python"} 74 | 75 | headers = { 76 | 'Connection': "keep-alive", 77 | 'Upgrade-Insecure-Requests': "1", 78 | 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36", 79 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 80 | 'Accept-Encoding': "gzip, deflate, br", 81 | 'Accept-Language': "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7", 82 | 'Cookie': "BAIDUID=7B7D88053B10EB435DB1E212DBF145BF:FG=1; BIDUPSID=7B7D88053B10EB435DB1E212DBF145BF; PSTM=1551098874; BDRCVFR[pNjdDcNFITf]=mk3SLVN4HKm; delPer=0; BD_CK_SAM=1; BD_UPN=123253; BD_HOME=1; locale=zh; H_PS_PSSID=1444_21092_18559_26350_28415; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; PSINO=1; H_PS_645EC=b710sJmEk7esA9h2dxosAL9oFPrYrIk%2FdoimlGSz7DCKfuSVp27CVuygqGuvEqwhznOf", 83 | 'cache-control': "no-cache", 84 | } 85 | 86 | response = requests.request("GET", url, headers=headers, params=querystring) 87 | 88 | print(response.text) 89 | ``` 90 | 91 | > 小提示,使用curl复制非常方便,curl内容后面会讲到。 92 | 93 | 94 | 95 | 可以看到手动复制的方法很简单,但是效率低,而且不能自动登录,即cookies失效,还需要人手动再去浏览器中复制,不适合爬虫场景。 96 | 97 | 98 | 99 | # 自动 100 | 101 | 这次我们说一个稍微简单点的网站:Github的模拟登陆。文章我只爱去哪写过,完成内容可以到这里来看:[使用Selenium与Requests模拟登陆](https://zhangslob.github.io/2018/07/17/%E4%BD%BF%E7%94%A8Selenium%E4%B8%8ERequests%E6%A8%A1%E6%8B%9F%E7%99%BB%E9%99%86/) 102 | 103 | 核心代码: 104 | 105 | ```python 106 | #!/usr/bin/env python 107 | # -*- coding: utf-8 -*- 108 | import re 109 | import requests 110 | 111 | headers = { 112 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 113 | 'Accept-Encoding': 'gzip, deflate, br', 114 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 115 | 'Connection': 'keep-alive', 116 | 'Host': 'github.com', 117 | 'Upgrade-Insecure-Requests': '1', 118 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' 119 | } 120 | s = requests.session() 121 | s.headers.update(headers) 122 | 123 | 124 | def get_token(): 125 | url = 'https://github.com/login' 126 | response = s.get(url, timeout=10) 127 | pat = 'name=\"authenticity_token\" value=\"(.*?)\"' 128 | return re.findall(pat, response.text)[0] 129 | 130 | 131 | def login(authenticity_token, account, password): 132 | payload = { 133 | 'commit': 'Sign in', 134 | 'utf8': '\u2713', 135 | 'authenticity_token': authenticity_token, 136 | 'login': account, 137 | 'password': password, 138 | } 139 | url = 'https://github.com/session' 140 | response = s.post(url, data=payload) 141 | print(response.text) 142 | # do whatever you want 143 | 144 | 145 | if __name__ == '__main__': 146 | account, password = '', '' 147 | authenticity_token = get_token() 148 | login(authenticity_token, account, password) 149 | ``` 150 | 151 | 有几点说下: 152 | 153 | 1. github首页有一个authenticity_token,需要这个token维持登录状态。 154 | 2. github的密码是明文,这是非常少见的,大多数网站都是有加密的。一般可以通过开发者工具打断点来解决。 155 | 3. github的登录是不需要输入验证码的,实在是太少见了。国内的基本上都需要验证码的,验证码也是爬虫中很令人头疼的一块,大体上分为两种方法:机器学习、神经网络训练模型;接第三方打码平台。这个我们以后再讲。 156 | 157 | 158 | 159 | # 实践 160 | 161 | 尝试完成美团登录。 162 | 163 | 网址:[美团网](https://passport.meituan.com/account/unitivelogin?service=www&continue=http%3A%2F%2Fwww.meituan.com%2Faccount%2Fsettoken%3Fcontinue%3Dhttps%253A%252F%252Fwww.meituan.com%252Fmeishi%252F526725%252F) 164 | 165 | ![](https://ws1.sinaimg.cn/large/006tKfTcly1g0m4xnm044j30qy0inq7n.jpg) 166 | 167 | 有几个重点参数需要解决: 168 | 169 | 1. **password** 170 | 2. **fingerprint** 171 | 3. **csrf** 172 | 4. **_token** 173 | 174 | --- 175 | 176 | 如果有任何疑问,请[在此交流](https://github.com/zhangslob/docs/issues/2)。 -------------------------------------------------------------------------------- /docs/requests/basic_useage.md: -------------------------------------------------------------------------------- 1 | # Requests基础使用 2 | 3 | 在google中搜索requests的结果: 4 | 5 | ![](https://ws3.sinaimg.cn/large/006tKfTcly1g1igx3ei57j313a0pfn38.jpg) 6 | 7 | 有中文也有英文文档,如果英文还OK就可以去撸英文文档。中文文档地址:[Requests: 让 HTTP 服务人类]() 8 | 9 | requests作为Python最出名的第三方库,各方面的教程已经非常完善,尤其是基础用法。 10 | 11 | 12 | 13 | # 安装 14 | 15 | 最简单的当然是使用pip安装: 16 | 17 | ```bash 18 | $ pip install requests 19 | ``` 20 | 21 | 如果你的Python3版本对应的pip软链是pip3,记得更换为pip3。 22 | 23 | 如果安装有问题,参考:[安装 Requests]() 24 | 25 | 26 | 27 | # 发送请求 28 | 29 | ```python 30 | >>> import requests 31 | >>> r = requests.get('https://api.github.com/events') 32 | >>> r = requests.post('http://httpbin.org/post', data = {'key':'value'}) 33 | >>> r = requests.put('http://httpbin.org/put', data = {'key':'value'}) 34 | >>> r = requests.delete('http://httpbin.org/delete') 35 | >>> r = requests.head('http://httpbin.org/get') 36 | >>> r = requests.options('http://httpbin.org/get') 37 | ``` 38 | 39 | 参见:[快速上手]() 40 | 41 | 可以看到使用requests发送请求非常简单,我们来看看使用urllib的代码 42 | 43 | ```python 44 | import urllib.request 45 | 46 | req = urllib.request.Request('http://www.example.com/') 47 | req.add_header('Referer', 'http://www.python.org/') 48 | r = urllib.request.urlopen(req) 49 | 50 | result = f.read().decode('utf-8') 51 | ``` 52 | 53 | 关于requests与urllib,强烈建议是选择requests,这里有一个非常经典的回答 [What are the differences between the urllib, urllib2, and requests module?](https://stackoverflow.com/questions/2018026/what-are-the-differences-between-the-urllib-urllib2-and-requests-module) 54 | 55 | 56 | 57 | 至于其他的参见的添加headers,这里就不说了,看文档即可。 58 | 59 | 这一节任务就是把[快速上手]()的内容看完,并完成下面的作业。 60 | 61 | # 作业 62 | 63 | 给你三个网址,试试使用requests抓取html 64 | 65 | 1. `https://www.baidu.com` 66 | 2. `https://www.toutiao.com` 67 | 3. `https://www.zhihu.com` 68 | 4. `https://www.google.com` 69 | 70 | 你可能会发现一些奇怪的现象,别着急,下一节会继续讲。 -------------------------------------------------------------------------------- /docs/requests/request_attention.md: -------------------------------------------------------------------------------- 1 | # 注意事项 2 | 3 | 有时间一定要熟悉Requests的文档:[Requests: 让 HTTP 服务人类¶](https://2.python-requests.org//zh_CN/latest/#requests-http) 4 | 5 | Requests 完全满足今日 web 的需求。 6 | 7 | - Keep-Alive & 连接池 8 | - 国际化域名和 URL 9 | - 带持久 Cookie 的会话 10 | - 浏览器式的 SSL 认证 11 | - 自动内容解码 12 | - 基本/摘要式的身份认证 13 | - 优雅的 key/value Cookie 14 | - 自动解压 15 | - Unicode 响应体 16 | - HTTP(S) 代理支持 17 | - 文件分块上传 18 | - 流下载 19 | - 连接超时 20 | - 分块请求 21 | - 支持 `.netrc` 22 | 23 | 24 | 25 | 我会列举一些使用Requests时需要注意的点。 26 | 27 | 28 | 29 | ## SSL证书认证 30 | 31 | Requests 可以为 HTTPS 请求验证 SSL 证书,就像 web 浏览器一样。SSL 验证默认是开启的,如果证书验证失败,Requests 会抛出 SSLError: 32 | 33 | ```python 34 | >>> requests.get('https://requestb.in') 35 | requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com' 36 | ``` 37 | 38 | 如果你将 `verify` 设置为 False,Requests 也能忽略对 SSL 证书的验证。 39 | 40 | ```python 41 | >>> requests.get('https://kennethreitz.org', verify=False) 42 | 43 | ``` 44 | 45 | 默认情况下, `verify` 是设置为 True 的。选项 `verify` 仅应用于主机证书。 46 | 47 | > 对于私有证书,你也可以传递一个 CA_BUNDLE 文件的路径给 `verify`。你也可以设置 # `REQUEST_CA_BUNDLE` 环境变量。 48 | 49 | 50 | 51 | ## 超时设置 52 | 53 | 为防止服务器不能及时响应,大部分发至外部服务器的请求都应该带着 timeout 参数。在默认情况下,除非显式指定了 timeout 值,requests 是不会自动进行超时处理的。如果没有 timeout,你的代码可能会挂起若干分钟甚至更长时间。 54 | 55 | ```python 56 | r = requests.get('https://github.com', timeout=10) 57 | ``` 58 | 59 | 60 | 61 | 我一般会把超时时间设为10秒。 62 | 63 | 64 | 65 | ## 封装 66 | 67 | 如果你需要频繁使用Requests发送请求是,最好是将其封装成一个通用方法。 68 | 69 | 例如这里是对post方法的简单封装,当然需要根据自己需求来做。 70 | 71 | ```python 72 | import requests 73 | import json as json_ 74 | 75 | 76 | class Session(object): 77 | 78 | def __init__(self): 79 | self.session = requests.session() 80 | 81 | def post(self, url, data=None, json=None, **kwargs): 82 | retry_times = 0 83 | while True: 84 | try: 85 | res = self.session.post(url, data=data, json=json, **kwargs) 86 | except (requests.exceptions.ConnectionError, requests.ReadTimeout): 87 | pass 88 | else: 89 | try: 90 | res.json() 91 | except json_.JSONDecodeError: 92 | retry_times += 1 93 | if retry_times > 20: 94 | return None 95 | else: 96 | continue 97 | 98 | return res 99 | ``` 100 | 101 | 102 | 103 | 还可以看看这里,借鉴别人的思想:[Python’s encapsulation of requests](https://developpaper.com/pythons-encapsulation-of-requests/) 104 | 105 | ```python 106 | # -*- coding:utf-8 -*- 107 | 108 | import requests 109 | from concurrent.futures import ThreadPoolExecutor 110 | Read from Tools.Config import Config configuration file 111 | From Tools. Log import Log # log management 112 | From Tools. tools import decoLOG log decoration 113 | 114 | ''' 115 | Functions: Requests class 116 | Usage method: 117 | Author: Guo Kechang 118 | Time of completion: 20180224 119 | Update: 120 | Update time: 121 | ''' 122 | class Requests(object): 123 | def __init__(self): 124 | self.session = requests.session() 125 | self.header = {} 126 | # By default, URLs come from configuration files to facilitate switching between different test environments, and can also be set dynamically. 127 | self.URL = Config().getURL() 128 | # Default 60s, can be set dynamically 129 | self.timeout = 60 130 | # When HTTP connection is abnormal, the number of reconnections is 3 by default. It can be set dynamically. 131 | self.iRetryNum = 3 132 | 133 | self.errorMsg = "" 134 | # Content = {Use Case Number: Response Data} 135 | self.responses = {} 136 | # Content = {Use Case Number: Exception Information} 137 | self.resErr={} 138 | 139 | 140 | # Retention of original post usage 141 | # bodyData: request's data 142 | @decoLOG 143 | def post(self, bodyData): 144 | response = None 145 | self.errorMsg = "" 146 | 147 | try: 148 | response = self.session.post(self.URL, data=bodyData.encode('utf-8'), headers=self.header, timeout=self.timeout) 149 | response.raise_for_status() 150 | except Exception as e: 151 | self.errorMsg = str(e) 152 | Log (). logger. error ("HTTP request exception, exception information:% s"% self. errorMsg) 153 | return response 154 | 155 | 156 | # Complex request concurrent processing, in the form of thread pool, use case > thread pool capacity: thread pool capacity is concurrent, otherwise, use case number is concurrent 157 | # dicDatas: {Use Case Number: Use Case Data} 158 | @decoLOG 159 | def req_all(self, dicDatas, iThreadNum=5): 160 | 161 | if len(dict(dicDatas)) < 1: 162 | Log (). logger. error ("No test object, please confirm and try again..." "" 163 | return self.responses.clear() 164 | 165 | # Request use case set conversion (use case number, use case data) 166 | seed = [i for i in dicDatas.items()] 167 | self.responses.clear() 168 | 169 | # Thread pool concurrent execution, iThreadNum concurrent number 170 | with ThreadPoolExecutor(iThreadNum) as executor: 171 | executor.map(self.req_single,seed) 172 | 173 | # Returns response information for all requests ({use case number: response data}), HTTP connection exception: corresponding to None 174 | return self.responses 175 | 176 | # For single use case submission, HTTP connection failures can be reconnected, and the maximum number of reconnections can be dynamically set 177 | def req_single(self, listData, reqType="post", iLoop=1): 178 | response = None 179 | # If the maximum number of reconnections is reached, the submission ends after the connection 180 | if iLoop == self.iRetryNum: 181 | if reqType == "post": 182 | try: 183 | response = requests.post(self.URL, data=listData[1].encode('utf-8'), headers=self.header, 184 | timeout=self.timeout) 185 | response.raise_for_status() 186 | except Exception as e: 187 | # Exception information is saved only when the maximum number of connections is reached, but the maximum number of connections is not reached. Exception information is empty. 188 | self.resErr[listData[0]] = str(e) 189 | Log (). logger. error ("HTTP request exception, exception information:% s [% d]% (str (e), iLoop)) 190 | 191 | self.responses[listData[0]] = response 192 | else: 193 | # for future: other request method expand 194 | pass 195 | # The maximum number of connections is not reached, and if an exception occurs, a reconnection attempt is made 196 | else: 197 | if reqType == "post": 198 | try: 199 | response = requests.post(self.URL, data=listData[1].encode('utf-8'), headers=self.header, 200 | timeout=self.timeout) 201 | response.raise_for_status() 202 | except Exception as e: 203 | Log (). logger. error ("HTTP request exception, exception information:% s [% d]% (str (e), iLoop)) 204 | # Increased number of reconnections 205 | iLoop += 1 206 | # Reconnect 207 | self.req_single(listData, reqType, iLoop) 208 | # Current connection termination 209 | return None 210 | self.responses[listData[0]] = response 211 | else: 212 | # for future: other request method expand 213 | pass 214 | 215 | # Set up SoapAction to complete the header setting of web service interface quickly 216 | def setSoapAction(self, soapAction): 217 | self.header["SOAPAction"] = soapAction 218 | self.header["Content-Type"] = "text/xml;charset=UTF-8" 219 | self.header["Connection"] = "Keep-Alive" 220 | self.header["User-Agent"] = "InterfaceAutoTest-run" 221 | ``` 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /docs/requests/request_source_code.md: -------------------------------------------------------------------------------- 1 | # Requests源码分析 2 | 3 | 这一节我们来看看requests是如何发送一个request的,这一节内容可能比较多,有很多底层代码,我自己也看的头疼,建议阅读前先喝瓶酸奶以保持轻松的心情。如果你准备好了,请往下看。 4 | 5 | 我们在Pycharm中按住win点击get,会来到get方法的源码: 6 | 7 | ```python 8 | def get(url, params=None, **kwargs): 9 | r"""Sends a GET request. 10 | 11 | :param url: URL for the new :class:`Request` object. 12 | :param params: (optional) Dictionary, list of tuples or bytes to send 13 | in the body of the :class:`Request`. 14 | :param \*\*kwargs: Optional arguments that ``request`` takes. 15 | :return: :class:`Response ` object 16 | :rtype: requests.Response 17 | """ 18 | 19 | kwargs.setdefault('allow_redirects', True) 20 | return request('get', url, params=params, **kwargs) 21 | ``` 22 | 23 | 发现调用了request方法,继续往下看 24 | 25 | ```python 26 | def request(method, url, **kwargs): 27 | """Constructs and sends a :class:`Request `. 28 | 29 | :param method: method for the new :class:`Request` object. 30 | :param url: URL for the new :class:`Request` object. 31 | :param params: (optional) Dictionary, list of tuples or bytes to send 32 | in the body of the :class:`Request`. 33 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like 34 | object to send in the body of the :class:`Request`. 35 | :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. 36 | :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. 37 | :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. 38 | :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload. 39 | ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` 40 | or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string 41 | defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers 42 | to add for the file. 43 | :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. 44 | :param timeout: (optional) How many seconds to wait for the server to send data 45 | before giving up, as a float, or a :ref:`(connect timeout, read 46 | timeout) ` tuple. 47 | :type timeout: float or tuple 48 | :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``. 49 | :type allow_redirects: bool 50 | :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. 51 | :param verify: (optional) Either a boolean, in which case it controls whether we verify 52 | the server's TLS certificate, or a string, in which case it must be a path 53 | to a CA bundle to use. Defaults to ``True``. 54 | :param stream: (optional) if ``False``, the response content will be immediately downloaded. 55 | :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. 56 | :return: :class:`Response ` object 57 | :rtype: requests.Response 58 | 59 | Usage:: 60 | 61 | >>> import requests 62 | >>> req = requests.request('GET', 'https://httpbin.org/get') 63 | 64 | """ 65 | 66 | # By using the 'with' statement we are sure the session is closed, thus we 67 | # avoid leaving sockets open which can trigger a ResourceWarning in some 68 | # cases, and look like a memory leak in others. 69 | with sessions.Session() as session: 70 | return session.request(method=method, url=url, **kwargs) 71 | ``` 72 | 73 | 这里就对所有requests可以使用的参数做了一一解释,在公众号文章有翻译:[大佬的话(一)]()。看到,这里是启用了一个session(在[HTTP协议]()中讲的会话),继续往下看: 74 | 75 | ```python 76 | def request(self, method, url, 77 | params=None, data=None, headers=None, cookies=None, files=None, 78 | auth=None, timeout=None, allow_redirects=True, proxies=None, 79 | hooks=None, stream=None, verify=None, cert=None, json=None): 80 | """Constructs a :class:`Request `, prepares it and sends it. 81 | Returns :class:`Response ` object. 82 | 83 | :param method: method for the new :class:`Request` object. 84 | :param url: URL for the new :class:`Request` object. 85 | :param params: (optional) Dictionary or bytes to be sent in the query 86 | string for the :class:`Request`. 87 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like 88 | object to send in the body of the :class:`Request`. 89 | :param json: (optional) json to send in the body of the 90 | :class:`Request`. 91 | :param headers: (optional) Dictionary of HTTP Headers to send with the 92 | :class:`Request`. 93 | :param cookies: (optional) Dict or CookieJar object to send with the 94 | :class:`Request`. 95 | :param files: (optional) Dictionary of ``'filename': file-like-objects`` 96 | for multipart encoding upload. 97 | :param auth: (optional) Auth tuple or callable to enable 98 | Basic/Digest/Custom HTTP Auth. 99 | :param timeout: (optional) How long to wait for the server to send 100 | data before giving up, as a float, or a :ref:`(connect timeout, 101 | read timeout) ` tuple. 102 | :type timeout: float or tuple 103 | :param allow_redirects: (optional) Set to True by default. 104 | :type allow_redirects: bool 105 | :param proxies: (optional) Dictionary mapping protocol or protocol and 106 | hostname to the URL of the proxy. 107 | :param stream: (optional) whether to immediately download the response 108 | content. Defaults to ``False``. 109 | :param verify: (optional) Either a boolean, in which case it controls whether we verify 110 | the server's TLS certificate, or a string, in which case it must be a path 111 | to a CA bundle to use. Defaults to ``True``. 112 | :param cert: (optional) if String, path to ssl client cert file (.pem). 113 | If Tuple, ('cert', 'key') pair. 114 | :rtype: requests.Response 115 | """ 116 | # Create the Request. 117 | req = Request( 118 | method=method.upper(), 119 | url=url, 120 | headers=headers, 121 | files=files, 122 | data=data or {}, 123 | json=json, 124 | params=params or {}, 125 | auth=auth, 126 | cookies=cookies, 127 | hooks=hooks, 128 | ) 129 | prep = self.prepare_request(req) 130 | 131 | proxies = proxies or {} 132 | 133 | settings = self.merge_environment_settings( 134 | prep.url, proxies, stream, verify, cert 135 | ) 136 | 137 | # Send the request. 138 | send_kwargs = { 139 | 'timeout': timeout, 140 | 'allow_redirects': allow_redirects, 141 | } 142 | send_kwargs.update(settings) 143 | resp = self.send(prep, **send_kwargs) 144 | 145 | return resp 146 | ``` 147 | 148 | 把所有的参数加起来,创建了一个Request,发送这个Request,调用了`self.send`方法 149 | 150 | ```python 151 | def send(self, request, **kwargs): 152 | """Send a given PreparedRequest. 153 | 154 | :rtype: requests.Response 155 | """ 156 | # Set defaults that the hooks can utilize to ensure they always have 157 | # the correct parameters to reproduce the previous request. 158 | kwargs.setdefault('stream', self.stream) 159 | kwargs.setdefault('verify', self.verify) 160 | kwargs.setdefault('cert', self.cert) 161 | kwargs.setdefault('proxies', self.proxies) 162 | 163 | # It's possible that users might accidentally send a Request object. 164 | # Guard against that specific failure case. 165 | if isinstance(request, Request): 166 | raise ValueError('You can only send PreparedRequests.') 167 | 168 | # Set up variables needed for resolve_redirects and dispatching of hooks 169 | allow_redirects = kwargs.pop('allow_redirects', True) 170 | stream = kwargs.get('stream') 171 | hooks = request.hooks 172 | 173 | # Get the appropriate adapter to use 174 | adapter = self.get_adapter(url=request.url) 175 | 176 | # Start time (approximately) of the request 177 | start = preferred_clock() 178 | 179 | # Send the request 180 | r = adapter.send(request, **kwargs) 181 | 182 | # Total elapsed time of the request (approximately) 183 | elapsed = preferred_clock() - start 184 | r.elapsed = timedelta(seconds=elapsed) 185 | 186 | # Response manipulation hooks 187 | r = dispatch_hook('response', hooks, r, **kwargs) 188 | 189 | # Persist cookies 190 | if r.history: 191 | 192 | # If the hooks create history then we want those cookies too 193 | for resp in r.history: 194 | extract_cookies_to_jar(self.cookies, resp.request, resp.raw) 195 | 196 | extract_cookies_to_jar(self.cookies, request, r.raw) 197 | 198 | # Redirect resolving generator. 199 | gen = self.resolve_redirects(r, request, **kwargs) 200 | 201 | # Resolve redirects if allowed. 202 | history = [resp for resp in gen] if allow_redirects else [] 203 | 204 | # Shuffle things around if there's history. 205 | if history: 206 | # Insert the first (original) request at the start 207 | history.insert(0, r) 208 | # Get the last request made 209 | r = history.pop() 210 | r.history = history 211 | 212 | # If redirects aren't being followed, store the response on the Request for Response.next(). 213 | if not allow_redirects: 214 | try: 215 | r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs)) 216 | except StopIteration: 217 | pass 218 | 219 | if not stream: 220 | r.content 221 | 222 | return r 223 | ``` 224 | 225 | 发送request是调用`r = adapter.send(request, **kwargs)`这一行,继续往下看, 226 | 227 | ```python 228 | def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): 229 | """Sends PreparedRequest object. Returns Response object. 230 | 231 | :param request: The :class:`PreparedRequest ` being sent. 232 | :param stream: (optional) Whether to stream the request content. 233 | :param timeout: (optional) How long to wait for the server to send 234 | data before giving up, as a float, or a :ref:`(connect timeout, 235 | read timeout) ` tuple. 236 | :type timeout: float or tuple or urllib3 Timeout object 237 | :param verify: (optional) Either a boolean, in which case it controls whether 238 | we verify the server's TLS certificate, or a string, in which case it 239 | must be a path to a CA bundle to use 240 | :param cert: (optional) Any user-provided SSL certificate to be trusted. 241 | :param proxies: (optional) The proxies dictionary to apply to the request. 242 | :rtype: requests.Response 243 | """ 244 | 245 | try: 246 | conn = self.get_connection(request.url, proxies) 247 | except LocationValueError as e: 248 | raise InvalidURL(e, request=request) 249 | 250 | self.cert_verify(conn, request.url, verify, cert) 251 | url = self.request_url(request, proxies) 252 | self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies) 253 | 254 | chunked = not (request.body is None or 'Content-Length' in request.headers) 255 | 256 | if isinstance(timeout, tuple): 257 | try: 258 | connect, read = timeout 259 | timeout = TimeoutSauce(connect=connect, read=read) 260 | except ValueError as e: 261 | # this may raise a string formatting error. 262 | err = ("Invalid timeout {}. Pass a (connect, read) " 263 | "timeout tuple, or a single float to set " 264 | "both timeouts to the same value".format(timeout)) 265 | raise ValueError(err) 266 | elif isinstance(timeout, TimeoutSauce): 267 | pass 268 | else: 269 | timeout = TimeoutSauce(connect=timeout, read=timeout) 270 | 271 | try: 272 | if not chunked: 273 | resp = conn.urlopen( 274 | method=request.method, 275 | url=url, 276 | body=request.body, 277 | headers=request.headers, 278 | redirect=False, 279 | assert_same_host=False, 280 | preload_content=False, 281 | decode_content=False, 282 | retries=self.max_retries, 283 | timeout=timeout 284 | ) 285 | 286 | # Send the request. 287 | else: 288 | if hasattr(conn, 'proxy_pool'): 289 | conn = conn.proxy_pool 290 | 291 | low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT) 292 | 293 | try: 294 | low_conn.putrequest(request.method, 295 | url, 296 | skip_accept_encoding=True) 297 | 298 | for header, value in request.headers.items(): 299 | low_conn.putheader(header, value) 300 | 301 | low_conn.endheaders() 302 | 303 | for i in request.body: 304 | low_conn.send(hex(len(i))[2:].encode('utf-8')) 305 | low_conn.send(b'\r\n') 306 | low_conn.send(i) 307 | low_conn.send(b'\r\n') 308 | low_conn.send(b'0\r\n\r\n') 309 | 310 | # Receive the response from the server 311 | try: 312 | # For Python 2.7, use buffering of HTTP responses 313 | r = low_conn.getresponse(buffering=True) 314 | except TypeError: 315 | # For compatibility with Python 3.3+ 316 | r = low_conn.getresponse() 317 | 318 | resp = HTTPResponse.from_httplib( 319 | r, 320 | pool=conn, 321 | connection=low_conn, 322 | preload_content=False, 323 | decode_content=False 324 | ) 325 | except: 326 | # If we hit any problems here, clean up the connection. 327 | # Then, reraise so that we can handle the actual exception. 328 | low_conn.close() 329 | raise 330 | 331 | except (ProtocolError, socket.error) as err: 332 | raise ConnectionError(err, request=request) 333 | 334 | except MaxRetryError as e: 335 | if isinstance(e.reason, ConnectTimeoutError): 336 | # TODO: Remove this in 3.0.0: see #2811 337 | if not isinstance(e.reason, NewConnectionError): 338 | raise ConnectTimeout(e, request=request) 339 | 340 | if isinstance(e.reason, ResponseError): 341 | raise RetryError(e, request=request) 342 | 343 | if isinstance(e.reason, _ProxyError): 344 | raise ProxyError(e, request=request) 345 | 346 | if isinstance(e.reason, _SSLError): 347 | # This branch is for urllib3 v1.22 and later. 348 | raise SSLError(e, request=request) 349 | 350 | raise ConnectionError(e, request=request) 351 | 352 | except ClosedPoolError as e: 353 | raise ConnectionError(e, request=request) 354 | 355 | except _ProxyError as e: 356 | raise ProxyError(e) 357 | 358 | except (_SSLError, _HTTPError) as e: 359 | if isinstance(e, _SSLError): 360 | # This branch is for urllib3 versions earlier than v1.22 361 | raise SSLError(e, request=request) 362 | elif isinstance(e, ReadTimeoutError): 363 | raise ReadTimeout(e, request=request) 364 | else: 365 | raise 366 | 367 | return self.build_response(request, resp) 368 | ``` 369 | 370 | 继续看`conn = self.get_connection(request.url, proxies)`这一段,再一层层往下看,看到这里 371 | 372 | ```python 373 | pool_classes_by_scheme = { 374 | 'http': HTTPConnectionPool, 375 | 'https': HTTPSConnectionPool, 376 | } 377 | ``` 378 | 379 | 发现这里有两个连接池,一个是处理HTTP的、一个是处理HTTPS的,再点进去,发现HTTPSConnectionPool是继承HTTPConnectionPool的,所以我们只需要查看HTTPConnectionPool的就行了,最终的地址:[connectionpool.py]() 380 | 381 | 注意看这里:[urlopen]() 382 | 383 | ```python 384 | """ 385 | Get a connection from the pool and perform an HTTP request. This is the 386 | lowest level call for making a request, so you'll need to specify all 387 | the raw details. 388 | """ 389 | ``` 390 | 391 | >从池中获取连接并执行HTTP请求。这就是请求的最低级别调用,因此需要指定原始细节。 392 | 393 | 最终找到创建连接的地方:[connection.py]() 394 | 395 | ```python 396 | 397 | # This function is copied from socket.py in the Python 2.7 standard 398 | # library test suite. Added to its signature is only `socket_options`. 399 | # One additional modification is that we avoid binding to IPv6 servers 400 | # discovered in DNS if the system doesn't have IPv6 functionality. 401 | def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, 402 | source_address=None, socket_options=None): 403 | """Connect to *address* and return the socket object. 404 | 405 | Convenience function. Connect to *address* (a 2-tuple ``(host, 406 | port)``) and return the socket object. Passing the optional 407 | *timeout* parameter will set the timeout on the socket instance 408 | before attempting to connect. If no *timeout* is supplied, the 409 | global default timeout setting returned by :func:`getdefaulttimeout` 410 | is used. If *source_address* is set it must be a tuple of (host, port) 411 | for the socket to bind as a source address before making the connection. 412 | An host of '' or port 0 tells the OS to use the default. 413 | """ 414 | 415 | host, port = address 416 | if host.startswith('['): 417 | host = host.strip('[]') 418 | err = None 419 | 420 | # Using the value from allowed_gai_family() in the context of getaddrinfo lets 421 | # us select whether to work with IPv4 DNS records, IPv6 records, or both. 422 | # The original create_connection function always returns all records. 423 | family = allowed_gai_family() 424 | 425 | for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): 426 | af, socktype, proto, canonname, sa = res 427 | sock = None 428 | try: 429 | sock = socket.socket(af, socktype, proto) 430 | 431 | # If provided, set socket level options before connecting. 432 | _set_socket_options(sock, socket_options) 433 | 434 | if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: 435 | sock.settimeout(timeout) 436 | if source_address: 437 | sock.bind(source_address) 438 | sock.connect(sa) 439 | return sock 440 | 441 | except socket.error as e: 442 | err = e 443 | if sock is not None: 444 | sock.close() 445 | sock = None 446 | 447 | if err is not None: 448 | raise err 449 | 450 | raise socket.error("getaddrinfo returns an empty list") 451 | ``` 452 | 453 | 注释这样写: 454 | 455 | >此函数从Python 2.7标准库测试套件中的socket.py复制。 添加到它的签名只是`socket_options`。 456 | >另一个修改是,如果系统没有IPv6功能,我们将避免绑定到在DNS中发现的IPv6服务器。 457 | 458 | 其中关键代码是: 459 | 460 | 1. `sock = socket.socket(af, socktype, proto)` 461 | 2. `sock.bind(source_address)` 462 | 3. `sock.connect(sa)` 463 | 464 | 底层是Python的socket编程,Python 中,我们用 socket()函数来创建套接字,语法格式如下: 465 | 466 | ```python 467 | socket.socket([family[, type[, proto]]]) 468 | ``` 469 | 470 | | 函数 | 描述 | 471 | | --------------------- | --------------------------------- | 472 | | s.bind() | 以元组的形式绑定地址(host,port) | 473 | | s.settimeout(timeout) | 设置套接字操作的超时期 | 474 | | s.connect() | 主动初始化TCP服务器连接 | 475 | | s.recv() | 接收TCP数据 | 476 | | s.close() | 关闭套接字 | 477 | 478 | 更多细节参考:[Python3 网络编程]() 479 | 480 | 481 | 482 | 关于发送请求的细节: 483 | 484 | ```python 485 | 486 | def request_chunked(self, method, url, body=None, headers=None): 487 | """ 488 | Alternative to the common request method, which sends the 489 | body with chunked encoding and not as one block 490 | """ 491 | headers = HTTPHeaderDict(headers if headers is not None else {}) 492 | skip_accept_encoding = 'accept-encoding' in headers 493 | skip_host = 'host' in headers 494 | self.putrequest( 495 | method, 496 | url, 497 | skip_accept_encoding=skip_accept_encoding, 498 | skip_host=skip_host 499 | ) 500 | for header, value in headers.items(): 501 | self.putheader(header, value) 502 | if 'transfer-encoding' not in headers: 503 | self.putheader('Transfer-Encoding', 'chunked') 504 | self.endheaders() 505 | 506 | if body is not None: 507 | stringish_types = six.string_types + (six.binary_type,) 508 | if isinstance(body, stringish_types): 509 | body = (body,) 510 | for chunk in body: 511 | if not chunk: 512 | continue 513 | if not isinstance(chunk, six.binary_type): 514 | chunk = chunk.encode('utf8') 515 | len_str = hex(len(chunk))[2:] 516 | self.send(len_str.encode('utf-8')) 517 | self.send(b'\r\n') 518 | self.send(chunk) 519 | self.send(b'\r\n') 520 | 521 | # After the if clause, to always have a closed body 522 | self.send(b'0\r\n\r\n') 523 | ``` 524 | 525 | 获取response 526 | 527 | ```python 528 | 529 | def read(self, amt=None): 530 | if self.fp is None: 531 | return b"" 532 | 533 | if self._method == "HEAD": 534 | self._close_conn() 535 | return b"" 536 | 537 | if amt is not None: 538 | # Amount is given, implement using readinto 539 | b = bytearray(amt) 540 | n = self.readinto(b) 541 | return memoryview(b)[:n].tobytes() 542 | else: 543 | # Amount is not given (unbounded read) so we must check self.length 544 | # and self.chunked 545 | 546 | if self.chunked: 547 | return self._readall_chunked() 548 | 549 | if self.length is None: 550 | s = self.fp.read() 551 | else: 552 | try: 553 | s = self._safe_read(self.length) 554 | except IncompleteRead: 555 | self._close_conn() 556 | raise 557 | self.length = 0 558 | self._close_conn() # we read everything 559 | return s 560 | ``` 561 | 562 | 563 | 564 | 看到这里,越来越感觉到自己的无知,什么样的人才能写出这么牛逼的代码啊。给大佬跪了。。。 -------------------------------------------------------------------------------- /docs/thread/Thread_process_coroutine.md: -------------------------------------------------------------------------------- 1 | # 线程、进程、协程 2 | 3 | 需要先对 IO 的概念有一定的认识: IO在计算机中指Input/Output,也就是输入和输出。 4 | 5 | # 并发与并行 6 | 7 | 并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。 8 | 9 | 当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。 10 | 11 | 并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。 12 | 13 | 区别 14 | 15 | 1. 并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。 16 | 2. 在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。 17 | 18 | 知乎上高赞例子: 19 | 20 | - 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 21 | - 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 22 | - 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 23 | 24 | 并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是**『同时』**。 25 | 26 | # 进程 27 | 一个进程好比是一个程序,它是 资源分配的最小单位 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程?答案又是肯定的。单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程。举个简单的例子,就算是十年前的单核CPU的电脑,也可以聊QQ的同时看视频。 28 | 29 | 电脑中有许多进程需要处于「同时」开启的状态,而利用CPU在进程间的快速切换,可以实现「同时」运行多个程序。而进程切换则意味着需要保留进程切换前的状态,以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。 30 | 31 | # 线程 32 | 如果说进程和进程之间相当于程序与程序之间的关系,那么线程与线程之间就相当于程序内的任务和任务之间的关系。所以线程是依赖于进程的,也称为 「微进程」 。它是 程序执行过程中的最小单元 。 33 | 34 | 一个程序内包含了多种任务。打个比方,用播放器看视频的时候,视频输出的画面和声音可以认为是两种任务。当你拖动进度条的时候又触发了另外一种任务。拖动进度条会导致画面和声音都发生变化,如果进程里没有线程的话,那么可能发生的情况就是: 35 | 36 | 拖动进度条->画面更新->声音更新。你会明显感到画面和声音和进度条不同步。 37 | 38 | 但是加上了线程之后,线程能够共享进程的大部分资源,并参与CPU的调度。意味着它能够在进程间进行切换,实现「并发」,从而反馈到使用上就是拖动进度条的同时,画面和声音都同步了。所以我们经常能听到的一个词是「多线程」,就是把一个程序分成多个任务去跑,让任务更快处理。不过线程和线程之间由于某些资源是独占的,会导致锁的问题。例如Python的GIL多线程锁。 39 | 40 | # 进程与线程的区别 41 | 1. 进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。 42 | 2. 进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。 43 | 3. 线程的调度与切换比进程快很多。 44 | 45 | **CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程** 46 | 47 | # 阻塞与非阻塞 48 | 49 | 阻塞是指调用线程或者进程被操作系统挂起。 50 | 非阻塞是指调用线程或者进程不会被操作系统挂起。 51 | 52 | # 同步与异步 53 | 同步是阻塞模式,异步是非阻塞模式。 54 | 55 | - 同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,知道收到返回信息才继续执行下去; 56 | - 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回式系统会通知进程进行处理,这样可以提高执行的效率。 57 | 58 | 由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。看下图 59 | 60 | ![](https://i.loli.net/2019/06/17/5d06ff59c968b75352.png) 61 | 62 | # 协程 63 | 64 | 协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。 65 | 66 | 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此: 67 | 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。 68 | 69 | 协程的好处: 70 | 1. 无需线程上下文切换的开销 71 | 2. 无需原子操作锁定及同步的开销 72 | 3. 方便切换控制流,简化编程模型 73 | 74 | 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。 75 | 76 | 缺点: 77 | 1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。 78 | 2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序 79 | 80 | 81 | # 最佳实践 82 | 83 | 1. 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。 84 | 2. 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。 85 | 3. 所以大并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 86 | 87 | 顺便一提,非常流行的一个爬虫框架Scrapy就是用到异步框架Twisted来进行任务的调度,这也是Scrapy框架高性能的原因之一。 88 | 89 | 最后推荐阅读:[《深入理解Python异步编程 上》](http://python.jobbole.com/88291/) 90 | -------------------------------------------------------------------------------- /docs/xpath/attention.md: -------------------------------------------------------------------------------- 1 | # 注意事项 2 | 3 | 获取xpath位置有两种方法,一直是直接在浏览器中右键`Copy Xpath`,这种一般是绝对位置,另一种是自己根据element属性,编写相对位置。 4 | 5 | 如果该网站允许加载JavaScript或者浏览器有渲染,会导致直接在浏览器中复制的Xpath位置不正确,所以建议是点击右键——显示网页源代码,从这里查看网页结构,一般可以根据`class`的`name`来编写xpath。 6 | 7 | 8 | 9 | 10 | # 作业 11 | 12 | 自己尝试使用xpath、css两种选择器去解析 [腾讯新闻](https://news.qq.com/) 的标题与内容,看看自己使用哪种解析方法比较顺手。 13 | 14 | -------------------------------------------------------------------------------- /docs/xpath/lxml.md: -------------------------------------------------------------------------------- 1 | # 使用lxml解析网页 2 | 3 | 这里介绍下一篇非常不错的xpath教程,[爬虫入门到精通-网页的解析(xpath)](https://zhuanlan.zhihu.com/p/25572729) 4 | 在很久以前,那时我还不会xpath,用的是bs4,在我看到这篇文章之后就学会了xpath,觉得xpath实在太好用了,以后就再也没使用过bs4了。 5 | 6 | 我直接把他的文章copy过来了。 7 | 8 | --- 9 | 10 | # xpath的解释 11 | 12 | XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。 13 | XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力。起初XPath的提出的初衷是将其作为一个通用的、介于XPointer与XSL间的语法模型。但是XPath很快的被开发者采用来当作小型查询语言。 14 | 15 | # XPath的基本使用 16 | 17 | 要使用xpath我们需要下载lxml,在爬虫入门到精通-环境的搭建这一章也说明怎么装,如果还没有安装的话,那就去下载安装吧 18 | 19 | > 补充,Windows用户可以在这里下载 [Lxml, a binding for the libxml2 and libxslt libraries.](https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml) 20 | 21 | 直接看代码实战吧。 22 | ```python 23 | from lxml import etree 24 | 25 | # 定义一个函数,给他一个html,返回xml结构 26 | def getxpath(html): 27 | return etree.HTML(html) 28 | 29 | # 下面是我们实战的第一个html 30 | sample1 = """ 31 | 32 | My page 33 | 34 | 35 |

Welcome to my page

36 |

This is the first paragraph.

37 | 38 | 39 | 40 | """ 41 | # 获取xml结构 42 | s1 = getxpath(sample1) 43 | 44 | # 获取标题(两种方法都可以) 45 | #有同学在评论区指出我这边相对路径和绝对路径有问题,我搜索了下 46 | #发现定义如下图 47 | s1.xpath('//title/text()') 48 | s1.xpath('/html/head/title/text()') 49 | ``` 50 | 51 | 相对路径与绝对路径 52 | ![](https://i.loli.net/2019/06/13/5d01b690d544a38354.png) 53 | 54 | ![](https://i.loli.net/2019/06/13/5d01b6a09c98910882.png) 55 | 56 | # 总结及注意事项 57 | 58 | - 获取文本内容用 text() 59 | - 获取注释用 comment() 60 | - 获取其它任何属性用@xx,如 61 | - @href 62 | - @src 63 | - @value 64 | 65 | 66 | ```python 67 | sample2 = """ 68 | 69 | 70 |
    71 |
  • Quote 1
  • 72 |
  • Quote 2 with link
  • 73 |
  • Quote 3 with another link
  • 74 |
  • Quote 4 title

    ...
  • 75 |
76 | 77 | 78 | """ 79 | s2 = getxpath(sample2) 80 | 81 | ``` 82 | 83 | ![](https://i.loli.net/2019/06/13/5d01b6ffe1b8699399.png) 84 | 85 | # 总结及注意事项 86 | 87 | - 上面的li 可以更换为任何标签,如 p、div 88 | - 位置默认以1开始的 89 | - 最后一个用 li[last()] 不能用 li[-1] 90 | - 这个一般在抓取网页的下一页,最后一页会用到 91 | 92 | ```python 93 | sample3 = """ 94 | 95 | 102 | 103 | 104 | """ 105 | s3 = getxpath(sample3) 106 | ``` 107 | 108 | ![](https://i.loli.net/2019/06/13/5d01b7271a2e628195.png) 109 | 110 | 111 | # 总结及注意事项 112 | 113 | - 根据html的属性或者文本直接定位到当前标签 114 | - 文本是 text()='xxx' 115 | - 其它属性是@xx='xxx' 116 | - 这个是我们用到最多的,如抓取知乎的xsrf(见下图) 117 | - 我们只要用如下代码就可以了//input[@name="_xsrf"]/@value 118 | 119 | ![](https://i.loli.net/2019/06/13/5d01b751c31e179416.png) 120 | 121 | ```python 122 | sample4 = u""" 123 | 124 | 125 | My page 126 | 127 | 128 |

Welcome to my page

129 |

This is the first paragraph.

130 |

131 | 编程语言python 132 | testjavascript 133 | C#JAVA 134 |

135 |

a

136 |

b

137 |

c

138 |

d

139 |

e

140 |

f

141 | 142 | 143 | 144 | """ 145 | s4 = etree.HTML(sample4) 146 | ``` 147 | 148 | ![](https://i.loli.net/2019/06/13/5d01b76b175c344186.png) 149 | 150 | # 总结及注意事项 151 | 152 | - 想要获取某个标签下所有的文本(包括子标签下的文本),使用string 153 | - 如

123来获取我啊

,这边如果想要得到的文本为"123来获取我啊",则需要使用string 154 | - starts-with 匹配字符串前面相等 155 | - contains 匹配任何位置相等 156 | - 当然其中的(@class,"content")也可以根据需要改成(text(),"content")或者其它属性(@src,"content") 157 | 158 | # 最后再次总结一下 159 | 看完本篇文章后,你应该要 160 | 161 | - 能学会基本所有的xpath的使用 162 | - css和这个的原理一样,所以就不介绍了,可以参考 163 | - CSS 选择器参考手册 164 | - 所有代码在kimg1234/pachong 165 | 166 | --- 167 | 168 | 最后说一下,按照上面的代码一行行敲下来,应该马上就能理解xpath的用法。这两年来,爬虫解析我一直使用的是`xpath+re`,这两个工具能完成所有的解析工作。 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /docs/xpath/scrapy_selector.md: -------------------------------------------------------------------------------- 1 | # Scraoy Selectors 2 | 3 | 当然了,首先给大家推荐的还是官方文档:[Selectors](https://docs.scrapy.org/en/latest/topics/selectors.html) 4 | 5 | Scrapy选择器构建于 lxml 库之上,这意味着它们在速度和解析准确性上非常相似。 6 | 7 | 本页面解释了选择器如何工作,并描述了相应的API。不同于 lxml API的臃肿,该API短小而简洁。这是因为 lxml 库除了用来选择标记化文档外,还可以用到许多任务上。 8 | 9 | # 使用选择器(selectors) 10 | 11 | 一般我会使用xpath来解析 12 | 13 | ```python 14 | >>> from scrapy.selector import Selector 15 | 16 | >>> body = 'good' 17 | >>> s = Selector(text=body) 18 | >>> s.xpath('//span/text()').extract() 19 | ['good'] 20 | ``` 21 | 22 | 将html导入`Selector`就可以得到一个选择器对象,直接使用xpath方法,接上xpath语法,就可以得到我们想要的。 23 | 24 | # 在scrapy中使用Selector 25 | 26 | 举个例子,直接在scrapy中使用xpath 27 | 28 | ```python 29 | import scrapy 30 | from scrapy.crawler import CrawlerProcess 31 | 32 | 33 | class TestSpider(scrapy.Spider): 34 | name = "test" 35 | start_urls = ["https://www.baidu.com/"] 36 | 37 | def parse(self, response): 38 | title = response.xpath('//*[@id="su"]/@value').extract_first() 39 | self.logger.info(title) 40 | 41 | 42 | if __name__ == "__main__": 43 | process = CrawlerProcess({ 44 | 'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' 45 | }) 46 | 47 | process.crawl(TestSpider) 48 | process.start() 49 | ``` 50 | 51 | 文件可以直接运行,可以看到日志输出:`2019-06-13 11:20:48 [test] INFO: 百度一下`,title已经取到了, 52 | 53 | 为了配合XPath,Scrapy除了提供了 `Selector` 之外,还提供了方法来避免每次从response中提取数据时生成selector的麻烦。 54 | 55 | Selector有四个基本的方法(点击相应的方法可以看到详细的API文档): 56 | - xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表 。 57 | - css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表. 58 | - re(): 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表。 59 | 60 | # extract与extract_first 61 | 62 | 我们修改上面的代码,将spider修改下: 63 | ```python 64 | class TestSpider(scrapy.Spider): 65 | name = "test" 66 | start_urls = ["https://www.baidu.com/"] 67 | 68 | def parse(self, response): 69 | title = response.xpath('//*[@id="su"]/@value').extract_first() 70 | title_ = response.xpath('//*[@id="su"]/@value').extract() 71 | self.logger.info(title) 72 | self.logger.info(title_) 73 | ``` 74 | 75 | 输出是: 76 | ```bash 77 | 2019-06-13 11:23:26 [test] INFO: 百度一下 78 | 2019-06-13 11:23:26 [test] INFO: ['百度一下'] 79 | ``` 80 | 可以看到`extract_first`返回的是字符串,`extract`是列表。 81 | 82 | 更准确的说,我们用xpath解析`列表页`时,`extract`的是整个列表,`extract_first`是列表里的第一个字符串。 83 | 84 | 比如: 85 | 86 | ```python 87 | >>> response.css('a::attr(href)').extract() 88 | ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html'] 89 | >>> response.css('a::attr(href)').extract_first() 90 | 'image1.html' 91 | ``` 92 | 93 | # css与re 94 | 95 | 我们知道,scrapy的selector是支持css和re的,但是我在平时的解析工作中几乎用不到这两种方法,所以这里不重点讲。 96 | 97 | 其实优先推荐的选择方法应该是css,因为会和前端开发中的css是一致的。如果你是前端开发工作者,那你一定会很快上手css选择器。 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 数据采集从入门到放弃 2 | nav: 3 | - 介绍: index.md 4 | - 目录: content.md 5 | - 第一章:爬虫介绍: 6 | - 爬虫介绍: introduce_spider/introduce_spider.md 7 | - 爬虫面试: introduce_spider/spider_interview.md 8 | - 第二章:HTTP协议介绍: 9 | - HTTP协议: network/HTTP.md 10 | - TCP协议: network/TCP.md 11 | - 实战:模拟登陆: network/session.md 12 | - 第三章:Requests使用: 13 | - Requests基础使用: requests/basic_useage.md 14 | - Requests源码分析: requests/request_source_code.md 15 | - 注意事项: requests/request_attention.md 16 | - 第四章:Xpath介绍: 17 | - lxml使用: xpath/lxml.md 18 | - scrapy的selector: xpath/scrapy_selector.md 19 | - 注意事项: xpath/attention.md 20 | - 第五章:MongoDB与MySQL: 21 | - MongoDB使用: database/MongoDB.md 22 | - MySQL使用: database/MySQL.md 23 | - 注意事项: database/attention.md 24 | - 第六章:多线程爬虫: 25 | - 线程、进程、协程(面试高频): thread/Thread_process_coroutine.md 26 | # - MySQL使用: database/MySQL.md 27 | # - 注意事项: database/attention.md 28 | 29 | theme: readthedocs 30 | 31 | --------------------------------------------------------------------------------