├── 0-目录.md ├── 0.0-前言.md ├── 0.1-序一.md ├── 0.3-序二.md ├── Chapter 1 开发环境配置 ├── 1.0-开发环境配置.md ├── 1.1-Python3的安装.md ├── 1.2-请求库的安装.md ├── 1.3-解析库的安装.md ├── 1.4-数据库的安装.md ├── 1.5-存储库的安装.md ├── 1.6-Web库的安装.md ├── 1.7-App爬取相关库的安装.md ├── 1.8-爬虫框架的安装.md └── 1.9-部署相关库的安装.md ├── Chapter 10 模拟登录 ├── 10.0-模拟登录.md ├── 10.1-模拟登录并爬取GitHub.md └── 10.2-Cookies池的搭建.md ├── Chapter 11 APP的爬取 ├── 11.0-APP的爬取.md ├── 11.1-Charles的使用.md ├── 11.2-mitmproxy的使用.md ├── 11.3-mitmdump爬取“得到”App电子书信息.md ├── 11.4-Appium的使用.md ├── 11.5-Appium爬取微信朋友圈.md └── 11.6-Appium+mitmdump爬取京东商品评论.md ├── Chapter 12 pyspider框架的使用 ├── 12.0-pyspider框架的使用.md ├── 12.1-pyspider框架介绍.md ├── 12.2-pyspider基本使用.md └── 12.3-pyspider用法详解.md ├── Chapter 13 Scrapy框架的使用 ├── 13.0-Scrapy框架的使用.md ├── 13.1-Scrapy框架介绍.md ├── 13.10-Scrapy通用爬虫.md ├── 13.11-Scrapyrt的使用.md ├── 13.12-Scrapy对接Docker.md ├── 13.13-Scrapy爬取新浪微博.md ├── 13.2-Scrapy入门.md ├── 13.3-Selector的用法.md ├── 13.4-Spider的用法.md ├── 13.5-Downloader Middleware的用法.md ├── 13.6-Spider Middleware的用法.md ├── 13.7-Item Pipeline的用法.md ├── 13.8-Scrapy对接Selenium.md └── 13.9-Scrapy对接Splash.md ├── Chapter 14 分布式爬虫 ├── 14.0-分布式爬虫.md ├── 14.1-分布式爬虫理念.md ├── 14.2-Scrapy-Redis源码解析.md ├── 14.3-Scrapy分布式实现.md └── 14.4-Bloom Filter的对接.md ├── Chapter 15 分布式爬虫的部署 ├── 15.0-分布式爬虫的部署.md ├── 15.1-Scrapyd分布式部署.md ├── 15.2-Scrapyd-Client的使用.md ├── 15.3-Scrapyd对接Docker.md ├── 15.4-Scrapyd批量部署.md └── 15.5-Gerapy分布式管理.md ├── Chapter 2 爬虫基础 ├── 2.0-爬虫基础.md ├── 2.1-HTTP基本原理.md ├── 2.2-Web网页基础.md ├── 2.3-爬虫基本原理.md ├── 2.4-会话和Cookies.md └── 2.5-代理基本原理.md ├── Chapter 3 基本库的使用 ├── 3.0-基本库的使用.md ├── 3.1-使用urllib.md ├── 3.2-使用requests.md ├── 3.3-正则表达式.md └── 3.4-爬取猫眼电影排行.md ├── Chapter 4 解析库的使用 ├── 4.0-解析库的使用.md ├── 4.1-XPath的使用.md ├── 4.2-BeautifulSoup的使用.md └── 4.3-pyquery的使用.md ├── Chapter 5 数据存储 ├── 5.0-数据存储.md ├── 5.1-文件存储.md ├── 5.2-关系型数据库存储.md └── 5.3-非关系型数据库存储.md ├── Chapter 6 Ajax数据爬取 ├── 6.0-Ajax数据爬取.md ├── 6.1-什么是Ajax.md ├── 6.2-Ajax分析方法.md ├── 6.3-Ajax结果提取.md └── 6.4-分析Ajax爬取今日头条街拍美图.md ├── Chapter 7 动态渲染页面抓取 ├── 7.0-动态渲染页面抓取.md ├── 7.1-Selenium的使用.md ├── 7.2-Splash的使用.md ├── 7.3-Splash负载均衡配置.md └── 7.4-使用Selenium爬取淘宝商品.md ├── Chapter 8 验证码的识别 ├── 8.0-验证码的识别.md ├── 8.1-图形验证码的识别.md ├── 8.2-极验滑动验证码识别.md ├── 8.3-点触验证码识别.md └── 8.4-微博宫格验证码识别.md ├── Chapter 9 代理的使用 ├── 9.0-代理的使用.md ├── 9.1-代理的设置.md ├── 9.2-代理池的维护.md ├── 9.3-付费代理的使用.md ├── 9.4-ADSL代理的使用.md └── 9.5-使用代理爬取微信公众号文章.md ├── README.md └── image ├── 1-1.jpg ├── 1-10.jpg ├── 1-11.jpg ├── 1-12.jpg ├── 1-13.jpg ├── 1-14.jpg ├── 1-15.jpg ├── 1-16.jpg ├── 1-17.png ├── 1-18.jpg ├── 1-19.jpg ├── 1-2.jpg ├── 1-20.jpg ├── 1-21.jpg ├── 1-22.jpg ├── 1-23.jpg ├── 1-24.jpg ├── 1-25.jpg ├── 1-26.jpg ├── 1-27.jpg ├── 1-28.jpg ├── 1-29.jpg ├── 1-3.jpg ├── 1-30.jpg ├── 1-31.jpg ├── 1-32.jpg ├── 1-33.jpg ├── 1-34.jpg ├── 1-35.jpg ├── 1-36.jpg ├── 1-37.jpg ├── 1-38.jpg ├── 1-39.jpg ├── 1-4.jpg ├── 1-40.png ├── 1-41.png ├── 1-42.jpg ├── 1-43.jpg ├── 1-44.jpg ├── 1-45.jpg ├── 1-46.jpg ├── 1-47.jpg ├── 1-48.png ├── 1-49.jpg ├── 1-5.jpg ├── 1-50.png ├── 1-51.jpg ├── 1-52.png ├── 1-53.jpg ├── 1-54.jpg ├── 1-55.jpg ├── 1-56.jpg ├── 1-57.png ├── 1-58.jpg ├── 1-59.jpg ├── 1-6.jpg ├── 1-60.jpg ├── 1-61.jpg ├── 1-62.jpg ├── 1-63.jpg ├── 1-64.jpg ├── 1-65.jpg ├── 1-66.jpg ├── 1-67.jpg ├── 1-68.jpg ├── 1-69.jpg ├── 1-7.jpg ├── 1-70.jpg ├── 1-71.jpg ├── 1-72.jpg ├── 1-73.jpg ├── 1-74.jpg ├── 1-75.png ├── 1-76.jpg ├── 1-77.jpg ├── 1-78.jpg ├── 1-79.jpg ├── 1-8.jpg ├── 1-80.png ├── 1-81.jpg ├── 1-82.jpg ├── 1-83.jpg ├── 1-84.jpg ├── 1-85.png ├── 1-86.jpg ├── 1-9.jpg ├── 10-1.png ├── 10-10.jpg ├── 10-11.jpg ├── 10-12.jpg ├── 10-13.jpg ├── 10-2.jpg ├── 10-3.jpg ├── 10-4.jpg ├── 10-5.jpg ├── 10-6.jpg ├── 10-7.png ├── 10-8.png ├── 10-9.jpg ├── 11-1.png ├── 11-10.png ├── 11-11.png ├── 11-12.jpg ├── 11-13.jpg ├── 11-14.jpg ├── 11-15.jpg ├── 11-16.jpg ├── 11-17.png ├── 11-18.jpg ├── 11-19.jpg ├── 11-2.png ├── 11-20.jpg ├── 11-21.jpg ├── 11-22.jpg ├── 11-23.jpg ├── 11-24.jpg ├── 11-25.png ├── 11-26.png ├── 11-27.png ├── 11-28.jpg ├── 11-29.png ├── 11-3.png ├── 11-30.jpg ├── 11-31.png ├── 11-32.png ├── 11-33.jpg ├── 11-34.jpg ├── 11-35.jpg ├── 11-36.jpg ├── 11-37.png ├── 11-38.jpg ├── 11-39.jpg ├── 11-4.png ├── 11-40.jpg ├── 11-41.jpg ├── 11-42.jpg ├── 11-43.jpg ├── 11-44.jpg ├── 11-45.jpg ├── 11-46.jpg ├── 11-47.jpg ├── 11-48.jpg ├── 11-49.jpg ├── 11-5.png ├── 11-50.jpg ├── 11-6.png ├── 11-7.png ├── 11-8.png ├── 11-9.png ├── 12-1.jpg ├── 12-10.jpg ├── 12-11.jpg ├── 12-12.png ├── 12-13.jpg ├── 12-14.jpg ├── 12-15.jpg ├── 12-16.jpg ├── 12-17.jpg ├── 12-18.jpg ├── 12-19.jpg ├── 12-2.jpg ├── 12-20.jpg ├── 12-21.png ├── 12-22.jpg ├── 12-23.jpg ├── 12-24.png ├── 12-25.png ├── 12-26.png ├── 12-27.jpg ├── 12-3.png ├── 12-4.png ├── 12-5.png ├── 12-6.jpg ├── 12-7.jpg ├── 12-8.jpg ├── 12-9.jpg ├── 13-1.jpg ├── 13-10.jpg ├── 13-11.jpg ├── 13-12.jpg ├── 13-13.jpg ├── 13-14.jpg ├── 13-15.jpg ├── 13-16.jpg ├── 13-17.jpg ├── 13-18.jpg ├── 13-19.png ├── 13-2.jpg ├── 13-20.png ├── 13-21.jpg ├── 13-22.jpg ├── 13-23.jpg ├── 13-24.png ├── 13-25.jpg ├── 13-26.jpg ├── 13-27.jpg ├── 13-28.jpg ├── 13-29.jpg ├── 13-3.jpg ├── 13-30.png ├── 13-31.jpg ├── 13-32.jpg ├── 13-33.jpg ├── 13-34.png ├── 13-35.jpg ├── 13-36.jpg ├── 13-37.jpg ├── 13-38.jpg ├── 13-39.jpg ├── 13-4.jpg ├── 13-5.jpg ├── 13-6.png ├── 13-7.jpg ├── 13-8.jpg ├── 13-9.jpg ├── 14-1.jpg ├── 14-10.jpg ├── 14-11.jpg ├── 14-12.jpg ├── 14-13.jpg ├── 14-2.jpg ├── 14-3.jpg ├── 14-4.jpg ├── 14-5.jpg ├── 14-6.jpg ├── 14-7.jpg ├── 14-8.jpg ├── 14-9.jpg ├── 15-1.png ├── 15-10.jpg ├── 15-11.jpg ├── 15-12.jpg ├── 15-13.jpg ├── 15-2.png ├── 15-3.png ├── 15-4.jpg ├── 15-5.png ├── 15-6.jpg ├── 15-7.jpg ├── 15-8.jpg ├── 15-9.jpg ├── 2-1.jpg ├── 2-10.png ├── 2-11.jpg ├── 2-12.jpg ├── 2-13.jpg ├── 2-2.png ├── 2-3.png ├── 2-4.jpg ├── 2-5.png ├── 2-6.jpg ├── 2-7.jpg ├── 2-8.jpg ├── 2-9.png ├── 3-1.png ├── 3-10.jpg ├── 3-11.jpg ├── 3-12.jpg ├── 3-13.jpg ├── 3-14.jpg ├── 3-15.jpg ├── 3-2-7.png ├── 3-2.jpg ├── 3-3.png ├── 3-4.png ├── 3-5.ico ├── 3-6.png ├── 3-7.jpg ├── 3-8.png ├── 3-9.jpg ├── 5-1.jpg ├── 5-2.jpg ├── 5-3.jpg ├── 5-4.jpg ├── 5-5.jpg ├── 5-6.jpg ├── 6-1.png ├── 6-10.png ├── 6-11.png ├── 6-12.png ├── 6-13.png ├── 6-14.png ├── 6-15.jpg ├── 6-16.jpg ├── 6-17.jpg ├── 6-18.jpg ├── 6-19.jpg ├── 6-2.png ├── 6-20.jpg ├── 6-21.jpg ├── 6-22.jpg ├── 6-3.png ├── 6-4.png ├── 6-5.png ├── 6-6.png ├── 6-7.png ├── 6-8.png ├── 6-9.png ├── 7-1.png ├── 7-10.png ├── 7-11.png ├── 7-12.png ├── 7-13.png ├── 7-14.jpg ├── 7-14.png ├── 7-15.jpg ├── 7-16.jpg ├── 7-17.png ├── 7-18.jpg ├── 7-19.png ├── 7-2.png ├── 7-20.jpg ├── 7-21.jpg ├── 7-22.png ├── 7-23.jpg ├── 7-24.jpg ├── 7-25.jpg ├── 7-26.jpg ├── 7-27.jpg ├── 7-28.jpg ├── 7-3.jpg ├── 7-4.jpg ├── 7-5.jpg ├── 7-6.png ├── 7-7.png ├── 7-8.png ├── 7-9.png ├── 8-1.png ├── 8-10.jpg ├── 8-11.jpg ├── 8-12.jpg ├── 8-13.jpg ├── 8-14.jpg ├── 8-15.png ├── 8-16.png ├── 8-17.jpg ├── 8-18.jpg ├── 8-19.jpg ├── 8-2.jpg ├── 8-20.jpg ├── 8-21.jpg ├── 8-22.jpg ├── 8-23.jpg ├── 8-24.png ├── 8-25.jpg ├── 8-26.png ├── 8-27.png ├── 8-28.png ├── 8-29.png ├── 8-3.jpg ├── 8-30.png ├── 8-31.jpg ├── 8-32.png ├── 8-33.jpg ├── 8-34.jpg ├── 8-4.jpg ├── 8-5.jpg ├── 8-6.jpg ├── 8-7.jpg ├── 8-8.jpg ├── 8-9.jpg ├── 9-1.jpg ├── 9-10.jpg ├── 9-11.jpg ├── 9-12.jpg ├── 9-13.jpg ├── 9-14.jpg ├── 9-15.jpg ├── 9-16.jpg ├── 9-17.jpg ├── 9-18.jpg ├── 9-19.jpg ├── 9-2.png ├── 9-20.jpg ├── 9-21.png ├── 9-22.jpg ├── 9-23.jpg ├── 9-24.jpg ├── 9-25.png ├── 9-26.jpg ├── 9-27.jpg ├── 9-28.jpg ├── 9-29.jpg ├── 9-3.jpg ├── 9-30.jpg ├── 9-4.jpg ├── 9-5.png ├── 9-6.jpg ├── 9-7.jpg ├── 9-8.png ├── 9-9.jpg └── cover.jpg /0-目录.md: -------------------------------------------------------------------------------- 1 | # Python3网络爬虫开发实战 2 | 3 | - [0-目录](0-目录.md) 4 | - [0.0-前言](0.0-前言.md) 5 | - [0.1-序一](0.1-序一.md) 6 | - [0.3-序二](0.3-序二.md) 7 | - [1-开发环境配置](Chapter 1/1-开发环境配置.md) 8 | - [1.1-Python3的安装](Chapter 1/1.1-Python3的安装.md) 9 | - [1.2-请求库的安装](Chapter 1/1.2-请求库的安装.md) 10 | - [1.3-解析库的安装](Chapter 1/1.3-解析库的安装.md) 11 | - [1.4-数据库的安装](Chapter 1/1.4-数据库的安装.md) 12 | - [1.5-存储库的安装](Chapter 1/1.5-存储库的安装.md) 13 | - [1.6-Web库的安装](Chapter 1/1.6-Web库的安装.md) 14 | - [1.7-App爬取相关库的安装](Chapter 1/1.7-App爬取相关库的安装.md) 15 | - [1.8-爬虫框架的安装](Chapter 1/1.8-爬虫框架的安装.md) 16 | - [1.9-部署相关库的安装](Chapter 1/1.9-部署相关库的安装.md) 17 | - [2-爬虫基础](Chapter 2/2-爬虫基础.md) 18 | - [2.1-HTTP基本原理](Chapter 2/2.1-HTTP基本原理.md) 19 | - [2.2-Web网页基础](Chapter 2/2.2-Web网页基础.md) 20 | - [2.3-爬虫基本原理](Chapter 2/2.3-爬虫基本原理.md) 21 | - [2.4-会话和Cookies](Chapter 2/2.4-会话和Cookies.md) 22 | - [2.5-代理基本原理](Chapter 2/2.5-代理基本原理.md) 23 | - [3-基本库的使用](Chapter 3/3-基本库的使用.md) 24 | - [3.1-使用urllib](Chapter 3/3.1-使用urllib.md) 25 | - [3.2-使用requests](Chapter 3/3.2-使用requests.md) 26 | - [3.3-正则表达式](Chapter 3/3.3-正则表达式.md) 27 | - [3.4-爬取猫眼电影排行](Chapter 3/3.4-爬取猫眼电影排行.md) 28 | - [4-解析库的使用](Chapter 4/4-解析库的使用.md) 29 | - [4.1-XPath的使用](Chapter 4/4.1-XPath的使用.md) 30 | - [4.2-BeautifulSoup的使用](Chapter 4/4.2-BeautifulSoup的使用.md) 31 | - [4.3-pyquery的使用](Chapter 4/4.3-pyquery的使用.md) 32 | - [5-数据存储](Chapter 5/5-数据存储.md) 33 | - [5.1-文件存储](Chapter 5/5.1-文件存储.md) 34 | - [5.2-关系型数据库存储](Chapter 5/5.2-关系型数据库存储.md) 35 | - [5.3-非关系型数据库存储](Chapter 5/5.3-非关系型数据库存储.md) 36 | - [6-Ajax数据爬取](Chapter 6/6-Ajax数据爬取.md) 37 | - [6.1-什么是Ajax](Chapter 6/6.1-什么是Ajax.md) 38 | - [6.2-Ajax分析方法](Chapter 6/6.2-Ajax分析方法.md) 39 | - [6.3-Ajax结果提取](Chapter 6/6.3-Ajax结果提取.md) 40 | - [6.4-分析Ajax爬取今日头条街拍美图](Chapter 6/6.4-分析Ajax爬取今日头条街拍美图.md) 41 | - [7-动态渲染页面抓取](Chapter 7/7-动态渲染页面抓取.md) 42 | - [7.1-Selenium的使用](Chapter 7/7.1-Selenium的使用.md) 43 | - [7.2-Splash的使用](Chapter 7/7.2-Splash的使用.md) 44 | - [7.3-Splash负载均衡配置](Chapter 7/7.3-Splash负载均衡配置.md) 45 | - [7.4-使用Selenium爬取淘宝商品](Chapter 7/7.4-使用Selenium爬取淘宝商品.md) 46 | - [8-验证码的识别](Chapter 8/8-验证码的识别.md) 47 | - [8.1-图形验证码的识别](Chapter 8/8.1-图形验证码的识别.md) 48 | - [8.2-极验滑动验证码识别](Chapter 8/8.2-极验滑动验证码识别.md) 49 | - [8.3-点触验证码识别](Chapter 8/8.3-点触验证码识别.md) 50 | - [8.4-微博宫格验证码识别](Chapter 8/8.4-微博宫格验证码识别.md) 51 | - [9-代理的使用](Chapter 9/9-代理的使用.md) 52 | - [9.1-代理的设置](Chapter 9/9.1-代理的设置.md) 53 | - [9.2-代理池的维护](Chapter 9/9.2-代理池的维护.md) 54 | - [9.3-付费代理的使用](Chapter 9/9.3-付费代理的使用.md) 55 | - [9.4-ADSL代理的使用](Chapter 9/9.4-ADSL代理的使用.md) 56 | - [9.5-使用代理爬取微信公众号文章](Chapter 9/9.5-使用代理爬取微信公众号文章.md) 57 | - [10-模拟登录](Chapter 10/10-模拟登录.md) 58 | - [10.1-模拟登录并爬取GitHub](Chapter 10/10.1-模拟登录并爬取GitHub.md) 59 | - [10.2-Cookies池的搭建](Chapter 10/10.2-Cookies池的搭建.md) 60 | - [11-APP的爬取](Chapter 11/11-APP的爬取.md) 61 | - [11.1-Charles的使用](Chapter 11/11.1-Charles的使用.md) 62 | - [11.2-mitmproxy的使用](Chapter 11/11.2-mitmproxy的使用.md) 63 | - [11.3-mitmdump爬取“得到”App电子书信息](Chapter 11/11.3-mitmdump爬取“得到”App电子书信息.md) 64 | - [11.4-Appium的使用](Chapter 11/11.4-Appium的使用.md) 65 | - [11.5-Appium爬取微信朋友圈](Chapter 11/11.5-Appium爬取微信朋友圈.md) 66 | - [11.6-Appium+mitmdump爬取京东商品评论](Chapter 11/11.6-Appium+mitmdump爬取京东商品评论.md) 67 | - [12-pyspider框架的使用](Chapter 12/12-pyspider框架的使用.md) 68 | - [12.1-pyspider框架介绍](Chapter 12/12.1-pyspider框架介绍.md) 69 | - [12.2-pyspider基本使用](Chapter 12/12.2-pyspider基本使用.md) 70 | - [12.3-pyspider用法详解](Chapter 12/12.3-pyspider用法详解.md) 71 | - [13-Scrapy框架的使用](Chapter 13/13-Scrapy框架的使用.md) 72 | - [13.1-Scrapy框架介绍](Chapter 13/13.1-Scrapy框架介绍.md) 73 | - [13.2-Scrapy入门](Chapter 13/13.2-Scrapy入门.md) 74 | - [13.3-Selector的用法](Chapter 13/13.3-Selector的用法.md) 75 | - [13.4-Spider的用法](Chapter 13/13.4-Spider的用法.md) 76 | - [13.5-Downloader Middleware的用法](Chapter 13/13.5-Downloader Middleware的用法.md) 77 | - [13.6-Spider Middleware的用法](Chapter 13/13.6-Spider Middleware的用法.md) 78 | - [13.7-Item Pipeline的用法](Chapter 13/13.7-Item Pipeline的用法.md) 79 | - [13.8-Scrapy对接Selenium](Chapter 13/13.8-Scrapy对接Selenium.md) 80 | - [13.9-Scrapy对接Splash](Chapter 13/13.9-Scrapy对接Splash.md) 81 | - [13.10-Scrapy通用爬虫](Chapter 13/13.10-Scrapy通用爬虫.md) 82 | - [13.11-Scrapyrt的使用](Chapter 13/13.11-Scrapyrt的使用.md) 83 | - [13.12-Scrapy对接Docker](Chapter 13/13.12-Scrapy对接Docker.md) 84 | - [13.13-Scrapy爬取新浪微博](Chapter 13/13.13-Scrapy爬取新浪微博.md) 85 | - [14-分布式爬虫](Chapter 14/14-分布式爬虫.md) 86 | - [14.1-分布式爬虫理念](Chapter 14/14.1-分布式爬虫理念.md) 87 | - [14.2-Scrapy-Redis源码解析](Chapter 14/14.2-Scrapy-Redis源码解析.md) 88 | - [14.3-Scrapy分布式实现](Chapter 14/14.3-Scrapy分布式实现.md) 89 | - [14.4-Bloom Filter的对接](Chapter 14/14.4-Bloom Filter的对接.md) 90 | - [15-分布式爬虫的部署](Chapter 15/15-分布式爬虫的部署.md) 91 | - [15.1-Scrapyd分布式部署](Chapter 15/15.1-Scrapyd分布式部署.md) 92 | - [15.2-Scrapyd-Client的使用](Chapter 15/15.2-Scrapyd-Client的使用.md) 93 | - [15.3-Scrapyd对接Docker](Chapter 15/15.3-Scrapyd对接Docker.md) 94 | - [15.4-Scrapyd批量部署](Chapter 15/15.4-Scrapyd批量部署.md) 95 | - [15.5-Gerapy分布式管理](Chapter 15/15.5-Gerapy分布式管理.md) 96 | -------------------------------------------------------------------------------- /0.0-前言.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | ## 为什么写这本书 4 | 5 | 在这个大数据时代,尤其是人工智能浪潮兴起的时代,不论是工程领域还是研究领域,数据已经成为必不可少的一部分,而数据的获取很大程度上依赖于爬虫的爬取,所以爬虫也逐渐变得火爆起来。 6 | 7 | 我是在 2015 年开始接触爬虫的,当时爬虫其实并没有这么火,我当时觉得能够把想要的数据抓取下来就是一件非常有成就感的事情,而且也可以顺便熟悉 Python,一举两得。在学习期间,我将学到的内容做好总结,发表到博客上。随着我发表的内容越来越多,博客的浏览量也越来越多,很多读者对我的博文给予了肯定的评价,这也给我的爬虫学习之路增添了很多动力。在学习的过程中,困难其实还是非常多的,最早学习时使用的是 Python 2,当时因为编码问题搞得焦头烂额。另外,那时候相关的中文资料还比较少,很多情况下还得自己慢慢去啃官方文档,走了不少弯路。随着学习的进行,我发现爬虫这部分内容涉及的知识点太多、太杂了。网页的结构、渲染方式不同,我们就得换不同的爬取方案来进行针对性的爬取。另外,网页信息的提取、爬取结果的保存也有五花八门的方案。随着移动互联网的兴起,App 的爬取也成了一个热点,而为了提高爬取速度又需要考虑并行爬取、分布式爬取方面的内容,爬虫的通用性、易用性、架构都需要好好优化。这么多杂糅的知识点对于一个爬虫初学者来说,学习的挑战性会非常高,同时学习过程中大家或许也会走我之前走过的弯路,浪费很多时间。后来有一天,图灵的王编辑联系了我,问我有没有意向写一本爬虫方面的书,我听到之后充满了欣喜和期待,这样既能把自己学过的知识点做一个系统整理,又可以跟广大爬虫爱好者分享自己的学习经验,还可以出版自己的作品,于是我很快就答应约稿了。 8 | 9 | 一开始觉得写书并不是一件那么难的事,后来真正写了才发现其中包含的艰辛。书相比博客来说,用词的严谨性要高很多,而且逻辑需要更加缜密,很多细节必须考虑得非常周全。前前后后写了大半年的时间,审稿和修改又花费了几个月的时间,一路走来甚是不易,不过最后看到书稿成型,觉得这一切都是值得的。在书中,我把我学习爬虫的很多经验都写了进去。环境配置是学习的第一步,环境配置不好,其他工作就没法开展,甚至可能很大程度上打击学习的积极性,所以我在第 1 章中着重介绍了环境的配置过程。而因为操作系统的不同,环境配置过程又各有不同,所以我把每个系统(Windows、Linux、Mac)的环境配置过程都亲自实践了一遍,并梳理记录下来,希望为各位读者在环境配置时多提供一些帮助。后面我又针对爬虫网站的不同情形分门别类地进行了说明,如 Ajax 分析爬取、动态渲染页面爬取、App 爬取、使用代理爬取、模拟登录爬取等知识,每个知识点我都选取了一些典型案例来说明,以便于读者更好地理解整个过程和用法。为了提高代码编写和爬取的效率,还可以使用一些爬虫框架辅助爬取,所以本书后面又介绍了两个流行的爬虫框架的用法,最后又介绍了一些分布式爬虫及部署方面的知识。总体来说,本书根据我个人觉得比较理想的学习路径介绍了学习爬虫的相关知识,并通过一些实战案例帮助读者更好地理解其中的原理。 10 | 11 | ## 本书内容 12 | 13 | 本书一共分为 15 章,归纳如下。 14 | 15 | 第 1 章介绍了本书所涉及的所有环境的配置详细流程,兼顾 Windows、Linux、Mac 三大平台。本章不用逐节阅读,需要的时候查阅即可。 16 | 17 | 第 2 章介绍了学习爬虫之前需要了解的基础知识,如 HTTP、爬虫、代理的基本原理、网页基本结构等内容,对爬虫没有任何了解的读者建议好好了解这一章的知识。 18 | 19 | 第 3 章介绍了最基本的爬虫操作,一般学习爬虫都是从这一步学起的。这一章介绍了最基本的两个请求库(urllib 和 requests)和正则表达式的基本用法。学会了这一章,就可以掌握最基本的爬虫技术了。 20 | 21 | 第 4 章介绍了页解析库的基本用法,包括 Beautiful Soup、XPath、pyquery 的基本使用方法,它们可以使得信息的提取更加方便、快捷,是爬虫必备利器。 22 | 23 | 第 5 章介绍了数据存储的常见形式及存储操作,包括 TXT、JSON、CSV 各种文件的存储,以及关系型数据库 MySQL 和非关系型数据库 MongoDB、Redis 存储的基本存储操作。学会了这些内容,我们可以灵活方便地保存爬取下来的数据。 24 | 25 | 第 6 章介绍了 Ajax 数据爬取的过程,一些网页的数据可能是通过 Ajax 请求 API 接口的方式加载的,用常规方法无法爬取,本章介绍了使用 Ajax 进行数据爬取的方法。 26 | 27 | 第 7 章介绍了动态渲染页面的爬取,现在越来越多的网站内容是经过 JavaScript 渲染得到的,而原始 HTML 文本可能不包含任何有效内容,而且渲染过程可能涉及某些 JavaScript 加密算法,可以使用 Selenium、Splash 等工具来实现模拟浏览器进行数据爬取的方法。 28 | 29 | 第 8 章介绍了验证码的相关处理方法。验证码是网站反爬虫的重要措施,我们可以通过本章了解到各类验证码的应对方案,包括图形验证码、极验验证码、点触验证码、微博宫格验证码的识别。 30 | 31 | 第 9 章介绍了代理的使用方法,限制 IP 的访问也是网站反爬虫的重要措施。另外,我们也可以使用代理来伪装爬虫的真实 IP,使用代理可以有效解决这个问题。通过本章,我们了解到代理的使用方法,还学习了代理池的维护方法,以及 ADSL 拨号代理的使用方法。 32 | 33 | 第 10 章介绍了模拟登录爬取的方法,某些网站需要登录才可以看到需要的内容,这时就需要用爬虫模拟登录网站再进行爬取了。本章介绍了最基本的模拟登录方法以及维护一个 Cookies 池的方法。 34 | 35 | 第 11 章介绍了 App 的爬取方法,包括基本的 Charles、mitmproxy 抓包软件的使用。此外,还介绍了 mitmdump 对接 Python 脚本进行实时抓取的方法,以及使用 Appium 完全模拟手机 App 的操作进行爬取的方法。 36 | 37 | 第 12 章介绍了 pyspider 爬虫框架及用法,该框架简洁易用、功能强大,可以节省大量开发爬虫的时间。本章结合案例介绍了使用该框架进行爬虫开发的方法。 38 | 39 | 第 13 章介绍了 Scrapy 爬虫框架及用法。Scrapy 是目前使用最广泛的爬虫框架,本章介绍了它的基本架构、原理及各个组件的使用方法,另外还介绍了 Scrapy 通用化配置、对接 Docker 的一些方法。 40 | 41 | 第 14 章介绍了分布式爬虫的基本原理及实现方法。为了提高爬取效率,分布式爬虫是必不可少的,本章介绍了使用 Scrapy 和 Redis 实现分布式爬虫的方法。 42 | 43 | 第 15 章介绍了分布式爬虫的部署及管理方法。方便快速地完成爬虫的分布式部署,可以节省开发者大量的时间。本章结合 Scrapy、Scrapyd、Docker、Gerapy 等工具介绍了分布式爬虫部署和管理的实现。 44 | 45 | ## 致谢 46 | 47 | 感谢我的父母、导师,没有他们创造的环境,我不可能完成此书的写作。 48 | 49 | 感谢在我学习过程中与我探讨技术的各位朋友,特别感谢汪海洋先生在我初学爬虫过程中给我提供的指导,特别感谢崔弦毅、苟桃、时猛先生在我写书过程中为我提供的思路和建议。感谢李园女士为本书设计了封面。 50 | 51 | 感谢为本书撰写推荐语的李舟军老师、宋睿华老师、梁斌老师、施水才老师(排名不分先后),感谢你们对本书的支持和推荐。 52 | 53 | 感谢王军花、陈兴璐编辑,在书稿的审核过程中给我提供了非常多的建议,没有你们的策划和敦促,我也难以顺利完成此书。 54 | 55 | 感谢为本书做出贡献的每一个人! 56 | 57 | ## 相关资源 58 | 59 | 本书中的所有代码都放在了 GitHub,详见 [https://github.com/Python3WebSpider](https://github.com/Python3WebSpider),书中每个实例对应的章节末也有说明。 60 | 61 | 由于本人水平有限,写作过程中难免存在一些错误和不足之处,恳请广大读者批评指正。如果发现错误,可以将其提交到图灵社区本书主页 http://www.ituring.com.cn/book/2003,以使本书更加完善,非常感谢! 62 | 63 | 另外,本书还设有专门的读者交流群,可以搜索 "进击的 Coder" 微信公众号获取,欢迎各位读者加入! 64 | 65 | —— 崔庆才 66 | 67 | ——2018 年 1 月 -------------------------------------------------------------------------------- /0.1-序一.md: -------------------------------------------------------------------------------- 1 | # 序一 2 | 3 | 人类社会已经进入大数据时代,大数据深刻改变着我们的工作和生活。随着互联网、移动互联网、社交网络等的迅猛发展,各种数量庞大、种类繁多、随时随地产生和更新的大数据,蕴含着前所未有的社会价值和商业价值。大数据成为 21 世纪最为重要的经济资源之一。正如马云所言:未来最大的能源不是石油而是大数据。对大数据的获取、处理与分析,以及基于大数据的智能应用,已成为提高未来竞争力的关键要素。 4 | 5 | 但如何获取这些宝贵数据呢?网络爬虫就是一种高效的信息采集利器,利用它可以快速、准确地采集我们想要的各种数据资源。因此,可以说,网络爬虫技术几乎已成为大数据时代 IT 从业者的必修课程。 6 | 7 | 我们需要采集的数据大多来源于互联网的各个网站。然而,不同的网站结构不一、布局复杂、渲染方式多样,有的网站还专门采取了一系列 “反爬” 的防范措施。因此,为准确高效地采集到需要的数据,我们需要采取具有针对性的反制措施。网络爬虫与反爬措施是矛与盾的关系,网络爬虫技术就是在这种针锋相对、见招拆招的不断斗争中,逐渐完善和发展起来的。 8 | 9 | 本书介绍了利用 Python 3 进行网络爬虫开发的各项技术,从环境配置、理论基础到进阶实战、分布式大规模采集,详细介绍了网络爬虫开发过程中需要了解的知识点,并通过多个案例介绍了不同场景下采用不同爬虫技术实现数据爬取的过程。 10 | 11 | 我坚信,每位读者学习和掌握了这些技术之后,成为一个爬虫高手将不再是梦想! 12 | 13 | —— 李舟军,北京航空航天大学教授,博士生导师 14 | 15 | ——2017 年 10 月 -------------------------------------------------------------------------------- /0.3-序二.md: -------------------------------------------------------------------------------- 1 | # 序二 2 | 3 | 众所周知,人工智能的这次浪潮和深度学习技术的突破密不可分,却很少有人会谈论另一位幕后英雄,即数据。如果不是网络上有如此多的图片,李飞飞教授也无法构建近千万的标注图片集合 ImageNet,从而成就深度学习技术在图像识别领域的突破。如果不是在网络上有了如此多的聊天数据,小冰也不会学习到人类的情商,在聊天中带给人类惊喜、欢笑和抚慰。人工智能的进步离不开数据和算法的结合,人类无意间产生的数据却能够让机器学习到超乎想象的 “智慧”,反过来服务人类。 4 | 5 | 在互联网时代,强大的爬虫技术造就了很多伟大的搜索引擎公司,让人类的记忆搜索能力得到巨大的延展。今天在移动互联网时代,爬虫技术仍然是支撑一些信息融合应用(如今日头条)的关键技术。但是,今天爬虫技术面临着更大的挑战。与互联网的共享机制不同,很多资源只有在登录之后才能访问,还采取了各种反爬虫措施,这就让爬虫不那么容易访问这些资源。无论是产品还是研究,都需要大量的优质数据来让机器更加智能。因此,在这个时代,大量的从业者急需一本全面介绍爬虫技术的书。如果你需要了解全面和前沿的爬虫技术,而且想迅速地上手实战,这本书就是首选。 6 | 7 | 我很荣幸认识崔庆才先生,他目前还是一名北京航空航天大学在读研究生,正处在一个对技术狂热追求的年纪。我听他讲了一些修炼爬虫技术的故事,很有意思。他在本科的时候因为一个项目开始接触爬虫,之后他用爬虫竟然得到了所在学校同学的照片,还帮助他的哥们儿追其他系的女孩。我问他是否也是用这些信息找到了女友,他甩了下头发,酷酷地说:“需要吗?” 8 | 9 | 崔庆才是个非常擅长学习的人,他玩什么都能玩到精通。他有一个很好的习惯,就是边学边写,他早期学习爬虫技术的时候,就开了博客,边学边分享他学到并实际操作过的经验,圈粉无数。我很受启发,这样的学习模式很高效,要教给别人之前自己必须弄得特别清楚。另一方面,互联网上的互动也给了他继续学习和精益求精的动力。 10 | 11 | 除了网络,图书是最成体系的经验分享。本书记录了崔庆才先生对爬虫实战技术最精华的部分。我已经迫不及待地想买一本,也一定会把它推荐给更多的朋友。 12 | 13 | —— 宋睿华,微软小冰首席科学家 14 | 15 | ——2017 年 10 月 -------------------------------------------------------------------------------- /Chapter 1 开发环境配置/1.0-开发环境配置.md: -------------------------------------------------------------------------------- 1 | # 第一章 开发环境配置 2 | 3 | 工欲善其事,必先利其器! 4 | 5 | 编写和运行程序之前,我们必须先把开发环境配置好。只有配置好了环境并且有了更方便的开发工具,我们才能更加高效地用程序实现相应的功能。然而很多情况下,我们可能在最开始就卡在环境配置上,如果这个过程花费了太多时间,学习的兴趣可能就下降了大半,所以本章专门对本书中所有的环境配置做一下说明。 6 | 7 | 本章将讲解书中使用的所有库及工具的安装过程。为了使书的条理更加清晰,本书将环境配置的过程统一合并为一章。本章不必逐节阅读,可以在需要的时候查阅。 8 | 9 | 在介绍安装过程时,我们会尽量兼顾各个平台。另外,书中也会指出一些常见的安装错误,以便快速高效地搭建好编程环境。 -------------------------------------------------------------------------------- /Chapter 1 开发环境配置/1.5-存储库的安装.md: -------------------------------------------------------------------------------- 1 | ## 1.5 存储库的安装 2 | 3 | 1.4 节中,我们介绍了几个数据库的安装方式,但这仅仅是用来存储数据的数据库,它们提供了存储服务,但如果想要和 Python 交互的话,还需要安装一些 Python 存储库,如 MySQL 需要安装 PyMySQL,MongoDB 需要安装 PyMongo 等。本节中,我们来说明一下这些存储库的安装方式。 4 | 5 | ### 1.5.1 PyMySQL 的安装 6 | 7 | 在 Python 3 中,如果想要将数据存储到 MySQL 中,就需要借助 PyMySQL 来操作,本节中我们介绍一下它的安装方式。 8 | 9 | #### 1. 相关链接 10 | 11 | * GitHub:[https://github.com/PyMySQL/PyMySQL](https://github.com/PyMySQL/PyMySQL) 12 | * 官方文档:[http://pymysql.readthedocs.io/](http://pymysql.readthedocs.io/) 13 | * PyPI:[https://pypi.python.org/pypi/PyMySQL](https://pypi.python.org/pypi/PyMySQL) 14 | 15 | #### 2. pip 安装 16 | 17 | 这里推荐使用 pip 安装,命令如下: 18 | 19 | ``` 20 | pip3 install pymysql 21 | ``` 22 | 23 | 执行完命令后即可完成安装。 24 | 25 | #### 3. 验证安装 26 | 27 | 为了验证库是否已经安装成功,可以在命令行下测试一下。这里首先输入 python3,进入命令行模式,接着输入如下内容: 28 | 29 | ```python 30 | $ python3 31 | >>> import pymysql 32 | >>> pymysql.VERSION 33 | (0, 7, 11, None) 34 | >>> 35 | ``` 36 | 37 | 如果成功输出了其版本内容,那么证明 PyMySQL 成功安装。 38 | 39 | ### 1.5.2 PyMongo 的安装 40 | 41 | 在 Python 中,如果想要和 MongoDB 进行交互,就需要借助于 PyMongo 库,这里就来了解一下它的安装方法。 42 | 43 | #### 1. 相关链接 44 | 45 | * GitHub:[https://github.com/mongodb/mongo-python-driver](https://github.com/mongodb/mongo-python-driver) 46 | * 官方文档:[https://api.mongodb.com/python/current/](https://api.mongodb.com/python/current/) 47 | * PyPI:[https://pypi.python.org/pypi/pymongo](https://pypi.python.org/pypi/pymongo) 48 | 49 | #### 2. pip 安装 50 | 51 | 这里推荐使用 pip 安装,命令如下: 52 | 53 | ``` 54 | pip3 install pymongo 55 | ``` 56 | 57 | 运行完毕之后,即可完成 PyMongo 的安装。 58 | 59 | #### 3. 验证安装 60 | 61 | 为了验证 PyMongo 库是否已经安装成功,可以在命令行下测试一下: 62 | 63 | ```python 64 | $ python3 65 | >>> import pymongo 66 | >>> pymongo.version 67 | '3.4.0' 68 | >>> 69 | ``` 70 | 71 | 如果成功输出了其版本内容,那么证明成功安装。 72 | 73 | ### 1.5.3 redis-py 的安装 74 | 75 | 对于 Redis 来说,我们要使用 redis-py 库来与其交互,这里就来介绍一下它的安装方法。 76 | 77 | #### 1. 相关链接 78 | 79 | * GitHub:[https://github.com/andymccurdy/redis-py](https://github.com/andymccurdy/redis-py) 80 | * 官方文档:[https://redis-py.readthedocs.io/](https://redis-py.readthedocs.io/) 81 | 82 | #### 2. pip 安装 83 | 84 | 这里推荐使用 pip 安装,命令如下: 85 | 86 | ``` 87 | pip3 install redis 88 | ``` 89 | 90 | 运行完毕之后,即可完成 redis-py 的安装。 91 | 92 | #### 3. 验证安装 93 | 94 | 为了验证 redis-py 库是否已经安装成功,可以在命令行下测试一下: 95 | 96 | ```python 97 | $ python3 98 | >>> import redis 99 | >>> redis.VERSION 100 | (2, 10, 5) 101 | >>> 102 | ``` 103 | 104 | 如果成功输出了其版本内容,那么证明成功安装了 redis-py。 105 | 106 | ### 1.5.4 RedisDump 的安装 107 | 108 | RedisDump 是一个用于 Redis 数据导入 / 导出的工具,是基于 Ruby 实现的,所以要安装 RedisDump,需要先安装 Ruby。 109 | 110 | #### 1. 相关链接 111 | 112 | 113 | * GitHub:[https://github.com/delano/redis-dump](https://github.com/delano/redis-dump) 114 | * 官方文档:[http://delanotes.com/redis-dump](http://delanotes.com/redis-dump) 115 | 116 | #### 2. 安装 Ruby 117 | 118 | 有关 Ruby 的安装方式可以参考 [http://www.ruby-lang.org/zh_cn/documentation/installation](http://www.ruby-lang.org/zh_cn/documentation/installation),这里列出了所有平台的所有安装方式,可以根据对应的平台选用合适的安装方式。 119 | 120 | #### 3. gem 安装 121 | 122 | 安装完成之后,就可以执行 gem 命令了,它类似于 Python 中的 pip 命令。利用 gem 命令,我们可以安装 RedisDump,具体如下: 123 | 124 | ``` 125 | gem install redis-dump 126 | ``` 127 | 128 | 执行完毕之后,即可完成 RedisDump 的安装。 129 | 130 | #### 4. 验证安装 131 | 132 | 安装成功后,就可以执行如下两个命令: 133 | 134 | ``` 135 | redis-dump 136 | redis-load 137 | ``` 138 | 139 | 如果可以成功调用,则证明安装成功。 -------------------------------------------------------------------------------- /Chapter 1 开发环境配置/1.6-Web库的安装.md: -------------------------------------------------------------------------------- 1 | ## 1.6 Web 库的安装 2 | 3 | 对于 Web,我们应该都不陌生,现在日常访问的网站都是 Web 服务程序搭建而成的。Python 同样不例外,也有一些这样的 Web 服务程序,比如 Flask、Django 等,我们可以拿它来开发网站和接口等。 4 | 5 | 在本书中,我们主要使用这些 Web 服务程序来搭建一些 API 接口,供我们的爬虫使用。例如,维护一个代理池,代理保存在 Redis 数据库中,我们要将代理池作为一个公共的组件使用,那么如何构建一个方便的平台来供我们获取这些代理呢?最合适不过的就是通过 Web 服务提供一个 API 接口,我们只需要请求接口即可获取新的代理,这样做简单、高效、实用! 6 | 7 | 书中用到的一些 Web 服务程序主要有 Flask 和 Tornado,这里就分别介绍它们的安装方法。 8 | 9 | ### 1.6.1 Flask 的安装 10 | 11 | Flask 是一个轻量级的 Web 服务程序,它简单、易用、灵活,这里主要用来做一些 API 服务。 12 | 13 | #### 1. 相关链接 14 | 15 | * GitHub:[https://github.com/pallets/flask](https://github.com/pallets/flask) 16 | * 官方文档:[http://flask.pocoo.org](http://flask.pocoo.org) 17 | * 中文文档:[http://docs.jinkan.org/docs/flask](http://docs.jinkan.org/docs/flask) 18 | * PyPI:[https://pypi.python.org/pypi/Flask](https://pypi.python.org/pypi/Flask) 19 | 20 | #### 2. pip 安装 21 | 22 | 这里推荐使用 pip 安装,命令如下: 23 | 24 | ``` 25 | pip3 install flask 26 | ``` 27 | 28 | 运行完毕后,就完成安装了。 29 | 30 | #### 3. 验证安装 31 | 32 | 安装成功后,可以运行如下实例代码测试一下: 33 | 34 | ```python 35 | from flask import Flask 36 | app = Flask(__name__) 37 | 38 | @app.route("/") 39 | def hello(): 40 | return "Hello World!" 41 | 42 | if __name__ == "__main__": 43 | app.run() 44 | ``` 45 | 可以发现,系统会在 5000 端口开启 Web 服务,控制台输出如下: 46 | ``` 47 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 48 | ``` 49 | 直接访问 [http://127.0.0.1:5000/](http://127.0.0.1:5000/),可以观察到网页中呈现了 Hello World!,如图 1-40 所示,一个最简单的 Flask 程序就运行成功了。 50 | 51 | ![](../image/1-40.png) 52 | 53 | 图 1-40 运行结果 54 | 55 | #### 4. 结语 56 | 57 | 后面,我们会利用 Flask + Redis 维护动态代理池和 Cookies 池。 58 | 59 | ### 1.6.2 Tornado 的安装 60 | 61 | Tornado 是一个支持异步的 Web 框架,通过使用非阻塞 I/O 流,它可以支撑成千上万的开放连接,效率非常高,本节就来介绍一下它的安装方式。 62 | 63 | #### 1. 相关链接 64 | 65 | * GitHub:[https://github.com/tornadoweb/tornado](https://github.com/tornadoweb/tornado) 66 | * PyPI:[https://pypi.python.org/pypi/tornado](https://pypi.python.org/pypi/tornado) 67 | * 官方文档:[http://www.tornadoweb.org](http://www.tornadoweb.org) 68 | 69 | #### 2. pip 安装 70 | 71 | 这里推荐使用 pip 安装,相关命令如下: 72 | 73 | ``` 74 | pip3 install tornado 75 | ``` 76 | 77 | 执行完毕后,即可完成安装。 78 | 79 | #### 3. 验证安装 80 | 81 | 同样,这里也可以用一个 Hello World 程序测试一下,代码如下: 82 | 83 | ```python 84 | import tornado.ioloop 85 | import tornado.web 86 | 87 | class MainHandler(tornado.web.RequestHandler): 88 | def get(self): 89 | self.write("Hello, world") 90 | 91 | def make_app(): 92 | return tornado.web.Application([(r"/", MainHandler), 93 | ]) 94 | 95 | if __name__ == "__main__": 96 | app = make_app() 97 | app.listen(8888) 98 | tornado.ioloop.IOLoop.current().start() 99 | ``` 100 | 101 | 直接运行程序,可以发现系统在 8888 端口运行了 Web 服务,控制台没有输出内容,此时访问 http://127.0.0.1:8888/,可以观察到网页中呈现了 Hello,world,如图 1-41 所示,这就说明 Tornado 成功安装了。 102 | 103 | ![](../image/1-41.png) 104 | 105 | 图 1-41 运行结果 106 | 107 | #### 4. 结语 108 | 109 | 后面,我们会利用 Tornado + Redis 来搭建一个 ADSL 拨号代理池。 -------------------------------------------------------------------------------- /Chapter 10 模拟登录/10.0-模拟登录.md: -------------------------------------------------------------------------------- 1 | # 第十章 模拟登录 2 | 3 | 很多情况下,页面的某些信息需要登录才可以查看。对于爬虫来说,需要爬取的信息如果需要登录才可以看到的话,那么我们就需要做一些模拟登录的事情。 4 | 5 | 在前面我们了解了会话和 Cookies 的用法。简单来说,打开网页然后模拟登录,这实际上是在客户端生成了 Cookies,而 Cookies 里面保存了 SessionID 的信息,登录之后的后续请求都会携带生成后的 Cookies 发送给服务器。服务器就会根据 Cookies 判断出对应的 SessionID,进而找到会话。如果当前会话是有效的,那么服务器就判断用户当前已经登录了,返回请求的页面信息,这样我们就可以看到登录之后的页面。 6 | 7 | 这里的核心就是获取登录之后的 Cookies。而要获取 Cookies,我们可以手动在浏览器里输入用户密码,然后再把 Cookies 复制下来,但是这样做明显会增加人工工作量。爬虫的目的不就是自动化吗?所以我们要做的就是用程序来完成这个过程,也就是用程序模拟登录。 8 | 9 | 接下来,我们将介绍模拟登录的相关方法以及如何维护一个 Cookies 池。 -------------------------------------------------------------------------------- /Chapter 11 APP的爬取/11.0-APP的爬取.md: -------------------------------------------------------------------------------- 1 | # 第十一章 APP 的爬取 2 | 3 | 前文介绍的都是爬取 Web 网页的内容。随着移动互联网的发展,越来越多的企业并没有提供 Web 网页端的服务,而是直接开发了 App,更多更全的信息都是通过 App 来展示的。那么针对 App 我们可以爬取吗?当然可以。 4 | 5 | App 的爬取相比 Web 端爬取更加容易,反爬虫能力没有那么强,而且数据大多是以 JSON 形式传输的,解析更加简单。在 Web 端,我们可以通过浏览器的开发者工具监听到各个网络请求和响应过程,在 App 端如果想要查看这些内容就需要借助抓包软件。常用的抓包软件有 WireShark、Filddler、Charles、mitmproxy、AnyProxy 等,它们的原理基本是相同的。我们可以通过设置代理的方式将手机处于抓包软件的监听之下,这样便可以看到 App 在运行过程中发生的所有请求和响应了,相当于分析 Ajax 一样。如果这些请求的 URL、参数等都是有规律的,那么总结出规律直接用程序模拟爬取即可,如果它们没有规律,那么我们可以利用另一个工具 mitmdump 对接 Python 脚本直接处理 Response。另外,App 的爬取肯定不能由人来完成,也需要做到自动化,所以我们还要对 App 进行自动化控制,这里用到的库是 Appium。 6 | 7 | 本章将介绍 Charles、mitmproxy、mitmdump、Appium 等库的用法。掌握了这些内容,我们可以完成绝大多数 App 数据的爬取。 -------------------------------------------------------------------------------- /Chapter 11 APP的爬取/11.1-Charles的使用.md: -------------------------------------------------------------------------------- 1 | # 11.1 Charles 的使用 2 | 3 | Charles 是一个网络抓包工具,我们可以用它来做 App 的抓包分析,得到 App 运行过程中发生的所有网络请求和响应内容,这就和 Web 端浏览器的开发者工具 Network 部分看到的结果一致。 4 | 5 | 相比 Fiddler 来说,Charles 的功能更强大,而且跨平台支持更好。所以我们选用 Charles 作为主要的移动端抓包工具,用于分析移动 App 的数据包,辅助完成 App 数据抓取工作。 6 | 7 | ### 1. 本节目标 8 | 9 | 本节我们以京东 App 为例,通过 Charles 抓取 App 运行过程中的网络数据包,然后查看具体的 Request 和 Response 内容,以此来了解 Charles 的用法。 10 | 11 | ### 2. 准备工作 12 | 13 | 请确保已经正确安装 Charles 并开启了代理服务,手机和 Charles 处于同一个局域网下,Charles 代理和 CharlesCA 证书设置好,另外需要开启 SSL 监听,具体的配置可以参考第 1 章的说明。 14 | 15 | ### 3. 原理 16 | 17 | 首先 Charles 运行在自己的 PC 上,Charles 运行的时候会在 PC 的 8888 端口开启一个代理服务,这个服务实际上是一个 HTTP/HTTPS 的代理。 18 | 19 | 确保手机和 PC 在同一个局域网内,我们可以使用手机模拟器通过虚拟网络连接,也可以使用手机真机和 PC 通过无线网络连接。 20 | 21 | 设置手机代理为 Charles 的代理地址,这样手机访问互联网的数据包就会流经 Charles,Charles 再转发这些数据包到真实的服务器,服务器返回的数据包再由 Charles 转发回手机,Charles 就起到中间人的作用,所有流量包都可以捕捉到,因此所有 HTTP 请求和响应都可以捕获到。同时 Charles 还有权力对请求和响应进行修改。 22 | 23 | ### 4. 抓包 24 | 25 | 初始状态下 Charles 的运行界面如图 11-1 所示: 26 | 27 | ![](../image/11-1.png) 28 | 29 | 图 11-1 Charles 运行界面 30 | 31 | Charles 会一直监听 PC 和手机发生的网络数据包,捕获到的数据包就会显示在左侧,随着时间的推移,捕获的数据包越来越多,左侧列表的内容也会越来越多。 32 | 33 | 可以看到,图中左侧显示了 Charles 抓取到的请求站点,我们点击任意一个条目便可以查看对应请求的详细信息,其中包括 Request、Response 等内容。 34 | 35 | 接下来清空 Charles 的抓取结果,点击左侧的扫帚按钮即可清空当前捕获到的所有请求。然后点击第二个监听按钮,确保监听按钮是打开的,这表示 Charles 正在监听 App 的网络数据流,如图 11-2 所示。 36 | 37 | ![](../image/11-2.png) 38 | 39 | 图 11-2 监听过程 40 | 41 | 这时打开手机京东,注意一定要提前设置好 Charles 的代理并配置好 CA 证书,否则没有效果。 42 | 43 | 打开任意一个商品,如 iPhone,然后打开它的商品评论页面,如图 11-3 所示。 44 | 45 | ![](../image/11-3.png) 46 | 47 | 图 11-3 评论页面 48 | 49 | 不断上拉加载评论,可以看到 Charles 捕获到这个过程中京东 App 内发生的所有网络请求,如图 11-4 所示。 50 | 51 | ![](../image/11-4.png) 52 | 53 | 图 11-4 监听结果 54 | 55 | 左侧列表中会出现一个 api.m.jd.com 链接,而且它在不停闪动,很可能就是当前 App 发出的获取评论数据的请求被 Charles 捕获到了。我们点击将其展开,继续上拉刷新评论。随着上拉的进行,此处又会出现一个个网络请求记录,这时新出现的数据包请求确定就是获取评论的请求。 56 | 57 | 为了验证其正确性,我们点击查看其中一个条目的详情信息。切换到 Contents 选项卡,这时我们发现一些 JSON 数据,核对一下结果,结果有 commentData 字段,其内容和我们在 App 中看到的评论内容一致,如图 11-5 所示。 58 | 59 | ![](../image/11-5.png) 60 | 61 | 图 11-5 Json 数据结果 62 | 63 | 这时可以确定,此请求对应的接口就是获取商品评论的接口。这样我们就成功捕获到了在上拉刷新的过程中发生的请求和响应内容。 64 | 65 | ### 5. 分析 66 | 67 | 现在分析一下这个请求和响应的详细信息。首先可以回到 Overview 选项卡,上方显示了请求的接口 URL,接着是响应状态 Status Code、请求方式 Method 等,如图 11-6 所示。 68 | 69 | ![](../image/11-6.png) 70 | 71 | 图 11-6 监听结果 72 | 73 | 这个结果和原本在 Web 端用浏览器开发者工具内捕获到的结果形式是类似的。 74 | 75 | 接下来点击 Contents 选项卡,查看该请求和响应的详情信息。 76 | 77 | 上半部分显示的是 Request 的信息,下半部分显示的是 Response 的信息。比如针对 Reqeust,我们切换到 Headers 选项卡即可看到该 Request 的 Headers 信息,针对 Response,我们切换到 JSON TEXT 选项卡即可看到该 Response 的 Body 信息,并且该内容已经被格式化,如图 11-7 所示。 78 | 79 | ![](../image/11-7.png) 80 | 81 | 图 11-7 监听结果 82 | 83 | 由于这个请求是 POST 请求,所以我们还需要关心的就是 POST 的表单信息,切换到 Form 选项卡即可查看,如图 11-8 所示。 84 | 85 | ![](../image/11-8.png) 86 | 87 | 图 11-8 监听结果 88 | 89 | 这样我们就成功抓取 App 中的评论接口的请求和响应,并且可以查看 Response 返回的 JSON 数据。 90 | 91 | 至于其他 App,我们同样可以使用这样的方式来分析。如果我们可以直接分析得到请求的 URL 和参数的规律,直接用程序模拟即可批量抓取。 92 | 93 | ### 6. 重发 94 | 95 | Charles 还有一个强大功能,它可以将捕获到的请求加以修改并发送修改后的请求。点击上方的修改按钮,左侧列表就多了一个以编辑图标为开头的链接,这就代表此链接对应的请求正在被我们修改,如图 11-9 所示。 96 | 97 | ![](../image/11-9.png) 98 | 99 | 图 11-9 编辑页面 100 | 101 | 我们可以将 Form 中的某个字段移除,比如这里将 partner 字段移除,然后点击 Remove。这时我们已经对原来请求携带的 Form Data 做了修改,然后点击下方的 Execute 按钮即可执行修改后的请求,如图 11-10 所示。 102 | 103 | ![](../image/11-10.png) 104 | 105 | 图 11-10 编辑页面 106 | 107 | 可以发现左侧列表再次出现了接口的请求结果,内容仍然不变,如图 11-11 所示。 108 | 109 | ![](../image/11-11.png) 110 | 111 | 图 11-11 重新请求后结果 112 | 113 | 删除 Form 表单中的 partner 字段并没有带来什么影响,所以这个字段是无关紧要的。 114 | 115 | 有了这个功能,我们就可以方便地使用 Charles 来做调试,可以通过修改参数、接口等来测试不同请求的响应状态,就可以知道哪些参数是必要的哪些是不必要的,以及参数分别有什么规律,最后得到一个最简单的接口和参数形式以供程序模拟调用使用。 116 | 117 | 118 | ### 7. 结语 119 | 120 | 以上内容便是通过 Charles 抓包分析 App 请求的过程。通过 Charles,我们成功抓取 App 中流经的网络数据包,捕获原始的数据,还可以修改原始请求和重新发起修改后的请求进行接口测试。 121 | 122 | 知道了请求和响应的具体信息,如果我们可以分析得到请求的 URL 和参数的规律,直接用程序模拟即可批量抓取,这当然最好不过了。 123 | 124 | 但是随着技术的发展,App 接口往往会带有密钥,我们并不能直接找到这些规律,那么怎么办呢?接下来,我们将了解利用 Charles 和 mitmdump 直接对接 Python 脚本实时处理抓取到的 Response 的过程。 -------------------------------------------------------------------------------- /Chapter 11 APP的爬取/11.3-mitmdump爬取“得到”App电子书信息.md: -------------------------------------------------------------------------------- 1 | # 11.3 mitmdump 爬取 “得到” App 电子书信息 2 | 3 | “得到” App 是罗辑思维出品的一款碎片时间学习的 App,其官方网站为 https://www.igetget.com,App 内有很多学习资源。不过 “得到” App 没有对应的网页版,所以信息必须要通过 App 才可以获取。这次我们通过抓取其 App 来练习 mitmdump 的用法。 4 | 5 | ### 1. 爬取目标 6 | 7 | 我们的爬取目标是 App 内电子书版块的电子书信息,并将信息保存到 MongoDB,如图 11-30 所示。 8 | 9 | ![](../image/11-30.jpg) 10 | 11 | 我们要把图书的名称、简介、封面、价格爬取下来,不过这次爬取的侧重点还是了解 mitmdump 工具的用法,所以暂不涉及自动化爬取,App 的操作还是手动进行。mitmdump 负责捕捉响应并将数据提取保存。 12 | 13 | ### 2. 准备工作 14 | 15 | 请确保已经正确安装好了 mitmproxy 和 mitmdump,手机和 PC 处于同一个局域网下,同时配置好了 mitmproxy 的 CA 证书,安装好 MongoDB 并运行其服务,安装 PyMongo 库,具体的配置可以参考第 1 章的说明。 16 | 17 | ### 3. 抓取分析 18 | 19 | 首先探寻一下当前页面的 URL 和返回内容,我们编写一个脚本如下所示: 20 | 21 | ```python 22 | def response(flow): 23 | print(flow.request.url) 24 | print(flow.response.text) 25 | ``` 26 | 27 | 这里只输出了请求的 URL 和响应的 Body 内容,也就是请求链接和响应内容这两个最关键的部分。脚本保存名称为 script.py。 28 | 29 | 接下来运行 mitmdump,命令如下所示: 30 | 31 | ``` 32 | mitmdump -s script.py 33 | ``` 34 | 35 | 打开 “得到” App 的电子书页面,便可以看到 PC 端控制台有相应输出。接着滑动页面加载更多电子书,控制台新出现的输出内容就是 App 发出的新的加载请求,包含了下一页的电子书内容。控制台输出结果示例如图 11-31 所示。 36 | 37 | ![](../image/11-31.png) 38 | 39 | 图 11-31 控制台输出 40 | 41 | 可以看到 URL 为 https://dedao.igetget.com/v3/discover/bookList 的接口,其后面还加了一个 sign 参数。通过 URL 的名称,可以确定这就是获取电子书列表的接口。在 URL 的下方输出的是响应内容,是一个 JSON 格式的字符串,我们将它格式化,如图 11-32 所示。 42 | 43 | ![](../image/11-32.png) 44 | 45 | 图 11-32 格式化结果 46 | 47 | 格式化后的内容包含一个 c 字段、一个 list 字段,list 的每个元素都包含价格、标题、描述等内容。第一个返回结果是电子书《情人》,而此时 App 的内容也是这本电子书,描述的内容和价格也是完全匹配的,App 页面如图 11-33 所示。 48 | 49 | ![](../image/11-33.jpg) 50 | 51 | 图 11-33 APP 页面 52 | 53 | 这就说明当前接口就是获取电子书信息的接口,我们只需要从这个接口来获取内容就好了。然后解析返回结果,将结果保存到数据库。 54 | 55 | ### 4. 数据抓取 56 | 57 | 接下来我们需要对接口做过滤限制,抓取如上分析的接口,再提取结果中的对应字段。 58 | 59 | 这里,我们修改脚本如下所示: 60 | 61 | ```python 62 | import json 63 | from mitmproxy import ctx 64 | 65 | def response(flow): 66 | url = 'https://dedao.igetget.com/v3/discover/bookList' 67 | if flow.request.url.startswith(url): 68 | text = flow.response.text 69 | data = json.loads(text) 70 | books = data.get('c').get('list') 71 | for book in books: 72 | ctx.log.info(str(book)) 73 | ``` 74 | 75 | 重新滑动电子书页面,在 PC 端控制台观察输出,如图 11-34 所示。 76 | 77 | ![](../image/11-34.jpg) 78 | 79 | 图 11-34 控制台输出 80 | 81 | 现在输出了图书的全部信息,一本图书信息对应一条 JSON 格式的数据。 82 | 83 | ### 5. 提取保存 84 | 85 | 接下来我们需要提取信息,再把信息保存到数据库中。方便起见,我们选择 MongoDB 数据库。 86 | 87 | 脚本还可以增加提取信息和保存信息的部分,修改代码如下所示: 88 | 89 | ```python 90 | import json 91 | import pymongo 92 | from mitmproxy import ctx 93 | 94 | client = pymongo.MongoClient('localhost') 95 | db = client['igetget'] 96 | collection = db['books'] 97 | 98 | 99 | def response(flow): 100 | global collection 101 | url = 'https://dedao.igetget.com/v3/discover/bookList' 102 | if flow.request.url.startswith(url): 103 | text = flow.response.text 104 | data = json.loads(text) 105 | books = data.get('c').get('list') 106 | for book in books: 107 | data = {'title': book.get('operating_title'), 108 | 'cover': book.get('cover'), 109 | 'summary': book.get('other_share_summary'), 110 | 'price': book.get('price') 111 | } 112 | ctx.log.info(str(data)) 113 | collection.insert(data) 114 | ``` 115 | 116 | 重新滑动页面,控制台便会输出信息,如图 11-35 所示。 117 | 118 | ![](../image/11-35.jpg) 119 | 120 | 图 11-35 控制台输出 121 | 122 | 现在输出的每一条内容都是经过提取之后的内容,包含了电子书的标题、封面、描述、价格信息。 123 | 124 | 最开始我们声明了 MongoDB 的数据库连接,提取出信息之后调用该对象的 insert() 方法将数据插入到数据库即可。 125 | 126 | 滑动几页,发现所有图书信息都被保存到 MongoDB 中,如图 11-36 所示。 127 | 128 | ![](../image/11-36.jpg) 129 | 130 | 目前为止,我们利用一个非常简单的脚本把 “得到” App 的电子书信息保存下来。 131 | 132 | ### 6. 本节代码 133 | 134 | 本节的代码地址是:[https://github.com/Python3WebSpider/IGetGet](https://github.com/Python3WebSpider/IGetGet)。 135 | 136 | ### 7. 结语 137 | 138 | 本节主要讲解了 mitmdump 的用法及脚本的编写方法。通过本节的实例,我们可以学习到如何实时将 App 的数据抓取下来。 -------------------------------------------------------------------------------- /Chapter 12 pyspider框架的使用/12.0-pyspider框架的使用.md: -------------------------------------------------------------------------------- 1 | # 第十二章 pyspider 框架的使用 2 | 3 | 前文基本上把爬虫的流程实现一遍,将不同的功能定义成不同的方法,甚至抽象出模块的概念。如微信公众号爬虫,我们已经有了爬虫框架的雏形,如调度器、队列、请求对象等,但是它的架构和模块还是太简单,远远达不到一个框架的要求。如果我们将各个组件独立出来,定义成不同的模块,也就慢慢形成了一个框架。有了框架之后,我们就不必关心爬虫的全部流程,异常处理、任务调度等都会集成在框架中。我们只需要关心爬虫的核心逻辑部分即可,如页面信息的提取、下一步请求的生成等。这样,不仅开发效率会提高很多,而且爬虫的健壮性也更强。 4 | 5 | 在项目实战过程中,我们往往会采用爬虫框架来实现抓取,这样可提升开发效率、节省开发时间。pyspider 就是一个非常优秀的爬虫框架,它的操作便捷、功能强大,利用它我们可以快速方便地完成爬虫的开发。 -------------------------------------------------------------------------------- /Chapter 12 pyspider框架的使用/12.1-pyspider框架介绍.md: -------------------------------------------------------------------------------- 1 | # 12.1 pyspider 框架介绍 2 | 3 | pyspider 是由国人 binux 编写的强大的网络爬虫系统,其 GitHub 地址为 https://github.com/binux/pyspider,官方文档地址为 http://docs.pyspider.org/。 4 | 5 | pyspider 带有强大的 WebUI、脚本编辑器、任务监控器、项目管理器以及结果处理器,它支持多种数据库后端、多种消息队列、JavaScript 渲染页面的爬取,使用起来非常方便。 6 | 7 | ### 1. pyspider 基本功能 8 | 9 | 我们总结了一下,PySpider 的功能有如下几点。 10 | * 提供方便易用的 WebUI 系统,可以可视化地编写和调试爬虫。 11 | * 提供爬取进度监控、爬取结果查看、爬虫项目管理等功能。 12 | * 支持多种后端数据库,如 MySQL、MongoDB、Redis、SQLite、Elasticsearch、PostgreSQL。 13 | * 支持多种消息队列,如 RabbitMQ、Beanstalk、Redis、Kombu。 14 | * 提供优先级控制、失败重试、定时抓取等功能。 15 | * 对接了 PhantomJS,可以抓取 JavaScript 渲染的页面。 16 | * 支持单机和分布式部署,支持 Docker 部署。 17 | 18 | 如果想要快速方便地实现一个页面的抓取,使用 pyspider 不失为一个好的选择。 19 | 20 | ### 2. 与 Scrapy 的比较 21 | 22 | 后面会介绍另外一个爬虫框架 Scrapy,我们学习完 Scrapy 之后会更容易理解此部分内容。我们先了解一下 pyspider 与 Scrapy 的区别。 23 | * pyspider 提供了 WebUI,爬虫的编写、调试都是在 WebUI 中进行的,而 Scrapy 原生是不具备这个功能的,采用的是代码和命令行操作,但可以通过对接 Portia 实现可视化配置。 24 | * pyspider 调试非常方便,WebUI 操作便捷直观,在 Scrapy 中则是使用 parse 命令进行调试,论方便程度不及 pyspider。 25 | * pyspider 支持 PhantomJS 来进行 JavaScript 渲染页面的采集,在 Scrapy 中可以对接 ScrapySplash 组件,需要额外配置。 26 | * PySpide r 中内置了 PyQuery 作为选择器,在 Scrapy 中对接了 XPath、CSS 选择器和正则匹配。 27 | * pyspider 的可扩展程度不足,可配制化程度不高,在 Scrapy 中可以通过对接 Middleware、Pipeline、Extension 等组件实现非常强大的功能,模块之间的耦合程度低,可扩展程度极高。 28 | 29 | 如果要快速实现一个页面的抓取,推荐使用 pyspider,开发更加便捷,如快速抓取某个普通新闻网站的新闻内容。如果要应对反爬程度很强、超大规模的抓取,推荐使用 Scrapy,如抓取封 IP、封账号、高频验证的网站的大规模数据采集。 30 | 31 | ### 3. pyspider 的架构 32 | 33 | pyspider 的架构主要分为 Scheduler(调度器)、Fetcher(抓取器)、Processer(处理器)三个部分,整个爬取过程受到 Monitor(监控器)的监控,抓取的结果被 Result Worker(结果处理器)处理,如图 12-1 所示。 34 | 35 | ![](../image/12-1.jpg) 36 | 37 | 图 12-1 pyspider 架构图 38 | 39 | Scheduler 发起任务调度,Fetcher 负责抓取网页内容,Processer 负责解析网页内容,然后将新生成的 Request 发给 Scheduler 进行调度,将生成的提取结果输出保存。 40 | 41 | pyspider 的任务执行流程的逻辑很清晰,具体过程如下所示。 42 | - 每个 pyspider 的项目对应一个 Python 脚本,该脚本中定义了一个 Handler 类,它有一个 on_start() 方法。爬取首先调用 on_start() 方法生成最初的抓取任务,然后发送给 Scheduler 进行调度。 43 | 44 | - Scheduler 将抓取任务分发给 Fetcher 进行抓取,Fetcher 执行并得到响应,随后将响应发送给 Processer。 45 | 46 | - Processer 处理响应并提取出新的 URL 生成新的抓取任务,然后通过消息队列的方式通知 Schduler 当前抓取任务执行情况,并将新生成的抓取任务发送给 Scheduler。如果生成了新的提取结果,则将其发送到结果队列等待 Result Worker 处理。 47 | 48 | - Scheduler 接收到新的抓取任务,然后查询数据库,判断其如果是新的抓取任务或者是需要重试的任务就继续进行调度,然后将其发送回 Fetcher 进行抓取。 49 | 50 | - 不断重复以上工作,直到所有的任务都执行完毕,抓取结束。 51 | 52 | - 抓取结束后,程序会回调 on_finished() 方法,这里可以定义后处理过程。 53 | 54 | ### 4. 结语 55 | 56 | 本节我们主要了解了 pyspider 的基本功能和架构。接下来我们会用实例来体验一下 pyspider 的抓取操作,然后总结它的各种用法。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.0-Scrapy框架的使用.md: -------------------------------------------------------------------------------- 1 | # 第十三章 Scrapy 框架的使用 2 | 3 | 在上一章我们了解了 pyspider 框架的用法,我们可以利用它快速完成爬虫的编写。不过 pyspider 框架也有一些缺点,比如可配置化程度不高,异常处理能力有限等,它对于一些反爬程度非常强的网站的爬取显得力不从心。所以本章我们再介绍一个爬虫框架 Scrapy。 4 | 5 | Scrapy 功能非常强大,爬取效率高,相关扩展组件多,可配置和可扩展程度非常高,它几乎可以应对所有反爬网站,是目前 Python 中使用最广泛的爬虫框架。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.1-Scrapy框架介绍.md: -------------------------------------------------------------------------------- 1 | # 13.1 Scrapy 框架介绍 2 | 3 | Scrapy 是一个基于 Twisted 的异步处理框架,是纯 Python 实现的爬虫框架,其架构清晰,模块之间的耦合程度低,可扩展性极强,可以灵活完成各种需求。我们只需要定制开发几个模块就可以轻松实现一个爬虫。 4 | 5 | ### 1. 架构介绍 6 | 7 | 首先我们来看下 Scrapy 框架的架构,如图 13-1 所示: 8 | 9 | ![](../image/13-1.jpg) 10 | 11 | 图 13-1 Scrapy 架构 12 | 13 | 它可以分为如下的几个部分。 14 | 15 | * Engine,引擎,用来处理整个系统的数据流处理,触发事务,是整个框架的核心。 16 | * Item,项目,它定义了爬取结果的数据结构,爬取的数据会被赋值成该对象。 17 | * Scheduler, 调度器,用来接受引擎发过来的请求并加入队列中,并在引擎再次请求的时候提供给引擎。 18 | * Downloader,下载器,用于下载网页内容,并将网页内容返回给蜘蛛。 19 | * Spiders,蜘蛛,其内定义了爬取的逻辑和网页的解析规则,它主要负责解析响应并生成提取结果和新的请求。 20 | * Item Pipeline,项目管道,负责处理由蜘蛛从网页中抽取的项目,它的主要任务是清洗、验证和存储数据。 21 | * Downloader Middlewares,下载器中间件,位于引擎和下载器之间的钩子框架,主要是处理引擎与下载器之间的请求及响应。 22 | * Spider Middlewares, 蜘蛛中间件,位于引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛输入的响应和输出的结果及新的请求。 23 | 24 | ### 2. 数据流 25 | 26 | Scrapy 中的数据流由引擎控制,其过程如下: 27 | 28 | * Engine 首先打开一个网站,找到处理该网站的 Spider 并向该 Spider 请求第一个要爬取的 URL。 29 | * Engine 从 Spider 中获取到第一个要爬取的 URL 并通过 Scheduler 以 Request 的形式调度。 30 | * Engine 向 Scheduler 请求下一个要爬取的 URL。 31 | * Scheduler 返回下一个要爬取的 URL 给 Engine,Engine 将 URL 通过 Downloader Middlewares 转发给 Downloader 下载。 32 | * 一旦页面下载完毕, Downloader 生成一个该页面的 Response,并将其通过 Downloader Middlewares 发送给 Engine。 33 | * Engine 从下载器中接收到 Response 并通过 Spider Middlewares 发送给 Spider 处理。 34 | * Spider 处理 Response 并返回爬取到的 Item 及新的 Request 给 Engine。 35 | * Engine 将 Spider 返回的 Item 给 Item Pipeline,将新的 Request 给 Scheduler。 36 | * 重复第二步到最后一步,直到 Scheduler 中没有更多的 Request,Engine 关闭该网站,爬取结束。 37 | 38 | 通过多个组件的相互协作、不同组件完成工作的不同、组件对异步处理的支持,Scrapy 最大限度地利用了网络带宽,大大提高了数据爬取和处理的效率。 39 | 40 | ### 3. 项目结构 41 | 42 | Scrapy 框架和 pyspider 不同,它是通过命令行来创建项目的,代码的编写还是需要 IDE。项目创建之后,项目文件结构如下所示: 43 | 44 | ``` 45 | scrapy.cfg 46 | project/ 47 | __init__.py 48 | items.py 49 | pipelines.py 50 | settings.py 51 | middlewares.py 52 | spiders/ 53 | __init__.py 54 | spider1.py 55 | spider2.py 56 | ... 57 | ``` 58 | 59 | 在此要将各个文件的功能描述如下: 60 | 61 | - scrapy.cfg:它是 Scrapy 项目的配置文件,其内定义了项目的配置文件路径、部署相关信息等内容。 62 | 63 | - items.py:它定义 Item 数据结构,所有的 Item 的定义都可以放这里。 64 | 65 | - pipelines.py:它定义 Item Pipeline 的实现,所有的 Item Pipeline 的实现都可以放这里。 66 | 67 | - settings.py:它定义项目的全局配置。 68 | 69 | - middlewares.py:它定义 Spider Middlewares 和 Downloader Middlewares 的实现。 70 | 71 | - spiders:其内包含一个个 Spider 的实现,每个 Spider 都有一个文件。 72 | 73 | ### 4. 结语 74 | 75 | 本节介绍了 Scrapy 框架的基本架构、数据流过程以及项目结构。后面我们会详细了解 Scrapy 的用法,感受它的强大。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.11-Scrapyrt的使用.md: -------------------------------------------------------------------------------- 1 | # 13.11 Scrapyrt 的使用 2 | 3 | Scrapyrt 为 Scrapy 提供了一个调度的 HTTP 接口。有了它我们不需要再执行 Scrapy 命令,而是通过请求一个 HTTP 接口即可调度 Scrapy 任务,我们就不需要借助于命令行来启动项目了。如果项目是在远程服务器运行,利用它来启动项目是个不错的选择。 4 | 5 | ### 1. 本节目标 6 | 7 | 我们以本章 Scrapy 入门项目为例来说明 Scrapyrt 的使用方法,项目源代码地址为:[https://github.com/Python3WebSpider/ScrapyTutorial](https://github.com/Python3WebSpider/ScrapyTutorial)。 8 | 9 | ### 2. 准备工作 10 | 11 | 请确保 Scrapyrt 已经正确安装并正常运行,具体安装可以参考第 1 章的说明。 12 | 13 | ### 3. 启动服务 14 | 15 | 首先将项目下载下来,在项目目录下运行 Scrapyrt,假设当前服务运行在 9080 端口上。下面将简单介绍 Scrapyrt 的使用方法。 16 | 17 | ### 4. GET 请求 18 | 19 | 目前,GET 请求方式支持如下的参数。 20 | * spider_name,Spider 名称,字符串类型,必传参数,如果传递的 Spider 名称不存在则会返回 404 错误。 21 | * url,爬取链接,字符串类型,如果起始链接没有定义的话就必须要传递,如果传递了该参数,Scrapy 会直接用该 URL 生成 Request,而直接忽略 start_requests() 方法和 start_urls 属性的定义。 22 | * callback,回调函数名称,字符串类型,可选参数,如果传递了就会使用此回调函数处理,否则会默认使用 Spider 内定义的回调函数。 23 | * max_requests,最大请求数量,数值类型,可选参数,它定义了 Scrapy 执行请求的 Request 的最大限制,如定义为 5,则最多只执行 5 次 Request 请求,其余的则会被忽略。 24 | * start_requests,是否要执行 start_request() 函数,布尔类型,可选参数,在 Scrapy 项目中如果定义了 start_requests() 方法,那么在项目启动时会默认调用该方法,但是在 Scrapyrt 就不一样了,它默认不执行 start_requests() 方法,如果要执行,需要将它设置为 true。 25 | 26 | 例如我们执行如下命令: 27 | 28 | ``` 29 | curl http://localhost:9080/crawl.json?spider_name=quotes&url=http://quotes.toscrape.com/ 30 | ``` 31 | 32 | 得到类似如下结果,如图 13-28 所示: 33 | 34 | ![](../image/13-28.jpg) 35 | 36 | 图 13-28 输出结果 37 | 38 | 返回的是一个 JSON 格式的字符串,我们解析它的结构,如下所示: 39 | 40 | ```json 41 | { 42 | "status": "ok", 43 | "items": [ 44 | { 45 | "text": "“The world as we have created it is a process of o...", 46 | "author": "Albert Einstein", 47 | "tags": [ 48 | "change", 49 | "deep-thoughts", 50 | "thinking", 51 | "world" 52 | ] 53 | }, 54 | ... 55 | { 56 | "text": "“... a mind needs books as a sword needs a whetsto...", 57 | "author": "George R.R. Martin", 58 | "tags": [ 59 | "books", 60 | "mind" 61 | ] 62 | } 63 | ], 64 | "items_dropped": [], 65 | "stats": { 66 | "downloader/request_bytes": 2892, 67 | "downloader/request_count": 11, 68 | "downloader/request_method_count/GET": 11, 69 | "downloader/response_bytes": 24812, 70 | "downloader/response_count": 11, 71 | "downloader/response_status_count/200": 10, 72 | "downloader/response_status_count/404": 1, 73 | "dupefilter/filtered": 1, 74 | "finish_reason": "finished", 75 | "finish_time": "2017-07-12 15:09:02", 76 | "item_scraped_count": 100, 77 | "log_count/DEBUG": 112, 78 | "log_count/INFO": 8, 79 | "memusage/max": 52510720, 80 | "memusage/startup": 52510720, 81 | "request_depth_max": 10, 82 | "response_received_count": 11, 83 | "scheduler/dequeued": 10, 84 | "scheduler/dequeued/memory": 10, 85 | "scheduler/enqueued": 10, 86 | "scheduler/enqueued/memory": 10, 87 | "start_time": "2017-07-12 15:08:56" 88 | }, 89 | "spider_name": "quotes" 90 | } 91 | ``` 92 | 93 | 这里省略了 items 绝大部分。status 显示了爬取的状态,items 部分是 Scrapy 项目的爬取结果,items_dropped 是被忽略的 Item 列表,stats 是爬取结果的统计情况。此结果和直接运行 Scrapy 项目得到的统计是相同的。 94 | 95 | 这样一来,我们就通过 HTTP 接口调度 Scrapy 项目并获取爬取结果,如果 Scrapy 项目部署在服务器上,我们可以通过开启一个 Scrapyrt 服务实现任务的调度并直接取到爬取结果,这很方便。 96 | 97 | ### 5. POST 请求 98 | 99 | 除了 GET 请求,我们还可以通过 POST 请求来请求 Scrapyrt。但是此处 Request Body 必须是一个合法的 JSON 配置,在 JSON 里面可以配置相应的参数,支持的配置参数更多。 100 | 101 | 目前,JSON 配置支持如下参数。 102 | 103 | * **spider_name**:Spider 名称,字符串类型,必传参数。如果传递的 Spider 名称不存在,则返回 404 错误。 104 | 105 | * **max_requests**:最大请求数量,数值类型,可选参数。它定义了 Scrapy 执行请求的 Request 的最大限制,如定义为 5,则表示最多只执行 5 次 Request 请求,其余的则会被忽略。 106 | 107 | * **request**:Request 配置,JSON 对象,必传参数。通过该参数可以定义 Request 的各个参数,必须指定 url 字段来指定爬取链接,其他字段可选。 108 | 109 | 我们看一个 JSON 配置实例,如下所示: 110 | 111 | ```json 112 | { 113 | "request": { 114 | "url": "http://quotes.toscrape.com/", 115 | "callback": "parse", 116 | "dont_filter": "True", 117 | "cookies": {"foo": "bar"} 118 | }, 119 | "max_requests": 2, 120 | "spider_name": "quotes" 121 | } 122 | ``` 123 | 我们执行如下命令传递该 Json 配置并发起 POST 请求: 124 | ``` 125 | curl http://localhost:9080/crawl.json -d '{"request": {"url": "http://quotes.toscrape.com/", "dont_filter": "True", "callback": "parse", "cookies": {"foo": "bar"}}, "max_requests": 2, "spider_name": "quotes"}' 126 | ``` 127 | 128 | 运行结果和上文类似,同样是输出了爬取状态、结果、统计信息等内容。 129 | 130 | ### 6. 结语 131 | 132 | 以上内容便是 Scrapyrt 的相关用法介绍。通过它,我们方便地调度 Scrapy 项目的运行并获取爬取结果。更多的使用方法可以参考官方文档:http://scrapyrt.readthedocs.io。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.12-Scrapy对接Docker.md: -------------------------------------------------------------------------------- 1 | # 13.12 Scrapy 对接 Docker 2 | 3 | 环境配置问题可能一直是我们头疼的,我们可能遇到过如下的情况: 4 | * 我们在本地写好了一个 Scrapy 爬虫项目,想要把它放到服务器上运行,但是服务器上没有安装 Python 环境。 5 | * 别人给了我们一个 Scrapy 爬虫项目,项目中使用包的版本和我们本地环境版本不一致,无法直接运行。 6 | * 我们需要同时管理不同版本的 Scrapy 项目,如早期的项目依赖于 Scrapy 0.25,现在的项目依赖于 Scrapy 1.4.0。 7 | 8 | 在这些情况下,我们需要解决的就是环境的安装配置、环境的版本冲突解决等问题。 9 | 10 | 对于 Python 来说,VirtualEnv 的确可以解决版本冲突的问题。但是,VirtualEnv 不太方便做项目部署,我们还是需要安装 Python 环境, 11 | 12 | 如何解决上述问题呢?答案是用 Docker。Docker 可以提供操作系统级别的虚拟环境,一个 Docker 镜像一般都包含一个完整的操作系统,而这些系统内也有已经配置好的开发环境,如 Python 3.6 环境等。 13 | 14 | 我们可以直接使用此 Docker 的 Python 3 镜像运行一个容器,将项目直接放到容器里运行,就不用再额外配置 Python 3 环境。这样就解决了环境配置的问题。 15 | 16 | 我们也可以进一步将 Scrapy 项目制作成一个新的 Docker 镜像,镜像里只包含适用于本项目的 Python 环境。如果要部署到其他平台,只需要下载该镜像并运行就好了,因为 Docker 运行时采用虚拟环境,和宿主机是完全隔离的,所以也不需要担心环境冲突问题。 17 | 18 | 如果我们能够把 Scrapy 项目制作成一个 Docker 镜像,只要其他主机安装了 Docker,那么只要将镜像下载并运行即可,而不必再担心环境配置问题或版本冲突问题。 19 | 20 | 接下来,我们尝试把一个 Scrapy 项目制作成一个 Docker 镜像。 21 | 22 | ### 1. 本节目标 23 | 24 | 我们要实现把前文 Scrapy 的入门项目打包成一个 Docker 镜像的过程。项目爬取的网址为:[http://quotes.toscrape.com/](http://quotes.toscrape.com/),本章 Scrapy 入门一节已经实现了 Scrapy 对此站点的爬取过程,项目代码为:[https://github.com/Python3WebSpider/ScrapyTutorial](https://github.com/Python3WebSpider/ScrapyTutorial),如果本地不存在的话可以 Clone 下来。 25 | 26 | ### 2. 准备工作 27 | 28 | 请确保已经安装好 Docker 和 MongoDB 并可以正常运行,如果没有安装可以参考第 1 章的安装说明。 29 | 30 | ### 3. 创建 Dockerfile 31 | 32 | 首先在项目的根目录下新建一个 requirements.txt 文件,将整个项目依赖的 Python 环境包都列出来,如下所示: 33 | 34 | ``` 35 | scrapy 36 | pymongo 37 | ``` 38 | 如果库需要特定的版本,我们还可以指定版本号,如下所示: 39 | ``` 40 | scrapy>=1.4.0 41 | pymongo>=3.4.0 42 | ``` 43 | 在项目根目录下新建一个 Dockerfile 文件,文件不加任何后缀名,修改内容如下所示: 44 | ```Dockerfile 45 | FROM python:3.6 46 | ENV PATH /usr/local/bin:$PATH 47 | ADD .. /code 48 | WORKDIR /code 49 | RUN pip3 install -r requirements.txt 50 | CMD scrapy crawl quotes 51 | ``` 52 | 53 | 第一行的 FROM 代表使用的 Docker 基础镜像,在这里我们直接使用 python:3.6 的镜像,在此基础上运行 Scrapy 项目。 54 | 55 | 第二行 ENV 是环境变量设置,将 /usr/local/bin:$PATH 赋值给 PATH,即增加 /usr/local/bin 这个环境变量路径。 56 | 57 | 第三行 ADD 是将本地的代码放置到虚拟容器中。它有两个参数:第一个参数是.,代表本地当前路径;第二个参数是 /code,代表虚拟容器中的路径,也就是将本地项目所有内容放置到虚拟容器的 /code 目录下,以便于在虚拟容器中运行代码。 58 | 59 | 第四行 WORKDIR 是指定工作目录,这里将刚才添加的代码路径设成工作路径。这个路径下的目录结构和当前本地目录结构是相同的,所以我们可以直接执行库安装命令、爬虫运行命令等。 60 | 61 | 第五行 RUN 是执行某些命令来做一些环境准备工作。由于 Docker 虚拟容器内只有 Python 3 环境,而没有所需要的 Python 库,所以我们运行此命令来在虚拟容器中安装相应的 Python 库如 Scrapy,这样就可以在虚拟容器中执行 Scrapy 命令了。 62 | 63 | 第六行 CMD 是容器启动命令。在容器运行时,此命令会被执行。在这里我们直接用 scrapy crawl quotes 来启动爬虫。 64 | 65 | ### 4. 修改 MongoDB 连接 66 | 67 | 接下来我们需要修改 MongoDB 的连接信息。如果我们继续用 localhost 是无法找到 MongoDB 的,因为在 Docker 虚拟容器里 localhost 实际指向容器本身的运行 IP,而容器内部并没有安装 MongoDB,所以爬虫无法连接 MongoDB。 68 | 69 | 这里的 MongoDB 地址可以有如下两种选择。 70 | 71 | * 如果只想在本机测试,我们可以将地址修改为宿主机的 IP,也就是容器外部的本机 IP,一般是一个局域网 IP,使用 ifconfig 命令即可查看。 72 | 73 | * 如果要部署到远程主机运行,一般 MongoDB 都是可公网访问的地址,修改为此地址即可。 74 | 75 | 在本节中,我们的目标是将项目打包成一个镜像,让其他远程主机也可运行这个项目。所以我们直接将此处 MongoDB 地址修改为某个公网可访问的远程数据库地址,修改 MONGO_URI 如下所示: 76 | 77 | ```python 78 | MONGO_URI = 'mongodb://admin:admin123@120.27.34.25:27017' 79 | ``` 80 | 81 | 此处地址可以修改为自己的远程 MongoDB 数据库地址。 82 | 83 | 这样项目的配置就完成了。 84 | 85 | ### 5. 构建镜像 86 | 87 | 接下来我们便可以构建镜像了,执行如下命令: 88 | 89 | ``` 90 | docker build -t quotes:latest . 91 | ``` 92 | 93 | 这样的输出就说明镜像构建成功。这时我们查看一下构建的镜像,如下所示: 94 | 95 | ``` 96 | Sending build context to Docker daemon 191.5 kB 97 | Step 1/6 : FROM python:3.6 98 | ---> 968120d8cbe8 99 | Step 2/6 : ENV PATH /usr/local/bin:$PATH 100 | ---> Using cache 101 | ---> 387abbba1189 102 | Step 3/6 : ADD . /code 103 | ---> a844ee0db9c6 104 | Removing intermediate container 4dc41779c573 105 | Step 4/6 : WORKDIR /code 106 | ---> 619b2c064ae9 107 | Removing intermediate container bcd7cd7f7337 108 | Step 5/6 : RUN pip3 install -r requirements.txt 109 | ---> Running in 9452c83a12c5 110 | ... 111 | Removing intermediate container 9452c83a12c5 112 | Step 6/6 : CMD scrapy crawl quotes 113 | ---> Running in c092b5557ab8 114 | ---> c8101aca6e2a 115 | Removing intermediate container c092b5557ab8 116 | Successfully built c8101aca6e2a 117 | ``` 118 | 出现类似输出就证明镜像构建成功了,这时执行如我们查看一下构建的镜像: 119 | ``` 120 | docker images 121 | ``` 122 | 返回结果中其中有一行就是: 123 | ``` 124 | quotes latest 41c8499ce210 2 minutes ago 769 MB 125 | ``` 126 | 127 | 这就是我们新构建的镜像。 128 | 129 | ### 6. 运行 130 | 131 | 我们可以先在本地测试运行,执行如下命令: 132 | 133 | ``` 134 | docker run quotes 135 | ``` 136 | 137 | 这样我们就利用此镜像新建并运行了一个 Docker 容器,运行效果完全一致,如图 13-29 所示。 138 | 139 | ![](./assets/13-29.jpg) 140 | 141 | 图 13-32 运行结果 142 | 143 | 如果出现类似图 13-29 的运行结果,这就证明构建的镜像没有问题。 144 | 145 | ### 7. 推送至 Docker Hub 146 | 147 | 构建完成之后,我们可以将镜像 Push 到 Docker 镜像托管平台,如 Docker Hub 或者私有的 Docker Registry 等,这样我们就可以从远程服务器下拉镜像并运行了。 148 | 149 | 以 Docker Hub 为例,如果项目包含一些私有的连接信息(如数据库),我们最好将 Repository 设为私有或者直接放到私有的 Docker Registry。 150 | 151 | 首先在 https://hub.docker.com 注册一个账号,新建一个 Repository,名为 quotes。比如,我的用户名为 germey,新建的 Repository 名为 quotes,那么此 Repository 的地址就可以用 germey/quotes 来表示。 152 | 153 | 为新建的镜像打一个标签,命令如下所示: 154 | 155 | ``` 156 | docker tag quotes:latest germey/quotes:latest 157 | ``` 158 | 159 | 推送镜像到 Docker Hub 即可,命令如下所示: 160 | 161 | ``` 162 | docker push germey/quotes 163 | ``` 164 | 165 | Docker Hub 便会出现新推送的 Docker 镜像了,如图 13-30 所示。 166 | 167 | ![](./assets/13-30.png) 168 | 169 | 图 13-30 推送结果 170 | 171 | 如果我们想在其他的主机上运行这个镜像,主机上装好 Docker 后,可以直接执行如下命令: 172 | 173 | ``` 174 | docker run germey/quotes 175 | ``` 176 | 177 | 这样就会自动下载镜像,然后启动容器运行,不需要配置 Python 环境,不需要关心版本冲突问题。 178 | 179 | 运行效果如图 13-31 所示: 180 | 181 | ![](./assets/13-31.jpg) 182 | 183 | 图 13-31 运行效果 184 | 185 | 整个项目爬取完成后,数据就可以存储到指定的数据库中。 186 | 187 | ### 8. 结语 188 | 189 | 我们讲解了将 Scrapy 项目制作成 Docker 镜像并部署到远程服务器运行的过程。使用此种方式,我们在本节开头所列出的问题都迎刃而解。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.4-Spider的用法.md: -------------------------------------------------------------------------------- 1 | # 13.4 Spider 的用法 2 | 3 | 在 Scrapy 中,要抓取网站的链接配置、抓取逻辑、解析逻辑里其实都是在 Spider 中配置的。在前一节实例中,我们发现抓取逻辑也是在 Spider 中完成的。本节我们就来专门了解一下 Spider 的基本用法。 4 | 5 | ### 1. Spider 运行流程 6 | 7 | 在实现 Scrapy 爬虫项目时,最核心的类便是 Spider 类了,它定义了如何爬取某个网站的流程和解析方式。简单来讲,Spider 要做的事就是如下两件。 8 | 9 | * 定义爬取网站的动作 10 | * 分析爬取下来的网页 11 | 12 | 对于 Spider 类来说,整个爬取循环如下所述。 13 | * 以初始的 URL 初始化 Request,并设置回调函数。 当该 Request 成功请求并返回时,将生成 Response,并作为参数传给该回调函数。 14 | * 在回调函数内分析返回的网页内容。返回结果可以有两种形式,一种是解析到的有效结果返回字典或 Item 对象。下一步可经过处理后(或直接)保存,另一种是解析得下一个(如下一页)链接,可以利用此链接构造 Request 并设置新的回调函数,返回 Request。 15 | * 如果返回的是字典或 Item 对象,可通过 Feed Exports 等形式存入到文件,如果设置了 Pipeline 的话,可以经由 Pipeline 处理(如过滤、修正等)并保存。 16 | * 如果返回的是 Reqeust,那么 Request 执行成功得到 Response 之后会再次传递给 Request 中定义的回调函数,可以再次使用选择器来分析新得到的网页内容,并根据分析的数据生成 Item。 17 | 18 | 通过以上几步循环往复进行,便完成了站点的爬取。 19 | 20 | ### 2. Spider 类分析 21 | 22 | 在上一节的例子中我们定义的 Spider 是继承自 scrapy.spiders.Spider,这个类是最简单最基本的 Spider 类,每个其他的 Spider 必须继承这个类,还有后文要说明的一些特殊 Spider 类也都是继承自它。 23 | 24 | 这个类里提供了 start_requests() 方法的默认实现,读取并请求 start_urls 属性,并根据返回的结果调用 parse() 方法解析结果。另外它还有一些基础属性,下面对其进行讲解: 25 | 26 | * name,爬虫名称,是定义 Spider 名字的字符串。Spider 的名字定义了 Scrapy 如何定位并初始化 Spider,所以其必须是唯一的。 不过我们可以生成多个相同的 Spider 实例,这没有任何限制。 name 是 Spider 最重要的属性,而且是必须的。如果该 Spider 爬取单个网站,一个常见的做法是以该网站的域名名称来命名 Spider。 例如,如果 Spider 爬取 mywebsite.com ,该 Spider 通常会被命名为 mywebsite 。 27 | * allowed_domains,允许爬取的域名,是可选配置,不在此范围的链接不会被跟进爬取。 28 | * start_urls,起始 URL 列表,当我们没有实现 start_requests() 方法时,默认会从这个列表开始抓取。 29 | * custom_settings,这是一个字典,是专属于本 Spider 的配置,此设置会覆盖项目全局的设置,而且此设置必须在初始化前被更新,所以它必须定义成类变量。 30 | * crawler,此属性是由 from_crawler() 方法设置的,代表的是本 Spider 类对应的 Crawler 对象,Crawler 对象中包含了很多项目组件,利用它我们可以获取项目的一些配置信息,如最常见的就是获取项目的设置信息,即 Settings。 31 | * settings,是一个 Settings 对象,利用它我们可以直接获取项目的全局设置变量。 32 | 33 | 除了一些基础属性,Spider 还有一些常用的方法,在此介绍如下: 34 | 35 | * start_requests(),此方法用于生成初始请求,它必须返回一个可迭代对象,此方法会默认使用 start_urls 里面的 URL 来构造 Request,而且 Request 是 GET 请求方式。如果我们想在启动时以 POST 方式访问某个站点,可以直接重写这个方法,发送 POST 请求时我们使用 FormRequest 即可。 36 | * parse(),当 Response 没有指定回调函数时,该方法会默认被调用,它负责处理 Response,处理返回结果,并从中提取出想要的数据和下一步的请求,然后返回。该方法需要返回一个包含 Request 或 Item 的可迭代对象。 37 | * closed(),当 Spider 关闭时,该方法会被调用,在这里一般会定义释放资源的一些操作或其他收尾操作。 38 | 39 | ### 3. 结语 40 | 41 | 以上的介绍可能初看起来有点摸不清头脑,不过不用担心,后面我们会有很多实例来使用这些属性和方法,慢慢会熟练掌握的。 -------------------------------------------------------------------------------- /Chapter 13 Scrapy框架的使用/13.6-Spider Middleware的用法.md: -------------------------------------------------------------------------------- 1 | # 13.6 Spider Middleware 的用法 2 | 3 | Spider Middleware 是介入到 Scrapy 的 Spider 处理机制的钩子框架。我们首先来看看它的架构,如图 13-1 所示。 4 | 5 | 当 Downloader 生成 Response 之后,Response 会被发送给 Spider,在发送给 Spider 之前,Response 会首先经过 Spider Middleware 处理,当 Spider 处理生成 Item 和 Request 之后,Item 和 Request 还会经过 Spider Middleware 的处理。 6 | 7 | Spider Middleware 有如下三个作用。 8 | 9 | * 我们可以在 Downloader 生成的 Response 发送给 Spider 之前,也就是在 Response 发送给 Spider 之前对 Response 进行处理。 10 | 11 | * 我们可以在 Spider 生成的 Request 发送给 Scheduler 之前,也就是在 Request 发送给 Scheduler 之前对 Request 进行处理。 12 | 13 | * 我们可以在 Spider 生成的 Item 发送给 Item Pipeline 之前,也就是在 Item 发送给 Item Pipeline 之前对 Item 进行处理。 14 | 15 | ### 1. 使用说明 16 | 17 | 需要说明的是,Scrapy 其实已经提供了许多 Spider Middleware,它们被 SPIDER_MIDDLEWARES_BASE 这个变量所定义。 18 | 19 | SPIDER_MIDDLEWARES_BASE 变量的内容如下: 20 | 21 | ```python 22 | { 23 | 'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50, 24 | 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500, 25 | 'scrapy.spidermiddlewares.referer.RefererMiddleware': 700, 26 | 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800, 27 | 'scrapy.spidermiddlewares.depth.DepthMiddleware': 900, 28 | } 29 | ``` 30 | 31 | 和 Downloader Middleware 一样,Spider Middleware 首先加入到 SPIDER_MIDDLEWARES 设置中,该设置会和 Scrapy 中 SPIDER_MIDDLEWARES_BASE 定义的 Spider Middleware 合并。然后根据键值的数字优先级排序,得到一个有序列表。第一个 Middleware 是最靠近引擎的,最后一个 Middleware 是最靠近 Spider 的。 32 | 33 | ### 2. 核心方法 34 | 35 | Scrapy 内置的 Spider Middleware 为 Scrapy 提供了基础的功能。如果我们想要扩展其功能,只需要实现某几个方法即可。 36 | 37 | 每个 Spider Middleware 都定义了以下一个或多个方法的类,核心方法有如下 4 个。 38 | 39 | * process_spider_input(response, spider) 40 | * process_spider_output(response, result, spider) 41 | * process_spider_exception(response, exception, spider) 42 | * process_start_requests(start_requests, spider) 43 | 44 | 只需要实现其中一个方法就可以定义一个 Spider Middleware。下面我们来看看这 4 个方法的详细用法。 45 | 46 | #### process_spider_input(response, spider) 47 | 48 | 当 Response 通过 Spider Middleware 时,该方法被调用,处理该 Response。 49 | 50 | 方法的参数有两个: 51 | * response,即 Response 对象,即被处理的 Response 52 | * spider,即 Spider 对象,即该 response 对应的 Spider 53 | 54 | process_spider_input() 应该返回 None 或者抛出一个异常。 55 | 56 | * 如果其返回 None ,Scrapy 将会继续处理该 Response,调用所有其他的 Spider Middleware 直到 Spider 处理该 Response。 57 | 58 | * 如果其抛出一个异常,Scrapy 将不会调用任何其他 Spider Middlewar e 的 process_spider_input() 方法,并调用 Request 的 errback() 方法。 errback 的输出将会以另一个方向被重新输入到中间件中,使用 process_spider_output() 方法来处理,当其抛出异常时则调用 process_spider_exception() 来处理。 59 | 60 | #### process_spider_output(response, result, spider) 61 | 62 | 当 Spider 处理 Response 返回结果时,该方法被调用。 63 | 64 | 方法的参数有三个: 65 | * response,即 Response 对象,即生成该输出的 Response 66 | * result,包含 Request 或 Item 对象的可迭代对象,即 Spider 返回的结果 67 | * spider,即 Spider 对象,即其结果对应的 Spider 68 | 69 | process_spider_output() 必须返回包含 Request 或 Item 对象的可迭代对象。 70 | 71 | #### process_spider_exception(response, exception, spider) 72 | 73 | 当 Spider 或 Spider Middleware 的 process_spider_input() 方法抛出异常时, 该方法被调用。 74 | 75 | 方法的参数有三个: 76 | 77 | * response,即 Response 对象,即异常被抛出时被处理的 Response 78 | * exception,即 Exception 对象,被抛出的异常 79 | * spider,即 Spider 对象,即抛出该异常的 Spider 80 | 81 | process_spider_exception() 必须要么返回 None , 要么返回一个包含 Response 或 Item 对象的可迭代对象。 82 | 83 | * 如果其返回 None ,Scrapy 将继续处理该异常,调用其他 Spider Middleware 中的 process_spider_exception() 方法,直到所有 Spider Middleware 都被调用。 84 | * 如果其返回一个可迭代对象,则其他 Spider Middleware 的 process_spider_output() 方法被调用, 其他的 process_spider_exception() 将不会被调用。 85 | 86 | #### process_start_requests(start_requests, spider) 87 | 88 | 该方法以 Spider 启动的 Request 为参数被调用,执行的过程类似于 process_spider_output() ,只不过其没有相关联的 Response 并且必须返回 Request。 89 | 90 | 方法的参数有两个: 91 | * start_requests,即包含 Request 的可迭代对象,即 Start Requests 92 | * spider,即 Spider 对象,即 Start Requests 所属的 Spider 93 | 94 | 其必须返回另一个包含 Request 对象的可迭代对象。 95 | 96 | ### 3. 结语 97 | 98 | 本节介绍了 Spider Middleware 的基本原理和自定义 Spider Middleware 的方法。Spider Middleware 使用的频率不如 Downloader Middleware 的高,在必要的情况下它可以用来方便数据的处理。 -------------------------------------------------------------------------------- /Chapter 14 分布式爬虫/14.0-分布式爬虫.md: -------------------------------------------------------------------------------- 1 | # 第十四章 分布式爬虫 2 | 3 | 在上一章中,我们了解了 Scrapy 爬虫框架的用法。这些框架都是在同一台主机上运行的,爬取效率比较有限。如果多台主机协同爬取,那么爬取效率必然会成倍增长,这就是分布式爬虫的优势。 4 | 5 | 本章我们就来了解一下分布式爬虫的基本原理,以及 Scrapy 实现分布式爬虫的流程。 -------------------------------------------------------------------------------- /Chapter 14 分布式爬虫/14.1-分布式爬虫理念.md: -------------------------------------------------------------------------------- 1 | # 14.1 分布式爬虫原理 2 | 3 | 我们在前面已经实现了 Scrapy 微博爬虫,虽然爬虫是异步加多线程的,但是我们只能在一台主机上运行,所以爬取效率还是有限的,分布式爬虫则是将多台主机组合起来,共同完成一个爬取任务,这将大大提高爬取的效率。 4 | 5 | ### 1. 分布式爬虫架构 6 | 7 | 在了解分布式爬虫架构之前,首先回顾一下 Scrapy 的架构,如图 13-1 所示。 8 | 9 | Scrapy 单机爬虫中有一个本地爬取队列 Queue,这个队列是利用 deque 模块实现的。如果新的 Request 生成就会放到队列里面,随后 Request 被 Scheduler 调度。之后,Request 交给 Downloader 执行爬取,简单的调度架构如图 14-1 所示。 10 | 11 | ![](../image/14-1.jpg) 12 | 13 | 图 14-1 调度架构 14 | 15 | 如果两个 Scheduler 同时从队列里面取 Request,每个 Scheduler 都有其对应的 Downloader,那么在带宽足够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错,爬取效率会翻倍。 16 | 17 | 这样,Scheduler 可以扩展多个,Downloader 也可以扩展多个。而爬取队列 Queue 必须始终为一个,也就是所谓的共享爬取队列。这样才能保证 Scheduer 从队列里调度某个 Request 之后,其他 Scheduler 不会重复调度此 Request,就可以做到多个 Schduler 同步爬取。这就是分布式爬虫的基本雏形,简单调度架构如图 14-2 所示。 18 | 19 | ![](../image/14-2.jpg) 20 | 21 | 图 14-2 调度架构 22 | 23 | 我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不需要各自维护爬取队列,而是从共享爬取队列存取 Request。但是各台主机还是有各自的 Scheduler 和 Downloader,所以调度和下载功能分别完成。如果不考虑队列存取性能消耗,爬取效率还是会成倍提高。 24 | 25 | ### 2. 维护爬取队列 26 | 27 | 那么这个队列用什么维护来好呢?我们首先需要考虑的就是性能问题,什么数据库存取效率高?我们自然想到基于内存存储的 Redis,而且 Redis 还支持多种数据结构,例如列表 List、集合 Set、有序集合 Sorted Set 等等,存取的操作也非常简单,所以在这里我们采用 Redis 来维护爬取队列。 28 | 29 | 这几种数据结构存储实际各有千秋,分析如下: 30 | * 列表数据结构有 lpush()、lpop()、rpush()、rpop() 方法,所以我们可以用它来实现一个先进先出式爬取队列,也可以实现一个先进后出栈式爬取队列。 31 | * 集合的元素是无序的且不重复的,这样我们可以非常方便地实现一个随机排序的不重复的爬取队列。 32 | * 有序集合带有分数表示,而 Scrapy 的 Request 也有优先级的控制,所以用有集合我们可以实现一个带优先级调度的队列。 33 | 34 | 这些不同的队列我们需要根据具体爬虫的需求灵活选择。 35 | 36 | ### 3. 怎样来去重 37 | 38 | Scrapy 有自动去重,它的去重使用了 Python 中的集合。这个集合记录了 Scrapy 中每个 Request 的指纹,这个指纹实际上就是 Request 的散列值。我们可以看看 Scrapy 的源代码,如下所示: 39 | 40 | ```python 41 | import hashlib 42 | def request_fingerprint(request, include_headers=None): 43 | if include_headers: 44 | include_headers = tuple(to_bytes(h.lower()) 45 | for h in sorted(include_headers)) 46 | cache = _fingerprint_cache.setdefault(request, {}) 47 | if include_headers not in cache: 48 | fp = hashlib.sha1() 49 | fp.update(to_bytes(request.method)) 50 | fp.update(to_bytes(canonicalize_url(request.url))) 51 | fp.update(request.body or b'') 52 | if include_headers: 53 | for hdr in include_headers: 54 | if hdr in request.headers: 55 | fp.update(hdr) 56 | for v in request.headers.getlist(hdr): 57 | fp.update(v) 58 | cache[include_headers] = fp.hexdigest() 59 | return cache[include_headers] 60 | ``` 61 | 62 | request_fingerprint() 就是计算 Request 指纹的方法,其方法内部使用的是 hashlib 的 sha1() 方法。计算的字段包括 Request 的 Method、URL、Body、Headers 这几部分内容,这里只要有一点不同,那么计算的结果就不同。计算得到的结果是加密后的字符串,也就是指纹。每个 Request 都有独有的指纹,指纹就是一个字符串,判定字符串是否重复比判定 Request 对象是否重复容易得多,所以指纹可以作为判定 Request 是否重复的依据。 63 | 64 | 那么我们如何判定重复呢?Scrapy 是这样实现的,如下所示: 65 | 66 | ```python 67 | def __init__(self): 68 | self.fingerprints = set() 69 | 70 | def request_seen(self, request): 71 | fp = self.request_fingerprint(request) 72 | if fp in self.fingerprints: 73 | return True 74 | self.fingerprints.add(fp) 75 | ``` 76 | 77 | 在去重的类 RFPDupeFilter 中,有一个 request_seen() 方法,这个方法有一个参数 request,它的作用就是检测该 Request 对象是否重复。这个方法调用 request_fingerprint() 获取该 Request 的指纹,检测这个指纹是否存在于 fingerprints 变量中,而 fingerprints 是一个集合,集合的元素都是不重复的。如果指纹存在,那么就返回 True,说明该 Request 是重复的,否则这个指纹加入到集合中。如果下次还有相同的 Request 传递过来,指纹也是相同的,那么这时指纹就已经存在于集合中,Request 对象就会直接判定为重复。这样去重的目的就实现了。 78 | 79 | Scrapy 的去重过程就是,利用集合元素的不重复特性来实现 Request 的去重。 80 | 81 | 对于分布式爬虫来说,我们肯定不能再用每个爬虫各自的集合来去重了。因为这样还是每个主机单独维护自己的集合,不能做到共享。多台主机如果生成了相同的 Request,只能各自去重,各个主机之间就无法做到去重了。 82 | 83 | 那么要实现去重,这个指纹集合也需要是共享的,Redis 正好有集合的存储数据结构,我们可以利用 Redis 的集合作为指纹集合,那么这样去重集合也是利用 Redis 共享的。每台主机新生成 Request 之后,把该 Request 的指纹与集合比对,如果指纹已经存在,说明该 Request 是重复的,否则将 Request 的指纹加入到这个集合中即可。利用同样的原理不同的存储结构我们也实现了分布式 Reqeust 的去重。 84 | 85 | ### 4. 防止中断 86 | 87 | 在 Scrapy 中,爬虫运行时的 Request 队列放在内存中。爬虫运行中断后,这个队列的空间就被释放,此队列就被销毁了。所以一旦爬虫运行中断,爬虫再次运行就相当于全新的爬取过程。 88 | 89 | 要做到中断后继续爬取,我们可以将队列中的 Request 保存起来,下次爬取直接读取保存数据即可获取上次爬取的队列。我们在 Scrapy 中指定一个爬取队列的存储路径即可,这个路径使用 JOB_DIR 变量来标识,我们可以用如下命令来实现: 90 | 91 | ``` 92 | scrapy crawl spider -s JOBDIR=crawls/spider 93 | ``` 94 | 95 | 更加详细的使用方法可以参见官方文档,链接为:https://doc.scrapy.org/en/latest/topics/jobs.html。 96 | 97 | 在 Scrapy 中,我们实际是把爬取队列保存到本地,第二次爬取直接读取并恢复队列即可。那么在分布式架构中我们还用担心这个问题吗?不需要。因为爬取队列本身就是用数据库保存的,如果爬虫中断了,数据库中的 Request 依然是存在的,下次启动就会接着上次中断的地方继续爬取。 98 | 99 | 所以,当 Redis 的队列为空时,爬虫会重新爬取;当 Redis 的队列不为空时,爬虫便会接着上次中断之处继续爬取。 100 | 101 | ### 5. 架构实现 102 | 103 | 我们接下来就需要在程序中实现这个架构了。首先实现一个共享的爬取队列,还要实现去重的功能。另外,重写一个 Scheduer 的实现,使之可以从共享的爬取队列存取 Request。 104 | 105 | 幸运的是,已经有人实现了这些逻辑和架构,并发布成叫 Scrapy-Redis 的 Python 包。接下来,我们看看 Scrapy-Redis 的源码实现,以及它的详细工作原理。 -------------------------------------------------------------------------------- /Chapter 14 分布式爬虫/14.3-Scrapy分布式实现.md: -------------------------------------------------------------------------------- 1 | # 14.3 Scrapy 分布式实现 2 | 3 | 接下来,我们会利用 Scrapy-Redis 来实现分布式的对接。 4 | 5 | ### 1. 准备工作 6 | 7 | 请确保已经成功实现了 Scrapy 新浪微博爬虫,Scrapy-Redis 库已经正确安装,如果还没安装,请参考第 1 章的安装说明。 8 | 9 | ### 2. 搭建 Redis 服务器 10 | 11 | 要实现分布式部署,多台主机需要共享爬取队列和去重集合,而这两部分内容都是存于 Redis 数据库中的,我们需要搭建一个可公网访问的 Redis 服务器。 12 | 13 | 推荐使用 Linux 服务器,可以购买阿里云、腾讯云、Azure 等提供的云主机,一般都会配有公网 IP,具体的搭建方式可以参考第 1 章中 Redis 数据库的安装方式。 14 | 15 | Redis 安装完成之后就可以远程连接了,注意部分商家(如阿里云、腾讯云)的服务器需要配置安全组放通 Redis 运行端口才可以远程访问。如果遇到不能远程连接的问题,可以排查安全组的设置。 16 | 17 | 需要记录 Redis 的运行 IP、端口、地址,供后面配置分布式爬虫使用。当前配置好的 Redis 的 IP 为服务器的 IP 120.27.34.25,端口为默认的 6379,密码为 foobared。 18 | 19 | ### 3. 部署代理池和 Cookies 池 20 | 21 | 新浪微博项目需要用到代理池和 Cookies 池,而之前我们的代理池和 Cookies 池都是在本地运行的。所以我们需要将二者放到可以被公网访问的服务器上运行,将代码上传到服务器,修改 Redis 的连接信息配置,用同样的方式运行代理池和 Cookies 池。 22 | 23 | 远程访问代理池和 Cookies 池提供的接口,来获取随机代理和 Cookies。如果不能远程访问,先确保其在 0.0.0.0 这个 Host 上运行,再检查安全组的配置。 24 | 25 | 如我当前配置好的代理池和 Cookies 池的运行 IP 都是服务器的 IP,120.27.34.25,端口分别为 5555 和 5556,如图 14-3 和图 14-4 所示。 26 | 27 | ![](../image/14-3.jpg) 28 | 29 | 图 14-3 代理池接口 30 | 31 | ![](../image/14-4.jpg) 32 | 33 | 图 14-4 Cookies 池接口 34 | 35 | 所以接下来我们就需要把 Scrapy 新浪微博项目中的访问链接修改如下: 36 | 37 | ```python 38 | PROXY_URL = 'http://120.27.34.25:5555/random' 39 | COOKIES_URL = 'http://120.27.34.25:5556/weibo/random' 40 | ``` 41 | 42 | 具体的修改方式根据实际配置的 IP 和端口做相应调整。 43 | 44 | ### 4. 配置 Scrapy-Redis 45 | 46 | 配置 Scrapy-Redis 非常简单,只需要修改一下 settings.py 配置文件即可。 47 | 48 | #### 核心配置 49 | 50 | 首先最主要的是,需要将调度器的类和去重的类替换为 Scrapy-Redis 提供的类,在 settings.py 里面添加如下配置即可: 51 | 52 | ```python 53 | SCHEDULER = "scrapy_redis.scheduler.Scheduler" 54 | DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" 55 | ``` 56 | 57 | #### Redis 连接配置 58 | 59 | 接下来配置 Redis 的连接信息,这里有两种配置方式。 60 | 61 | 第一种方式是通过连接字符串配置。我们可以用 Redis 的地址、端口、密码来构造一个 Redis 连接字符串,支持的连接形式如下所示: 62 | 63 | ```python 64 | redis://[:password]@host:port/db 65 | rediss://[:password]@host:port/db 66 | unix://[:password]@/path/to/socket.sock?db=db 67 | ``` 68 | 69 | password 是密码,比如要以冒号开头,中括号代表此选项可有可无,host 是 Redis 的地址,port 是运行端口,db 是数据库代号,其值默认是 0。 70 | 71 | 根据上文中提到我的 Redis 连接信息,构造这个 Redis 的连接字符串如下所示: 72 | 73 | ```python 74 | redis://:foobared@120.27.34.25:6379 75 | ``` 76 | 直接在 settings.py 里面配置为 REDIS_URL 变量即可: 77 | ```python 78 | REDIS_URL = 'redis://:foobared@120.27.34.25:6379' 79 | ``` 80 | 第二种配置方式是分项单独配置。这个配置就更加直观明了,如根据我的 Redis 连接信息,可以在 settings.py 中配置如下代码: 81 | ```python 82 | REDIS_HOST = '120.27.34.25' 83 | REDIS_PORT = 6379 84 | REDIS_PASSWORD = 'foobared' 85 | ``` 86 | 87 | 这段代码分开配置了 Redis 的地址、端口和密码。 88 | 89 | 注意,如果配置了 REDIS_URL,那么 Scrapy-Redis 将优先使用 REDIS_URL 连接,会覆盖上面的三项配置。如果想要分项单独配置的话,请不要配置 REDIS_URL。 90 | 91 | 在本项目中,我选择的是配置 REDIS_URL。 92 | 93 | #### 配置调度队列 94 | 95 | 此项配置是可选的,默认使用 PriorityQueue。如果想要更改配置,可以配置 SCHEDULER_QUEUE_CLASS 变量,如下所示: 96 | 97 | ```python 98 | SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' 99 | SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue' 100 | SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue' 101 | ``` 102 | 103 | 以上三行任选其一配置,即可切换爬取队列的存储方式。 104 | 105 | 在本项目中不进行任何配置,我们使用默认配置。 106 | 107 | #### 配置持久化 108 | 109 | 此配置是可选的,默认是 False。Scrapy-Redis 默认会在爬取全部完成后清空爬取队列和去重指纹集合。 110 | 111 | 如果不想自动清空爬取队列和去重指纹集合,可以增加如下配置: 112 | 113 | ```python 114 | SCHEDULER_PERSIST = True 115 | ``` 116 | 117 | 将 SCHEDULER_PERSIST 设置为 True 之后,爬取队列和去重指纹集合不会在爬取完成后自动清空,如果不配置,默认是 False,即自动清空。 118 | 119 | 值得注意的是,如果强制中断爬虫的运行,爬取队列和去重指纹集合是不会自动清空的。 120 | 121 | 在本项目中不进行任何配置,我们使用默认配置。 122 | 123 | #### 配置重爬 124 | 125 | 此配置是可选的,默认是 False。如果配置了持久化或者强制中断了爬虫,那么爬取队列和指纹集合不会被清空,爬虫重新启动之后就会接着上次爬取。如果想重新爬取,我们可以配置重爬的选项: 126 | 127 | ```python 128 | SCHEDULER_FLUSH_ON_START = True 129 | ``` 130 | 131 | 这样将 SCHEDULER_FLUSH_ON_START 设置为 True 之后,爬虫每次启动时,爬取队列和指纹集合都会清空。所以要做分布式爬取,我们必须保证只能清空一次,否则每个爬虫任务在启动时都清空一次,就会把之前的爬取队列清空,势必会影响分布式爬取。 132 | 133 | 注意,此配置在单机爬取的时候比较方便,分布式爬取不常用此配置。 134 | 135 | 在本项目中不进行任何配置,我们使用默认配置。 136 | 137 | #### Pipeline 配置 138 | 139 | 此配置是可选的,默认不启动 Pipeline。Scrapy-Redis 实现了一个存储到 Redis 的 Item Pipeline,启用了这个 Pipeline 的话,爬虫会把生成的 Item 存储到 Redis 数据库中。在数据量比较大的情况下,我们一般不会这么做。因为 Redis 是基于内存的,我们利用的是它处理速度快的特性,用它来做存储未免太浪费了,配置如下: 140 | 141 | ```python 142 | ITEM_PIPELINES = {'scrapy_redis.pipelines.RedisPipeline': 300} 143 | ``` 144 | 145 | 本项目不进行任何配置,即不启动 Pipeline。 146 | 147 | 到此为止,Scrapy-Redis 的配置就完成了。有的选项我们没有配置,但是这些配置在其他 Scrapy 项目中可能用到,要根据具体情况而定。 148 | 149 | ### 5. 配置存储目标 150 | 151 | 之前 Scrapy 新浪微博爬虫项目使用的存储是 MongoDB,而且 MongoDB 是本地运行的,即连接的是 localhost。但是,当爬虫程序分发到各台主机运行的时候,爬虫就会连接各自的的 MongoDB。所以我们需要在各台主机上都安装 MongoDB,这样有两个缺点:一是搭建 MongoDB 环境比较烦琐;二是这样各台主机的爬虫会把爬取结果分散存到各自主机上,不方便统一管理。 152 | 153 | 所以我们最好将存储目标存到同一个地方,例如都存到同一个 MongoDB 数据库中。我们可以在服务器上搭建一个 MongoDB 服务,或者直接购买 MongoDB 数据存储服务。 154 | 155 | 这里使用的就是服务器上搭建的的 MongoDB 服务,IP 仍然为 120.27.34.25,用户名为 admin,密码为 admin123。 156 | 157 | 修改配置 MONGO_URI 为如下: 158 | 159 | ```python 160 | MONGO_URI = 'mongodb://admin:admin123@120.27.34.25:27017' 161 | ``` 162 | 163 | 到此为止,我们就成功完成了 Scrapy 分布式爬虫的配置了。 164 | 165 | ### 6. 运行 166 | 167 | 接下来将代码部署到各台主机上,记得每台主机都需要配好对应的 Python 环境。 168 | 169 | 每台主机上都执行如下命令,即可启动爬取: 170 | 171 | ``` 172 | scrapy crawl weibocn 173 | ``` 174 | 175 | 每台主机启动了此命令之后,就会从配置的 Redis 数据库中调度 Request,做到爬取队列共享和指纹集合共享。同时每台主机占用各自的带宽和处理器,不会互相影响,爬取效率成倍提高。 176 | 177 | ### 7. 结果 178 | 179 | 一段时间后,我们可以用 RedisDesktop 观察远程 Redis 数据库的信息。这里会出现两个 Key:一个叫作 weibocn:dupefilter,用来储存指纹;另一个叫作 weibocn:requests,即爬取队列,如图 14-5 和图 14-6 所示。 180 | 181 | ![](../image/14-5.jpg) 182 | 183 | 图 14-5 去重指纹 184 | 185 | ![](../image/14-6.jpg) 186 | 187 | 图 14-6 爬取队列 188 | 189 | 随着时间的推移,指纹集合会不断增长,爬取队列会动态变化,爬取的数据也会被储存到 MongoDB 数据库中。 190 | 191 | 至此 Scrapy 分布式的配置已全部完成。 192 | 193 | ### 8. 本节代码 194 | 195 | 本节代码地址为:[https://github.com/Python3WebSpider/Weibo/tree/distributed](https://github.com/Python3WebSpider/Weibo/tree/distributed),注意这里是 distributed 分支。 196 | 197 | ### 9. 结语 198 | 199 | 本节通过对接 Scrapy-Redis 成功实现了分布式爬虫,但是部署还是有很多不方便的地方。另外,如果爬取量特别大的话,Redis 的内存也是个问题。在后文我们会继续了解相关优化方案。 -------------------------------------------------------------------------------- /Chapter 15 分布式爬虫的部署/15.0-分布式爬虫的部署.md: -------------------------------------------------------------------------------- 1 | # 第十五章 分布式爬虫的部署 2 | 3 | 在前一章我们成功实现了分布式爬虫,但是在这个过程中我们发现有很多不方便的地方。 4 | 5 | 在将 Scrapy 项目放到各台主机运行时,你可能采用的是文件上传或者 Git 同步的方式,但这样需要各台主机都进行操作,如果有 100 台、1000 台主机,那工作量可想而知。 6 | 7 | 本章我们就来了解一下,分布式爬虫部署方面可以采取的一些措施,以方便地实现批量部署和管理。 -------------------------------------------------------------------------------- /Chapter 15 分布式爬虫的部署/15.2-Scrapyd-Client的使用.md: -------------------------------------------------------------------------------- 1 | # 15.2 Scrapyd-Client 的使用 2 | 3 | 这里有现成的工具来完成部署过程,它叫作 Scrapyd-Client。本节将简单介绍使用 Scrapyd-Client 部署 Scrapy 项目的方法。 4 | 5 | ### 1. 准备工作 6 | 7 | 请先确保 Scrapyd-Client 已经正确安装,安装方式可以参考第 1 章的内容。 8 | 9 | ### 2. Scrapyd-Client 的功能 10 | 11 | Scrapyd-Client 为了方便 Scrapy 项目的部署,提供两个功能: 12 | * 将项目打包成 Egg 文件。 13 | * 将打包生成的 Egg 文件通过 addversion.json 接口部署到 Scrapyd 上。 14 | 15 | 也就是说,Scrapyd-Client 帮我们把部署全部实现了,我们不需要再去关心 Egg 文件是怎样生成的,也不需要再去读 Egg 文件并请求接口上传了,这一切的操作只需要执行一个命令即可一键部署。 16 | 17 | ### 3. Scrapyd-Client 部署 18 | 19 | 要部署 Scrapy 项目,我们首先需要修改一下项目的配置文件,例如我们之前写的 Scrapy 微博爬虫项目,在项目的第一层会有一个 scrapy.cfg 文件,它的内容如下: 20 | 21 | ```ini 22 | [settings] 23 | default = weibo.settings 24 | 25 | [deploy] 26 | #url = http://localhost:6800/ 27 | project = weibo 28 | ``` 29 | 在这里我们需要配置一下 deploy 部分,例如我们要将项目部署到 120.27.34.25 的 Scrapyd 上,就需要修改为如下内容: 30 | ```ini 31 | [deploy] 32 | url = http://120.27.34.25:6800/ 33 | project = weibo 34 | ``` 35 | 这样我们再在 scrapy.cfg 文件所在路径执行如下命令: 36 | ``` 37 | scrapyd-deploy 38 | ``` 39 | 运行结果如下: 40 | ``` 41 | Packing version 1501682277 42 | Deploying to project "weibo" in http://120.27.34.25:6800/addversion.json 43 | Server response (200): 44 | {"status": "ok", "spiders": 1, "node_name": "datacrawl-vm", "project": "weibo", "version": "1501682277"} 45 | ``` 46 | 47 | 返回这样的结果就代表部署成功了。 48 | 49 | 我们也可以指定项目版本,如果不指定的话默认为当前时间戳,指定的话通过 version 参数传递即可,例如: 50 | 51 | ``` 52 | scrapyd-deploy --version 201707131455 53 | ``` 54 | 55 | 值得注意的是在 Python3 的 Scrapyd 1.2.0 版本中我们不要指定版本号为带字母的字符串,需要为纯数字,否则可能会出现报错。 56 | 57 | 另外如果我们有多台主机,我们可以配置各台主机的别名,例如可以修改配置文件为: 58 | 59 | ```ini 60 | [deploy:vm1] 61 | url = http://120.27.34.24:6800/ 62 | project = weibo 63 | 64 | [deploy:vm2] 65 | url = http://139.217.26.30:6800/ 66 | project = weibo 67 | ``` 68 | 有多台主机的话就在此统一配置,一台主机对应一组配置,在 deploy 后面加上主机的别名即可,这样如果我们想将项目部署到 IP 为 139.217.26.30 的 vm2 主机,我们只需要执行如下命令: 69 | ``` 70 | scrapyd-deploy vm2 71 | ``` 72 | 73 | 这样我们就可以将项目部署到名称为 vm2 的主机上了。 74 | 75 | 如此一来,如果我们有多台主机,我们只需要在 scrapy.cfg 文件中配置好各台主机的 Scrapyd 地址,然后调用 scrapyd-deploy 命令加主机名称即可实现部署,非常方便。 76 | 77 | 如果 Scrapyd 设置了访问限制的话,我们可以在配置文件中加入用户名和密码的配置,同时端口修改一下,修改成 Nginx 代理端口,如在第一章我们使用的是 6801,那么这里就需要改成 6801,修改如下: 78 | 79 | ```ini 80 | [deploy:vm1] 81 | url = http://120.27.34.24:6801/ 82 | project = weibo 83 | username = admin 84 | password = admin 85 | 86 | [deploy:vm2] 87 | url = http://139.217.26.30:6801/ 88 | project = weibo 89 | username = germey 90 | password = germey 91 | ``` 92 | 93 | 这样通过加入 username 和 password 字段我们就可以在部署时自动进行 Auth 验证,然后成功实现部署。 94 | 95 | ### 4. 结语 96 | 97 | 本节介绍了利用 Scrapyd-Client 来方便地将项目部署到 Scrapyd 的过程,有了它部署不再是麻烦事。 -------------------------------------------------------------------------------- /Chapter 15 分布式爬虫的部署/15.3-Scrapyd对接Docker.md: -------------------------------------------------------------------------------- 1 | # 15.3 Scrapyd 对接 Docker 2 | 3 | 我们使用了 Scrapyd-Client 成功将 Scrapy 项目部署到 Scrapyd 运行,前提是需要提前在服务器上安装好 Scrapyd 并运行 Scrapyd 服务,而这个过程比较麻烦。如果同时将一个 Scrapy 项目部署到 100 台服务器上,我们需要手动配置每台服务器的 Python 环境,更改 Scrapyd 配置吗?如果这些服务器的 Python 环境是不同版本,同时还运行其他的项目,而版本冲突又会造成不必要的麻烦。 4 | 5 | 所以,我们需要解决一个痛点,那就是 Python 环境配置问题和版本冲突解决问题。如果我们将 Scrapyd 直接打包成一个 Docker 镜像,那么在服务器上只需要执行 Docker 命令就可以启动 Scrapyd 服务,这样就不用再关心 Python 环境问题,也不需要担心版本冲突问题。 6 | 7 | 接下来,我们就将 Scrapyd 打包制作成一个 Docker 镜像。 8 | 9 | ### 1. 准备工作 10 | 11 | 请确保本机已经正确安装好了 Docker,如没有安装可以参考第 1 章的安装说明。 12 | 13 | ### 2. 对接 Docker 14 | 15 | 接下来我们首先新建一个项目,然后新建一个 scrapyd.conf,即 Scrapyd 的配置文件,内容如下: 16 | 17 | ```ini 18 | [scrapyd] 19 | eggs_dir = eggs 20 | logs_dir = logs 21 | items_dir = 22 | jobs_to_keep = 5 23 | dbs_dir = dbs 24 | max_proc = 0 25 | max_proc_per_cpu = 10 26 | finished_to_keep = 100 27 | poll_interval = 5.0 28 | bind_address = 0.0.0.0 29 | http_port = 6800 30 | debug = off 31 | runner = scrapyd.runner 32 | application = scrapyd.app.application 33 | launcher = scrapyd.launcher.Launcher 34 | webroot = scrapyd.website.Root 35 | 36 | [services] 37 | schedule.json = scrapyd.webservice.Schedule 38 | cancel.json = scrapyd.webservice.Cancel 39 | addversion.json = scrapyd.webservice.AddVersion 40 | listprojects.json = scrapyd.webservice.ListProjects 41 | listversions.json = scrapyd.webservice.ListVersions 42 | listspiders.json = scrapyd.webservice.ListSpiders 43 | delproject.json = scrapyd.webservice.DeleteProject 44 | delversion.json = scrapyd.webservice.DeleteVersion 45 | listjobs.json = scrapyd.webservice.ListJobs 46 | daemonstatus.json = scrapyd.webservice.DaemonStatus 47 | ``` 48 | 49 | 在这里实际上是修改自官方文档的配置文件:[https://scrapyd.readthedocs.io/en/stable/config.html#example-configuration-file](https://scrapyd.readthedocs.io/en/stable/config.html#example-configuration-file),其中修改的地方有两个: 50 | * max_proc_per_cpu = 10,原本是 4,即 CPU 单核最多运行 4 个 Scrapy 任务,也就是说 1 核的主机最多同时只能运行 4 个 Scrapy 任务,在这里设置上限为 10,也可以自行设置。 51 | * bind_address = 0.0.0.0,原本是 127.0.0.1,不能公开访问,在这里修改为 0.0.0.0 即可解除此限制。 52 | 53 | 接下来新建一个 requirements.txt ,将一些 Scrapy 项目常用的库都列进去,内容如下: 54 | 55 | ``` 56 | requests 57 | selenium 58 | aiohttp 59 | beautifulsoup4 60 | pyquery 61 | pymysql 62 | redis 63 | pymongo 64 | flask 65 | django 66 | scrapy 67 | scrapyd 68 | scrapyd-client 69 | scrapy-redis 70 | scrapy-splash 71 | ``` 72 | 73 | 如果我们运行的 Scrapy 项目还有其他的库需要用到可以自行添加到此文件中。 74 | 75 | 最后我们新建一个 Dockerfile,内容如下: 76 | 77 | ```Dockerfile 78 | FROM python:3.6 79 | ADD .. /code 80 | WORKDIR /code 81 | COPY ./scrapyd.conf /etc/scrapyd/ 82 | EXPOSE 6800 83 | RUN pip3 install -r requirements.txt 84 | CMD scrapyd 85 | ``` 86 | 87 | 第一行 FROM 是指在 python:3.6 这个镜像上构建,也就是说在构建时就已经有了 Python 3.6 的环境。 88 | 89 | 第二行 ADD 是将本地的代码放置到虚拟容器中,它有两个参数,第一个参数是 . ,即代表本地当前路径,/code 代表虚拟容器中的路径,也就是将本地项目所有内容放置到虚拟容器的 /code 目录下。 90 | 91 | 第三行 WORKDIR 是指定工作目录,在这里将刚才我们添加的代码路径设成工作路径,在这个路径下的目录结构和我们当前本地目录结构是相同的,所以可以直接执行库安装命令等。 92 | 93 | 第四行 COPY 是将当前目录下的 scrapyd.conf 文件拷贝到虚拟容器的 /etc/scrapyd/ 目录下,Scrapyd 在运行的时候会默认读取这个配置。 94 | 95 | 第五行 EXPOSE 是声明运行时容器提供服务端口,注意这里只是一个声明,在运行时不一定就会在此端口开启服务。这样的声明一是告诉使用者这个镜像服务的运行端口,以方便配置映射。另一个用处则是在运行时使用随机端口映射时,会自动随机映射 EXPOSE 的端口。 96 | 97 | 第六行 RUN 是执行某些命令,一般做一些环境准备工作,由于 Docker 虚拟容器内只有 Python3 环境,而没有我们所需要的一些 Python 库,所以在这里我们运行此命令来在虚拟容器中安装相应的 Python 库,这样项目部署到 Scrapyd 中便可以正常运行了。 98 | 99 | 第七行 CMD 是容器启动命令,在容器运行时,会直接执行此命令,在这里我们直接用 scrapyd 来启动 Scrapyd 服务。 100 | 101 | 到现在基本的工作就完成了,运行如下命令进行构建: 102 | 103 | ``` 104 | docker build -t scrapyd:latest . 105 | ``` 106 | 107 | 构建成功后即可运行测试: 108 | 109 | ``` 110 | docker run -d -p 6800:6800 scrapyd 111 | ``` 112 | 113 | 运行之后我们打开:[http://localhost:6800](http://localhost:6800) 即可观察到 Scrapyd 服务,如图 15-2 所示: 114 | 115 | ![](./assets/15-2.png) 116 | 117 | 图 15-2 Scrapyd 主页 118 | 119 | 这样我们就完成了 Scrapyd Docker 镜像的构建并成功运行了。 120 | 121 | 然后我们可以将此镜像上传到 Docker Hub,例如我的 Docker Hub 用户名为 germey,新建了一个名为 scrapyd 的项目,首先可以打一个标签: 122 | 123 | ``` 124 | docker tag scrapyd:latest germey/scrapyd:latest 125 | ``` 126 | 127 | 这里请自行替换成你的项目名称。 128 | 129 | 然后 Push 即可: 130 | 131 | ``` 132 | docker push germey/scrapyd:latest 133 | ``` 134 | 135 | 之后我们在其他主机运行此命令即可启动 Scrapyd 服务: 136 | 137 | ``` 138 | docker run -d -p 6800:6800 germey/scrapyd 139 | ``` 140 | 141 | 执行命令后会发现 Scrapyd 就可以成功在其他服务器上运行了。 142 | 143 | ### 3. 结语 144 | 145 | 这样我们就利用 Docker 解决了 Python 环境的问题,在后一节我们再解决一个批量部署 Docker 的问题就可以解决批量部署问题了。 -------------------------------------------------------------------------------- /Chapter 15 分布式爬虫的部署/15.4-Scrapyd批量部署.md: -------------------------------------------------------------------------------- 1 | # 15.4 Scrapyd 批量部署 2 | 3 | 我们在上一节实现了 Scrapyd 和 Docker 的对接,这样每台主机就不用再安装 Python 环境和安装 Scrapyd 了,直接执行一句 Docker 命令运行 Scrapyd 服务即可。但是这种做法有个前提,那就是每台主机都安装 Docker,然后再去运行 Scrapyd 服务。如果我们需要部署 10 台主机的话,工作量确实不小。 4 | 5 | 一种方案是,一台主机已经安装好各种开发环境,我们取到它的镜像,然后用镜像来批量复制多台主机,批量部署就可以轻松实现了。 6 | 7 | 另一种方案是,我们在新建主机的时候直接指定一个运行脚本,脚本里写好配置各种环境的命令,指定其在新建主机的时候自动执行,那么主机创建之后所有的环境就按照自定义的命令配置好了,这样也可以很方便地实现批量部署。 8 | 9 | 目前很多服务商都提供云主机服务,如阿里云、腾讯云、Azure、Amazon 等,不同的服务商提供了不同的批量部署云主机的方式。例如,腾讯云提供了创建自定义镜像的服务,在新建主机的时候使用自定义镜像创建新的主机即可,这样就可以批量生成多个相同的环境。Azure 提供了模板部署的服务,我们可以在模板中指定新建主机时执行的配置环境的命令,这样在主机创建之后环境就配置完成了。 10 | 11 | 本节我们就来看看这两种批量部署的方式,来实现 Docker 和 Scrapyd 服务的批量部署。 12 | 13 | ### 1. 镜像部署 14 | 15 | 以腾讯云为例进行说明。首先需要有一台已经安装好环境的云主机,Docker 和 Scrapyd 镜像均已经正确安装,Scrapyd 镜像启动加到开机启动脚本中,可以在开机时自动启动。 16 | 17 | 接下来我们来看下腾讯云下批量部署相同云服务的方法。 18 | 19 | 首先进入到腾讯云后台,可以点击更多选项制作镜像,如图 15-3 所示。 20 | 21 | ![](../image/15-3.png) 22 | 23 | 图 15-3 制作镜像 24 | 25 | 然后输入镜像的一些配置信息,如图 15-4 所示。 26 | 27 | ![](../image/15-4.jpg) 28 | 29 | 图 15-4 镜像配置 30 | 31 | 最后确认制作镜像即可,稍等片刻即可制作成功。 32 | 33 | 接下来我们可以创建新的主机,在新建主机时选择已经制作好的镜像即可,如图 15-5 所示。 34 | 35 | ![](../image/15-5.png) 36 | 37 | 图 15-5 新建主机 38 | 39 | 后续配置过程按照提示进行即可。 40 | 41 | 配置完成之后登录新到云主机,即可看到当前主机 Docker 和 Scrapyd 镜像都已经安装好,Scrapyd 服务已经正常运行。 42 | 43 | 我们就通过自定义镜像的方式实现了相同环境的云主机的批量部署。 44 | 45 | ### 2. 模板部署 46 | 47 | Azure 的云主机在部署时都会使用一个部署模板,这个模板实际上是一个 JSON 文件,里面包含了很多部署时的配置选项,如主机名称、用户名、密码、主机型号等。在模板中我们可以指定新建完云主机之后执行的命令行脚本,如安装 Docker、运行镜像等。等部署工作全部完成之后,新创建的云主机就已经完成环境配置,同时运行相关服务。 48 | 49 | 这里提供一个部署 Linux 主机时自动安装 Docker 和运行 Scrapyd 镜像的模板,模板内容太多,源文件可以查看:https://github.com/Python3WebSpider/ScrapydDeploy/blob/master/azuredeploy.json。模板中 Microsoft.Compute/virtualMachines/extensions 部分有一个 commandToExecute 字段,它可以指定建立主机后自动执行的命令。这里的命令完成的是安装 Docker 并运行 Scrapyd 镜像服务的过程。 50 | 51 | 首先安装一个 Azure 组件,安装过程可以参考:https://docs.azure.cn/zh-cn/xplat-cli-install。之后就可以使用 azure 命令行进行部署。 52 | 53 | 登录 Azure,这里登录的是中国区,命令如下: 54 | 55 | ``` 56 | azure login -e AzureChinaCloud 57 | ``` 58 | 59 | 如果没有资源组的话需要新建一个资源组,命令如下: 60 | 61 | ``` 62 | azure group create myResourceGroup chinanorth 63 | ``` 64 | 65 | 其中 myResourceGroup 就是资源组的名称,可以自行定义。 66 | 67 | 接下来就可以使用该模板进行部署了,命令如下: 68 | 69 | ``` 70 | azure group deployment create --template-file azuredeploy.json myResourceGroup myDeploymentName 71 | ``` 72 | 73 | 这里 myResourceGroup 就是资源组的名称,myDeploymentName 是部署任务的名称。 74 | 75 | 例如,部署一台 Linux 主机的过程如下: 76 | 77 | ``` 78 | azure group deployment create --template-file azuredeploy.json MyResourceGroup SingleVMDeploy 79 | info: Executing command group deployment create 80 | info: Supply values for the following parameters 81 | adminUsername: datacrawl 82 | adminPassword: DataCrawl123 83 | vmSize: Standard_D2_v2 84 | vmName: datacrawl-vm 85 | dnsLabelPrefix: datacrawlvm 86 | storageAccountName: datacrawlstorage 87 | ``` 88 | 89 | 运行命令后会提示输入各个配置参数,如主机用户名、密码等。之后等待整个部署工作完成即可,命令行会自动退出。然后,我们登录云主机即可查看到 Docker 已经成功安装并且 Scrapyd 服务正常运行。 90 | 91 | ### 3. 结语 92 | 93 | 以上内容便是批量部署的两种方法。在大规模分布式爬虫架构中,如果需要批量部署多个爬虫环境,使用如上方法可以快速批量完成环境的搭建工作,而不用再去逐个主机配置环境。 94 | 95 | 到此为止,我们解决了批量部署的问题,创建主机完毕之后即可直接使用 Scrapyd 服务。 -------------------------------------------------------------------------------- /Chapter 15 分布式爬虫的部署/15.5-Gerapy分布式管理.md: -------------------------------------------------------------------------------- 1 | # 15.5 Gerapy 分布式管理 2 | 3 | 我们可以通过 Scrapyd-Client 将 Scrapy 项目部署到 Scrapyd 上,并且可以通过 Scrapyd API 来控制 Scrapy 的运行。那么,我们是否可以做到更优化?方法是否可以更方便可控? 4 | 5 | 我们重新分析一下当前可以优化的问题。 6 | 7 | * 使用 Scrapyd-Client 部署时,需要在配置文件中配置好各台主机的地址,然后利用命令行执行部署过程。如果我们省去各台主机的地址配置,将命令行对接图形界面,只需要点击按钮即可实现批量部署,这样就更方便了。 8 | 9 | * 使用 Scrapyd API 可以控制 Scrapy 任务的启动、终止等工作,但很多操作还是需要代码来实现,同时获取爬取日志还比较烦琐。如果我们有一个图形界面,只需要点击按钮即可启动和终止爬虫任务,同时还可以实时查看爬取日志报告,那这将大大节省我们的时间和精力。 10 | 11 | 所以我们的终极目标是如下内容。 12 | 13 | * 更方便地控制爬虫运行 14 | * 更直观地查看爬虫状态 15 | * 更实时地查看爬取结果 16 | * 更简单地实现项目部署 17 | * 更统一地实现主机管理 18 | 19 | 而这所有的工作均可通过 Gerapy 来实现。 20 | 21 | Gerapy 是一个基于 Scrapyd、Scrapyd API、Django、Vue.js 搭建的分布式爬虫管理框架。接下来将简单介绍它的使用方法。 22 | 23 | ### 1. 准备工作 24 | 25 | 在本节开始之前请确保已经正确安装好了 Gerapy,安装方式可以参考第一章。 26 | 27 | ### 2. 使用说明 28 | 29 | 首先可以利用 gerapy 命令新建一个项目,命令如下: 30 | 31 | ``` 32 | gerapy init 33 | ``` 34 | 35 | 这样会在当前目录下生成一个 gerapy 文件夹,然后进入 gerapy 文件夹,会发现一个空的 projects 文件夹,我们后文会提及。 36 | 37 | 这时先对数据库进行初始化: 38 | 39 | ``` 40 | gerapy migrate 41 | ``` 42 | 43 | 这样即会生成一个 SQLite 数据库,数据库中会用于保存各个主机配置信息、部署版本等。 44 | 45 | 接下来启动 Gerapy 服务,命令如下: 46 | 47 | ``` 48 | gerapy runserver 49 | ``` 50 | 51 | 这样即可在默认 8000 端口上开启 Gerapy 服务,我们浏览器打开:[http://localhost:8000](http://localhost:8000) 即可进入 Gerapy 的管理页面,在这里提供了主机管理和项目管理的功能。 52 | 53 | 主机管理中,我们可以将各台主机的 Scrapyd 运行地址和端口添加,并加以名称标记,添加之后便会出现在主机列表中,Gerapy 会监控各台主机的运行状况并以不同的状态标识,如图 15-6 所示: 54 | 55 | ![](../image/15-6.jpg) 56 | 57 | 图 15-6 主机列表 58 | 59 | 另外刚才我们提到在 gerapy 目录下有一个空的 projects 文件夹,这就是存放 Scrapy 目录的文件夹,如果我们想要部署某个 Scrapy 项目,只需要将该项目文件放到 projects 文件夹下即可。 60 | 61 | 比如这里我放了两个 Scrapy 项目,如图 15-7 所示: 62 | 63 | ![](../image/15-7.jpg) 64 | 65 | 图 15-7 项目目录 66 | 67 | 这时重新回到 Gerapy 管理界面,点击项目管理,即可看到当前项目列表,如图 15-8 所示: 68 | 69 | ![](../image/15-8.jpg) 70 | 71 | 图 15-8 项目列表 72 | 73 | 由于此处我有过打包和部署记录,在这里分别予以显示。 74 | 75 | Gerapy 提供了项目在线编辑功能,我们可以点击编辑即可可视化地对项目进行编辑,如图 15-9 所示: 76 | 77 | ![](../image/15-9.jpg) 78 | 79 | 图 15-9 可视化编辑 80 | 81 | 如果项目没有问题,可以点击部署进行打包和部署,部署之前需要打包项目,打包时可以指定版本描述,如图 15-10 所示: 82 | 83 | ![](../image/15-10.jpg) 84 | 85 | 图 15-10 项目打包 86 | 87 | 打包完成之后可以直接点击部署按钮即可将打包好的 Scrapy 项目部署到对应的云主机上,同时也可以批量部署,如图 15-11 所示: 88 | 89 | ![](../image/15-11.jpg) 90 | 91 | 图 15-11 部署页面 92 | 93 | 部署完毕之后就可以回到主机管理页面进行任务调度了,点击调度即可查看进入任务管理页面,可以当前主机所有任务的运行状态,如图 15-12 所示: 94 | 95 | ![](../image/15-12.jpg) 96 | 97 | 图 15-12 任务运行状态 98 | 99 | 我们可以通过点击新任务、停止等按钮来实现任务的启动和停止等操作,同时也可以通过展开任务条目查看日志详情,如图 15-13 所示: 100 | 101 | ![](../image/15-13.jpg) 102 | 103 | 图 15-13 查看日志 104 | 105 | 这样我们就可以实时查看到各个任务运行状态了。 106 | 107 | 以上便是 Gerapy 的一些功能的简单介绍,使用它我们可以更加方便地管理、部署和监控 Scrapy 项目,尤其是对分布式爬虫来说。 108 | 109 | 更多的信息可以查看 Gerapy 的 GitHub 地址:[https://github.com/Gerapy](https://github.com/Gerapy)。 110 | 111 | ### 3. 结语 112 | 113 | 本节我们介绍了 Gerapy 的简单使用,利用它我们可以方便地实现 Scrapy 项目的部署、管理等操作,可以大大提高效率。 -------------------------------------------------------------------------------- /Chapter 2 爬虫基础/2.0-爬虫基础.md: -------------------------------------------------------------------------------- 1 | # 第二章 爬虫基础 2 | 3 | 在写爬虫之前,我们还需要了解一些基础知识,如 HTTP 原理、网页的基础知识、爬虫的基本原理、Cookies 的基本原理等。本章中,我们就对这些基础知识做一个简单的总结。 -------------------------------------------------------------------------------- /Chapter 2 爬虫基础/2.3-爬虫基本原理.md: -------------------------------------------------------------------------------- 1 | ## 2.3 爬虫的基本原理 2 | 3 | 我们可以把互联网比作一张大网,而爬虫(即网络爬虫)便是在网上爬行的蜘蛛。把网的节点比作一个个网页,爬虫爬到这就相当于访问了该页面,获取了其信息。可以把节点间的连线比作网页与网页之间的链接关系,这样蜘蛛通过一个节点后,可以顺着节点连线继续爬行到达下一个节点,即通过一个网页继续获取后续的网页,这样整个网的节点便可以被蜘蛛全部爬行到,网站的数据就可以被抓取下来了。 4 | 5 | ### 2.3.1 爬虫概述 6 | 7 | 简单来说,爬虫就是获取网页并提取和保存信息的自动化程序,下面概要介绍一下。 8 | 9 | #### 1. 获取网页 10 | 11 | 爬虫首先要做的工作就是获取网页,这里就是获取网页的源代码。源代码里包含了网页的部分有用信息,所以只要把源代码获取下来,就可以从中提取想要的信息了。 12 | 13 | 前面讲了请求和响应的概念,向网站的服务器发送一个请求,返回的响应体便是网页源代码。所以,最关键的部分就是构造一个请求并发送给服务器,然后接收到响应并将其解析出来,那么这个流程怎样实现呢?总不能手工去截取网页源码吧? 14 | 15 | 不用担心,Python 提供了许多库来帮助我们实现这个操作,如 urllib、requests 等。我们可以用这些库来帮助我们实现 HTTP 请求操作,请求和响应都可以用类库提供的数据结构来表示,得到响应之后只需要解析数据结构中的 Body 部分即可,即得到网页的源代码,这样我们可以用程序来实现获取网页的过程了。 16 | 17 | #### 2. 提取信息 18 | 19 | 获取网页源代码后,接下来就是分析网页源代码,从中提取我们想要的数据。首先,最通用的方法便是采用正则表达式提取,这是一个万能的方法,但是在构造正则表达式时比较复杂且容易出错。 20 | 21 | 另外,由于网页的结构有一定的规则,所以还有一些根据网页节点属性、CSS 选择器或 XPath 来提取网页信息的库,如 Beautiful Soup、pyquery、lxml 等。使用这些库,我们可以高效快速地从中提取网页信息,如节点的属性、文本值等。 22 | 23 | 提取信息是爬虫非常重要的部分,它可以使杂乱的数据变得条理清晰,以便我们后续处理和分析数据。 24 | 25 | #### 3. 保存数据 26 | 27 | 提取信息后,我们一般会将提取到的数据保存到某处以便后续使用。这里保存形式有多种多样,如可以简单保存为 TXT 文本或 JSON 文本,也可以保存到数据库,如 MySQL 和 MongoDB 等,也可保存至远程服务器,如借助 SFTP 进行操作等。 28 | 29 | #### 4. 自动化程序 30 | 31 | 说到自动化程序,意思是说爬虫可以代替人来完成这些操作。首先,我们手工当然可以提取这些信息,但是当量特别大或者想快速获取大量数据的话,肯定还是要借助程序。爬虫就是代替我们来完成这份爬取工作的自动化程序,它可以在抓取过程中进行各种异常处理、错误重试等操作,确保爬取持续高效地运行。 32 | 33 | ### 2.3.2 能抓怎样的数据 34 | 35 | 在网页中我们能看到各种各样的信息,最常见的便是常规网页,它们对应着 HTML 代码,而最常抓取的便是 HTML 源代码。 36 | 37 | 另外,可能有些网页返回的不是 HTML 代码,而是一个 JSON 字符串(其中 API 接口大多采用这样的形式),这种格式的数据方便传输和解析,它们同样可以抓取,而且数据提取更加方便。 38 | 39 | 此外,我们还可以看到各种二进制数据,如图片、视频和音频等。利用爬虫,我们可以将这些二进制数据抓取下来,然后保存成对应的文件名。 40 | 41 | 另外,还可以看到各种扩展名的文件,如 CSS、JavaScript 和配置文件等,这些其实也是最普通的文件,只要在浏览器里面可以访问到,就可以将其抓取下来。 42 | 43 | 上述内容其实都对应各自的 URL,是基于 HTTP 或 HTTPS 协议的,只要是这种数据,爬虫都可以抓取。 44 | 45 | ### 2.3.3 JavaScript 渲染页面 46 | 有时候,我们在用 urllib 或 requests 抓取网页时,得到的源代码实际和浏览器中看到的不一样。 47 | 48 | 这是一个非常常见的问题。现在网页越来越多地采用 Ajax、前端模块化工具来构建,整个网页可能都是由 JavaScript 渲染出来的,也就是说原始的 HTML 代码就是一个空壳,例如: 49 | 50 | ``` 51 | 52 | 53 | 54 | 55 | This is a Demo 56 | 57 | 58 |
59 |
60 | 61 | 62 | 63 | ``` 64 | 65 | body 节点里面只有一个 id 为 container 的节点,但是需要注意在 body 节点后引入了 app.js,它便负责整个网站的渲染。 66 | 67 | 在浏览器中打开这个页面时,首先会加载这个 HTML 内容,接着浏览器会发现其中引入了一个 app.js 文件,然后便会接着去请求这个文件,获取到该文件后,便会执行其中的 JavaScript 代码,而 JavaScript 则会改变 HTML 中的节点,向其添加内容,最后得到完整的页面。 68 | 69 | 但是在用 urllib 或 requests 等库请求当前页面时,我们得到的只是这个 HTML 代码,它不会帮助我们去继续加载这个 JavaScript 文件,这样也就看不到浏览器中的内容了。 70 | 71 | 这也解释了为什么有时我们得到的源代码和浏览器中看到的不一样。 72 | 73 | 因此,使用基本 HTTP 请求库得到的源代码可能跟浏览器中的页面源代码不太一样。对于这样的情况,我们可以分析其后台 Ajax 接口,也可使用 Selenium、Splash 这样的库来实现模拟 JavaScript 渲染。 74 | 75 | 后面,我们会详细介绍如何采集 JavaScript 渲染的网页。 76 | 77 | 本节介绍了爬虫的一些基本原理,这可以帮助我们在后面编写爬虫时更加得心应手。 -------------------------------------------------------------------------------- /Chapter 2 爬虫基础/2.4-会话和Cookies.md: -------------------------------------------------------------------------------- 1 | ## 2.4 会话和 Cookies 2 | 3 | 在浏览网站的过程中,我们经常会遇到需要登录的情况,有些页面只有登录之后才可以访问,而且登录之后可以连续访问很多次网站,但是有时候过一段时间就需要重新登录。还有一些网站,在打开浏览器时就自动登录了,而且很长时间都不会失效,这种情况又是为什么?其实这里面涉及会话(Session)和 Cookies 的相关知识,本节就来揭开它们的神秘面纱。 4 | 5 | ### 2.4.1 静态网页和动态网页 6 | 7 | 在开始之前,我们需要先了解一下静态网页和动态网页的概念。这里还是前面的示例代码,内容如下: 8 | 9 | ``` 10 | 11 | 12 | 13 | 14 | This is a Demo 15 | 16 | 17 |
18 |
19 |

