# python-scrape-django **Repository Path**: custer_git/python-scrape-django ## Basic Information - **Project Name**: python-scrape-django - **Description**: [imooc](https://coding.imooc.com/learn/list/92.html) 短期项目学习 2018年5月3日 开始 配合 omnifocus 和 omniplan 学习 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2018-05-03 - **Last Updated**: 2022-06-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # python-scrape-django ####项目介绍 [imooc Python 分布式爬虫打造搜索引擎](https://coding.imooc.com/learn/list/92.html) 短期项目学习 2018年5月3日 开始 配合 omnifocus 和 omniplan 学习 ###学习流程 * [第1章 课程介绍](#info) * [第2章 windows下搭建开发环境](#env) [1. 2-1 pycharm的安装和简单使用 (09:07) ](#2-1) [2. 2-2 mysql和navicat的安装和使用 (16:20)](#2-2) [3. 2-3 windows和linux下安装python2和python3 (06:49)](#2-3) [4. 2-4 虚拟环境的安装和配置 (30:53)](#2-4) * [第3章 爬虫基础知识回顾](#scrapy-intro) [1. 3-1 技术选型 爬虫能做什么 (09:50)](#3-1) [2. 3-2 正则表达式-1 (18:31)](#3-2) [3. 3-3 正则表达式-2 (19:04)](#3-3) [4. 3-4 正则表达式-3 (20:16)](#3-4) [5. 3-5 深度优先和广度优先原理 (25:15)](#3-5) [6. 3-6 url去重方法 (07:44)](#3-6) [7. 3-7 彻底搞清楚unicode和utf8编码 (18:31)](#3-7) * [第4章 scrapy爬取知名技术文章网站](#scrapy-tech) [1. 4-1 scrapy安装以及目录结构介绍 (22:33)](#4-1) [2. 4-2 pycharm 调试scrapy 执行流程 (12:35)](#4-2) [3、4、5. 4-3 xpath的用法 - 1、2、3 (22:17)](#4-3) [6. 4-6 css选择器实现字段解析 - 1 (17:21)](#4-6) [7. 4-7 css选择器实现字段解析 - 2 (16:31)](#4-7) [8. 4-8 编写spider爬取jobbole的所有文章 - 1 (15:40)](#4-8) [9. 4-9 编写spider爬取jobbole的所有文章 - 2 (09:45)](#4-9) [10. 4-10 items设计 - 1 (14:49)](#4-10) [11. 4-11 items设计 - 2 (15:45)](#4-11) [12. 4-12 items设计 - 3 (17:05)](#4-12) [13. 4-13 数据表设计和保存item到json文件 (18:17)](#4-13) [14. 4-14 通过pipeline保存数据到mysql - 1 (18:41)](#4-14) [15. 4-15 通过pipeline保存数据到mysql - 2 (17:58)](#4-15) [16. 4-16 scrapy item loader机制 - 1 (17:26)](#4-16) [17. 4-17 scrapy item loader机制- 2 (20:31)](#4-17) * [第5章 scrapy爬取知名问答网站](#scrapy-quaro) [1. 5-1 session和cookie自动登录机制 (20:10)](#5-1) [2. 5-2 (补充)selenium模拟知乎登录-2017-12-29 (18:02)](#5-2) [3. 5-3 requests模拟登陆知乎 - 1 (13:32)](#5-3) [4. 5-4 requests模拟登陆知乎 - 2 (13:16)](#5-4) [5. 5-5 requests模拟登陆知乎 - 3 (12:21)](#5-5) [6. 5-6 scrapy模拟知乎登录 (20:46)](#5-6) [7. 5-7 知乎分析以及数据表设计1 (15:17)](#5-7) [8. 5-8 知乎分析以及数据表设计 - 2 (13:35)](#5-8) [9. 5-9 item loder方式提取question - 1 (14:57)](#5-9) [10. 5-10 item loder方式提取question - 2 (15:20)](#5-10) [11. 5-11 item loder方式提取question - 3 (06:45)](#5-11) [12. 5-12 知乎spider爬虫逻辑的实现以及answer的提取 - 1 (15:54)](#5-12) [13. 5-13 知乎spider爬虫逻辑的实现以及answer的提取 - 2 (17:04)](#5-13) [14. 5-14 保存数据到mysql中 -1 (17:27)](#5-14) [15. 5-15 保存数据到mysql中 -2 (17:22)](#5-15) [16. 5-16 保存数据到mysql中 -3 (16:09)](#5-16) [17. 5-17 (补充小节)知乎验证码登录 - 1_1 (16:41)](#5-17) [18. 5-18 (补充小节)知乎验证码登录 - 2_1 (10:32)](#5-18) [19. 5-19 (补充)知乎倒立文字识别-1 (24:02)](#5-19) [20. 5-20 (补充)知乎倒立文字识别-2 (20:30)](#5-20) * [第6章 通过CrawlSpider对招聘网站进行整站爬取](#scrapy-job) [1. 6-1 数据表结构设计 (15:33)](#6-1) [2. 6-2 CrawlSpider源码分析-新建CrawlSpider与settings配置 (12:50)](#6-2) [3. 6-3 CrawlSpider源码分析 (25:29)](#6-3) [4. 6-4 Rule和LinkExtractor使用 (14:28)](#6-4) [5. 6-5 item loader方式解析职位 (24:46)](#6-5) [6. 6-6 职位数据入库-1 (19:01)](#6-6) [7. 6-7 职位信息入库-2 (11:19)](#6-7) * [第7章 Scrapy突破反爬虫的限制](#scrapy-break) [1. 7-1 爬虫和反爬的对抗过程以及策略 (20:17) ](#7-1) [2. 7-2 scrapy架构源码分析 (10:45)](#7-2) [3. 7-3 Requests和Response介绍 (10:18) ](#7-3) [4. 7-4 通过downloadmiddleware随机更换user-agent-1 (17:00)](#7-4) [5. 7-5 通过downloadmiddleware随机更换user-agent - 2 (17:13)](#7-5) [6. 7-6 scrapy实现ip代理池 - 1 (16:51)](#7-6) [7. 7-7 scrapy实现ip代理池 - 2 (17:39)](#7-7) [8. 7-8 scrapy实现ip代理池 - 3 (18:46)](#7-8) [9. 7-9 云打码实现验证码识别 (22:37)](#7-9) [10. 7-10 cookie禁用、自动限速、自定义spider的settings (07:22)](#7-10) * [第8章 scrapy进阶开发](#scrapy-upon) [1. 8-1 selenium动态网页请求与模拟登录知乎 (21:24)](#8-1) [2. 8-2 selenium模拟登录微博, 模拟鼠标下拉 (11:06)](#8-2) [3. 8-3 chromedriver不加载图片、phantomjs获取动态网页 (09:59)](#8-3) [4. 8-4 selenium集成到scrapy中 (19:43)](#8-4) [5. 8-5 其余动态网页获取技术介绍-chrome无界面运行、scrapy-splash、selenium-grid, splinter (07:50)](#8-5) [6. 8-6 scrapy的暂停与重启 (12:58)](#8-6) [7. 8-7 scrapy url去重原理 (05:45)](#8-7) [8. 8-8 scrapy telnet服务 (07:37)](#8-8) [9. 8-9 spider middleware 详解 (15:25)](#8-9) [10. 8-10 scrapy的数据收集 (13:44)](#8-10) [11. 8-11 scrapy信号详解 (13:05)](#8-11) [12. 8-12 scrapy扩展开发 (13:16)](#8-12) * [第9章 scrapy-redis分布式爬虫](#scrapy-redis) [1. 9-1 分布式爬虫要点 (08:39)](#9-1) [2. 9-2 redis基础知识 - 1 (20:31) ](#9-2) [3. 9-3 redis基础知识 - 2 (15:58)](#9-3) [4. 9-4 scrapy-redis编写分布式爬虫代码 (21:06)](#9-4) [5. 9-5 scrapy源码解析-connection.py、defaults.py- (11:05)](#9-5) [6. 9-6 scrapy-redis源码剖析-dupefilter.py- (05:29)](#9-6) [7. 9-7 scrapy-redis源码剖析- pipelines.py、 queue.py- (10:41)](#9-7) [8. 9-8 scrapy-redis源码分析- scheduler.py、spider.py- (11:52)](#9-8) [9. 9-9 集成bloomfilter到scrapy-redis中 (19:30)](#9-9) * [第10章 elasticsearch搜索引擎的使用](#elasticsearch) [1. 10-1 elasticsearch介绍 (18:21)](#10-1) [2. 10-2 elasticsearch安装 (13:24)](#10-2) [3. 10-3 elasticsearch-head插件以及kibana的安装 (24:09)](#10-3) [4. 10-4 elasticsearch的基本概念 (12:15)](#10-4) [5. 10-5 倒排索引 (11:24)](#10-5) [6. 10-6 elasticsearch 基本的索引和文档CRUD操作 (18:44)](#10-6) [7. 10-7 elasticsearch的mget和bulk批量操作 (12:36)](#10-7) [8. 10-8 elasticsearch的mapping映射管理 (21:03)](#10-8) [9. 10-9 elasticsearch的简单查询 - 1 (14:56)](#10-9) [10. 10-10 elasticsearch的简单查询 - 2 (11:12)](#10-10) [11. 10-11 elasticsearch的bool组合查询 (22:58)](#10-11) [12. 10-12 scrapy写入数据到elasticsearch中 - 1 (14:16)](#10-12) [13. 10-13 scrapy写入数据到elasticsearch中 - 2 (11:15)](#10-13) * [第11章 django搭建搜索网站](#django) [1. 11-1 es完成搜索建议-搜索建议字段保存 - 1 (13:45)](#11-1) [2. 11-2 es完成搜索建议-搜索建议字段保存 - 2 (13:34)](#11-2) [3. 11-3 django实现elasticsearch的搜索建议 - 1 (19:57)](#11-3) [4. 11-4 django实现elasticsearch的搜索建议 - 2 (18:15)](#11-4) [5. 11-5 django实现elasticsearch的搜索功能 -1 (14:06)](#11-5) [6. 11-6 django实现elasticsearch的搜索功能 -2 (13:14)](#11-6) [7. 11-7 django实现搜索结果分页 (09:12)](#11-7) [8. 11-8 搜索记录、热门搜索功能实现 - 1 (14:34) ](#11-8) [9. 11-9 搜索记录、热门搜索功能实现 - 2 (14:04)](#11-9) * [第12章 scrapyd部署scrapy爬虫](#scrapyd) * [第13章 课程总结](#end) #### 第1章 课程介绍 #### 第2章 windows下搭建开发环境 ##### 2-1 pycharm的安装和简单使用 1. IDE -- pycharm 2. 数据库 -- mysql、redis、elasticsearch 3. 开发环境 -- virtualenv ##### 2-2 mysql和navicat的安装和使用 ##### 2-3 windows和linux下安装python2和python3 ##### 2-4 虚拟环境的安装和配置 #### 第3章 爬虫基础知识回顾 ##### 3-1 技术选型 爬虫能做什么 基础知识:技术选型 scrapy vs requests+beautifulsoup 1. requests 和 beautifulsoup 都是库,scrapy 是框架 2. scrapy 框架中可以加入 requests 和 beautifulsoup 3. scrapy 基于 twisted, 性能是最大的优势 4. scrapy 方便扩展,提供了很多内置的功能 5. scrapy 内置的 css 和 xpath selector 非常方便,beautifulsoup 最大的缺点就死慢 常见类型的网页分类 1. 静态网页 2. 动态网页 3. webservice(restapi) 爬虫能做什么 爬虫作用 1. 搜索引擎 -- 百度、google、垂直领域搜索引擎 2. 推荐引擎 -- 今日头条 3. 机器学习的数据样本 4. 数据分析(如 金融数据分析)、舆情分析等 ##### 3-2 正则表达式-1 正则表达式介绍(模式匹配) 1. 特殊字符 1)^ $ * ? + {2} {2,} {2,5} | 2)[] [^] [a-z] . 3)\s \S \w \W 4)[\u4E00-\u9FA5] () \d 2. 正则表达式的简单应用及python示例 ``` import re line = "custercode123" regex_str = "^c.*" # ^ 代表以什么开头 . 代表任意字符 * 代表前面的字符可以重复任意多遍 if re.match(regex_str, line): # regex_str代表模式字符串, line代表要匹配的字符串 print("yes") # 如果匹配成功会有返回函数的 ``` ![](http://ovc37dj11.bkt.clouddn.com/15253328378132.jpg) 贪婪匹配默认从后往前匹配的,匹配成功就返回,不再继续 ![](http://ovc37dj11.bkt.clouddn.com/15253355710058.jpg) 在前面 加上 ? 就强制了 从前面开始匹配 ![](http://ovc37dj11.bkt.clouddn.com/15253358501186.jpg) ##### 3-3 正则表达式-2 ![](http://ovc37dj11.bkt.clouddn.com/15253370315465.jpg) ![](http://ovc37dj11.bkt.clouddn.com/15253378547718.jpg) ##### 3-4 正则表达式-3 ![](http://ovc37dj11.bkt.clouddn.com/15253393383433.jpg) ``` 总结: ^ 以什么开头 $ 以什么结尾 * 限定前面字符出现任意多次 >= 0 次 + 限定前面字符出现任意多次 >= 1 次 ?代表非贪婪模式 {2}限定前面词出现次数,确定的2次 {2,} 大于等于2次 {2,5}大于等于2次,小于5次 | 竖线两旁的只要出现一个就可以 [] 里的任意字符只要出现都可以 [^] 取反 [a-z] 区间 a-z 字符 . 任意字符 包含 \w \s 空格 \S 非空格 \w 字母数字下划线 \W 取反 [\u4E00-\u9FA5] 汉字区间 ()取子字符串 \d 数字 ``` ``` # 例子: 提取出生日期 line_1 = "XXX出生于2001年6月1日" line_2 = "XXX出生于2001/6/1" line_3 = "XXX出生于2001-6-1" line_4 = "XXX出生于2001-06-01" line_5 = "XXX出生于2001-06" # 一个正则表达式要处理这么多种情况 regex_str_1 = ".*出生于(\d{4}[年/-]\d{1,2}([月/-]\d{1,2}|[月/-]$|$))" # 要提取的子字符串用()括号括起来 match_obj_1 = re.match(regex_str_1, line_1) if match_obj_1: print(match_obj_1.group(1)) ``` ![](http://ovc37dj11.bkt.clouddn.com/15253407268335.jpg) ##### 3-5 深度优先和广度优先原理 1. 网站的树结构 2. 深度优先算法和实现 3. 广度优先算法和实现 首先分析网站 url 的结构 ![](http://ovc37dj11.bkt.clouddn.com/15253408410489.jpg) 然后分析网站 url 链接跳转的结构 ![](http://ovc37dj11.bkt.clouddn.com/15253412338148.jpg) 爬虫会循环环路爬取,会造成死循环 (通过首页开始爬取,但一般每个子页面都有导航) 使用去重,把已经爬取的 url 放入 一个 list 中,可以跳过重复的 url 通过二叉树模型遍历来简化网站爬取 来理解爬取原理 ![](http://ovc37dj11.bkt.clouddn.com/15253417682322.jpg) 1. 深度优先输出 A、B、D、E、I、C、F、G、H (递归实现) 2. 广度优先输出 A、B、C、D、E、F、G、H、I (队列实现) ``` #深度优先过程 def depth_tree(tree_node): if tree_node is not None: print(tree_node._data) if tree_node._left is not None: return depth_tree(tree_node._left) if tree_node._right is not None: return depth_tree(tree_node._right) ``` ``` #广度优先过程 def level_queue(root): """利用队列实现树的广度优先遍历""" if root is None: return my_queue = [] node = root my_queue.append(node) while my_queue: node = my_queue.pop(0) print(node.elem) if node.lchild is not None: my_queue.append(node.lchild) if node.rchild is not None: my_queue.append(node.rchild) ``` ##### 3-6 url去重方法 爬虫去重策略 1. 将访问过的 url 保存到数据库中 2. 将访问过的 url 保存到 set 中,只需要 o(1) 的代价就可以查询 url > 100000000 * 2 byte * 50 个字符 / 1024/1024/1024 = 9G (1亿条url-9G内存) 3. url 经过 md5 等方法哈希后保存到 set 中 > 字符经过md5编码,可以将字符缩减到固定的长度md5一般是128 bit 大概为16byte,一个url 占用 16,上面占用为 2 * 50 = 100 byte,压缩6.25倍,1亿条url占用1.44G内存 4. 用 bitmap 方法,将访问过的 url 通过 hash 函数映射到某一位 > 申请 8 位 为 1 个 bit,将 url 映射到 8个不同的位置,缺点是冲突非常高,可能将多个 url 映射到一个位上 5. bloomfilter 方法对 bitmap 进行改进,多重 hash 函数降低冲突 > 1个亿 url / 8 / 1024 /1024 = 12M scrapy 实际上运用的是第三种方法 ##### 3-7 彻底搞清楚unicode和utf8编码 字符串编码历史 1. 计算机只能处理数字,文本转换为数字才能处理。计算机中8个bit作为一个字节,所以一个字节能表示最大的数字就是255 2. 计算机是美国人发明的,所以一个字节可以表示所有字符了,所以ASCII(一个字节)编码就成为美国人的标准编码 3. 但是ASCII处理中文明显是不够的,中文不止255个汉字,所以中国制定了GB2312编码,用两个字节表示一个汉字。GB2312还把ASCII包含进去了,同理,日文,韩文等等上百个国家为了解决这个问题就都发展了一套字节的编码,标准就越来越多,如果出现多种语言混合显示就一定会出现乱码 4. 于是 unicode 就出现了,将所有语言统一到一套编码里 5. 看下ASCII 和 unicode 编码: 1. 字母A 用 ASCII 编码十进制是 65,二进制是 0100 0001 2. 汉字“中”已经超出了 ASCII 编码的范围,用 unicode 编码是 20013 二进制是 01001110 00101101 3. A用 unicode 编码只需要前面补0二进制是 00000000 0100 0001 6. 乱码问题解决了,但是如果内容全是英文,unicode 编码比 ASCII 需要多一倍的存储空间,同时如果传输需要多一倍的传输 7. 所以出现了可变长的编码“utf-8”,把英文变长一个字节,汉字3个字节。特别生僻的变成4-6字节,如果传输大量的英文,utf8作用就很明显了 ![](http://ovc37dj11.bkt.clouddn.com/15253482192966.jpg) #### 第4章 scrapy爬取知名技术文章网站 ##### 4-1 scrapy安装以及目录结构介绍 第一步、新建虚拟环境 ``` conda info --envs # 查看当前所有虚拟环境 conda create --name spider python=3.6 ``` 第二步、安装scrapy `pip install -i https://pypi.douban.com/simple scrapy` 第三步、新建scrapy项目 因为pycharm中没有提供新建 scrapy 项目功能,我们需要手动创建 `scrapy startproject ArticleSpider` ![](http://ovc37dj11.bkt.clouddn.com/15253496755287.jpg) 使用 pycharm 打开新建的工程 ![](http://ovc37dj11.bkt.clouddn.com/15253501428999.jpg) pipelines 和数据存储相关 middlewares 可以存放我们自己定义的中间件,让scrapy变得更加可控 iterms 相当于Django中的 form,但是字段类型单一,比较简单 spiders 文件夹存放具体某一个网站的爬虫 提供了命令 `scrapy genspider example example.com` ![](http://ovc37dj11.bkt.clouddn.com/15253504155284.jpg) ![](http://ovc37dj11.bkt.clouddn.com/15253504708524.jpg) #####4-2 pycharm 调试scrapy 执行流程 第一步、更改pycharm编译环境 ![](http://ovc37dj11.bkt.clouddn.com/15253581812099.jpg) 第二步、学习初始代码 ![](http://ovc37dj11.bkt.clouddn.com/15253582389681.jpg) ``` # -*- coding: utf-8 -*- import scrapy class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] start_urls = ['http://blog.jobbole.com/'] def parse(self, response): pass ``` start_urls 列表可以放入想爬取的所有 url Spider 里面有个 start_request(self) 函数 ![](http://ovc37dj11.bkt.clouddn.com/15253585069304.jpg) ``` def start_requests(self): for url in self.start_urls: yield self.make_requests_from_url(url) ``` for 循环遍历 url 传递给 make_requests_from_url(url) 函数 ``` def make_requests_from_url(self, url): """ This method is deprecated. """ return Request(url, dont_filter=True) ``` 调用 Request 是 Scrapy 的下载器,下载完成之后会继续往下执行 `def parse(self, respinse): pass` 这就是最简单的 Scrapy 原理,数据流向 所以每个 url 下载之后都会进入 def parse(self, response)函数 因为 Pycharm 没有提供 Scrap 框架所以我们自己写个 `main.py` 来进行调试 使用 Scrapy 内置函数 `from scrapy.cmdline import execute` 需要设置工程的目录,要将主目录设置正确调用 execute 是不会找到这个项目工程目录的,我们的 scrapy 命令要在这个工程目录里运行 我们可以把目录拷贝进来实际上是可以的 `sys.path.append("/Users/xxxx/ArticleSpider")` 但是在服务器上运行,这个路径就要改变,所以可以使用python的一个技巧,获取当前`main.py`当前的路径,然后这个路径的父目录就是这个项目工程目录 `os.path.abspath(__file__)` 这个 `__file__` 指的是当前 `main.py` 文件,通过 `os.path.abspath(__file__)` 获取到当前 `main.py` 文件的路径 再通过 `os.path.dirname()` 获取到父目录,所在的文件夹所在的目录 ``` import sys import os print(os.path.dirname(os.path.abspath(__file__))) ``` ![](http://ovc37dj11.bkt.clouddn.com/15253595930155.jpg) 下一步关键,调用 `execute()` 函数来调用 Scrapy 命令, 函数 `execute()` 可以传递一个数组,第一个参数是 "scrapy"命令,然后是 "crawl" Scrapy 启动 spider 的命令是 `scrapy crawl + 名字` 名字和 name 是相同的 ![](http://ovc37dj11.bkt.clouddn.com/15253598606207.jpg) 所以我们启动 jobbole 的 spider 命令就是 `scrapy crawl jobbole` windows 可能会出现缺少 win32api module 错误 ![](http://ovc37dj11.bkt.clouddn.com/15253601221116.jpg) 安装 `pip install pypiwin32` 来解决问题 ![](http://ovc37dj11.bkt.clouddn.com/15253599800481.jpg) 启动 spider 成功,这样就可以把这个命令配置到 `main.py` 中来 `execute(["scrapy", "crawl", "jobbole"])` ![](http://ovc37dj11.bkt.clouddn.com/15253603563985.jpg) 这样配置好就可以打断点来调试了,调试之前还需要设置 `settings.py` ![](http://ovc37dj11.bkt.clouddn.com/15253604993302.jpg) 默认是遵循 robots 协议,修改为 `ROBOTSTXT_OBEY = False` 不设置的话 Scrapy 默认会读取每个网站上 robots 协议,它会把我们不符合 robots协议的 url 给过滤掉,*不设置 False 会发现爬虫很快就停止了*因为爬虫会遵循 robots 协议把很多 url 给过滤掉 现在可以开始来调试了,debug main.py 文件 ![](http://ovc37dj11.bkt.clouddn.com/15253608241339.jpg) 有了这个基础之后就可以来做解析了,如何从返回 html 中去获取到我们需要的指定字段的值,然后把她们保存到我们数据库中 ##### 4-3 xpath的用法 - 1、2、3 上面介绍了如何来启动 Scrapy 项目,下面来学习 xpath 来从我们 html 中,提取出我们需要的值 ![](http://ovc37dj11.bkt.clouddn.com/15253612128129.jpg) 我们可以提取的值 title、时间、多少评论、正文内容 * xpath简介 1. xpath使用路径表达式在 xml 和 html 中进行导航 2. xpath包含标准函数库 3. xpath是一个 w3c 的标准 * xpath术语 xpath节点关系 父节点、子节点、同袍节点、先辈节点、后代节点xpath提取元素的时候帮助我们理解 * xpath语法 表达式 | 说明 ------|------ article | 选取所有article元素的所有子节点 /article | 选取根元素article article/a | 选取所有属于article的子元素的a元素 //div | 选取所有div子元素(不论出现在文档任何地方) article//div | 选取所有属于article元素的后代的div元素,不管它出现在article之下的任何位置 //@class | 选取所有名为class的属性 /article/div[1]. | 选取属于article子元素的第一个div元素 /article/div[last()]. | 选取属于article子元素的最后一个div元素 /article/div[last()-1]| 选取属于article子元素的倒数第二个div元素 //div[@lang] | 选取所有拥有lang属性的div元素 //div[@lang='eng'] | 选取所有lang属性为eng的div元素 /div/* | 选取属于div元素的所有子节点 //* | 选取所有元素 //div[@*] | 选取所有带属性的title元素 /div/a或//div/p | 选取所有div元素的a和p元素 //span或//ul | 选取文档中的span和ul元素 article/div/p或//span | 选取所有属于article元素的div元素的p元素,以及文档中所有的span元素 回到代码 把 `start_urls = ['http://blog.jobbole.com/113568/']` 改成我们需要抓取的文章的地址 然后直接调用 `response.xpath()` 方法 ![](http://ovc37dj11.bkt.clouddn.com/15253627386379.jpg) `/html/body/div[3]/div[3]/div[1]/div[1]` 这里div[3]是第三个不是从0开始的 检查我们是否正确可以 右键查看 ![](http://ovc37dj11.bkt.clouddn.com/15253629973588.jpg) `re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1") ` 这里我们看`re_selector`返回的是什么值 ``` # -*- coding: utf-8 -*- import scrapy class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] # start_urls = ['http://blog.jobbole.com/'] start_urls = ['http://blog.jobbole.com/113568/'] def parse(self, response): re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1") pass ``` 来`Debug ‘main’`查看 ![](http://ovc37dj11.bkt.clouddn.com/15253633734195.jpg) 看下chrome 浏览器的 xpath 路径 `//*[@id="post-113568"]/div[1]/h1` ![](http://ovc37dj11.bkt.clouddn.com/15253636049591.jpg) 如果想要获取 值 而不要标签, 需要调用 `/text()` 函数 `//*[@id="post-113568"]/div[1]/h1/text()` ![](http://ovc37dj11.bkt.clouddn.com/15254126270509.jpg) 这样就只取到值了 > 注意:页面结构可能是运行了所有js动态产生的页面结构源码,所以 div 的层级可能不相同,就取不到值了 xpath用法 用 id 是最为准确的 scrapy 通过 shell 脚本来调试学习会更为的快速 在命令行 `scrapy shell ` 是对这个 url 进行调试 ![](http://ovc37dj11.bkt.clouddn.com/15254131106466.jpg) ![](http://ovc37dj11.bkt.clouddn.com/15254132638550.jpg) 输出 title 可以看出是一个 `Selector` 对象,可以使用 `extract()` 函数获取,返回一个数组,访问数组里的第一个值 xpath 返回的是 `Selector` 是可以继续执行的的, 而 `extract()` 返回的就是一个数组了 ![](http://ovc37dj11.bkt.clouddn.com/15254135160776.jpg) 继续爬取时间 `create_date = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()")` `create_date.extract()[0]` 去掉回车换行符 `create_date.extract()[0].strip()` 去掉结尾的点号 `create_date.extract()[0].strip().replace("·", "")` 最后再去掉空格 `create_date.extract()[0].strip().replace("·", "").strip()` ![](http://ovc37dj11.bkt.clouddn.com/15254151614273.jpg) 继续获取称赞数、收藏数、评论数 ![](http://ovc37dj11.bkt.clouddn.com/15254166468920.jpg) 这里有 class 里有很多属性,选取一个唯一的比如 `vote-post-up` ``` >>> response.xpath("//span[@class='vote-post-up']/text()").extract() [] ``` 上面的写法就查找不到,因为 `vote-post-up` 是众多 class 中的一个 所以*调用scrapy 的一个方法 `contains` 内置方法,第一个参数属性值* ```response.xpath("//span[contains(@class, 'vote-post-up')]")``` 这样就可以取到值了,再继续取 下面的标签 h10 和 text()值 ![](http://ovc37dj11.bkt.clouddn.com/15254189179337.jpg) ``` >>> response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0] ' 2 收藏' ``` 这里想要只需要数字2 就需要正则表达式 ``` In [1]: import re In [2]: match_re = re.match(r".*(\d+).*", "2 收藏") In [3]: if match_re: ...: print (match_re.group(1)) ...: 2 In [4]: ``` 这里取评论数 ``` response.xpath("//a[@href='#article-comment']/span").extract() ``` 提取评论数 ``` comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract()[0] match_re = re.match(r".*(\d+).*", comment_nums) if match_re: comment_nums = match_re.group(1) ``` 提取正文内容 ![](http://ovc37dj11.bkt.clouddn.com/15254214798417.jpg) 可以看到所有正文内容是放在 `
` 里的 ![](http://ovc37dj11.bkt.clouddn.com/15254216040678.jpg) `response.xpath("//div[@class='entry']").extract()` *这里需要说明的是获取到正文,如何获取正文的内容呢?一种简单的方式是把 entry 中所有的 div,或者说 div 下面的所有内容包括标签 全部保存。因为关于一片文章的正文分析实际上比较复杂的工作,因为不同的网站,它的正文内容排版是不同的,所以说怎么从这里面提取出它原来的样式,实际上是比较的课题,我们本课程不做这个课题的研究,所以我们只管将它的源码 html 保存下来,以后如果需要再对这里面继续做进一步的提取,和做样式的修改的话,就可以继续使用这一步的提取* `content = response.xpath("//div[@class='entry']").extract()[0]` 下面继续获取 标签 tag ![](http://ovc37dj11.bkt.clouddn.com/15254221349997.jpg) `tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract()` ![](http://ovc37dj11.bkt.clouddn.com/15254223324277.jpg) 使用列表生成式来删除重复的评论,完成过滤 `[element for element in tag_list if element.strip().endswith("评论")]` 对数组通过 逗号 join 的方式联系成字符串 `tags = ",".join(tag_list)` ##### 4-6 css选择器实现字段解析 - 1 css 选择器和 xpath 功能相同都是用来 **定位一个元素** 表达式 | 说明 ------|------ *|选取所有节点 "#container"| 选择id为container的节点 .container|选取所有class包含container的节点 li a | 选取所有 li 下的所有 a 节点 ul + p | 选择 ul 后面的第一个 p 元素 div#container>ul | 选取id 为 container 的 div 的第一个 ul 子元素 ul~p|选取与ul相邻的所有p元素 a[title]|选取所有有title属性的a元素 a[href="http://jobbole.com"]|选取所有href属性为jobbole.com值的a元素 a[href*="jobble"]|选取所有href属性包含jobbole的a元素 a[href^="http"]|选取所有href属性值以http开头的a元素 a[href$=".jpg"]|选取所有href属性值以.jpg结尾的a元素 input[type=radio]:checked|选取选中的radio的元素 div:not(#container)|选取所有id非container的div属性 li:nth-child(3)|选取第三个li元素 tr:nth-child(2n)|选取第偶数个tr ``` >>> response.css(".entry-header h1") [] >>> response.css(".entry-header h1").extract() ['

最终一轮面试被 Google 刷掉,这是一种什么样的体验?

'] >>> response.css(".entry-header h1::text").extract() ['最终一轮面试被 Google 刷掉,这是一种什么样的体验?'] >>> ``` ##### 4-7 css选择器实现字段解析 - 2 ![](http://ovc37dj11.bkt.clouddn.com/15254282436294.jpg) 点赞数量 `>>> response.css(".vote-post-up h10::text").extract()[0] '1'` 收藏数量 ![](http://ovc37dj11.bkt.clouddn.com/15254866129354.jpg) `>>> response.css("span.bookmark-btn::text").extract()[0] ' 2 收藏'` 这里 `bookmark-btn` 是唯一的也可以去掉的 然后使用正则表达式,就可以匹配 ``` match_re = re.match(r".*(\d+).*", fav_nums) if match_re: fav_nums = match_re.group(1) ``` 继续提取评论数量 ![](http://ovc37dj11.bkt.clouddn.com/15254868447092.jpg) 通过a标签href的值来确定是哪一个标签 ![](http://ovc37dj11.bkt.clouddn.com/15254871013387.jpg) `>>> response.css("a[href='#article-comment'] span::text").extract(). [' 评论']` 然后写正则表达式 ``` match_re = re.match(r".*(\d+).*", comment_nums) if match_re: comment_nums = match_re.group(1) ``` 继续提取content 正文部分,通过前面 xpath 方法 `content = response.xpath("//div[@class='entry']").extract()[0]` 可以转换为 css 选择器定位 即 `response.css('div.entry').extract()[0]` ![](http://ovc37dj11.bkt.clouddn.com/15254873521506.jpg) 继续提取标签 ``` >>> response.css("p.entry-meta-hide-on-mobile a::text").extract()[0] '职场' >>> response.css("p.entry-meta-hide-on-mobile a::text").extract() ['职场', 'Google', '职场'] ``` 然后转接成字符串,只要把前面代码拷贝下来就可以了 ``` tag_list = [element for element in tag_list if element.strip().endswith("评论")] tags = ",".join(tag_list) ``` 这里调试,需要取消正则表达式匹配的非贪婪模式 ![](http://ovc37dj11.bkt.clouddn.com/15254877012193.jpg) ``` match_re = re.match(r".*?(\d+).*", comment_nums) if match_re: comment_nums = match_re.group(1) ``` 调试之后的代码片段 ``` # -*- coding: utf-8 -*- import scrapy import re class JobboleSpider(scrapy.Spider): name = 'jobbole' allowed_domains = ['blog.jobbole.com'] # start_urls = ['http://blog.jobbole.com/'] start_urls = ['http://blog.jobbole.com/113568/'] def parse(self, response): # re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1") # firefox # re_selector2 = response.xpath('//*[@id="post-113568"]/div[1]/h1/text()') # chrome title = response.xpath('//div[@class="entry-header"]/h1/text()').extract()[0] create_date = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()').extract()[0].strip().replace("·","").strip() praise_nums = response.xpath("//span[contains(@class, 'vote-post-up')]/h10/text()").extract()[0] fav_nums = response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0] match_re = re.match(r".*?(\d+).*", fav_nums) if match_re: fav_nums = match_re.group(1) comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract()[0] match_re = re.match(r".*?(\d+).*", comment_nums) if match_re: comment_nums = match_re.group(1) content = response.xpath("//div[@class='entry']").extract()[0] tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract() tag_list = [element for element in tag_list if not element.strip().endswith("评论")] tags = ",".join(tag_list) # 通过css选择器提取字段 title_css = response.css(".entry-header h1::text").extract() create_date_css = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip() fav_nums_css = response.css("span.bookmark-btn::text").extract()[0] match_re = re.match(r".*?(\d+).*", fav_nums_css) if match_re: fav_nums_css = match_re.group(1) comment_nums_css = response.css("a[href='#article-comment'] span::text").extract()[0] match_re = re.match(r".*?(\d+).*", comment_nums_css) if match_re: comment_nums_css = match_re.group(1) content_css = response.css('div.entry').extract()[0] tag_list_css = response.css("p.entry-meta-hide-on-mobile a::text").extract() tag_list_css = [element for element in tag_list_css if not element.strip().endswith("评论")] tags_css = ",".join(tag_list) ``` [这里代码变动](https://gitee.com/custer_git/python-scrape-django/commit/2c18332eb87f3a267cba1afe7beb4f1e9478cc3e#ac531e3d9fd4010bb2139ff9da7e729dc41a2b13_17_17) 我们使用 `extract()[0]` 的时候因为是数组下标所以需要有异常处理,我们替换使用 `extract_first()`函数就解决了这个问题 ``` def extract_first(self, default=None): """ Return the result of ``.extract()`` for the first element in this list. If the list is empty, return the default value. """ for x in self: return x.extract() else: return default get = extract_first ``` `extract_first()`函数会有一个默认值 `None` ,如果提取不到就有默认值,我们可以 `extract_first("")` [这里是代码变动](https://gitee.com/custer_git/python-scrape-django/commit/6ba1983d38565f5dd7e84039d51f957bbd7a1014#46857ba80b3b157285d4dcb7995154ba2ef965bd_282_285) ##### 4-8 编写spider爬取jobbole的所有文章 - 1 上面介绍了使用 xpath 和 css 选择器来获取字段,本节中介绍使用 scrapy 来获取所有列表。通过列表页来爬取所有文章 url,将文章字段解析出来,一页一页将第二页链接发送给 scrapy,自动下载后面的页面,这里不是通过 urllib 自己写,而是通过 改写 `def parse(self, response)` 函数 这里也不是通过 某一个文章的 url 了 `start_urls = ['http://blog.jobbole.com/all-posts/']` ![](http://ovc37dj11.bkt.clouddn.com/15255802117094.jpg) 我们是需要 ![](http://ovc37dj11.bkt.clouddn.com/15255802779385.jpg) **这里注意要取到 a 属性 href 在css 选择器中应该这样写 `a::attr(href)`** 所以就是 `response.css(".floated-thumb .post-thumb a::attr(href)")` ![](http://ovc37dj11.bkt.clouddn.com/15255821836619.jpg) 这里可以用 for 循环提取 所有的 url ``` >>> response.css(".floated-thumb .post-thumb a::attr(href)").extract() ['http://blog.jobbole.com/113942/', 'http://blog.jobbole.com/113938/', 'http://blog.jobbole.com/113936/', 'http://blog.jobbole.com/113930/', 'http://blog.jobbole.com/113926/', 'http://blog.jobbole.com/113923/', 'http://blog.jobbole.com/113920/', 'http://blog.jobbole.com/113917/', 'http://blog.jobbole.com/113912/', 'http://blog.jobbole.com/113909/', 'http://blog.jobbole.com/113744/', 'http://blog.jobbole.com/113905/', 'http://blog.jobbole.com/113901/', 'http://blog.jobbole.com/113896/', 'http://blog.jobbole.com/113894/', 'http://blog.jobbole.com/113568/', 'http://blog.jobbole.com/113885/', 'http://blog.jobbole.com/113883/', 'http://blog.jobbole.com/113879/', 'http://blog.jobbole.com/113820/', 'http://top.jobbole.com/38654/?utm_source=blog.jobbole.com&utm_medium=sidebar-top-news', 'http://top.jobbole.com/38647/?utm_source=blog.jobbole.com&utm_medium=sidebar-top-news', 'http://top.jobbole.com/38635/?utm_source=blog.jobbole.com&utm_medium=sidebar-top-news', 'http://top.jobbole.com/38641/?utm_source=blog.jobbole.com&utm_medium=sidebar-top-news', 'http://top.jobbole.com/38630/?utm_source=blog.jobbole.com&utm_medium=sidebar-top-news', 'http://hao.jobbole.com/mlpack/?utm_source=blog.jobbole.com&utm_medium=sidebar-resources', 'http://hao.jobbole.com/whitewidow/?utm_source=blog.jobbole.com&utm_medium=sidebar-resources', 'http://hao.jobbole.com/caffe/?utm_source=blog.jobbole.com&utm_medium=sidebar-resources', 'http://hao.jobbole.com/static_code_analysis_tool_list_opensource_enterprise/?utm_source=blog.jobbole.com&utm_medium=sidebar-resources', 'http://hao.jobbole.com/hotswapagent%ef%bc%9a%e6%94%af%e6%8c%81%e6%97%a0%e9%99%90%e6%ac%a1%e9%87%8d%e5%ae%9a%e4%b9%89%e8%bf%90%e8%a1%8c%e6%97%b6%e7%b1%bb%e4%b8%8e%e8%b5%84%e6%ba%90-2/?utm_source=blog.jobbole.com&utm_medium=sidebar-resources'] ``` 这里可以看到 也取出来了许多不相关的 url 比如`http://top.jobbole.com/38641/` 所以我们还要进一步做限定 ![](http://ovc37dj11.bkt.clouddn.com/15255825908499.jpg) 所以在写代码的时候应该经常调试,这样可以避免许多问题,这里改写 css 选择器 id 的选择器前面加一个 `#` ``` >>> response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() ['http://blog.jobbole.com/113942/', 'http://blog.jobbole.com/113938/', 'http://blog.jobbole.com/113936/', 'http://blog.jobbole.com/113930/', 'http://blog.jobbole.com/113926/', 'http://blog.jobbole.com/113923/', 'http://blog.jobbole.com/113920/', 'http://blog.jobbole.com/113917/', 'http://blog.jobbole.com/113912/', 'http://blog.jobbole.com/113909/', 'http://blog.jobbole.com/113744/', 'http://blog.jobbole.com/113905/', 'http://blog.jobbole.com/113901/', 'http://blog.jobbole.com/113896/', 'http://blog.jobbole.com/113894/', 'http://blog.jobbole.com/113568/', 'http://blog.jobbole.com/113885/', 'http://blog.jobbole.com/113883/', 'http://blog.jobbole.com/113879/', 'http://blog.jobbole.com/113820/'] ``` 这里我们已经完成了 提取url的过程,代码片段 ``` def parse(self, response): """ 1. 获取文章列表页中的文章 url 并交给 scrapy 下载后给解析函数进行具体字段的解析 2. 获取下一页的 url 并交给 scrapy 下载,下载完成后交给parse函数 :param response: :return: """ # 解析列表页中的所有文章 url 并交给 scrapy 下载后解析 post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() for post_url in post_urls: # 这里提取出了url 下一步 下载 下一步 交给自己的解析函数 print(post_url) ``` 提取url 之后,如何交给scrapy 进行下载,下载完成之后调用我们自己定义的解析函数,这里我们需要使用**scrapy另一个类 request `from scrapy.http import Request`** 这样调用 Request `Request(url=post_url)`,但是这个url 是我们文章详情页的url,不是现在文章的列表页,所以文章的详情页应该调用下面的逻辑,进行具体字段的解析。所以我们把下面的逻辑放入一个新的函数 `def parse_detail(self, response):` 中,这个函数的作用就是用来解析文章的具体字段,这个函数也是回调函数,我们希望上面的`Request`完成之后调用这个函数,所以我们 `Request(url=post_url, callback=self.parse_detial)` 但是这里我们需要注意,这里的 url 是带有完整域名的 url,但是很多网站是没有完整的域名的,所以需要把当前域名和这个提取的不完整的 href 域名进行 join 链接,这里是完整地址所以可以直接调用,如果不是完整的地址,就需要手动拼接 `response.url + post_url` 可以使用 python urllib 提供的 parse 函数 `from urllib import parse` 而在 python2 中是 `import urlparse` 这里面有 urljoin()函数,可以查看源代码 ![](http://ovc37dj11.bkt.clouddn.com/15255839451268.jpg) urljoin() 有两个参数,第一个 base,第二个是 url,需要不要传递主域名,他会自动把域名分解取出来,加上后面的url,所以我们可以直接传递当前返回的 `response.url` 和 `post_url` `Request(url=parse.urljoin(response.url, post_url), callback=self.parse_detial)` 这样就初始化了 `Request`,如何将它交给 `Scrapy` 下载,直接使用 `yield` 关键字就可以了 `yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse_detial)` ##### 4-9 编写spider爬取jobbole的所有文章 - 2 接下来要提取出下一页的 url 并交给 Scrapy 进行下载,首先让我们来先分析下页面的结构 ![](http://ovc37dj11.bkt.clouddn.com/15255844683841.jpg) 这里有 `` 我们通过 `class="next page-numbers"` 就可以定位到 `href` 的值了 怎么通过**两个 class 定位一个位置** `response.css(".next .page-numbers")` 这样表示这 `page-numbers` 在 `next`的下一层,但是我们只需要把中间空格去掉,就代表着一个class name 既有 `next` 又有 `page-numbsers`所以 `>>> response.css(".next.page-numbers").extract_first("") ''` 然后我们通过`::attr(href)`就可以取到`href`值 `>>> response.css(".next.page-number ").extract_first("") 'http://blog.jobbole.com/all-posts/page/2/'` 这样就提取到了下一页 url 值,继续要做判断,如果取到了url,和前面一样交给 Scrapy 下载器,**但是这里需要注意 callback,不再是提取文章具体字段的处理函数了,因为我们现在这个url实际上它还是列表页url,所以应该会过来继续执行parse,注意这里只传递进来parse函数名称作为参数,没有做调用,因为Scrapy框架底层是通过 Twitst 完成的,它会基于函数名自动调用函数** 这里通过 pycharm 进行调试 ![](http://ovc37dj11.bkt.clouddn.com/15255854776680.jpg) 这里面提取所有 post_urls 出来了,文章的具体链接 ![](http://ovc37dj11.bkt.clouddn.com/15255856390467.jpg) 继续走下去异步调用 callback 函数 response就是具体的一个文章的url 了 然后看 评论和收藏的数据都有问题 ![](http://ovc37dj11.bkt.clouddn.com/15255857714970.jpg) 这里我们先查看这个网页的具体内容 ![](http://ovc37dj11.bkt.clouddn.com/15255858427294.jpg) 因为这里只有评论和收藏这两个字,没有数字,既然没有数字,我们来分析下我们的代码,既然没有取到就设置为0,这里修改代码 ``` comment_nums_css = response.css("a[href='#article-comment'] span::text").extract_first(0) match_re = re.match(r".*?(\d+).*", comment_nums_css) if match_re: comment_nums_css = int(match_re.group(1)) else: comment_nums = 0 ``` 全部修改好之后,再重新调试一下 以上简单几行代码就完成了爬取列表页所有文章列表解析具体字段,然后在继续解析下一列表页所有文章字段,[变动的代码](https://gitee.com/custer_git/python-scrape-django/commit/57c7be5141b7ee8b795f97578454405ff5a41fa6#46857ba80b3b157285d4dcb7995154ba2ef965bd_285_320) **[这里的爬虫代码片段gist](https://gitee.com/custer_git/codes/nuco2ymldqh8wb9xfk0pi87)** ##### 4-10 items设计 - 1 数据爬取的主要目的,是从非结构性的数据源来提取到结构性的数据,把非结构性的网页,很多网页爬取之后结构化成我们自己定义的结构。我们提取到数据之后,怎么把数据返回,一个简单的方式是把数据存放到字典中,然后通过字典返回给 Scrapy,虽然字典很好用,但是字典缺少结构性的东西,比如容易打错字段的名字等,为了完整的格式化, Scrapy 给我们提供了 item 类,像 django 一样指定字段,在对item 实例化之后,在 Scrapy 里面对它做 yield 的时候,可以在 pipelines 里集中处理对数据的保存、去重等等,以上就是 item 带给我们的好处 下面看如何定义 item,我们这里处理文章,所以定义 ``` class ArticleItem(scrapy.Item): title = scrapy.Field() ``` Scrpay Item 只有 Field,所以赋值的时候更像一个字典完成的 这里我们要保存 title、create_date、url然后这里还有一张图片,封面图是使用文章内容的第一张图片的,即文章的第一张图片是列表页的封面图的,这里就介绍**另一个 Scrapy 中 Request 使用的细节** 看之前的代码我们在 path里面获取到了文章具体的url,但是实际上在列表页中我们可以看到,实际上可以获取到这个图片的 ``` # 解析列表页中的所有文章 url 并交给 scrapy 下载后解析 post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract() for post_url in post_urls: yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse_detial) ``` 我们希望获取到这个图片的url 并把图片的url 放到 Request 里,再通过 Request 把这个值传递给 response ,因为这是个异步调用 ![](http://ovc37dj11.bkt.clouddn.com/15255920117176.jpg) **我们这里就要用到`Request`另一个变量 `meta`,这里很关键,因为以后极有可能会用到很多,`meat={}`传递过来的是字典** 接下来我们分析如何从列表页提取出封面图 ![](http://ovc37dj11.bkt.clouddn.com/15255922208485.jpg) ![](http://ovc37dj11.bkt.clouddn.com/15255922576311.jpg) 之前我们获取过这个 a 属性 `#archive .floated-thumb .post-thumb a::attr(href)` 可以看到上面的两个图,一个是有 域名的,一个是没有域名的,所以它是直接加到当前的域名之下的。**这个时候`parse.urljoin()`函数优势就体现出来了,传递进来的post_url,没有域名,就从response.url里提取域名给你放进来拼接,如果post_url有域名,就忽略了response.url** 现在来提取 image,来修改下 post_url 代码,之前说过**我们一旦对selector调用了extract()之后,就返回成一个数组,我们就无法进行二次selector,现在我们获取到 a 之后,我们还希望继续做进一步 selector,这里就是 Scrapy 给我们提供的方便之处,可以使用嵌套的selector** 这里 post_urls 就改成 post_nodes,可以通过post_node获取到 image_url ``` # 解析列表页中的所有文章 url 并交给 scrapy 下载后解析 post_nodes = response.css("#archive .floated-thumb .post-thumb a") for post_node in post_nodes: # 这里提取出了url 下一步 下载 下一步 交给自己的解析函数 image_url = post_node.css("img::attr(src)").extract_first("") post_url = post_node.css("::attr(href)").extract_first("") ``` 有了这个image_url就可以传递给meta了 `yield Request(url=parse.urljoin(response.url, post_url), meta={"front_image_url": image_url}, callback=self.parse_detial)` 现在就可以使用pycharm debug 一下,看response 怎么从meta里取出来,以及看嵌套的 selector 方法能不能获取到 image以及post_url ![](http://ovc37dj11.bkt.clouddn.com/15255943725889.jpg) 这里可以看出 response meta 字典里有 front_image_url `front_image_url = response.meta["front_image_url"]` 这里保险起见我们可以使用`get()`方法来取,不会抛出异常 `front_image_url = response.meta.get("front_iamge_url", "")` 传递的默认值为空,这样文章封面图就好了 ##### 4-11 items设计 - 2 现在继续分析 `Item` ``` class JobBoleArticleItem(scrapy.Item): title = scrapy.Field() create_date = scrapy.Field() url = scrapy.Field() url_object_id = scrapy.Field() front_image_url = scrapy.Field() front_image_path = scrapy.Field() praise_nums = scrapy.Field() comment_nums = scrapy.Field() fav_nums = scrapy.Field() tags = scrapy.Field() content = scrapy.Field() ``` url 是可变的长度,对 url 做 md5,使得url变成固定的长度。这样就完成了对Item 的定义 现在我们在 jobbole.py中把值填写到 item 中 首先 `from ArticleSpider.items import JobBoleArticleItem` 然后对Item进行初始化 `article_item = JobBoleArticleItem() ` ``` article_item["title"] = title article_item["url"] = response.url article_item["create_date"] = create_date article_item["front_image_url"] = front_image_url article_item["praise_nums"] = praise_nums article_item["comment_nums"] = comment_nums article_item["fav_nums"] = fav_nums article_item["tags"] = tags article_item["content"] = content ``` 填充完成之后就开始调用 `yield article_item` 这样就会传递到 `pipelines` 里去 `pipeline` 就可以接受 item 默认这样已经写好了 ``` class ArticlespiderPipeline(object): def process_item(self, item, spider): return item ``` 还需要配置好环境变量 ``` ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, } ``` 然后调试下 pycharm 看能不能传递到 pipeline 中 ![](http://ovc37dj11.bkt.clouddn.com/15256007444816.jpg) 这样看就传递到了 pipeline 中了,然后就可以操作数据了,比如 drop 掉,或者保存到数据库中,pipeline 主要做数据存储,我们先不介绍它,先继续做完善工作 **我们继续分析下 item ,前面我们已经获取到了 封面url `front_image_url`,如果这个时候我们想将 url 图片下载下来,并保存到我们本地,如何来做?** Scrapy 给我们提供了一种自动下载图片的机制,只需要在`settings.py`中配置好,只需要在`ITEM_PIPELINES`中添加`imagepapeline`就可以了,我们可以在源码中找到它 ![](http://ovc37dj11.bkt.clouddn.com/15256031047818.jpg) ``` ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, 'scrapy.pipelines.images.ImagesPipeline': 1, } ``` ITEM_PIPELINES 是item流经的管道,这个管道后面的数值是顺序,数字越小,就越早,设置为1,就代表先走`ImagesPipeline`这个管道,继续在设置中 `IMAGES_URLS_FIELD = "front_image_url"` 这样 pipeline 就回去 item 中找 front_image_url这个字段,然后下载图片下来,我们希望下载到某一个目录之下 设置图片的保存路径使用 `IMAGES_STORE = ""`可以设置绝对路径,也可以设置相对路径放到工程目录之下,在工程下新建一个文件夹 `image` 在`settings.py`文件中 使用`import os`可以获取当前文件路径`os.path.dirname(__file__)`获取当前目录名称,然后通过`project_dir = os.path.abspath(os.path.dirname(__file__))` 使用 `os.path.join(project_dir, 'images')` ![](http://ovc37dj11.bkt.clouddn.com/15256045278554.jpg) ##### 4-12 items设计 - 3 这样就设置好了图片的存放路径,图片还需要安装第三方库`pip install pillow` `pip install -i https://pypi.douban.com/simple pillow` 安装成功我们来运行下 ![](http://ovc37dj11.bkt.clouddn.com/15256051697022.jpg) 这样就把图片成功保存在了本地,图片已经保存起来了,我们是否可以提取出本地图片的路径,把本地图片路径和 item`front_image_url`绑定,**我们只需要自己定制 pipeline 就可以了,继承`from scrapy.pipelines.images import ImagesPipeline` `class ArticleImagePipeline(ImagesPipeline):`** 查看`ImagesPipeline`有哪些方法可以重载`get_media_requests` 还可以配置过滤下载图片的大小,图片最小高度最小宽度 ``` IMAGES_MIN_HEIGHT = 100 IMAGES_MIN_WIDTH = 100 ``` ![](http://ovc37dj11.bkt.clouddn.com/15256545762085.jpg) 这些都是可以设置的参数 学习下提供的方法 `get_media_requests` ``` def get_media_requests(self, item, info): return [Request(x) for x in item.get(self.images_urls_field, [])] ``` 通过 `for` 循环获取到 `url`,然后传递给`Request`,所以之前的 `front_image_url` 必须是可以迭代循环的,所以我们给定的是一个数组,之前是string类型就不行 ![](http://ovc37dj11.bkt.clouddn.com/15256549065352.jpg) 通过传递给`Request`交给下载器下载,下一个函数 ``` def item_completed(self, results, item, info): if isinstance(item, dict) or self.images_result_field in item.fields: item[self.images_result_field] = [x for ok, x in results if ok] return item ``` 可以重载`item_completed`函数获取到实际下载地址,所以我们在这里只重载这个函数,路径是存放在 results 里的,通过 results 获取到文件实际的存储路径,首先我们是不知道 results 是什么结构的,我们应该先打断点调试 首先我们把之前下载好的图片给删除 ![](http://ovc37dj11.bkt.clouddn.com/15256554505617.jpg) 然后设置`settings.py`,使用我们重载的 `ArticleImagePipeline` ![](http://ovc37dj11.bkt.clouddn.com/15256555589565.jpg) ``` ITEM_PIPELINES = { 'ArticleSpider.pipelines.ArticlespiderPipeline': 300, # 'scrapy.pipelines.images.ImagesPipeline': 1, 'ArticleSpider.pipelines.ArticleImagePipeline': 1, } IMAGES_URLS_FIELD = "front_image_url" project_dir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(project_dir, 'images') ``` 现在再对`main.py`调试,现在就进入到了我们重载的`ArticleImagePipeline` ![](http://ovc37dj11.bkt.clouddn.com/15256557679170.jpg) 这里可以看到 `results` 是一个 `list` ,这个 `list` 里面实际上每个元素是一个 `tuple`,第一个值是 `0` 对应的是 `True`,表示的是否成功,第二个值是`dict`字典,里面有一个 `path`,这就是文件保存的路径,这样就可以获取到文件本地保存的路径 ``` class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok, value in results: image_file_path = value["path"] item["front_image_path"] = image_file_path return item pass ``` `results`是一个 `tuple`第一个表示是否成功我们用 `ok`,第二个是`dict`字典值,所以我们取出 `path`获取文件存放的路径,我们把获取到的文件存放路径保存到`item`中`item["front_image_path"] = image_file_path`,**注意我们重写这个pipeline需要把 item return出去,因为下一个 pipeline 还要处理** ![](http://ovc37dj11.bkt.clouddn.com/15256564422477.jpg) debug 可以看到文件的路径已经保存进来了 `front_image_path`,所以到这个`ArticlespiderPipeline` pipeline的时候需要获取的字段值都已经处理差不多了,所以下一步就在这和数据库或者文件系统通信,可以保存到本地文件,保存到`mysql`、`monogodb`或者发送到`elasticsearch` 现在还有一个字段 `url_object_id`,通过对`url`做`md5`处理,把`url`处理成唯一的且长度固定的值,所以我们可以在 `jobbole.py`里做`md5`,我们新建一个 `utils` 文件夹来存放一些常用的函数,这 `utils` 文件夹中新建一个 `common.py` 文件来先写`md5`函数 ![](http://ovc37dj11.bkt.clouddn.com/15256574211177.jpg) 注意这里说`unicode-objects`是不能`hash`的,因为`python3`里面所有的字符都是 `unicode`,但是`hash`函数是不接受`unicode`的,所以这里我们用 `.encode("utf-8")`,所以在传递参数的时候我们可以先判断是否是`unicode`,`python3`中已经没有`unicode`关键词,变为了 `str` ``` import hashlib def get_md5(url): if isinstance(url, str): url = url.encode("utf-8") m = hashlib.md5() m.update(url) return m.hexdigest() if __name__ == "__main__": print(get_md5("http://jobbole.com".encode("utf-8"))) ``` 写好这个之后,我们就可以开始在`jobbole.py`里写md5处理 `from ArticleSpider.utils.common import get_md5` `article_item['url_object_id'] = get_md5(response.url)` ##### 4-13 数据表设计和保存item到json文件 这里我们先建一个保存`json`的`pipeline`叫`class JsonWithEncodingPipeline(object):`第一步是要打开我们的`json`文件,所以在初始化的时候打开`json` 文件,这里我们使用第三方`python`开发包`import codecs`,与普通的`open`函数区别在于文件编码,这样可以避免很多编码的繁杂工作,`codecs`和`open`参数一样,第一个参数是传入`json`文件,第二个参数是打开的模式,第三个参数是`encoding`我们采用`utf-8`编码 ``` def __init__(self): self.file = codecs.open('article.json', 'w', encoding='utf-8') ``` 打开之后我们就需要调用 `def process_item` 函数,`def process_item(self, item, spider):` 我们处理 `item`关键的地方,在这里我们把 `item` 写入到文件当中,首先把`item`转换成字符串,采用 `json.dump()`函数 要先声明`import json` ,第二个参数 `ensure_ascii=False`,这样写入中文才不会出错`lines = json.dumps(dict(item), ensure_ascii=False) + "\n"`,我们把字符串写入到文件当中`self.file.write(lines)`,使用`def process_item(self, item, spider):`一定要`return item`,因为下一个`pipeline`可能会继续使用`item`。现在已经打开了`json`文件和写入了`json`文件,下一步就是关闭`json`文件,我们可以调用信号量`def spider_closed(self,spider): self.file.close()` 最后在 `settings.py` 中,把这`'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,`配置进来 然后运行下,看下最后生成了`article.json`文件 ![](http://ovc37dj11.bkt.clouddn.com/15256685234636.jpg) 这是我们的`pipelines.py`文件 ![](http://ovc37dj11.bkt.clouddn.com/15256685572021.jpg) ``` import codecs import json from scrapy.pipelines.images import ImagesPipeline class ArticlespiderPipeline(object): def process_item(self, item, spider): return item class JsonWithEncodingPipeline(object): def __init__(self): self.file = codecs.open('article.json', 'w', encoding='utf-8') def process_item(self, item, spider): lines = json.dumps(dict(item), ensure_ascii=False) + "\n" self.file.write(lines) return item def spider_closed(self,spider): self.file.close() class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok, value in results: image_file_path = value["path"] item["front_image_path"] = image_file_path return item ``` [自定义JsonWithEncodingPipeline代码片段](https://gitee.com/custer_git/python-scrape-django/commit/e73c0463da7e90dfdd9208485431c5cc1c6fee0b#07dc8f290a683679a5d188a0a8d4fbf3cbae3e62_12_12) Scrapy本身也建立了写入Json的机制,很方便的把 item 导出成各种类型的文件,这里以 Json 为例子讲下`from scrapy.exporters import JsonItemExporter` ![](http://ovc37dj11.bkt.clouddn.com/15256689331503.jpg) 我们可以看下源码,`Scrapy`提供了多种 `exporter` 格式包括 `csv`、`xml`等,这里我们学习下 `JsonItemExporter` 的使用 ``` class JsonExporterPipeline(object): def __init__(self): self.file = open('articleexport.json', 'wb') ``` 这里新建一个`JsonExporterPipeline`注意我们打开`json`这里一般使用`open`函数以`wb`方式来打开二进制文件,这里还需要使用`JsonItemExporter()`来做实例化,需要传递参数,第一个就是打开的`file`,第二个就是`encoding="utf-8"`,然后是`ensure_ascii=False`。完整代码段`self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)`这样我们就可以调用`self.exporter.start_exporting()` 然后需要 close 文件,可以直接调用`def close_spider(self, spider):` 方法 然后调用`self.exporter.finish_exporting()`最后在`self.file.close()` 然后还得做`def process_item(self, item, spider):`直接调用`self.exporter.export_item(item)`最后在导出`return item` 最后在`settings.py`中配置成调用Scrapy提供的json exprot工具`'ArticleSpider.pipelines.JsonExporterPipeline': 2,` 然后再运行下看能不能导出 ![](http://ovc37dj11.bkt.clouddn.com/15256701205191.jpg) 可以看到最后它给我们加了`[]`这里看源码 ![](http://ovc37dj11.bkt.clouddn.com/15256701777068.jpg) `JsonItemExporter`的方法使用比较简单,但是导出`csv`和`xml`的方法比较复杂,提供的功能比较多,可以查源码学习或者查询官方文档和博客。 [调用Scrapy提供的json export导出json文件 代码片段](https://gitee.com/custer_git/python-scrape-django/commit/7cb37e3b6274aac1f421755b9b8baa4f1de2b4ee#f0f7e11a5f8b95720ed49e3c8352c86bfdb4ebc3_7_7) ##### 4-14 通过pipeline保存数据到mysql - 1 首先保存到`mysql`之前需要做数据库的设计,因为我们要爬取的就是文章的具体字段,所以设计数据表的时候只需要一张表就可以完成了,这张表的内容实际上是和我们item中的字段是一致的 ![](http://ovc37dj11.bkt.clouddn.com/15256716255677.jpg) 数据库和上图 `item`的关系就相当于`django`里`model`和`form`的关系,是一一对应的,我们可以通过 `navicate` 来建表需要注意选择 `Character Set: utf8`和`Collation: utf8_general_ci` ![](http://ovc37dj11.bkt.clouddn.com/15256719293094.jpg) ![](http://ovc37dj11.bkt.clouddn.com/15256720627484.jpg) 这里`create_date`是`date`日期类型,但是我们在处理的时候是以字符串`str`类型来提取的,所以需要把它转换成日期类型,在`jobbole.py`中创建日期的时候需要做转换,添加代码首先导入python 的库`import datetime` ``` try: create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date() except Exception as e: create_date = datetime.datetime.now().date() ``` 现在我们在`main.py`debug看下有没有成功 ![](http://ovc37dj11.bkt.clouddn.com/15256750190989.jpg) 现在回数据库继续设计数据表 ![](http://ovc37dj11.bkt.clouddn.com/15256754679495.jpg) 这里 `content` 是 `longtext` 类型,对字符长度没有做限制,可以添加任意长度的字符。再把`url_object_id`设置成主键。 再将数据保存到数据库中,需要先安装驱动`pip install mysqlclient` ![](http://ovc37dj11.bkt.clouddn.com/15256766193517.jpg) 在 ubuntu下安装出错可能缺少相应的库,需要先安装 `sudo apt-get install libmysqlclient-dev`,如果`cento`s可以安装`sudo yum install python-devel mysql-devel` 安装好了之后,我们就开始自定义pipeline,先`import MySQLdb`用来数据存储`class MysqlPipeline(object):`首先要做的是对数据库的连接,所以在实例化的时候先连接数据库 `def __init__(self): self.conn = MySQLdb.connect('host', 'user', 'password', 'dbname', charset='utf8', use_unicode=True)` 还有其他的参数可以看源代码 ![](http://ovc37dj11.bkt.clouddn.com/15256772270946.jpg) 获取了 `connect` 之后再获取` cursor`,执行数据库具体操作是通过`cursor`完成的。`self.cursor = self.conn.cursor()` 需要保存到数据库中,就需要重载方法`def process_item(self, item, spider):`在这里就可以通过`curosr`来执行`sql`语句 ``` class MysqlPipeline(object): def __init__(self): self.conn = MySQLdb.connect('host', 'user', 'password', 'dbname', charset='utf8', use_unicode=True) self.cursor = self.conn.cursor() def process_item(self, item, spider): insert_sql = """ insert into jobbole_article(title, create_date, url, url_object_id, front_image_url, front_image_path, praise_nums, comment_nums, fav_nums, tags, content) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ self.cursor.execute(insert_sql, (item["title"], item["create_date"], item["url"], item["url_object_id"], item["front_image_url"], item["front_image_path"], item["praise_nums"], item["comment_nums"], item["fav_nums"], item["tags"], item["content"],)) ``` 这样就可以执行完整的`sql` 语句来,还需要配置好`host`,`name`,`password` 注意执行了 `sql` 操作之后一定要 `commit` 操作`self.conn.commit()` ![](http://ovc37dj11.bkt.clouddn.com/15256807716148.jpg) 这样就完成了[自定义 pipeline 让数据自动写入数据库中 代码片段](https://gitee.com/custer_git/python-scrape-django/commit/94b873fd49d5eaa35f6d4bfa6bf407aaeb903363#f0f7e11a5f8b95720ed49e3c8352c86bfdb4ebc3_9_9) ##### 4-15 通过pipeline保存数据到mysql - 2 下面介绍插入`mysql`另一种方法,因为目前解析速度是超过`mysql`的写入速度的,如果到后期爬取的`url`也就是 `item` 越来越多,实际上插入速度也就跟不当解析速度的,所以这里就会堵塞 `twist` 框架给我们提供了异步操作,目前我们这里`execute`和`commit`操作是同步的操作,`twist`给我们提供了另一个工具**连接池**,使`sql`操作变成异步操作 为了演示这个功能,我们新建一个 `class MysqlTwistedPipeline(object):`使用`twist`提供的`api`来完成异步操作 我们先把 mysql 配置写到 `settings.py` 中 ``` MYSQL_HOST= "localhost" MYSQL_DBNAME = "article_spider" MYSQL_USER = "root" MYSQL_PASSWORD = "root1234" ``` **这里我们可以通过`@classmethod`定义`def from_settings(cls, settings):`这个方法是可以被Scrapy调用的,这样就可以像字典一样读取settings.py的配置`host=settings["MYSQL_HOST"]`** 可以配置 `'ArticleSpider.pipelines.MysqlTwistedPipeline': 2,`来debug调试一下 ![](http://ovc37dj11.bkt.clouddn.com/15256829181377.jpg) 可以看到我们刚才定义的值都在里面 ``` class MysqlTwistedPipeline(object): @classmethod def from_settings(cls, settings): host = settings["MYSQL_HOST"] db = settings["MYSQL_DBNAME"] user = settings["MYSQL_USER"] passwd = settings["MYSQL_PASSWORD"] ``` 然后来使用`twist`给我们提供的异步`API` 首先调用python第三方包`from twisted.enterprise import adbapi`这个adbapi就可以把我们的mysql操作变成异步操作,使用连接池函数,需要**传递可变参数** `adbapi.ConnectionPool("MySQLdb", )`,使用`dict` ``` class MysqlTwistedPipeline(object): @classmethod def from_settings(cls, settings): dbparms = dict( host=settings["MYSQL_HOST"], db=settings["MYSQL_DBNAME"], user=settings["MYSQL_USER"], passwd=settings["MYSQL_PASSWORD"], charset='utf8', cursorclass=MySQLdb.cursors.DictCursor, use_unicode=True, ) adbapi.ConnectionPool("MySQLdb", **dbparms) ``` 注意我们这里是`@classmethod`,我们的`def from_settings(cls, settings)`中的`cls`就是指定的是`MysqlTwistedPipeline`,所以最后我们需要调用这个类,并把参数传递进来,这就是实例化对象, ``` class MysqlTwistedPipeline(object): @classmethod def from_settings(cls, settings): dbparms = dict( host=settings["MYSQL_HOST"], db=settings["MYSQL_DBNAME"], user=settings["MYSQL_USER"], passwd=settings["MYSQL_PASSWORD"], charset='utf8', cursorclass=MySQLdb.cursors.DictCursor, use_unicode=True, ) dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms) return cls(dbpool) ``` 实例化 pipeline,要接受这个参数使用`def __init__(self, dbpool): self.dbpool = dbpool`这样启动Scrapy的时候,就将dbpool传递进来了,然后就是具体处理我们的 `def process_item(self, item, spider):` ``` class MysqlTwistedPipeline(object): def __init__(self, dbpool): self.dbpool = dbpool @classmethod def from_settings(cls, settings): dbparms = dict( host=settings["MYSQL_HOST"], db=settings["MYSQL_DBNAME"], user=settings["MYSQL_USER"], passwd=settings["MYSQL_PASSWORD"], charset='utf8', cursorclass=MySQLdb.cursors.DictCursor, use_unicode=True, ) dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms) return cls(dbpool) ``` 这样处理 `process_item` 就可以直接调用连接池,做我们的数据插入 ``` def process_item(self, item, spider): # 使用twisted将mysql插入变成异步执行 self.dbpool.runInteraction(self.do_insert, item) ``` **⚠️注意⚠️这里使用的`dbpool.runInteraction()`方法,第一个参数就是我们自己定义的函数,它就是将这个函数变成异步操作,所以在第一个传递进来的函数里面写具体插入的逻辑,第二个参数就是 item 就是我们要插入的数据**, ``` def do_insert(self, cursor, item): # 执行具体的插入 insert_sql = """ insert into jobbole_article(title, create_date, url, url_object_id, front_image_url, front_image_path, praise_nums, comment_nums, fav_nums, tags, content) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_sql, ( item["title"], item["create_date"], item["url"], item["url_object_id"], item["front_image_url"], item["front_image_path"], item["praise_nums"], item["comment_nums"], item["fav_nums"], item["tags"], item["content"],)) ``` 这里的插入逻辑和之前的插入逻辑是相同的,只是使用了传递进来的 `cursor`来执行,也不再需要 `commit` 了,它会自动帮我们完成。 但是这是异步操作,所以需要我们自己来处理错误,错误处理是有专门的处理函数的,先把返回的赋值给query对象`query = self.dbpool.runInteraction(self.do_insert, item)`,这样我们就可以在 query对象里添加`query.addErrback()`,这样就可以添加异步错误处理函数`def handle_error(self, failure, item, spider)`第一个参数是`self`、第二个参数就是具体的错误`failure`,第三个参数就是 `item`,第四个参数就是当前的 `spider` ``` def process_item(self, item, spider): # 使用twisted将mysql插入变成异步执行 query = self.dbpool.runInteraction(self.do_insert, item) query.addErrback(self.handle_error, item, spider) # 处理异常 def handle_error(self, failure, item, spider): # 处理异步插入的异常 print(failure) ``` 如果不传递 item和spider,下面也就不需要了,但是 failure 是必须的 ``` def process_item(self, item, spider): # 使用twisted将mysql插入变成异步执行 query = self.dbpool.runInteraction(self.do_insert, item) query.addErrback(self.handle_errorr) # 处理异常 def handle_error(self, failure): # 处理异步插入的异常 print(failure) ``` 这样就完成了插入操作的异步逻辑,这里可以调试一下看是否可以完成异步插入,在调试之前,我们需要把`'ArticleSpider.pipelines.MysqlTwistedPipeline': 2,`设置进来 注意这里有个错误 ![](http://ovc37dj11.bkt.clouddn.com/15256881900619.jpg) 因为在 `pipelines.py`中还需要另外的 `import MySQLdb.cursors`然后在运行一下 Twist 给关系型数据库提供了异步的操作 还可以集成django orm [scrapy-plugins/scrapy-djangoitem](https://github.com/scrapy-plugins/scrapy-djangoitem) [异步操作保存到数据库 代码片段](https://gitee.com/custer_git/python-scrape-django/commit/8cab27a7e8f6e1a8a27e9a2e4ab68fc76e7c1584#46857ba80b3b157285d4dcb7995154ba2ef965bd_559_559) ##### 4-16 scrapy item loader机制 - 1 `item loader`实际上是一个容器,这个容器里面可以配置,`item `里的某个字段需要用到`css` 哪一种规则来解析它。配置完成之后直接调用`item`方法,就可以直接生成 `item loader`。 首先需要`from scrapy.loader import ItemLoader `然后看一下如何通过 `ItemLoader` 来生成我们的 `Item` 通过 item loader 加载 item 首先定义 ItemLoader()的实例化对象 ![](http://ovc37dj11.bkt.clouddn.com/15256993785755.jpg) 可以看见有两个重要参数,一个是 `item`,一个是 `response`,我们把这两个传递进来 `item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)` `item`传递进来一定要实例化`JobBoleArticleItem()`,`response`就等于Scrapy传递的`response`。下面就说如果通过之前定义的CSS规则把数据传递进来,首先 `item_loader.add_css()``item_loader.add_xpath()``item_loader.add_value` item主要就三种方法,这里主要学习 `add_css()`方法,`add_css()`里面第一个参数实际上就是对应我们 `items.py`里面的 `field` 字段加上 css selector选择规则`item_loader.add_css("title", ".entry-header h1::text")`。如果不是通过 `CSS Selector`选择的直接取值获取的就可以通过`add_value()`,比如`item_loader.add_value("url", response.url)` 通过CSS 选择器的规则`.entry-header h1::text`把值选择完成之后再填充进来 ``` # 通过 item loader 加载 item item_loader = ItemLoader(item=JobBoleArticleItem(), response=response) item_loader.add_css("title", ".entry-header h1::text") item_loader.add_value("url", response.url) item_loader.add_value("url_object_id", get_md5(response.url)) item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text") item_loader.add_value("front_image_url", [front_image_url]) item_loader.add_css("praise_nums", ".vote-post-up h10::text") item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text") item_loader.add_css("fav_nums", "span.bookmark-btn::text") item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text") item_loader.add_css("content", "div.entry") ``` 现在可以看到我们为 `item_loader` 添加了很多规则,但是实际上添加规则之后如何才能获取到 `item` 呢?这里要注意 `article_item = item_loader.load_item()`,调用`load_item()`这个方法,才会对上面定义的规则进行解析,生产的才是我们 `item `对象。前面先不注释我们来debug下 ![](http://ovc37dj11.bkt.clouddn.com/15257010695924.jpg) 这里调试可以看见返回的`article_item`是 `JobBoleArticleItem`类型,而且返回的值`_values`都是 `list`。第二 `comment_nums`返回的是`[' 评论']`正真在做 `item`解析的时候,我们实际上是还需要在这里面加一层过滤的,还希望在上面加一些处理函数。哪怕我们传递的`url`只是一个值,它保存的还是`list`对象 这是调用默认 `item`方法的两个问题 1. 把所有返回值变成list,可以只取 list 的第一个值吗?2.做一些处理函数,比如之前正则表达式的提取,提取完成之后把值返回回来 是可以解决这些问题的。我们发现使用 `item_loader`之后代码变得非常整洁,变得可配置性,实际上我们可以把规则写进数据库,或者文件,任何我们想要写的地方都可以,所以解析的时候是可以动态的提取这些值的。比如写到数据库中,每次解析的时候,都去查询一下,查询之后做一个匹配映射,这样就大大的方便了可配置性 第二个代码量减少了很多,使用`extract()[0]`可能会出现没有值而下标越界的问题,而`item_loader`都帮我们做了,所以代码减少了很多。 如果要解决前面的问题,就必须来修改我们之前的`items.py`的代码 ``` class JobBoleArticleItem(scrapy.Item): title = scrapy.Field() create_date = scrapy.Field() url = scrapy.Field() url_object_id = scrapy.Field() front_image_url = scrapy.Field() front_image_path = scrapy.Field() praise_nums = scrapy.Field() comment_nums = scrapy.Field() fav_nums = scrapy.Field() tags = scrapy.Field() content = scrapy.Field() ``` 之前 item 实际上是有两个参数是可以自定义的,第一个是`input_processor`代表item这个值传递进来的时候,可以做一些预处理,做预处理,我们还需要使用 Scrapy给我们提供的另一个`from scrapy.loader.processors import MapCompose` 这个`MapCompose` 可以将我们的预处理连续调用两个函数,可以传递任意多的函数,比如在`title`后面加上 jobbole,我们定义一个函数`def add_jobbole(value):`这个函数会接受一个`value`值,实际上这个值就是我们`title`里取到的实际值,比如 `title`里是一个数组有四个值,则每一个值都会调用`add_jobbole()`这个函数 ``` def add_jobbole(value): return value + "-jobbole" class JobBoleArticleItem(scrapy.Item): title = scrapy.Field( input_processor = MapCompose(add_jobbole) ) ``` 这样就完成了一个传递进来的值的预处理,要注意学会这样的用法,实际上两个函数也是可以的,甚至我们可以使用匿名函数`lambda` `input_processor = MapCompose(lambda x:x+"-jobbole")` ![](http://ovc37dj11.bkt.clouddn.com/15257025059415.jpg) 所以说这个`title`运行了我们的 `lambda` 函数 ##### 4-17 scrapy item loader机制- 2 ![](http://ovc37dj11.bkt.clouddn.com/15257030634188.jpg) 这里尝试两个预处理函数也没有问题,连续调用两个函数,从左到右,依次调用了这两个函数。可以处理时间的转换问题 ![](http://ovc37dj11.bkt.clouddn.com/15257033709007.jpg) 这样实际变成了`date`对象,不再是 `string`对象,所以之前的正则表达式提取也是可以改变的。这样这个问题就解决了,还有一个问题就是传递进来的是一个数组,只要数组中的第一个值,怎么设置,需要使用到`Scrapy`给我们提供的另一个函数`from scrapy.loader.processors import MapCompose, TakeFirst` 有了`TakeFirst`之后我们就可以使用了`output_processor = TakeFirst()`这样就只取`list` 中的第一个值 ![](http://ovc37dj11.bkt.clouddn.com/15257036828867.jpg) 这里就只有 date 对象的一个值了,说明我们的`output_processor = TakeFirst()`已经有效了,现在就可以来完善代码了,如果有100个字段都需要取第一个,那是不是需要调用`output_processor = TakeFirst()`100次呢? 为了完成所有的字段都只取第一个,我们可以自己定义一个 `item_loader` 首先要继承Scrapy的一个类`from scrapy.loader import ItemLoader` 我们重载这个类`class ArticleItemLoader(ItemLoader):`看下这个类`ItemLoader`源代码, ![](http://ovc37dj11.bkt.clouddn.com/15257041454720.jpg) 可以设置`default_output_processor = Identity()` ``` class ArticleItemLoader(ItemLoader): # 自定义itemLoader default_output_processor = TakeFirst() ``` 这样就可以调用`TakeFirst()`,这样在用`loader`的时候就不能在用`ItemLoader`了,就得使用我们自定义的`ArticleItemLoader` `item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)` ![](http://ovc37dj11.bkt.clouddn.com/15257044473042.jpg) 这样就都是一个值是`str`类型的了。这样我们的终点就是来完成`input_processor` 比如说我们要做 md5、正则表达式的转换、create_date的方式,这些逻辑全部拿到items.py来做了,对另外的网站做预处理,这样就达到了代码的重用,首先我们来完成`praise_nums`数字的转换 ``` def get_nums(value): match_re = re.match(r".*?(\d+).*", value) if match_re: nums = int(match_re.group(1)) else: nums = 0 return nums ``` 点赞数、收藏数、评论数,都是可以重复使用的 ![](http://ovc37dj11.bkt.clouddn.com/15257050010017.jpg) 好,下面还需要对 `tags`做`join`处理 这里需要注意的是`tag`通过`p.entry-meta-hide-on-mobile a::text`提取出来的是`list`,所以这个还用`TakeFirst()`就不太合适了,我们需要使用的是 `Join`可以使`list` 连接起来 `from scrapy.loader.processors import MapCompose, TakeFirst, Join` `tags = scrapy.Field(output_processor = Join(","))` ![](http://ovc37dj11.bkt.clouddn.com/15257055205623.jpg) 我们不需要这里出现评论,所以如果出现我们移除就可以了 ``` def remove_comment_tags(value): # 去掉tags中提取的评论 if "评论" in value: return "" else: return value ``` 再应用到`input_processor=MapCompose(remove_comment_tags),` 还有一个很重要的地方就是`front_image_url`,当我们下载图片的时候,就是配置了`imagePipeline`之后,我们这个字段传递进去就必须是一个`list`,而且我们处理的时候也是将`front_image_url`放到数组中去传递,但是现在要`default_output_processor = TakeFirst()`之后,`front_image_url = scrapy.Field()`这个地方就变成了字符串。这样当我们把`item`交给`imagePipeline`下载的时候,他就会抛出异常。这个地方需要引起重视,对这个字段的处理就不能用`default_output_processor = TakeFirst()`,关键是如何才能让他保持原有的值,同时变成`list`,这里有个小技巧,写个函数什么都不做`def return_value(value): return value`,然后在 `MapCompose(return_value)`里调用 ``` front_image_url = scrapy.Field( output_processor=MapCompose(return_value) ) ``` 这样就起到了作用第一没有修改 image_url 中的值,第二覆盖掉`default_output_processor = TakeFirst()` 下面还有一个问题就是之前写的插入sql语句,现在在sql语句中也需要做一定的改动,之前是直接取得`front_image_url`这个字段,现在这个字段是一个`list`,所以在取这个字段的时候一定要记住取这个`list`当中的第一个值,不然的话做 sql insert 的时候报的错也可能看不懂 ``` class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): for ok, value in results: image_file_path = value["path"] item["front_image_path"] = image_file_path return item ``` 之前写的处理imagePipeline的地方,现在也要做一定的修改,他是针对我们所有的item都会生效,所以这里面可以做个判断 ``` class ArticleImagePipeline(ImagesPipeline): def item_completed(self, results, item, info): if "front_image_path" in item: for ok, value in results: image_file_path = value["path"] item["front_image_path"] = image_file_path return item ``` 这样判断只处理有`front_image_path`的`pipeline`,`item`是类似`dict`的模式,所以这种判断也是有效的 修改完成之后做一下调试 ![](http://ovc37dj11.bkt.clouddn.com/15257070120955.jpg) [使用item_loader方法的修改代码片段]() #### 第5章 scrapy爬取知名问答网站 ##### 5-1 session和cookie自动登录机制 ##### 5-2 (补充)selenium模拟知乎登录-2017-12-29 ##### 5-3 requests模拟登陆知乎 - 1 ##### 5-4 requests模拟登陆知乎 - 2 ##### 5-5 requests模拟登陆知乎 - 3 ##### 5-6 scrapy模拟知乎登录 ##### 5-7 知乎分析以及数据表设计1 ##### 5-8 知乎分析以及数据表设计 - 2 ##### 5-9 item loder方式提取question - 1 ##### 5-10 item loder方式提取question - 2 ##### 5-11 item loder方式提取question - 3 ##### 5-12 知乎spider爬虫逻辑的实现以及answer的提取 - 1 ##### 5-13 知乎spider爬虫逻辑的实现以及answer的提取 - 2 ##### 5-14 保存数据到mysql中 -1 ##### 5-15 保存数据到mysql中 -2 ##### 5-16 保存数据到mysql中 -3 ##### 5-17 (补充小节)知乎验证码登录 - 1_1 ##### 5-18 (补充小节)知乎验证码登录 - 2_1 ##### 5-19 (补充)知乎倒立文字识别-1 ##### 5-20 (补充)知乎倒立文字识别-2 #### 第6章 通过CrawlSpider对招聘网站进行整站爬取 ##### 6-1 数据表结构设计 ##### 6-2 CrawlSpider源码分析-新建CrawlSpider与settings配置 ##### 6-3 CrawlSpider源码分析 ##### 6-4 Rule和LinkExtractor使用 ##### 6-5 item loader方式解析职位 ##### 6-6 职位数据入库-1 ##### 6-7 职位信息入库-2 #### 第7章 Scrapy突破反爬虫的限制 ##### 7-1 爬虫和反爬的对抗过程以及策略 ##### 7-2 scrapy架构源码分析 ##### 7-3 Requests和Response介绍 ##### 7-4 通过downloadmiddleware随机更换user-agent-1 ##### 7-5 通过downloadmiddleware随机更换user-agent - 2 ##### 7-6 scrapy实现ip代理池 - 1 ##### 7-7 scrapy实现ip代理池 - 2 ##### 7-8 scrapy实现ip代理池 - 3 ##### 7-9 云打码实现验证码识别 ##### 7-10 cookie禁用、自动限速、自定义spider的settings #### 第8章 scrapy进阶开发 ##### 8-1 selenium动态网页请求与模拟登录知乎 ##### 8-2 selenium模拟登录微博, 模拟鼠标下拉 ##### 8-3 chromedriver不加载图片、phantomjs获取动态网页 ##### 8-4 selenium集成到scrapy中 ##### 8-5 其余动态网页获取技术介绍-chrome无界面运行、scrapy-splash、selenium-grid, splinter ##### 8-6 scrapy的暂停与重启 ##### 8-7 scrapy url去重原理 ##### 8-8 scrapy telnet服务 ##### 8-9 spider middleware 详解 ##### 8-10 scrapy的数据收集 ##### 8-11 scrapy信号详解 ##### 8-12 scrapy扩展开发 #### 第9章 scrapy-redis分布式爬虫 ##### 9-1 分布式爬虫要点 ##### 9-2 redis基础知识 - 1 ##### 9-3 redis基础知识 - 2 ##### 9-4 scrapy-redis编写分布式爬虫代码 ##### 9-5 scrapy源码解析-connection.py、defaults.py- ##### 9-6 scrapy-redis源码剖析-dupefilter.py- ##### 9-7 scrapy-redis源码剖析- pipelines.py、 queue.py- ##### 9-8 scrapy-redis源码分析- scheduler.py、spider.py- ##### 9-9 集成bloomfilter到scrapy-redis中 #### 第10章 elasticsearch搜索引擎的使用 ##### 10-1 elasticsearch介绍 ##### 10-2 elasticsearch安装 ##### 10-3 elasticsearch-head插件以及kibana的安装 ##### 10-4 elasticsearch的基本概念 ##### 10-5 倒排索引 ##### 10-6 elasticsearch 基本的索引和文档CRUD操作 ##### 10-7 elasticsearch的mget和bulk批量操作 ##### 10-8 elasticsearch的mapping映射管理 ##### 10-9 elasticsearch的简单查询 - 1 ##### 10-10 elasticsearch的简单查询 - 2 ##### 10-11 elasticsearch的bool组合查询 ##### 10-12 scrapy写入数据到elasticsearch中 - 1 ##### 10-13 scrapy写入数据到elasticsearch中 - 2 #### 第11章 django搭建搜索网站 ##### 11-1 es完成搜索建议-搜索建议字段保存 - 1 ##### 11-2 es完成搜索建议-搜索建议字段保存 - 2 ##### 11-3 django实现elasticsearch的搜索建议 - 1 ##### 11-4 django实现elasticsearch的搜索建议 - 2 ##### 11-5 django实现elasticsearch的搜索功能 -1 ##### 11-6 django实现elasticsearch的搜索功能 -2 ##### 11-7 django实现搜索结果分页 ##### 11-8 搜索记录、热门搜索功能实现 - 1 ##### 11-9 搜索记录、热门搜索功能实现 - 2 #### 第12章 scrapyd部署scrapy爬虫 #### 第13章 课程总结 #### 软件架构 软件架构说明 #### 安装教程 1. xxxx 2. xxxx 3. xxxx #### 使用说明 1. xxxx 2. xxxx 3. xxxx #### 参与贡献 1. Fork 本项目 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 码云特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. 码云官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目 5. 码云官方提供的使用手册 [http://git.mydoc.io/](http://git.mydoc.io/) 6. 码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)