Hello World

20 |

Hello, this is a paragraph.

21 |
22 |
23 | 24 | 25 | ``` 26 | 27 | 这是最基本的 HTML 代码,我们将其保存为一个 .html 文件,然后把它放在某台具有固定公网 IP 的主机上,主机上装上 Apache 或 Nginx 等服务器,这样这台主机就可以作为服务器了,其他人便可以通过访问服务器看到这个页面,这就搭建了一个最简单的网站。 28 | 29 | 这种网页的内容是 HTML 代码编写的,文字、图片等内容均通过写好的 HTML 代码来指定,这种页面叫作静态网页。它加载速度快,编写简单,但是存在很大的缺陷,如可维护性差,不能根据 URL 灵活多变地显示内容等。例如,我们想要给这个网页的 URL 传入一个 name 参数,让其在网页中显示出来,是无法做到的。 30 | 31 | 因此,动态网页应运而生,它可以动态解析 URL 中参数的变化,关联数据库并动态呈现不同的页面内容,非常灵活多变。我们现在遇到的大多数网站都是动态网站,它们不再是一个简单的 HTML,而是可能由 JSP、PHP、Python 等语言编写的,其功能比静态网页强大和丰富太多了。 32 | 33 | 此外,动态网站还可以实现用户登录和注册的功能。再回到开头提到的问题,很多页面是需要登录之后才可以查看的。按照一般的逻辑来说,输入用户名和密码登录之后,肯定是拿到了一种类似凭证的东西,有了它,我们才能保持登录状态,才能访问登录之后才能看到的页面。 34 | 35 | 那么,这种神秘的凭证到底是什么呢?其实它就是会话和 Cookies 共同产生的结果,下面我们来一探究竟。 36 | 37 | ### 2.4.2 无状态 HTTP 38 | 在了解会话和 Cookies 之前,我们还需要了解 HTTP 的一个特点,叫作无状态。 39 | 40 | HTTP 的无状态是指 HTTP 协议对事务处理是没有记忆能力的,也就是说服务器不知道客户端是什么状态。当我们向服务器发送请求后,服务器解析此请求,然后返回对应的响应,服务器负责完成这个过程,而且这个过程是完全独立的,服务器不会记录前后状态的变化,也就是缺少状态记录。这意味着如果后续需要处理前面的信息,则必须重传,这导致需要额外传递一些前面的重复请求,才能获取后续响应,然而这种效果显然不是我们想要的。为了保持前后状态,我们肯定不能将前面的请求全部重传一次,这太浪费资源了,对于这种需要用户登录的页面来说,更是棘手。 41 | 42 | 这时两个用于保持 HTTP 连接状态的技术就出现了,它们分别是会话和 Cookies。会话在服务端,也就是网站的服务器,用来保存用户的会话信息;Cookies 在客户端,也可以理解为浏览器端,有了 Cookies,浏览器在下次访问网页时会自动附带上它发送给服务器,服务器通过识别 Cookies 并鉴定出是哪个用户,然后再判断用户是否是登录状态,然后返回对应的响应。 43 | 44 | 我们可以理解为 Cookies 里面保存了登录的凭证,有了它,只需要在下次请求携带 Cookies 发送请求而不必重新输入用户名、密码等信息重新登录了。 45 | 46 | 因此在爬虫中,有时候处理需要登录才能访问的页面时,我们一般会直接将登录成功后获取的 Cookies 放在请求头里面直接请求,而不必重新模拟登录。 47 | 48 | 好了,了解会话和 Cookies 的概念之后,我们在来详细剖析它们的原理。 49 | 50 | #### 1. 会话 51 | 52 | 会话,其本来的含义是指有始有终的一系列动作 / 消息。比如,打电话时,从拿起电话拨号到挂断电话这中间的一系列过程可以称为一个会话。 53 | 54 | 而在 Web 中,会话对象用来存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在会话对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个会话对象。当会话过期或被放弃后,服务器将终止该会话。 55 | 56 | #### 2. Cookies 57 | Cookies 指某些网站为了辨别用户身份、进行会话跟踪而存储在用户本地终端上的数据。 58 | 59 | ##### 会话维持 60 | 那么,我们怎样利用 Cookies 保持状态呢?当客户端第一次请求服务器时,服务器会返回一个响应头中带有 Set-Cookie 字段的响应给客户端,用来标记是哪一个用户,客户端浏览器会把 Cookies 保存起来。当浏览器下一次再请求该网站时,浏览器会把此 Cookies 放到请求头一起提交给服务器,Cookies 携带了会话 ID 信息,服务器检查该 Cookies 即可找到对应的会话是什么,然后再判断会话来以此来辨认用户状态。 61 | 62 | 在成功登录某个网站时,服务器会告诉客户端设置哪些 Cookies 信息,在后续访问页面时客户端会把 Cookies 发送给服务器,服务器再找到对应的会话加以判断。如果会话中的某些设置登录状态的变量是有效的,那就证明用户处于登录状态,此时返回登录之后才可以查看的网页内容,浏览器再进行解析便可以看到了。 63 | 64 | 反之,如果传给服务器的 Cookies 是无效的,或者会话已经过期了,我们将不能继续访问页面,此时可能会收到错误的响应或者跳转到登录页面重新登录。 65 | 66 | 所以,Cookies 和会话需要配合,一个处于客户端,一个处于服务端,二者共同协作,就实现了登录会话控制。 67 | 68 | ##### 属性结构 69 | 接下来,我们来看看 Cookies 都有哪些内容。这里以知乎为例,在浏览器开发者工具中打开 Application 选项卡,然后在左侧会有一个 Storage 部分,最后一项即为 Cookies,将其点开,如图 2-13 所示,这些就是 Cookies。 70 | 71 | ![](../image/2-13.jpg) 72 | 73 | 图 2-13 Cookies 列表 74 | 75 | 可以看到,这里有很多条目,其中每个条目可以称为 Cookie。它有如下几个属性。 76 | 77 | * Name,即该 Cookie 的名称。Cookie 一旦创建,名称便不可更改 78 | * Value,即该 Cookie 的值。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。 79 | * Max Age,即该 Cookie 失效的时间,单位秒,也常和 Expires 一起使用,通过它可以计算出其有效时间。Max Age 如果为正数,则该 Cookie 在 Max Age 秒之后失效。如果为负数,则关闭浏览器时 Cookie 即失效,浏览器也不会以任何形式保存该 Cookie。 80 | * Path,即该 Cookie 的使用路径。如果设置为 /path/,则只有路径为 /path/ 的页面可以访问该 Cookie。如果设置为 /,则本域名下的所有页面都可以访问该 Cookie。 81 | * Domain,即可以访问该 Cookie 的域名。例如如果设置为 .zhihu.com,则所有以 zhihu.com,结尾的域名都可以访问该 Cookie。 82 | * Size 字段,即此 Cookie 的大小。 83 | * Http 字段,即 Cookie 的 httponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来访问此 Cookie。 84 | * Secure,即该 Cookie 是否仅被使用安全协议传输。安全协议。安全协议有 HTTPS,SSL 等,在网络上传输数据之前先将数据加密。默认为 false。 85 | 86 | ##### 会话 Cookie 和持久 Cookie 87 | 88 | 从表面意思来说,会话 Cookie 就是把 Cookie 放在浏览器内存里,浏览器在关闭之后该 Cookie 即失效;持久 Cookie 则会保存到客户端的硬盘中,下次还可以继续使用,用于长久保持用户登录状态。 89 | 90 | 其实严格来说,没有会话 Cookie 和持久 Cookie 之 分,只是由 Cookie 的 Max Age 或 Expires 字段决定了过期的时间。 91 | 92 | 因此,一些持久化登录的网站其实就是把 Cookie 的有效时间和会话有效期设置得比较长,下次我们再访问页面时仍然携带之前的 Cookie,就可以直接保持登录状态。 93 | 94 | ### 2.4.3 常见误区 95 | 96 | 在谈论会话机制的时候,常常听到这样一种误解 ——“只要关闭浏览器,会话就消失了”。可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对会话来说,也是一样,除非程序通知服务器删除一个会话,否则服务器会一直保留。比如,程序一般都是在我们做注销操作时才去删除会话。 97 | 98 | 但是当我们关闭浏览器时,浏览器不会主动在关闭之前通知服务器它将要关闭,所以服务器根本不会有机会知道浏览器已经关闭。之所以会有这种错觉,是因为大部分会话机制都使用会话 Cookie 来保存会话 ID 信息,而关闭浏览器后 Cookies 就消失了,再次连接服务器时,也就无法找到原来的会话了。如果服务器设置的 Cookies 保存到硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 Cookies 发送给服务器,则再次打开浏览器,仍然能够找到原来的会话 ID,依旧还是可以保持登录状态的。 99 | 100 | 而且恰恰是由于关闭浏览器不会导致会话被删除,这就需要服务器为会话设置一个失效时间,当距离客户端上一次使用会话的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把会话删除以节省存储空间。 101 | 102 | 由于涉及到一些专业名词知识,本节的部分内容参考来源如下: 103 | * Session 百度百科:[https://baike.baidu.com/item/session/479100](https://baike.baidu.com/item/session/479100) 104 | * Cookie 百度百科:[https://baike.baidu.com/item/cookie/1119](https://baike.baidu.com/item/cookie/1119) 105 | * HTTP Cookie 维基百科:[https://en.wikipedia.org/wiki/HTTP_cookie](https://en.wikipedia.org/wiki/HTTP_cookie) 106 | * Session 和几种状态保持方案理解:[http://www.mamicode.com/info-detail-46545.html](http://www.mamicode.com/info-detail-46545.html) -------------------------------------------------------------------------------- /Chapter 2 爬虫基础/2.5-代理基本原理.md: -------------------------------------------------------------------------------- 1 | ## 2.5 代理的基本原理 2 | 3 | 我们在做爬虫的过程中经常会遇到这样的情况,最初爬虫正常运行,正常抓取数据,一切看起来都是那么美好,然而一杯茶的功夫可能就会出现错误,比如 403 Forbidden,这时候打开网页一看,可能会看到 “您的 IP 访问频率太高” 这样的提示。出现这种现象的原因是网站采取了一些反爬虫措施。比如,服务器会检测某个 IP 在单位时间内的请求次数,如果超过了这个阈值,就会直接拒绝服务,返回一些错误信息,这种情况可以称为封 IP。 4 | 5 | 既然服务器检测的是某个 IP 单位时间的请求次数,那么借助某种方式来伪装我们的 IP,让服务器识别不出是由我们本机发起的请求,不就可以成功防止封 IP 了吗? 6 | 7 | 一种有效的方式就是使用代理,后面会详细说明代理的用法。在这之前,需要先了解下代理的基本原理,它是怎样实现 IP 伪装的呢? 8 | 9 | ### 2.5.1 基本原理 10 | 11 | 代理实际上指的就是代理服务器,英文叫作 proxy server,它的功能是代理网络用户去取得网络信息。形象地说,它是网络信息的中转站。在我们正常请求一个网站时,是发送了请求给 Web 服务器,Web 服务器把响应传回给我们。如果设置了代理服务器,实际上就是在本机和服务器之间搭建了一个桥,此时本机不是直接向 Web 服务器发起请求,而是向代理服务器发出请求,请求会发送给代理服务器,然后由代理服务器再发送给 Web 服务器,接着由代理服务器再把 Web 服务器返回的响应转发给本机。这样我们同样可以正常访问网页,但这个过程中 Web 服务器识别出的真实 IP 就不再是我们本机的 IP 了,就成功实现了 IP 伪装,这就是代理的基本原理。 12 | 13 | ### 2.5.2 代理的作用 14 | 15 | 那么,代理有什么作用呢?我们可以简单列举如下。 16 | 17 | * 突破自身 IP 访问限制,访问一些平时不能访问的站点。 18 | * 访问一些单位或团体内部资源,如使用教育网内地址段免费代理服务器,就可以用于对教育网开放的各类 FTP 下载上传,以及各类资料查询共享等服务。 19 | * 提高访问速度,通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度。 20 | * 隐藏真实 IP,上网者也可以通过这种方法隐藏自己的 IP,免受攻击,对于爬虫来说,我们用代理就是为了隐藏自身 IP,防止自身的 IP 被封锁。 21 | 22 | ### 2.5.3 爬虫代理 23 | 24 | 对于爬虫来说,由于爬虫爬取速度过快,在爬取过程中可能遇到同一个 IP 访问过于频繁的问题,此时网站就会让我们输入验证码登录或者直接封锁 IP,这样会给爬取带来极大的不便。 25 | 26 | 使用代理隐藏真实的 IP,让服务器误以为是代理服务器在请求自己。这样在爬取过程中通过不断更换代理,就不会被封锁,可以达到很好的爬取效果。 27 | 28 | ### 2.5.4 代理分类 29 | 30 | 代理分类时,既可以根据协议区分,也可以根据其匿名程度区分,下面分别总结如下: 31 | 32 | #### 1. 根据协议区分 33 | 34 | 根据代理的协议,代理可以分为如下类别: 35 | 36 | * FTP 代理服务器,主要用于访问 FTP 服务器,一般有上传、下载以及缓存功能,端口一般为 21、2121 等。 37 | * HTTP 代理服务器,主要用于访问网页,一般有内容过滤和缓存功能,端口一般为 80、8080、3128 等。 38 | * SSL/TLS 代理,主要用于访问加密网站,一般有 SSL 或 TLS 加密功能(最高支持 128 位加密强度),端口一般为 443。 39 | * RTSP 代理,主要用于 Realplayer 访问 Real 流媒体服务器,一般有缓存功能,端口一般为 554。 40 | * Telnet 代理,主要用于 telnet 远程控制(黑客入侵计算机时常用于隐藏身份),端口一般为 23。 41 | * POP3/SMTP 代理,主要用于 POP3/SMTP 方式收发邮件,一般有缓存功能,端口一般为 110/25。 42 | * SOCKS 代理,只是单纯传递数据包,不关心具体协议和用法,所以速度快很多,一般有缓存功能,端口一般为 1080。SOCKS 代理协议又分为 SOCKS4 和 SOCKS5,SOCKS4 协议只支持 TCP,而 SOCKS5 协议支持 TCP 和 UDP,还支持各种身份验证机制、服务器端域名解析等。简单来说,SOCK4 能做到的 SOCKS5 都可以做到,但 SOCKS5 能做到的 SOCK4 不一定能做到。 43 | 44 | #### 2. 根据匿名程度区分 45 | 46 | 根据代理的匿名程度,代理可以分为如下类别。 47 | 48 | * 高度匿名代理,高度匿名代理会将数据包原封不动的转发,在服务端看来就好像真的是一个普通客户端在访问,而记录的 IP 是代理服务器的 IP。 49 | * 普通匿名代理,普通匿名代理会在数据包上做一些改动,服务端上有可能发现这是个代理服务器,也有一定几率追查到客户端的真实 IP。代理服务器通常会加入的 HTTP 头有 HTTP_VIA 和 HTTP_X_FORWARDED_FOR。 50 | * 透明代理,透明代理不但改动了数据包,还会告诉服务器客户端的真实 IP。这种代理除了能用缓存技术提高浏览速度,能用内容过滤提高安全性之外,并无其他显著作用,最常见的例子是内网中的硬件防火墙。 51 | * 间谍代理,间谍代理指组织或个人创建的,用于记录用户传输的数据,然后进行研究、监控等目的代理服务器。 52 | 53 | ### 2.5.5 常见代理设置 54 | 55 | * 使用网上的免费代理,最好使用高匿代理,使用前抓取下来筛选一下可用代理,也可以进一步维护一个代理池。 56 | * 使用付费代理服务,互联网上存在许多代理商,可以付费使用,质量比免费代理好很多。 57 | * ADSL 拨号,拨一次号换一次 IP,稳定性高,也是一种比较有效的解决方案。 58 | 59 | 在后文我们会详细介绍这几种代理的使用方式。 60 | 61 | 由于涉及到一些专业名词知识,本节的部分内容参考来源如下: 62 | * 代理服务器 维基百科:[https://zh.wikipedia.org/wiki/ 代理服务器]([https://zh.wikipedia.org/wiki/ 代理服务器) 63 | * 代理 百度百科:[https://baike.baidu.com/item/ 代理 / 3242667](https://baike.baidu.com/item/ 代理 / 3242667) -------------------------------------------------------------------------------- /Chapter 3 基本库的使用/3.0-基本库的使用.md: -------------------------------------------------------------------------------- 1 | # 第三章 基本库的使用 2 | 3 | 学习爬虫,最初的操作便是模拟浏览器向服务器发出请求,那么我们需要从哪个地方做起呢?请求需要我们自己来构造吗?需要关心请求这个数据结构的实现吗?需要了解 HTTP、TCP、IP 层的网络传输通信吗?需要知道服务器的响应和应答原理吗? 4 | 5 | 可能你不知道无从下手,不过不用担心,Python 的强大之处就是提供了功能齐全的类库来帮助我们完成这些请求。最基础的 HTTP 库有 urllib、httplib2、requests、treq 等。 6 | 7 | 拿 urllib 这个库来说,有了它,我们只需要关心请求的链接是什么,需要传的参数是什么,以及如何设置可选的请求头就好了,不用深入到底层去了解它到底是怎样传输和通信的。有了它,两行代码就可以完成一个请求和响应的处理过程,得到网页内容,是不是感觉方便极了? 8 | 9 | 接下来,就让我们从最基础的部分开始了解这些库的使用方法吧。 -------------------------------------------------------------------------------- /Chapter 4 解析库的使用/4.0-解析库的使用.md: -------------------------------------------------------------------------------- 1 | # 第四章 解析库的使用 2 | 3 | 上一章中,我们实现了一个最基本的爬虫,但提取页面信息时使用的是正则表达式,这还是比较烦琐,而且万一有地方写错了,可能导致匹配失败,所以使用正则表达式提取页面信息多多少少还是有些不方便。 4 | 5 | 对于网页的节点来说,它可以定义 id、class 或其他属性。而且节点之间还有层次关系,在网页中可以通过 XPath 或 CSS 选择器来定位一个或多个节点。那么,在页面解析时,利用 XPath 或 CSS 选择器来提取某个节点,然后再调用相应方法获取它的正文内容或者属性,不就可以提取我们想要的任意信息了吗? 6 | 7 | 在 Python 中,怎样实现这个操作呢?不用担心,这种解析库已经非常多,其中比较强大的库有 lxml、Beautiful Soup、pyquery 等,本章就来介绍这 3 个解析库的用法。有了它们,我们就不用再为正则表达式发愁,而且解析效率也会大大提高。 -------------------------------------------------------------------------------- /Chapter 5 数据存储/5.0-数据存储.md: -------------------------------------------------------------------------------- 1 | # 第五章 数据存储 2 | 3 | 用解析器解析出数据之后,接下来就是存储数据了。保存的形式可以多种多样,最简单的形式是直接保存为文本文件,如 TXT、JSON、CSV 等。另外,还可以保存到数据库中,如关系型数据库 MySQL,非关系型数据库 MongoDB、Redis 等。 -------------------------------------------------------------------------------- /Chapter 6 Ajax数据爬取/6.0-Ajax数据爬取.md: -------------------------------------------------------------------------------- 1 | # 第六章 Ajax 数据爬取 2 | 3 | 有时候我们在用 requests 抓取页面的时候,得到的结果可能和在浏览器中看到的不一样:在浏览器中可以看到正常显示的页面数据,但是使用 requests 得到的结果并没有。这是因为 requests 获取的都是原始的 HTML 文档,而浏览器中的页面则是经过 JavaScript 处理数据后生成的结果,这些数据的来源有多种,可能是通过 Ajax 加载的,可能是包含在 HTML 文档中的,也可能是经过 JavaScript 和特定算法计算后生成的。 4 | 5 | 对于第一种情况,数据加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完后,会再向服务器请求某个接口获取数据,然后数据才被处理从而呈现到网页上,这其实就是发送了一个 Ajax 请求。 6 | 7 | 照 Web 发展的趋势来看,这种形式的页面越来越多。网页的原始 HTML 文档不会包含任何数据,数据都是通过 Ajax 统一加载后再呈现出来的,这样在 Web 开发上可以做到前后端分离,而且降低服务器直接渲染页面带来的压力。 8 | 9 | 所以如果遇到这样的页面,直接利用 requests 等库来抓取原始页面,是无法获取到有效数据的,这时需要分析网页后台向接口发送的 Ajax 请求,如果可以用 requests 来模拟 Ajax 请求,那么就可以成功抓取了。 10 | 11 | 所以,本章我们的主要目的是了解什么是 Ajax 以及如何去分析和抓取 Ajax 请求。 -------------------------------------------------------------------------------- /Chapter 6 Ajax数据爬取/6.1-什么是Ajax.md: -------------------------------------------------------------------------------- 1 | # 6.1 什么是 Ajax 2 | 3 | Ajax,全称为 Asynchronous JavaScript and XML,即异步的 JavaScript 和 XML。它不是一门编程语言,而是利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。 4 | 5 | 对于传统的网页,如果想更新其内容,那么必须要刷新整个页面,但有了 Ajax,便可以在页面不被全部刷新的情况下更新其内容。在这个过程中,页面实际上是在后台与服务器进行了数据交互,获取到数据之后,再利用 JavaScript 改变网页,这样网页内容就会更新了。 6 | 7 | 可以到 W3School 上体验几个 Demo 来感受一下:[http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp](http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp)。 8 | 9 | ### 1. 实例引入 10 | 11 | 浏览网页的时候,我们会发现很多网页都有下滑查看更多的选项。比如,拿微博来说,以我的主页为例:[https://m.weibo.cn/u/2830678474](https://m.weibo.cn/u/2830678474),切换到微博页面,一直下滑,可以发现下滑几个微博之后,再向下就没有了,转而会出现一个加载的动画,不一会儿下方就继续出现了新的微博内容,这个过程其实就是 Ajax 加载的过程,如图 6-1 所示。 12 | 13 | ![](../image/6-1.png) 14 | 15 | 图 6-1 页面加载过程 16 | 17 | 我们注意到页面其实并没有整个刷新,也就意味着页面的链接没有变化,但是网页中却多了新内容,也就是后面刷出来的新微博。这就是通过 Ajax 获取新数据并呈现的过程。 18 | 19 | ### 2. 基本原理 20 | 21 | 初步了解了 Ajax 之后,我们再来详细了解它的基本原理。发送 Ajax 请求到网页更新的这个过程可以简单分为以下 3 步: 22 | 23 | * 发送请求 24 | * 解析内容 25 | * 渲染网页 26 | 27 | 下面我们分别来详细介绍一下这几个过程。 28 | 29 | #### 发送请求 30 | 31 | 我们知道 JavaScript 可以实现页面的各种交互功能,Ajax 也不例外,它也是由 JavaScript 实现的,实际上执行了如下代码: 32 | 33 | ```js 34 | var xmlhttp; 35 | if (window.XMLHttpRequest) { 36 | //code for IE7+, Firefox, Chrome, Opera, Safari 37 | xmlhttp=new XMLHttpRequest();} else {//code for IE6, IE5 38 | xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); 39 | } 40 | xmlhttp.onreadystatechange=function() {if (xmlhttp.readyState==4 && xmlhttp.status==200) {document.getElementById("myDiv").innerHTML=xmlhttp.responseText; 41 | } 42 | } 43 | xmlhttp.open("POST","/ajax/",true); 44 | xmlhttp.send(); 45 | ``` 46 | 47 | 这是 JavaScript 对 Ajax 最底层的实现,实际上就是新建了 XMLHttpRequest 对象,然后调用 onreadystatechange 属性设置了监听,然后调用 open() 和 send() 方法向某个链接(也就是服务器)发送了请求。前面用 Python 实现请求发送之后,可以得到响应结果,但这里请求的发送变成 JavaScript 来完成。由于设置了监听,所以当服务器返回响应时,onreadystatechange 对应的方法便会被触发,然后在这个方法里面解析响应内容即可。 48 | 49 | #### 解析内容 50 | 51 | 得到响应之后,onreadystatechange 属性对应的方法便会被触发,此时利用 xmlhttp 的 responseText 属性便可取到响应内容。这类似于 Python 中利用 requests 向服务器发起请求,然后得到响应的过程。那么返回内容可能是 HTML,可能是 JSON,接下来只需要在方法中用 JavaScript 进一步处理即可。比如,如果是 JSON 的话,可以进行解析和转化。 52 | 53 | #### 渲染网页 54 | 55 | JavaScript 有改变网页内容的能力,解析完响应内容之后,就可以调用 JavaScript 来针对解析完的内容对网页进行下一步处理了。比如,通过 document.getElementById().innerHTML 这样的操作,便可以对某个元素内的源代码进行更改,这样网页显示的内容就改变了,这样的操作也被称作 DOM 操作,即对 Document 网页文档进行操作,如更改、删除等。 56 | 57 | 上例中,`document.getElementById("myDiv").innerHTML=xmlhttp.responseText` 便将 ID 为 myDiv 的节点内部的 HTML 代码更改为服务器返回的内容,这样 myDiv 元素内部便会呈现出服务器返回的新数据,网页的部分内容看上去就更新了。 58 | 59 | 我们观察到,这 3 个步骤其实都是由 JavaScript 完成的,它完成了整个请求、解析和渲染的过程。 60 | 61 | 再回想微博的下拉刷新,这其实就是 JavaScript 向服务器发送了一个 Ajax 请求,然后获取新的微博数据,将其解析,并将其渲染在网页中。 62 | 63 | 因此,我们知道,真实的数据其实都是一次次 Ajax 请求得到的,如果想要抓取这些数据,需要知道这些请求到底是怎么发送的,发往哪里,发了哪些参数。如果我们知道了这些,不就可以用 Python 模拟这个发送操作,获取到其中的结果了吗? 64 | 65 | 在下一节中,我们就来了解哪里可以看到这些后台 Ajax 操作,了解它到底是怎么发送的,发送了什么参数。 -------------------------------------------------------------------------------- /Chapter 6 Ajax数据爬取/6.2-Ajax分析方法.md: -------------------------------------------------------------------------------- 1 | # 6.2 Ajax 分析方法 2 | 3 | 这里还以前面的微博为例,我们知道拖动刷新的内容由 Ajax 加载,而且页面的 URL 没有变化,那么应该到哪里去查看这些 Ajax 请求呢? 4 | 5 | ### 1. 查看请求 6 | 7 | 这里还需要借助浏览器的开发者工具,下面以 Chrome 浏览器为例来介绍。 8 | 9 | 首先,用 Chrome 浏览器打开微博的链接 https://m.weibo.cn/u/2830678474,随后在页面中点击鼠标右键,从弹出的快捷菜单中选择 “检查” 选项,此时便会弹出开发者工具,如图 6-2 所示。 10 | 11 | ![](../image/6-2.png) 12 | 13 | 图 6-2 开发者工具 14 | 15 | 此时在 Elements 选项卡中便会观察到网页的源代码,右侧便是节点的样式。 16 | 17 | 不过这不是我们想要寻找的内容。切换到 Network 选项卡,随后重新刷新页面,可以发现这里出现了非常多的条目,如图 6-3 所示。 18 | 19 | ![](../image/6-3.png) 20 | 21 | 图 6-3 Network 面板结果 22 | 23 | 前面也提到过,这里其实就是在页面加载过程中浏览器与服务器之间发送请求和接收响应的所有记录。 24 | 25 | Ajax 其实有其特殊的请求类型,它叫作 xhr。在图 6-4 中,我们可以发现一个名称以 getIndex 开头的请求,其 Type 为 xhr,这就是一个 Ajax 请求。用鼠标点击这个请求,可以查看这个请求的详细信息。 26 | 27 | ![](../image/6-4.png) 28 | 图 6-4 详细信息 29 | 30 | 在右侧可以观察到其 Request Headers、URL 和 Response Headers 等信息。其中 Request Headers 中有一个信息为 X-Requested-With:XMLHttpRequest,这就标记了此请求是 Ajax 请求,如图 6-5 所示。 31 | 32 | ![](../image/6-5.png) 33 | 图 6-5 详细信息 34 | 35 | 随后点击一下 Preview,即可看到响应的内容,它是 JSON 格式的。这里 Chrome 为我们自动做了解析,点击箭头即可展开和收起相应内容,如图 6-6 所示。 36 | 37 | 观察可以发现,这里的返回结果是我的个人信息,如昵称、简介、头像等,这也是用来渲染个人主页所使用的数据。JavaScript 接收到这些数据之后,再执行相应的渲染方法,整个页面就渲染出来了。 38 | 39 | ![](../image/6-6.png) 40 | 41 | 图 6-6 JSON 结果 42 | 43 | 另外,也可以切换到 Response 选项卡,从中观察到真实的返回数据,如图 6-7 所示。 44 | 45 | ![](../image/6-7.png) 46 | 47 | 图 6-7 Response 内容 48 | 49 | 接下来,切回到第一个请求,观察一下它的 Response 是什么,如图 6-8 所示。 50 | 51 | ![](../image/6-8.png) 52 | 53 | 图 6-8 Response 内容 54 | 55 | 这是最原始的链接 [https://m.weibo.cn/u/2830678474](https://m.weibo.cn/u/2830678474) 返回的结果,其代码只有不到 50 行,结构也非常简单,只是执行了一些 JavaScript。 56 | 57 | 所以说,我们看到的微博页面的真实数据并不是最原始的页面返回的,而是后来执行 JavaScript 后再次向后台发送了 Ajax 请求,浏览器拿到数据后再进一步渲染出来的。 58 | 59 | ### 2. 过滤请求 60 | 61 | 接下来,再利用 Chrome 开发者工具的筛选功能筛选出所有的 Ajax 请求。在请求的上方有一层筛选栏,直接点击 XHR,此时在下方显示的所有请求便都是 Ajax 请求了,如图 6-9 所示。 62 | 63 | ![](../image/6-9.png) 64 | 65 | 图 6-9 Ajax 请求 66 | 67 | 接下来,不断滑动页面,可以看到页面底部有一条条新的微博被刷出,而开发者工具下方也一个个地出现 Ajax 请求,这样我们就可以捕获到所有的 Ajax 请求了。 68 | 69 | 随意点开一个条目,都可以清楚地看到其 Request URL、Request Headers、Response Headers、Response Body 等内容,此时想要模拟请求和提取就非常简单了。 70 | 71 | 图 6-10 所示的内容便是我的某一页微博的列表信息。 72 | 73 | ![](../image/6-10.png) 74 | 75 | 图 6-10 微博列表信息 76 | 77 | 到现在为止,我们已经可以分析出 Ajax 请求的一些详细信息了,接下来只需要用程序模拟这些 Ajax 请求,就可以轻松提取我们所需要的信息了。 78 | 79 | 在下一节中,我们用 Python 实现 Ajax 请求的模拟,从而实现数据的抓取。 -------------------------------------------------------------------------------- /Chapter 6 Ajax数据爬取/6.3-Ajax结果提取.md: -------------------------------------------------------------------------------- 1 | ## 6.3 Ajax 结果提取 2 | 这里仍然以微博为例,接下来用 Python 来模拟这些 Ajax 请求,把我发过的微博爬取下来。 3 | 4 | #### 1. 分析请求 5 | 打开 Ajax 的 XHR 过滤器,然后一直滑动页面以加载新的微博内容。可以看到,会不断有 Ajax 请求发出。 6 | 7 | 选定其中一个请求,分析它的参数信息。点击该请求,进入详情页面,如图 6-11 所示。 8 | 9 | ![](../image/6-11.png) 10 | 11 | 图 6-11 详情页面 12 | 13 | 可以发现,这是一个 GET 类型的请求,请求链接为 [https://m.weibo.cn/api/container/getIndex?type=uid&value=2145291155&containerid=1076032145291155&page=2](https://m.weibo.cn/api/container/getIndex?type=uid&value=2145291155&containerid=1076032145291155&page=2),请求的参数有四个:type、value、containerid、page。 14 | 15 | 随后再看看其他请求,可以发现,它们的 type、value 和 containerid 始终如一。type 始终为 uid,value 的值就是页面链接中的数字,其实这就是用户的 id。另外,还有 containerid。可以发现,它就是 107603 加上用户 id。改变的值就是 page,很明显这个参数是用来控制分页的,page=1 代表第一页,page=2 代表第二页,以此类推。 16 | 17 | #### 2. 分析响应 18 | 随后,观察这个请求的响应内容,如图 6-12 所示。 19 | 20 | ![](../image/6-12.png) 21 | 22 | 图 6-12 响应内容 23 | 24 | 这个内容是 JSON 格式的,浏览器开发者工具自动做了解析以方便我们查看。可以看到,最关键的两部分信息就是 cardlistInfo 和 cards:前者包含一个比较重要的信息 total,观察后可以发现,它其实是微博的总数量,我们可以根据这个数字来估算分页数;后者则是一个列表,它包含 10 个元素,展开其中一个看一下,如图 6-13 所示。 25 | 26 | ![](../image/6-13.png) 27 | 28 | 图 6-13 列表内容 29 | 30 | 可以发现,这个元素有一个比较重要的字段 mblog。展开它,可以发现它包含的正是微博的一些信息,比如 attitudes_count(赞数目)、comments_count(评论数目)、reposts_count(转发数目)、created_at(发布时间)、text(微博正文)等,而且它们都是一些格式化的内容。 31 | 32 | 这样我们请求一个接口,就可以得到 10 条微博,而且请求时只需要改变 page 参数即可。 33 | 34 | 这样的话,我们只需要简单做一个循环,就可以获取所有微博了。 35 | 36 | #### 3. 实战演练 37 | 这里我们用程序模拟这些 Ajax 请求,将我的前 10 页微博全部爬取下来。 38 | 39 | 首先,定义一个方法来获取每次请求的结果。在请求时,page 是一个可变参数,所以我们将它作为方法的参数传递进来,相关代码如下: 40 | 41 | ```python 42 | from urllib.parse import urlencode 43 | import requests 44 | base_url = 'https://m.weibo.cn/api/container/getIndex?' 45 | 46 | headers = { 47 | 'Host': 'm.weibo.cn', 48 | 'Referer': 'https://m.weibo.cn/u/2830678474', 49 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) 50 | Chrome/58.0.3029.110 Safari/537.36', 51 | 'X-Requested-With': 'XMLHttpRequest', 52 | } 53 | 54 | def get_page(page): 55 | params = { 56 | 'type': 'uid', 57 | 'value': '2830678474', 58 | 'containerid': '1076032830678474', 59 | 'page': page 60 | } 61 | url = base_url + urlencode(params) 62 | try: 63 | response = requests.get(url, headers=headers) 64 | if response.status_code == 200: 65 | return response.json() 66 | except requests.ConnectionError as e: 67 | print('Error', e.args) 68 | ``` 69 | 70 | 首先,这里定义了 base_url 来表示请求的 URL 的前半部分。接下来,构造参数字典,其中 type、value 和 containerid 是固定参数,page 是可变参数。接下来,调用 urlencode 方法将参数转化为 URL 的 GET 请求参数,即类似于 type=uid&value=2145291155&containerid=1076032145291155&page=2 这样的形式。随后,base_url 与参数拼合形成一个新的 URL。接着,我们用 requests 请求这个链接,加入 headers 参数。然后判断响应的状态码,如果是 200,则直接调用 json 方法将内容解析为 JSON 返回,否则不返回任何信息。如果出现异常,则捕获并输出其异常信息。 71 | 72 | 随后,我们需要定义一个解析方法,用来从结果中提取想要的信息,比如这次想保存微博的 id、正文、赞数、评论数和转发数这几个内容,那么可以先遍历 cards,然后获取 mblog 中的各个信息,赋值为一个新的字典返回即可: 73 | 74 | ```python 75 | from pyquery import PyQuery as pq 76 | 77 | def parse_page(json): 78 | if json: 79 | items = json.get('data').get('cards') 80 | for item in items: 81 | item = item.get('mblog') 82 | weibo = {} 83 | weibo['id'] = item.get('id') 84 | weibo['text'] = pq(item.get('text')).text() 85 | weibo['attitudes'] = item.get('attitudes_count') 86 | weibo['comments'] = item.get('comments_count') 87 | weibo['reposts'] = item.get('reposts_count') 88 | yield weibo 89 | ``` 90 | 91 | 这里我们借助 pyquery 将正文中的 HTML 标签去掉。 92 | 93 | 最后,遍历一下 page,一共 10 页,将提取到的结果打印输出即可: 94 | 95 | ```python 96 | if __name__ == '__main__': 97 | for page in range(1, 11): 98 | json = get_page(page) 99 | results = parse_page(json) 100 | for result in results: 101 | print(result) 102 | ``` 103 | 另外,我们还可以加一个方法将结果保存到 MongoDB 数据库: 104 | ```python 105 | from pymongo import MongoClient 106 | 107 | client = MongoClient() 108 | db = client['weibo'] 109 | collection = db['weibo'] 110 | 111 | def save_to_mongo(result): 112 | if collection.insert(result): 113 | print('Saved to Mongo') 114 | ``` 115 | 这样所有功能就实现完成了。运行程序后,样例输出结果如下: 116 | ``` 117 | {'id': '4134879836735238', 'text': ' 惊不惊喜,刺不刺激,意不意外,感不感动 ', 'attitudes': 3, 118 | 'comments': 1, 'reposts': 0} 119 | Saved to Mongo 120 | {'id': '4143853554221385', 'text': ' 曾经梦想仗剑走天涯,后来过安检给收走了。分享单曲远走高飞 ', 121 | 'attitudes': 5, 'comments': 1, 'reposts': 0} 122 | Saved to Mongo 123 | ``` 124 | 125 | 查看一下 MongoDB,相应的数据也被保存到 MongoDB,如图 6-14 所示。 126 | 127 | ![](../image/6-14.png) 128 | 129 | 图 6-14 保存结果 130 | 131 | 这样,我们就顺利通过分析 Ajax 并编写爬虫爬取下来微博列表。最后,给出本节的代码地址:[https://github.com/Python3WebSpider/WeiboList](https://github.com/Python3WebSpider/WeiboList)。 132 | 133 | 本节的目的是为了演示 Ajax 的模拟请求过程,爬取的结果不是重点。该程序仍有很多可以完善的地方,如页码的动态计算、微博查看全文等,若感兴趣,可以尝试一下。 134 | 135 | 通过这个实例,我们主要学会了怎样去分析 Ajax 请求,怎样用程序来模拟抓取 Ajax 请求。了解了抓取原理之后,下一节的 Ajax 实战演练会更加得心应手。 -------------------------------------------------------------------------------- /Chapter 6 Ajax数据爬取/6.4-分析Ajax爬取今日头条街拍美图.md: -------------------------------------------------------------------------------- 1 | ## 6.4 分析 Ajax 爬取今日头条街拍美图 2 | 3 | 本节中,我们以今日头条为例来尝试通过分析 Ajax 请求来抓取网页数据的方法。这次要抓取的目标是今日头条的街拍美图,抓取完成之后,将每组图片分文件夹下载到本地并保存下来。 4 | 5 | #### 1. 准备工作 6 | 7 | 在本节开始之前,请确保已经安装好 requests 库。如果没有安装,可以参考第 1 章。 8 | 9 | #### 2. 抓取分析 10 | 11 | 在抓取之前,首先要分析抓取的逻辑。打开今日头条的首页 [http://www.toutiao.com/](http://www.toutiao.com/),如图 6-15 所示。 12 | 13 | ![](../image/6-15.jpg) 14 | 图 6-15 首页内容 15 | 16 | 右上角有一个搜索入口,这里尝试抓取街拍美图,所以输入 “街拍” 二字搜索一下,结果如图 6-16 所示。 17 | 18 | ![](../image/6-16.jpg) 19 | 图 6-16 搜索结果 20 | 21 | 这时打开开发者工具,查看所有的网络请求。首先,打开第一个网络请求,这个请求的 URL 就是当前的链接 [http://www.toutiao.com/search/?keyword = 街拍](http://www.toutiao.com/search/?keyword = 街拍),打开 Preview 选项卡查看 Response Body。如果页面中的内容是根据第一个请求得到的结果渲染出来的,那么第一个请求的源代码中必然会包含页面结果中的文字。为了验证,我们可以尝试搜索一下搜索结果的标题,比如 “路人” 二字,如图 6-17 所示。 22 | 23 | ![](../image/6-17.jpg) 24 | 图 6-17 搜索结果 25 | 26 | 我们发现,网页源代码中并没有包含这两个字,搜索匹配结果数目为 0。因此,可以初步判断这些内容是由 Ajax 加载,然后用 JavaScript 渲染出来的。接下来,我们可以切换到 XHR 过滤选项卡,查看一下有没有 Ajax 请求。 27 | 28 | 不出所料,此处出现了一个比较常规的 Ajax 请求,看看它的结果是否包含了页面中的相关数据。 29 | 30 | 点击 data 字段展开,发现这里有许多条数据。点击第一条展开,可以发现有一个 title 字段,它的值正好就是页面中第一条数据的标题。再检查一下其他数据,也正好是一一对应的,如图 6-18 所示。 31 | 32 | ![](../image/6-18.jpg) 33 | 图 6-18 对比结果 34 | 35 | 这就确定了这些数据确实是由 Ajax 加载的。 36 | 37 | 我们的目的是要抓取其中的美图,这里一组图就对应前面 data 字段中的一条数据。每条数据还有一个 image_detail 字段,它是列表形式,这其中就包含了组图的所有图片列表,如图 6-19 所示。 38 | 39 | ![](../image/6-19.jpg) 40 | 图 6-19 图片列表信息 41 | 42 | 因此,我们只需要将列表中的 url 字段提取出来并下载下来就好了。每一组图都建立一个文件夹,文件夹的名称就为组图的标题。 43 | 44 | 接下来,就可以直接用 Python 来模拟这个 Ajax 请求,然后提取出相关美图链接并下载。但是在这之前,我们还需要分析一下 URL 的规律。 45 | 46 | 切换回 Headers 选项卡,观察一下它的请求 URL 和 Headers 信息,如图 6-20 所示。 47 | 48 | ![](../image/6-20.jpg) 49 | 图 6-20 请求信息 50 | 51 | 可以看到,这是一个 GET 请求,请求 URL 的参数有 offset、format、keyword、autoload、count 和 cur_tab。我们需要找出这些参数的规律,因为这样才可以方便地用程序构造出来。 52 | 53 | 接下来,可以滑动页面,多加载一些新结果。在加载的同时可以发现,Network 中又出现了许多 Ajax 请求,如图 6-21 所示。 54 | 55 | ![](../image/6-21.jpg) 56 | 图 6-21 Ajax 请求 57 | 58 | 这里观察一下后续链接的参数,发现变化的参数只有 offset,其他参数都没有变化,而且第二次请求的 offset 值为 20,第三次为 40,第四次为 60,所以可以发现规律,这个 offset 值就是偏移量,进而可以推断出 count 参数就是一次性获取的数据条数。因此,我们可以用 offset 参数来控制数据分页。这样一来,我们就可以通过接口批量获取数据了,然后将数据解析,将图片下载下来即可。 59 | 60 | #### 3. 实战演练 61 | 我们刚才已经分析了一下 Ajax 请求的逻辑,下面就用程序来实现美图下载吧。 62 | 63 | 首先,实现方法 get_page 来加载单个 Ajax 请求的结果。其中唯一变化的参数就是 offset,所以我们将它当作参数传递,实现如下: 64 | 65 | ```python 66 | import requests 67 | from urllib.parse import urlencode 68 | 69 | def get_page(offset): 70 | params = { 71 | 'offset': offset, 72 | 'format': 'json', 73 | 'keyword': ' 街拍 ', 74 | 'autoload': 'true', 75 | 'count': '20', 76 | 'cur_tab': '3', 77 | } 78 | url = 'http://www.toutiao.com/search_content/?' + urlencode(params) 79 | try: 80 | response = requests.get(url) 81 | if response.status_code == 200: 82 | return response.json() 83 | except requests.ConnectionError: 84 | return None 85 | ``` 86 | 87 | 这里我们用 urlencode 方法构造请求的 GET 参数,然后用 requests 请求这个链接,如果返回状态码为 200,则调用 response 的 json 方法将结果转为 JSON 格式,然后返回。 88 | 89 | 接下来,再实现一个解析方法:提取每条数据的 image_detail 字段中的每一张图片链接,将图片链接和图片所属的标题一并返回,此时可以构造一个生成器。实现代码如下: 90 | 91 | ```python 92 | def get_images(json): 93 | if json.get('data'): 94 | for item in json.get('data'): 95 | title = item.get('title') 96 | images = item.get('image_detail') 97 | for image in images: 98 | yield {'image': image.get('url'), 99 | 'title': title 100 | } 101 | ``` 102 | 接下来,实现一个保存图片的方法 save_image,其中 item 就是前面 get_images 方法返回的一个字典。在该方法中,首先根据 item 的 title 来创建文件夹,然后请求这个图片链接,获取图片的二进制数据,以二进制的形式写入文件。图片的名称可以使用其内容的 MD5 值,这样可以去除重复。相关代码如下: 103 | ```python 104 | import os 105 | from hashlib import md5 106 | 107 | def save_image(item): 108 | if not os.path.exists(item.get('title')): 109 | os.mkdir(item.get('title')) 110 | try: 111 | response = requests.get(item.get('image')) 112 | if response.status_code == 200: 113 | file_path = '{0}/{1}.{2}'.format(item.get('title'), md5(response.content).hexdigest(), 'jpg') 114 | if not os.path.exists(file_path): 115 | with open(file_path, 'wb') as f: 116 | f.write(response.content) 117 | else: 118 | print('Already Downloaded', file_path) 119 | except requests.ConnectionError: 120 | print('Failed to Save Image') 121 | ``` 122 | 最后,只需要构造一个 offset 数组,遍历 offset,提取图片链接,并将其下载即可: 123 | ```python 124 | from multiprocessing.pool import Pool 125 | 126 | def main(offset): 127 | json = get_page(offset) 128 | for item in get_images(json): 129 | print(item) 130 | save_image(item) 131 | 132 | 133 | GROUP_START = 1 134 | GROUP_END = 20 135 | 136 | if __name__ == '__main__': 137 | pool = Pool() 138 | groups = ([x * 20 for x in range(GROUP_START, GROUP_END + 1)]) 139 | pool.map(main, groups) 140 | pool.close() 141 | pool.join() 142 | ``` 143 | 144 | 这里定义了分页的起始页数和终止页数,分别为 GROUP_START 和 GROUP_END,还利用了多进程的进程池,调用其 map 方法实现多进程下载。 145 | 146 | 这样整个程序就完成了,运行之后可以发现街拍美图都分文件夹保存下来了,如图 6-22 所示。 147 | 148 | ![](../image/6-22.jpg) 149 | 150 | 图 6-22 保存结果 151 | 152 | 最后,我们给出本节的代码地址:[https://github.com/Python3WebSpider/Jiepai](https://github.com/Python3WebSpider/Jiepai)。 153 | 154 | 通过本节,我们了解了 Ajax 分析的流程、Ajax 分页的模拟以及图片的下载过程。 155 | 156 | 本节的内容需要熟练掌握,在后面的实战中我们还会用到很多次这样的分析和抓取。 -------------------------------------------------------------------------------- /Chapter 7 动态渲染页面抓取/7.0-动态渲染页面抓取.md: -------------------------------------------------------------------------------- 1 | # 第七章 动态渲染页面抓取 2 | 3 | 在前一章中,我们了解了 Ajax 的分析和抓取方式,这其实也是 JavaScript 动态渲染的页面的一种情形,通过直接分析 Ajax,我们仍然可以借助 requests 或 urllib 来实现数据爬取。 4 | 5 | 不过 JavaScript 动态渲染的页面不止 Ajax 这一种。比如中国青年网(详见 [http://news.youth.cn/gn/](http://news.youth.cn/gn/)),它的分页部分是由 JavaScript 生成的,并非原始 HTML 代码,这其中并不包含 Ajax 请求。比如 ECharts 的官方实例(详见 [http://echarts.baidu.com/demo.html](http://echarts.baidu.com/demo.html)),其图形都是经过 JavaScript 计算之后生成的。再有淘宝这种页面,它即使是 Ajax 获取的数据,但是其 Ajax 接口含有很多加密参数,我们难以直接找出其规律,也很难直接分析 Ajax 来抓取。 6 | 7 | 为了解决这些问题,我们可以直接使用模拟浏览器运行的方式来实现,这样就可以做到在浏览器中看到是什么样,抓取的源码就是什么样,也就是可见即可爬。这样我们就不用再去管网页内部的 JavaScript 用了什么算法渲染页面,不用管网页后台的 Ajax 接口到底有哪些参数。 8 | 9 | Python 提供了许多模拟浏览器运行的库,如 Selenium、Splash、PyV8、Ghost 等。本章中,我们就来介绍一下 Selenium 和 Splash 的用法。有了它们,就不用再为动态渲染的页面发愁了。 -------------------------------------------------------------------------------- /Chapter 7 动态渲染页面抓取/7.3-Splash负载均衡配置.md: -------------------------------------------------------------------------------- 1 | # 7.3 Splash 负载均衡配置 2 | 3 | 用 Splash 做页面抓取时,如果爬取的量非常大,任务非常多,用一个 Splash 服务来处理的话,未免压力太大了,此时可以考虑搭建一个负载均衡器来把压力分散到各个服务器上。这相当于多台机器多个服务共同参与任务的处理,可以减小单个 Splash 服务的压力。 4 | 5 | ### 1. 配置 Splash 服务 6 | 7 | 要搭建 Splash 负载均衡,首先要有多个 Splash 服务。假如这里在 4 台远程主机的 8050 端口上都开启了 Splash 服务,它们的服务地址分别为 41.159.27.223:8050、41.159.27.221:8050、41.159.27.9:8050 和 41.159.117.119:8050,这 4 个服务完全一致,都是通过 Docker 的 Splash 镜像开启的。访问其中任何一个服务时,都可以使用 Splash 服务。 8 | 9 | ### 2. 配置负载均衡 10 | 11 | 接下来,可以选用任意一台带有公网 IP 的主机来配置负载均衡。首先,在这台主机上装好 Nginx,然后修改 Nginx 的配置文件 nginx.conf,添加如下内容: 12 | 13 | ```python 14 | http { 15 | upstream splash { 16 | least_conn; 17 | server 41.159.27.223:8050; 18 | server 41.159.27.221:8050; 19 | server 41.159.27.9:8050; 20 | server 41.159.117.119:8050; 21 | } 22 | server { 23 | listen 8050; 24 | location / {proxy_pass http://splash;} 25 | } 26 | } 27 | ``` 28 | 29 | 30 | 这样我们通过 upstream 字段定义了一个名字叫作 splash 的服务集群配置。其中 least_conn 代表最少链接负载均衡,它适合处理请求处理时间长短不一造成服务器过载的情况。 31 | 32 | 当然,我们也可以不指定配置,具体如下: 33 | 34 | ```python 35 | upstream splash { 36 | server 41.159.27.223:8050; 37 | server 41.159.27.221:8050; 38 | server 41.159.27.9:8050; 39 | server 41.159.117.119:8050; 40 | } 41 | ``` 42 | 43 | 这样默认以轮询策略实现负载均衡,每个服务器的压力相同。此策略适合服务器配置相当、无状态且短平快的服务使用。 44 | 45 | 另外,我们还可以指定权重,配置如下: 46 | 47 | ```python 48 | upstream splash { 49 | server 41.159.27.223:8050 weight=4; 50 | server 41.159.27.221:8050 weight=2; 51 | server 41.159.27.9:8050 weight=2; 52 | server 41.159.117.119:8050 weight=1; 53 | } 54 | ``` 55 | 56 | 57 | 这里 weight 参数指定各个服务的权重,权重越高,分配到处理的请求越多。假如不同的服务器配置差别比较大的话,可以使用此种配置。 58 | 59 | 最后,还有一种 IP 散列负载均衡,配置如下: 60 | 61 | ```python 62 | upstream splash { 63 | ip_hash; 64 | server 41.159.27.223:8050; 65 | server 41.159.27.221:8050; 66 | server 41.159.27.9:8050; 67 | server 41.159.117.119:8050; 68 | } 69 | ``` 70 | 71 | 服务器根据请求客户端的 IP 地址进行散列计算,确保使用同一个服务器响应请求,这种策略适合有状态的服务,比如用户登录后访问某个页面的情形。对于 Splash 来说,不需要应用此设置。 72 | 73 | 我们可以根据不同的情形选用不同的配置,配置完成后重启一下 Nginx 服务: 74 | 75 | ``` 76 | sudo nginx -s reload 77 | ``` 78 | 79 | 这样直接访问 Nginx 所在服务器的 8050 端口,即可实现负载均衡了。 80 | 81 | 82 | ### 3. 配置认证 83 | 84 | 现在 Splash 是可以公开访问的,如果不想让其公开访问,还可以配置认证,这仍然借助于 Nginx。可以在 server 的 location 字段中添加 auth_basic 和 auth_basic_user_file 字段,具体配置如下: 85 | 86 | ```python 87 | http { 88 | upstream splash { 89 | least_conn; 90 | server 41.159.27.223:8050; 91 | server 41.159.27.221:8050; 92 | server 41.159.27.9:8050; 93 | server 41.159.117.119:8050; 94 | } 95 | server { 96 | listen 8050; 97 | location / { 98 | proxy_pass http://splash; 99 | auth_basic "Restricted"; 100 | auth_basic_user_file /etc/nginx/conf.d/.htpasswd; 101 | } 102 | } 103 | } 104 | ``` 105 | 这里使用的用户名和密码配置放置在 /etc/nginx/conf.d 目录下,我们需要使用 htpasswd 命令创建。例如,创建一个用户名为 admin 的文件,相关命令如下: 106 | ``` 107 | htpasswd -c .htpasswd admin 108 | ``` 109 | 接下来就会提示我们输入密码,输入两次之后,就会生成密码文件,其内容如下: 110 | ``` 111 | cat .htpasswd 112 | admin:5ZBxQr0rCqwbc 113 | ``` 114 | 配置完成之后我们重启一下 Nginx 服务,运行如下命令: 115 | ``` 116 | sudo nginx -s reload 117 | ``` 118 | 119 | 这样访问认证就成功配置好了。 120 | 121 | ### 4. 测试 122 | 123 | 最后,我们可以用代码来测试一下负载均衡的配置,看看到底是不是每次请求会切换 IP。利用 [http://httpbin.org/get](http://httpbin.org/get) 测试即可,代码实现如下: 124 | 125 | ```python 126 | import requests 127 | from urllib.parse import quote 128 | import re 129 | 130 | lua = ''' 131 | function main(splash, args) 132 | local treat = require("treat") 133 | local response = splash:http_get("http://httpbin.org/get") 134 | return treat.as_string(response.body) 135 | end 136 | ''' 137 | 138 | url = 'http://splash:8050/execute?lua_source=' + quote(lua) 139 | response = requests.get(url, auth=('admin', 'admin')) 140 | ip = re.search('(\d+\.\d+\.\d+\.\d+)', response.text).group(1) 141 | print(ip) 142 | ``` 143 | 144 | 这里 URL 中的 splash 字符串请自行替换成自己的 Nginx 服务器 IP。这里我修改了 Hosts,设置了 splash 为 Nginx 服务器 IP。 145 | 146 | 多次运行代码之后,可以发现每次请求的 IP 都会变化,比如第一次的结果: 147 | 148 | ``` 149 | 41.159.27.223 150 | ``` 151 | 152 | 第二次的结果: 153 | 154 | ``` 155 | 41.159.27.9 156 | ``` 157 | 158 | 这就说明负载均衡已经成功实现了。 159 | 160 | 161 | 本节中,我们成功实现了负载均衡的配置。配置负载均衡后,可以多个 Splash 服务共同合作,减轻单个服务的负载,这还是比较有用的。 -------------------------------------------------------------------------------- /Chapter 8 验证码的识别/8.0-验证码的识别.md: -------------------------------------------------------------------------------- 1 | # 第八章 验证码的识别 2 | 3 | 目前,许多网站采取各种各样的措施来反爬虫,其中一个措施便是使用验证码。随着技术的发展,验证码的花样越来越多。验证码最初是几个数字组合的简单的图形验证码,后来加入了英文字母和混淆曲线。有的网站还可能看到中文字符的验证码,这使得识别愈发困难。 4 | 5 | 后来 12306 验证码的出现使得行为验证码开始发展起来,用过 12306 的用户肯定多少为它的验证码头疼过。我们需要识别文字,点击与文字描述相符的图片,验证码完全正确,验证才能通过。现在这种交互式验证码越来越多,如极验滑动验证码需要滑动拼合滑块才可以完成验证,点触验证码需要完全点击正确结果才可以完成验证,另外还有滑动宫格验证码、计算题验证码等。 6 | 7 | 验证码变得越来越复杂,爬虫的工作也变得愈发艰难。有时候我们必须通过验证码的验证才可以访问页面。本章就专门针对验证码的识别做统一讲解。 8 | 9 | 本章涉及的验证码有普通图形验证码、极验滑动验证码、点触验证码、微博宫格验证码,这些验证码识别的方式和思路各有不同。了解这几个验证码的识别方式之后,我们可以举一反三,用类似的方法识别其他类型验证码。 -------------------------------------------------------------------------------- /Chapter 8 验证码的识别/8.1-图形验证码的识别.md: -------------------------------------------------------------------------------- 1 | # 8.1 图形验证码的识别 2 | 3 | 我们首先识别最简单的一种验证码,即图形验证码。这种验证码最早出现,现在也很常见,一般由 4 位字母或者数字组成。例如,中国知网的注册页面有类似的验证码,链接为:[http://my.cnki.net/elibregister/commonRegister.aspx](http://my.cnki.net/elibregister/commonRegister.aspx),页面如图 8-1 所示: 4 | 5 | ![](../image/8-1.png) 6 | 7 | 图 8-1 知网注册页面 8 | 9 | 表单的最后一项就是图形验证码,我们必须完全正确输入图中的字符才可以完成注册。 10 | 11 | ### 1. 本节目标 12 | 13 | 以知网的验证码为例,讲解利用 OCR 技术识别图形验证码的方法。 14 | 15 | ### 2. 准备工作 16 | 17 | 识别图形验证码需要库 tesserocr。安装此库可以参考第 1 章的安装说明。 18 | 19 | ### 3. 获取验证码 20 | 21 | 为了便于实验,我们先将验证码的图片保存到本地。 22 | 23 | 打开开发者工具,找到验证码元素。验证码元素是一张图片,它的 src 属性是 CheckCode.aspx。我们直接打开这个链接 [http://my.cnki.net/elibregister/CheckCode.aspx](http://my.cnki.net/elibregister/CheckCode.aspx),就可以看到一个验证码,右键保存即可,将其命名为 code.jpg,如图 8-2 所示。 24 | 25 | ![](../image/8-2.jpg) 26 | 27 | 图 8-2 验证码 28 | 29 | 这样我们就可以得到一张验证码图片,以供测试识别使用。 30 | 31 | ### 4. 识别测试 32 | 33 | 接下来新建一个项目,将验证码图片放到项目根目录下,用 tesserocr 库识别该验证码,代码如下所示: 34 | 35 | ```python 36 | import tesserocr 37 | from PIL import Image 38 | 39 | image = Image.open('code.jpg') 40 | result = tesserocr.image_to_text(image) 41 | print(result) 42 | ``` 43 | 在这里我们新建了一个 Image 对象,调用了 tesserocr 的 image_to_text() 方法。传入该 Image 对象即可完成识别,实现过程非常简单,结果如下所示: 44 | ``` 45 | JR42 46 | ``` 47 | 另外,tesserocr 还有一个更加简单的方法,这个方法可直接将图片文件转为字符串,代码如下所示: 48 | ```python 49 | import tesserocr 50 | print(tesserocr.file_to_text('image.png')) 51 | ``` 52 | 53 | 不过,此种方法的识别效果不如上一种方法好。 54 | 55 | ### 5. 验证码处理 56 | 57 | 接下来我们换一个验证码,将其命名为 code2.jpg,如图 8-3 所示。 58 | 59 | ![](../image/8-3.jpg) 60 | 61 | 图 8-3 验证码 62 | 63 | 重新用下面的代码来测试: 64 | 65 | ```python 66 | import tesserocr 67 | from PIL import Image 68 | 69 | image = Image.open('code2.jpg') 70 | result = tesserocr.image_to_text(image) 71 | print(result) 72 | ``` 73 | 可以看到如下输出结果: 74 | ``` 75 | FFKT 76 | ``` 77 | 78 | 这次识别和实际结果有偏差,这是因为验证码内的多余线条干扰了图片的识别。 79 | 80 | 对于这种情况,我们还需要做一下额外的处理,如转灰度、二值化等操作。 81 | 82 | 我们可以利用 Image 对象的 convert() 方法参数传入 L,即可将图片转化为灰度图像,代码如下所示: 83 | 84 | ```python 85 | image = image.convert('L') 86 | image.show() 87 | ``` 88 | 传入 1 即可将图片进行二值化处理,如下所示: 89 | ```python 90 | image = image.convert('1') 91 | image.show() 92 | ``` 93 | 我们还可以指定二值化的阈值。上面的方法采用的是默认阈值 127。不过我们不能直接转化原图,要将原图先转为灰度图像,然后再指定二值化阈值,代码如下所示: 94 | ```python 95 | image = image.convert('L') 96 | threshold = 80 97 | table = [] 98 | for i in range(256): 99 | if i < threshold: 100 | table.append(0) 101 | else: 102 | table.append(1) 103 | 104 | image = image.point(table, '1') 105 | image.show() 106 | ``` 107 | 108 | 在这里,变量 threshold 代表二值化阈值,阈值设置为 80。之后我们看看结果,如图 8-4 所示。 109 | 110 | ![](../image/8-4.jpg) 111 | 112 | 图 8-4 处理结果 113 | 114 | 我们发现原来验证码中的线条已经去除,整个验证码变得黑白分明。这时重新识别验证码,代码如下所示: 115 | 116 | ```python 117 | import tesserocr 118 | from PIL import Image 119 | 120 | image = Image.open('code2.jpg') 121 | 122 | image = image.convert('L') 123 | threshold = 127 124 | table = [] 125 | for i in range(256): 126 | if i < threshold: 127 | table.append(0) 128 | else: 129 | table.append(1) 130 | 131 | image = image.point(table, '1') 132 | result = tesserocr.image_to_text(image) 133 | print(result) 134 | ``` 135 | 即可发现运行结果变成如下所示: 136 | ```python 137 | PFRT 138 | ``` 139 | 140 | 那么,针对一些有干扰的图片,我们做一些灰度和二值化处理,这会提高图片识别的正确率。 141 | 142 | ### 6. 本节代码 143 | 144 | 本节代码地址为:[https://github.com/Python3WebSpider/CrackImageCode](https://github.com/Python3WebSpider/CrackImageCode)。 145 | 146 | ### 7. 结语 147 | 148 | 本节我们了解了利用 tesserocr 识别验证码的过程。我们可以直接用简单的图形验证码得到结果,也可以对验证码图片做预处理来提高识别的准确度。 -------------------------------------------------------------------------------- /Chapter 9 代理的使用/9.0-代理的使用.md: -------------------------------------------------------------------------------- 1 | # 第九章 代理的使用 2 | 3 | 我们在做爬虫的过程中经常会遇到这样的情况,最初爬虫正常运行,正常抓取数据,一切看起来都是那么的美好,然而一杯茶的功夫可能就会出现错误,比如 403 Forbidden,这时候打开网页一看,可能会看到 “您的 IP 访问频率太高” 这样的提示,或者跳出一个验证码让我们输入,输入之后才可能解封,但是输入之后过一会儿就又这样了。 4 | 5 | 出现这样的现象的原因是网站采取了一些反爬虫的措施,比如服务器会检测某个 IP 在单位时间内的请求次数,如果超过了这个阈值,那么会直接拒绝服务,返回一些错误信息,这种情况可以称之为封 IP,于是乎就成功把我们的爬虫禁掉了。 6 | 7 | 既然服务器检测的是某个 IP 单位时间的请求次数,那么我们借助某种方式来伪装我们的 IP,让服务器识别不出是由我们本机发起的请求,不就可以成功防止封 IP 了吗? 8 | 9 | 所以这时候代理就派上用场了。本章会详细介绍代理的基本知识及各种代理的使用方式,包括代理的设置、代理池的维护、付费代理的使用、ADSL 拨号代理的搭建方法等内容,以帮助爬虫脱离封 IP 的 “苦海”。 -------------------------------------------------------------------------------- /Chapter 9 代理的使用/9.3-付费代理的使用.md: -------------------------------------------------------------------------------- 1 | # 9.3 付费代理的使用 2 | 3 | 相对免费代理来说,付费代理的稳定性更高。本节将介绍爬虫付费代理的相关使用过程。 4 | 5 | ### 1. 付费代理分类 6 | 7 | 付费代理分为两类: 8 | 9 | * 一类提供接口获取海量代理,按天或者按量收费,如讯代理。 10 | * 一类搭建了代理隧道,直接设置固定域名代理,如阿布云代理。 11 | 12 | 本节分别以两家代表性的代理网站为例,讲解这两类代理的使用方法。 13 | 14 | ### 2. 讯代理 15 | 16 | 讯代理的代理效率较高(作者亲测),官网为 http://www.xdaili.cn/,如图 9-5 所示。 17 | 18 | ![](../image/9-5.png) 19 | 20 | 图 9-5 讯代理官网 21 | 22 | 讯代理上可供选购的代理有多种类别,包括如下几种(参考官网介绍)。 23 | 24 | * 优质代理: 适合对代理 IP 需求量非常大,但能接受代理有效时长较短(10~30 分钟),小部分不稳定的客户 25 | * 独享动态: 适合对代理 IP 稳定性要求非常高,且可以自主控制的客户,支持地区筛选。 26 | * 独享秒切: 适合对代理 IP 稳定性要求非常高,且可以自主控制的客户,快速获取 IP,地区随机分配 27 | * 动态混拨: 适合对代理 IP 需求量大,代理 IP 使用时效短(3 分钟),切换快的客户 28 | * 优质定制: 如果优质代理的套餐不能满足您的需求,请使用定制服务 29 | 30 | 一般选择第一类别优质代理即可,这种代理的量比较大,但是其稳定性不高,一些代理不可用。所以这种代理的使用就需要借助于上一节所说的代理池,自己再做一次筛选,以确保代理可用。 31 | 32 | 读者可以购买一天时长来试试效果。购买之后,讯代理会提供一个 API 来提取代理,如图 9-6 所示。 33 | 34 | ![](../image/9-6.jpg) 35 | 36 | 图 9-6 提取页面 37 | 38 | 比如在这里我的提取 API 为:[http://www.xdaili.cn/ipagent/greatRecharge/getGreatIp?spiderId=da289b78fec24f19b392e04106253f2a&orderno=YZ20177140586mTTnd7&returnType=2&count=20](http://www.xdaili.cn/ipagent/greatRecharge/getGreatIp?spiderId=da289b78fec24f19b392e04106253f2a&orderno=YZ20177140586mTTnd7&returnType=2&count=20),可能已过期,在此仅做演示。 39 | 40 | 在这里指定了提取数量为 20,提取格式为 JSON,直接访问链接即可提取代理,结果如图 9-7 所示。 41 | 42 | ![](../image/9-7.jpg) 43 | 44 | 图 9-7 提取结果 45 | 46 | 接下来我们要做的就是解析这个 JSON,然后将其放入代理池中。 47 | 48 | 如果信赖讯代理的话,我们也可以不做代理池筛选,直接使用代理。不过我个人还是推荐使用代理池筛选,以提高代理可用概率。 49 | 50 | 根据上一节代理池的写法,我们只需要在 Crawler 中再加入一个 crawl 开头的方法即可。方法实现如下所示: 51 | 52 | ```python 53 | def crawl_xdaili(self): 54 | """ 55 | 获取讯代理 56 | :return: 代理 57 | """ 58 | url = 'http://www.xdaili.cn/ipagent/greatRecharge/getGreatIp?spiderId=da289b78fec24f19b392e04106253f2a&orderno=YZ20177140586mTTnd7&returnType=2&count=20' 59 | html = get_page(url) 60 | if html: 61 | result = json.loads(html) 62 | proxies = result.get('RESULT') 63 | for proxy in proxies: 64 | yield proxy.get('ip') + ':' + proxy.get('port') 65 | ``` 66 | 67 | 这样我们就在代理池中接入了讯代理。获取讯代理的结果之后,解析 JSON,返回代理即可。 68 | 69 | 代理池运行之后就会抓取和检测该接口返回的代理,如果代理可用,那么分数就会被设为 100,通过代理池接口即可获取到这些可用代理。 70 | 71 | ### 3. 阿布云代理 72 | 73 | 阿布云代理提供了代理隧道,代理速度快且非常稳定,其官网为 https://www.abuyun.com/,如图 9-8 所示。 74 | 75 | ![](../image/9-8.png) 76 | 77 | 图 9-8 阿布云官网 78 | 79 | 阿布云代理主要分为两种:专业版和动态版,另外还有定制版(参考官网介绍)。 80 | 81 | * 专业版,多个请求锁定一个代理 IP,海量 IP 资源池需求,近 300 个区域全覆盖,代理 IP 可连续使用 1 分钟,适用于请求 IP 连续型业务 82 | * 动态版,每个请求一个随机代理 IP,海量 IP 资源池需求,近 300 个区域全覆盖,适用于爬虫类业务 83 | * 定制版,灵活按照需求定制,定制 IP 区域,定制 IP 使用时长,定制 IP 每秒请求数 84 | 85 | 关于专业版和动态版的更多介绍可以查看官网:[https://www.abuyun.com/http-proxy/dyn-intro.html](https://www.abuyun.com/http-proxy/dyn-intro.html)。 86 | 87 | 对于爬虫来说,我们推荐使用动态版,购买之后可以在后台看到代理隧道的用户名和密码,如图 9-9 所示。 88 | 89 | ![](../image/9-9.jpg) 90 | 91 | 图 9-9 阿布云代理后台 92 | 93 | 整个代理的连接域名为 proxy.abuyun.com,端口为 9020,它们均是固定的,但是每次使用之后 IP 都会更改,该过程其实就是利用了代理隧道实现(参考官网介绍)。 94 | 95 | 其官网原理介绍如下: 96 | 97 | * 云代理通过代理隧道的形式提供高匿名代理服务,支持 HTTP/HTTPS 协议。 98 | * 云代理在云端维护一个全局 IP 池供代理隧道使用,池中的 IP 会不间断更新,以保证同一时刻 IP 池中有几十到几百个可用代理 IP。 99 | * 需要注意的是代理 IP 池中有部分 IP 可能会在当天重复出现多次。 100 | * 动态版 HTTP 代理隧道会为每个请求从 IP 池中挑选一个随机代理 IP。 101 | * 无须切换代理 IP,每一个请求一个随机代理 IP。 102 | * HTTP 代理隧道有并发请求限制,默认每秒只允许 5 个请求。如果需要更多请求数,请额外购买。 103 | 104 | 注意,默认套餐的并发请求是 5 个。如果需要更多请求数,则须另外购买。 105 | 106 | 使用教程的官网链接为:https://www.abuyun.com/http-proxy/dyn-manual-python.html。教程提供了 requests、urllib、Scrapy 的接入方式。 107 | 108 | 现在我们以 requests 为例,接入代码如下所示: 109 | 110 | ```python 111 | import requests 112 | 113 | url = 'http://httpbin.org/get' 114 | 115 | # 代理服务器 116 | proxy_host = 'proxy.abuyun.com' 117 | proxy_port = '9020' 118 | 119 | # 代理隧道验证信息 120 | proxy_user = 'H01234567890123D' 121 | proxy_pass = '0123456789012345' 122 | 123 | proxy_meta = 'http://%(user) s:%(pass) s@%(host) s:%(port) s' % { 124 | 'host': proxy_host, 125 | 'port': proxy_port, 126 | 'user': proxy_user, 127 | 'pass': proxy_pass, 128 | } 129 | proxies = { 130 | 'http': proxy_meta, 131 | 'https': proxy_meta, 132 | } 133 | response = requests.get(url, proxies=proxies) 134 | print(response.status_code) 135 | print(response.text) 136 | ``` 137 | 在这里其实就是使用了代理认证,在前面我们也提到过类似的设置方法,运行结果如下: 138 | ```json 139 | 200 140 | {"args": {}, 141 | "headers": { 142 | "Accept": "*/*", 143 | "Accept-Encoding": "gzip, deflate", 144 | "Connection": "close", 145 | "Host": "httpbin.org", 146 | "User-Agent": "python-requests/2.18.1" 147 | }, 148 | "origin": "60.207.237.111", 149 | "url": "http://httpbin.org/get" 150 | } 151 | ``` 152 | 153 | 输出结果的 origin 即为代理 IP 的实际地址。这段代码可以多次运行测试,我们发现每次请求 origin 都会在变化,这就是动态版代理的效果。 154 | 155 | 这种效果其实跟之前的代理池的随机代理效果类似,都是随机取出了一个当前可用代理。但是,与维护代理池相比,此服务的配置简单,使用更加方便,更省时省力。在价格可以接受的情况下,个人推荐此种代理。 156 | 157 | ### 4. 结语 158 | 159 | 以上内容便是付费代理的相关使用方法,付费代理稳定性比免费代理更高。读者可以自行选购合适的代理。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python3 网络爬虫开发实战 2 | 3 | 本书介绍了如何利用 Python 3 开发网络爬虫。书中首先详细介绍了环境配置过程和爬虫基础知识;然后讨论了 urllib、requests 等请求库,Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法;接着通过多个案例介绍了如何进行 Ajax 数据爬取,如何使用 Selenium 和 Splash 进行动态网站爬取;接着介绍了爬虫的一些技巧,比如使用代理爬取和维护动态代理池的方法,ADSL 拨号代理的使用,图形、 极验、点触、宫格等各类验证码的破解方法,模拟登录网站爬取的方法及 Cookies 池的维护。 4 | 此外,本书还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等工具实现 App 爬取 的方法,紧接着介绍了 pyspider 框架和 Scrapy 框架的使用,以及分布式爬虫的知识,最后介绍了 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、Gerapy 爬虫管理等方面的知识。 5 | 6 | 本书由图灵教育 - 人民邮电出版社出版发行,版权所有,禁止转载。 7 | 8 | 作者:崔庆才 9 | 10 | ![](image/cover.jpg) 11 | 12 | 购买地址: 13 | * [https://item.jd.com/12333540.html](https://item.jd.com/12333540.html) 14 | 15 | 加读者群: 16 | 17 | ![](http://qiniu.cuiqingcai.com/wp-content/uploads/2017/05/qrcode_for_gh_5b0546ddd2d0_430.jpg) 18 | 19 | 视频资源: 20 | 21 | [Python3 爬虫三大案例实战分享](https://edu.hellobi.com/course/156) 22 | 23 | [自己动手,丰衣足食!Python3 网络爬虫实战案例](https://edu.hellobi.com/course/157) 24 | -------------------------------------------------------------------------------- /image/1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-1.jpg -------------------------------------------------------------------------------- /image/1-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-10.jpg -------------------------------------------------------------------------------- /image/1-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-11.jpg -------------------------------------------------------------------------------- /image/1-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-12.jpg -------------------------------------------------------------------------------- /image/1-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-13.jpg -------------------------------------------------------------------------------- /image/1-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-14.jpg -------------------------------------------------------------------------------- /image/1-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-15.jpg -------------------------------------------------------------------------------- /image/1-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-16.jpg -------------------------------------------------------------------------------- /image/1-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-17.png -------------------------------------------------------------------------------- /image/1-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-18.jpg -------------------------------------------------------------------------------- /image/1-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-19.jpg -------------------------------------------------------------------------------- /image/1-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-2.jpg -------------------------------------------------------------------------------- /image/1-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-20.jpg -------------------------------------------------------------------------------- /image/1-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-21.jpg -------------------------------------------------------------------------------- /image/1-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-22.jpg -------------------------------------------------------------------------------- /image/1-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-23.jpg -------------------------------------------------------------------------------- /image/1-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-24.jpg -------------------------------------------------------------------------------- /image/1-25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-25.jpg -------------------------------------------------------------------------------- /image/1-26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-26.jpg -------------------------------------------------------------------------------- /image/1-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-27.jpg -------------------------------------------------------------------------------- /image/1-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-28.jpg -------------------------------------------------------------------------------- /image/1-29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-29.jpg -------------------------------------------------------------------------------- /image/1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-3.jpg -------------------------------------------------------------------------------- /image/1-30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-30.jpg -------------------------------------------------------------------------------- /image/1-31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-31.jpg -------------------------------------------------------------------------------- /image/1-32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-32.jpg -------------------------------------------------------------------------------- /image/1-33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-33.jpg -------------------------------------------------------------------------------- /image/1-34.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-34.jpg -------------------------------------------------------------------------------- /image/1-35.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-35.jpg -------------------------------------------------------------------------------- /image/1-36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-36.jpg -------------------------------------------------------------------------------- /image/1-37.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-37.jpg -------------------------------------------------------------------------------- /image/1-38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-38.jpg -------------------------------------------------------------------------------- /image/1-39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-39.jpg -------------------------------------------------------------------------------- /image/1-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-4.jpg -------------------------------------------------------------------------------- /image/1-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-40.png -------------------------------------------------------------------------------- /image/1-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-41.png -------------------------------------------------------------------------------- /image/1-42.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-42.jpg -------------------------------------------------------------------------------- /image/1-43.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-43.jpg -------------------------------------------------------------------------------- /image/1-44.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-44.jpg -------------------------------------------------------------------------------- /image/1-45.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-45.jpg -------------------------------------------------------------------------------- /image/1-46.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-46.jpg -------------------------------------------------------------------------------- /image/1-47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-47.jpg -------------------------------------------------------------------------------- /image/1-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-48.png -------------------------------------------------------------------------------- /image/1-49.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-49.jpg -------------------------------------------------------------------------------- /image/1-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-5.jpg -------------------------------------------------------------------------------- /image/1-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-50.png -------------------------------------------------------------------------------- /image/1-51.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-51.jpg -------------------------------------------------------------------------------- /image/1-52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-52.png -------------------------------------------------------------------------------- /image/1-53.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-53.jpg -------------------------------------------------------------------------------- /image/1-54.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-54.jpg -------------------------------------------------------------------------------- /image/1-55.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-55.jpg -------------------------------------------------------------------------------- /image/1-56.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-56.jpg -------------------------------------------------------------------------------- /image/1-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-57.png -------------------------------------------------------------------------------- /image/1-58.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-58.jpg -------------------------------------------------------------------------------- /image/1-59.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-59.jpg -------------------------------------------------------------------------------- /image/1-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-6.jpg -------------------------------------------------------------------------------- /image/1-60.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-60.jpg -------------------------------------------------------------------------------- /image/1-61.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-61.jpg -------------------------------------------------------------------------------- /image/1-62.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-62.jpg -------------------------------------------------------------------------------- /image/1-63.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-63.jpg -------------------------------------------------------------------------------- /image/1-64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-64.jpg -------------------------------------------------------------------------------- /image/1-65.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-65.jpg -------------------------------------------------------------------------------- /image/1-66.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-66.jpg -------------------------------------------------------------------------------- /image/1-67.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-67.jpg -------------------------------------------------------------------------------- /image/1-68.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-68.jpg -------------------------------------------------------------------------------- /image/1-69.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-69.jpg -------------------------------------------------------------------------------- /image/1-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-7.jpg -------------------------------------------------------------------------------- /image/1-70.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-70.jpg -------------------------------------------------------------------------------- /image/1-71.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-71.jpg -------------------------------------------------------------------------------- /image/1-72.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-72.jpg -------------------------------------------------------------------------------- /image/1-73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-73.jpg -------------------------------------------------------------------------------- /image/1-74.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-74.jpg -------------------------------------------------------------------------------- /image/1-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-75.png -------------------------------------------------------------------------------- /image/1-76.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-76.jpg -------------------------------------------------------------------------------- /image/1-77.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-77.jpg -------------------------------------------------------------------------------- /image/1-78.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-78.jpg -------------------------------------------------------------------------------- /image/1-79.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-79.jpg -------------------------------------------------------------------------------- /image/1-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-8.jpg -------------------------------------------------------------------------------- /image/1-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-80.png -------------------------------------------------------------------------------- /image/1-81.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-81.jpg -------------------------------------------------------------------------------- /image/1-82.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-82.jpg -------------------------------------------------------------------------------- /image/1-83.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-83.jpg -------------------------------------------------------------------------------- /image/1-84.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-84.jpg -------------------------------------------------------------------------------- /image/1-85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-85.png -------------------------------------------------------------------------------- /image/1-86.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-86.jpg -------------------------------------------------------------------------------- /image/1-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/1-9.jpg -------------------------------------------------------------------------------- /image/10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-1.png -------------------------------------------------------------------------------- /image/10-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-10.jpg -------------------------------------------------------------------------------- /image/10-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-11.jpg -------------------------------------------------------------------------------- /image/10-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-12.jpg -------------------------------------------------------------------------------- /image/10-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-13.jpg -------------------------------------------------------------------------------- /image/10-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-2.jpg -------------------------------------------------------------------------------- /image/10-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-3.jpg -------------------------------------------------------------------------------- /image/10-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-4.jpg -------------------------------------------------------------------------------- /image/10-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-5.jpg -------------------------------------------------------------------------------- /image/10-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-6.jpg -------------------------------------------------------------------------------- /image/10-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-7.png -------------------------------------------------------------------------------- /image/10-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-8.png -------------------------------------------------------------------------------- /image/10-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/10-9.jpg -------------------------------------------------------------------------------- /image/11-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-1.png -------------------------------------------------------------------------------- /image/11-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-10.png -------------------------------------------------------------------------------- /image/11-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-11.png -------------------------------------------------------------------------------- /image/11-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-12.jpg -------------------------------------------------------------------------------- /image/11-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-13.jpg -------------------------------------------------------------------------------- /image/11-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-14.jpg -------------------------------------------------------------------------------- /image/11-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-15.jpg -------------------------------------------------------------------------------- /image/11-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-16.jpg -------------------------------------------------------------------------------- /image/11-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-17.png -------------------------------------------------------------------------------- /image/11-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-18.jpg -------------------------------------------------------------------------------- /image/11-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-19.jpg -------------------------------------------------------------------------------- /image/11-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-2.png -------------------------------------------------------------------------------- /image/11-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-20.jpg -------------------------------------------------------------------------------- /image/11-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-21.jpg -------------------------------------------------------------------------------- /image/11-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-22.jpg -------------------------------------------------------------------------------- /image/11-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-23.jpg -------------------------------------------------------------------------------- /image/11-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-24.jpg -------------------------------------------------------------------------------- /image/11-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-25.png -------------------------------------------------------------------------------- /image/11-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-26.png -------------------------------------------------------------------------------- /image/11-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-27.png -------------------------------------------------------------------------------- /image/11-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-28.jpg -------------------------------------------------------------------------------- /image/11-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-29.png -------------------------------------------------------------------------------- /image/11-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-3.png -------------------------------------------------------------------------------- /image/11-30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-30.jpg -------------------------------------------------------------------------------- /image/11-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-31.png -------------------------------------------------------------------------------- /image/11-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-32.png -------------------------------------------------------------------------------- /image/11-33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-33.jpg -------------------------------------------------------------------------------- /image/11-34.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-34.jpg -------------------------------------------------------------------------------- /image/11-35.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-35.jpg -------------------------------------------------------------------------------- /image/11-36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-36.jpg -------------------------------------------------------------------------------- /image/11-37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-37.png -------------------------------------------------------------------------------- /image/11-38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-38.jpg -------------------------------------------------------------------------------- /image/11-39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-39.jpg -------------------------------------------------------------------------------- /image/11-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-4.png -------------------------------------------------------------------------------- /image/11-40.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-40.jpg -------------------------------------------------------------------------------- /image/11-41.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-41.jpg -------------------------------------------------------------------------------- /image/11-42.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-42.jpg -------------------------------------------------------------------------------- /image/11-43.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-43.jpg -------------------------------------------------------------------------------- /image/11-44.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-44.jpg -------------------------------------------------------------------------------- /image/11-45.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-45.jpg -------------------------------------------------------------------------------- /image/11-46.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-46.jpg -------------------------------------------------------------------------------- /image/11-47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-47.jpg -------------------------------------------------------------------------------- /image/11-48.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-48.jpg -------------------------------------------------------------------------------- /image/11-49.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-49.jpg -------------------------------------------------------------------------------- /image/11-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-5.png -------------------------------------------------------------------------------- /image/11-50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-50.jpg -------------------------------------------------------------------------------- /image/11-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-6.png -------------------------------------------------------------------------------- /image/11-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-7.png -------------------------------------------------------------------------------- /image/11-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-8.png -------------------------------------------------------------------------------- /image/11-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/11-9.png -------------------------------------------------------------------------------- /image/12-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-1.jpg -------------------------------------------------------------------------------- /image/12-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-10.jpg -------------------------------------------------------------------------------- /image/12-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-11.jpg -------------------------------------------------------------------------------- /image/12-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-12.png -------------------------------------------------------------------------------- /image/12-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-13.jpg -------------------------------------------------------------------------------- /image/12-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-14.jpg -------------------------------------------------------------------------------- /image/12-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-15.jpg -------------------------------------------------------------------------------- /image/12-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-16.jpg -------------------------------------------------------------------------------- /image/12-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-17.jpg -------------------------------------------------------------------------------- /image/12-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-18.jpg -------------------------------------------------------------------------------- /image/12-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-19.jpg -------------------------------------------------------------------------------- /image/12-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-2.jpg -------------------------------------------------------------------------------- /image/12-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-20.jpg -------------------------------------------------------------------------------- /image/12-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-21.png -------------------------------------------------------------------------------- /image/12-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-22.jpg -------------------------------------------------------------------------------- /image/12-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-23.jpg -------------------------------------------------------------------------------- /image/12-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-24.png -------------------------------------------------------------------------------- /image/12-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-25.png -------------------------------------------------------------------------------- /image/12-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-26.png -------------------------------------------------------------------------------- /image/12-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-27.jpg -------------------------------------------------------------------------------- /image/12-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-3.png -------------------------------------------------------------------------------- /image/12-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-4.png -------------------------------------------------------------------------------- /image/12-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-5.png -------------------------------------------------------------------------------- /image/12-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-6.jpg -------------------------------------------------------------------------------- /image/12-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-7.jpg -------------------------------------------------------------------------------- /image/12-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-8.jpg -------------------------------------------------------------------------------- /image/12-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/12-9.jpg -------------------------------------------------------------------------------- /image/13-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-1.jpg -------------------------------------------------------------------------------- /image/13-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-10.jpg -------------------------------------------------------------------------------- /image/13-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-11.jpg -------------------------------------------------------------------------------- /image/13-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-12.jpg -------------------------------------------------------------------------------- /image/13-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-13.jpg -------------------------------------------------------------------------------- /image/13-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-14.jpg -------------------------------------------------------------------------------- /image/13-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-15.jpg -------------------------------------------------------------------------------- /image/13-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-16.jpg -------------------------------------------------------------------------------- /image/13-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-17.jpg -------------------------------------------------------------------------------- /image/13-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-18.jpg -------------------------------------------------------------------------------- /image/13-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-19.png -------------------------------------------------------------------------------- /image/13-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-2.jpg -------------------------------------------------------------------------------- /image/13-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-20.png -------------------------------------------------------------------------------- /image/13-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-21.jpg -------------------------------------------------------------------------------- /image/13-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-22.jpg -------------------------------------------------------------------------------- /image/13-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-23.jpg -------------------------------------------------------------------------------- /image/13-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-24.png -------------------------------------------------------------------------------- /image/13-25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-25.jpg -------------------------------------------------------------------------------- /image/13-26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-26.jpg -------------------------------------------------------------------------------- /image/13-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-27.jpg -------------------------------------------------------------------------------- /image/13-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-28.jpg -------------------------------------------------------------------------------- /image/13-29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-29.jpg -------------------------------------------------------------------------------- /image/13-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-3.jpg -------------------------------------------------------------------------------- /image/13-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-30.png -------------------------------------------------------------------------------- /image/13-31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-31.jpg -------------------------------------------------------------------------------- /image/13-32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-32.jpg -------------------------------------------------------------------------------- /image/13-33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-33.jpg -------------------------------------------------------------------------------- /image/13-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-34.png -------------------------------------------------------------------------------- /image/13-35.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-35.jpg -------------------------------------------------------------------------------- /image/13-36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-36.jpg -------------------------------------------------------------------------------- /image/13-37.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-37.jpg -------------------------------------------------------------------------------- /image/13-38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-38.jpg -------------------------------------------------------------------------------- /image/13-39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-39.jpg -------------------------------------------------------------------------------- /image/13-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-4.jpg -------------------------------------------------------------------------------- /image/13-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-5.jpg -------------------------------------------------------------------------------- /image/13-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-6.png -------------------------------------------------------------------------------- /image/13-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-7.jpg -------------------------------------------------------------------------------- /image/13-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-8.jpg -------------------------------------------------------------------------------- /image/13-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/13-9.jpg -------------------------------------------------------------------------------- /image/14-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-1.jpg -------------------------------------------------------------------------------- /image/14-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-10.jpg -------------------------------------------------------------------------------- /image/14-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-11.jpg -------------------------------------------------------------------------------- /image/14-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-12.jpg -------------------------------------------------------------------------------- /image/14-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-13.jpg -------------------------------------------------------------------------------- /image/14-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-2.jpg -------------------------------------------------------------------------------- /image/14-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-3.jpg -------------------------------------------------------------------------------- /image/14-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-4.jpg -------------------------------------------------------------------------------- /image/14-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-5.jpg -------------------------------------------------------------------------------- /image/14-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-6.jpg -------------------------------------------------------------------------------- /image/14-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-7.jpg -------------------------------------------------------------------------------- /image/14-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-8.jpg -------------------------------------------------------------------------------- /image/14-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/14-9.jpg -------------------------------------------------------------------------------- /image/15-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-1.png -------------------------------------------------------------------------------- /image/15-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-10.jpg -------------------------------------------------------------------------------- /image/15-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-11.jpg -------------------------------------------------------------------------------- /image/15-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-12.jpg -------------------------------------------------------------------------------- /image/15-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-13.jpg -------------------------------------------------------------------------------- /image/15-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-2.png -------------------------------------------------------------------------------- /image/15-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-3.png -------------------------------------------------------------------------------- /image/15-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-4.jpg -------------------------------------------------------------------------------- /image/15-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-5.png -------------------------------------------------------------------------------- /image/15-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-6.jpg -------------------------------------------------------------------------------- /image/15-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-7.jpg -------------------------------------------------------------------------------- /image/15-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-8.jpg -------------------------------------------------------------------------------- /image/15-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/15-9.jpg -------------------------------------------------------------------------------- /image/2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-1.jpg -------------------------------------------------------------------------------- /image/2-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-10.png -------------------------------------------------------------------------------- /image/2-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-11.jpg -------------------------------------------------------------------------------- /image/2-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-12.jpg -------------------------------------------------------------------------------- /image/2-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-13.jpg -------------------------------------------------------------------------------- /image/2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-2.png -------------------------------------------------------------------------------- /image/2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-3.png -------------------------------------------------------------------------------- /image/2-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-4.jpg -------------------------------------------------------------------------------- /image/2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-5.png -------------------------------------------------------------------------------- /image/2-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-6.jpg -------------------------------------------------------------------------------- /image/2-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-7.jpg -------------------------------------------------------------------------------- /image/2-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-8.jpg -------------------------------------------------------------------------------- /image/2-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/2-9.png -------------------------------------------------------------------------------- /image/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-1.png -------------------------------------------------------------------------------- /image/3-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-10.jpg -------------------------------------------------------------------------------- /image/3-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-11.jpg -------------------------------------------------------------------------------- /image/3-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-12.jpg -------------------------------------------------------------------------------- /image/3-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-13.jpg -------------------------------------------------------------------------------- /image/3-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-14.jpg -------------------------------------------------------------------------------- /image/3-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-15.jpg -------------------------------------------------------------------------------- /image/3-2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-2-7.png -------------------------------------------------------------------------------- /image/3-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-2.jpg -------------------------------------------------------------------------------- /image/3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-3.png -------------------------------------------------------------------------------- /image/3-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-4.png -------------------------------------------------------------------------------- /image/3-5.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-5.ico -------------------------------------------------------------------------------- /image/3-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-6.png -------------------------------------------------------------------------------- /image/3-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-7.jpg -------------------------------------------------------------------------------- /image/3-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-8.png -------------------------------------------------------------------------------- /image/3-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/3-9.jpg -------------------------------------------------------------------------------- /image/5-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-1.jpg -------------------------------------------------------------------------------- /image/5-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-2.jpg -------------------------------------------------------------------------------- /image/5-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-3.jpg -------------------------------------------------------------------------------- /image/5-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-4.jpg -------------------------------------------------------------------------------- /image/5-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-5.jpg -------------------------------------------------------------------------------- /image/5-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/5-6.jpg -------------------------------------------------------------------------------- /image/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-1.png -------------------------------------------------------------------------------- /image/6-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-10.png -------------------------------------------------------------------------------- /image/6-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-11.png -------------------------------------------------------------------------------- /image/6-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-12.png -------------------------------------------------------------------------------- /image/6-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-13.png -------------------------------------------------------------------------------- /image/6-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-14.png -------------------------------------------------------------------------------- /image/6-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-15.jpg -------------------------------------------------------------------------------- /image/6-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-16.jpg -------------------------------------------------------------------------------- /image/6-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-17.jpg -------------------------------------------------------------------------------- /image/6-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-18.jpg -------------------------------------------------------------------------------- /image/6-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-19.jpg -------------------------------------------------------------------------------- /image/6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-2.png -------------------------------------------------------------------------------- /image/6-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-20.jpg -------------------------------------------------------------------------------- /image/6-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-21.jpg -------------------------------------------------------------------------------- /image/6-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-22.jpg -------------------------------------------------------------------------------- /image/6-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-3.png -------------------------------------------------------------------------------- /image/6-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-4.png -------------------------------------------------------------------------------- /image/6-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-5.png -------------------------------------------------------------------------------- /image/6-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-6.png -------------------------------------------------------------------------------- /image/6-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-7.png -------------------------------------------------------------------------------- /image/6-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-8.png -------------------------------------------------------------------------------- /image/6-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/6-9.png -------------------------------------------------------------------------------- /image/7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-1.png -------------------------------------------------------------------------------- /image/7-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-10.png -------------------------------------------------------------------------------- /image/7-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-11.png -------------------------------------------------------------------------------- /image/7-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-12.png -------------------------------------------------------------------------------- /image/7-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-13.png -------------------------------------------------------------------------------- /image/7-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-14.jpg -------------------------------------------------------------------------------- /image/7-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-14.png -------------------------------------------------------------------------------- /image/7-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-15.jpg -------------------------------------------------------------------------------- /image/7-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-16.jpg -------------------------------------------------------------------------------- /image/7-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-17.png -------------------------------------------------------------------------------- /image/7-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-18.jpg -------------------------------------------------------------------------------- /image/7-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-19.png -------------------------------------------------------------------------------- /image/7-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-2.png -------------------------------------------------------------------------------- /image/7-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-20.jpg -------------------------------------------------------------------------------- /image/7-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-21.jpg -------------------------------------------------------------------------------- /image/7-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-22.png -------------------------------------------------------------------------------- /image/7-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-23.jpg -------------------------------------------------------------------------------- /image/7-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-24.jpg -------------------------------------------------------------------------------- /image/7-25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-25.jpg -------------------------------------------------------------------------------- /image/7-26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-26.jpg -------------------------------------------------------------------------------- /image/7-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-27.jpg -------------------------------------------------------------------------------- /image/7-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-28.jpg -------------------------------------------------------------------------------- /image/7-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-3.jpg -------------------------------------------------------------------------------- /image/7-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-4.jpg -------------------------------------------------------------------------------- /image/7-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-5.jpg -------------------------------------------------------------------------------- /image/7-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-6.png -------------------------------------------------------------------------------- /image/7-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-7.png -------------------------------------------------------------------------------- /image/7-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-8.png -------------------------------------------------------------------------------- /image/7-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/7-9.png -------------------------------------------------------------------------------- /image/8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-1.png -------------------------------------------------------------------------------- /image/8-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-10.jpg -------------------------------------------------------------------------------- /image/8-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-11.jpg -------------------------------------------------------------------------------- /image/8-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-12.jpg -------------------------------------------------------------------------------- /image/8-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-13.jpg -------------------------------------------------------------------------------- /image/8-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-14.jpg -------------------------------------------------------------------------------- /image/8-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-15.png -------------------------------------------------------------------------------- /image/8-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-16.png -------------------------------------------------------------------------------- /image/8-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-17.jpg -------------------------------------------------------------------------------- /image/8-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-18.jpg -------------------------------------------------------------------------------- /image/8-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-19.jpg -------------------------------------------------------------------------------- /image/8-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-2.jpg -------------------------------------------------------------------------------- /image/8-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-20.jpg -------------------------------------------------------------------------------- /image/8-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-21.jpg -------------------------------------------------------------------------------- /image/8-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-22.jpg -------------------------------------------------------------------------------- /image/8-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-23.jpg -------------------------------------------------------------------------------- /image/8-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-24.png -------------------------------------------------------------------------------- /image/8-25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-25.jpg -------------------------------------------------------------------------------- /image/8-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-26.png -------------------------------------------------------------------------------- /image/8-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-27.png -------------------------------------------------------------------------------- /image/8-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-28.png -------------------------------------------------------------------------------- /image/8-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-29.png -------------------------------------------------------------------------------- /image/8-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-3.jpg -------------------------------------------------------------------------------- /image/8-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-30.png -------------------------------------------------------------------------------- /image/8-31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-31.jpg -------------------------------------------------------------------------------- /image/8-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-32.png -------------------------------------------------------------------------------- /image/8-33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-33.jpg -------------------------------------------------------------------------------- /image/8-34.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-34.jpg -------------------------------------------------------------------------------- /image/8-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-4.jpg -------------------------------------------------------------------------------- /image/8-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-5.jpg -------------------------------------------------------------------------------- /image/8-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-6.jpg -------------------------------------------------------------------------------- /image/8-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-7.jpg -------------------------------------------------------------------------------- /image/8-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-8.jpg -------------------------------------------------------------------------------- /image/8-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/8-9.jpg -------------------------------------------------------------------------------- /image/9-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-1.jpg -------------------------------------------------------------------------------- /image/9-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-10.jpg -------------------------------------------------------------------------------- /image/9-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-11.jpg -------------------------------------------------------------------------------- /image/9-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-12.jpg -------------------------------------------------------------------------------- /image/9-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-13.jpg -------------------------------------------------------------------------------- /image/9-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-14.jpg -------------------------------------------------------------------------------- /image/9-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-15.jpg -------------------------------------------------------------------------------- /image/9-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-16.jpg -------------------------------------------------------------------------------- /image/9-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-17.jpg -------------------------------------------------------------------------------- /image/9-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-18.jpg -------------------------------------------------------------------------------- /image/9-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-19.jpg -------------------------------------------------------------------------------- /image/9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-2.png -------------------------------------------------------------------------------- /image/9-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-20.jpg -------------------------------------------------------------------------------- /image/9-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-21.png -------------------------------------------------------------------------------- /image/9-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-22.jpg -------------------------------------------------------------------------------- /image/9-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-23.jpg -------------------------------------------------------------------------------- /image/9-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-24.jpg -------------------------------------------------------------------------------- /image/9-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-25.png -------------------------------------------------------------------------------- /image/9-26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-26.jpg -------------------------------------------------------------------------------- /image/9-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-27.jpg -------------------------------------------------------------------------------- /image/9-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-28.jpg -------------------------------------------------------------------------------- /image/9-29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-29.jpg -------------------------------------------------------------------------------- /image/9-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-3.jpg -------------------------------------------------------------------------------- /image/9-30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-30.jpg -------------------------------------------------------------------------------- /image/9-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-4.jpg -------------------------------------------------------------------------------- /image/9-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-5.png -------------------------------------------------------------------------------- /image/9-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-6.jpg -------------------------------------------------------------------------------- /image/9-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-7.jpg -------------------------------------------------------------------------------- /image/9-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-8.png -------------------------------------------------------------------------------- /image/9-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/9-9.jpg -------------------------------------------------------------------------------- /image/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LZHaini/Python3WebSpider/5fdbda992ef17b1b6153eb6213d70e80834fbddd/image/cover.jpg --------------------------------------------------------------------------------