# 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") # 如果匹配成功会有返回函数的
```

贪婪匹配默认从后往前匹配的,匹配成功就返回,不再继续

在前面 加上 ? 就强制了 从前面开始匹配

##### 3-3 正则表达式-2


##### 3-4 正则表达式-3

```
总结:
^ 以什么开头
$ 以什么结尾
* 限定前面字符出现任意多次 >= 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))
```

##### 3-5 深度优先和广度优先原理
1. 网站的树结构
2. 深度优先算法和实现
3. 广度优先算法和实现
首先分析网站 url 的结构

然后分析网站 url 链接跳转的结构

爬虫会循环环路爬取,会造成死循环 (通过首页开始爬取,但一般每个子页面都有导航) 使用去重,把已经爬取的 url 放入 一个 list 中,可以跳过重复的 url
通过二叉树模型遍历来简化网站爬取 来理解爬取原理

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作用就很明显了

#### 第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`

使用 pycharm 打开新建的工程

pipelines 和数据存储相关
middlewares 可以存放我们自己定义的中间件,让scrapy变得更加可控
iterms 相当于Django中的 form,但是字段类型单一,比较简单
spiders 文件夹存放具体某一个网站的爬虫
提供了命令 `scrapy genspider example example.com`


#####4-2 pycharm 调试scrapy 执行流程
第一步、更改pycharm编译环境

第二步、学习初始代码

```
# -*- 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) 函数

```
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__)))
```

下一步关键,调用 `execute()` 函数来调用 Scrapy 命令, 函数 `execute()` 可以传递一个数组,第一个参数是 "scrapy"命令,然后是 "crawl"
Scrapy 启动 spider 的命令是 `scrapy crawl + 名字` 名字和 name 是相同的

所以我们启动 jobbole 的 spider 命令就是 `scrapy crawl jobbole`
windows 可能会出现缺少 win32api module 错误

安装 `pip install pypiwin32` 来解决问题

启动 spider 成功,这样就可以把这个命令配置到 `main.py` 中来
`execute(["scrapy", "crawl", "jobbole"])`

这样配置好就可以打断点来调试了,调试之前还需要设置 `settings.py`

默认是遵循 robots 协议,修改为 `ROBOTSTXT_OBEY = False`
不设置的话 Scrapy 默认会读取每个网站上 robots 协议,它会把我们不符合 robots协议的 url 给过滤掉,*不设置 False 会发现爬虫很快就停止了*因为爬虫会遵循 robots 协议把很多 url 给过滤掉
现在可以开始来调试了,debug main.py 文件

有了这个基础之后就可以来做解析了,如何从返回 html 中去获取到我们需要的指定字段的值,然后把她们保存到我们数据库中
##### 4-3 xpath的用法 - 1、2、3
上面介绍了如何来启动 Scrapy 项目,下面来学习 xpath 来从我们 html 中,提取出我们需要的值

我们可以提取的值 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()` 方法

`/html/body/div[3]/div[3]/div[1]/div[1]` 这里div[3]是第三个不是从0开始的
检查我们是否正确可以 右键查看

`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’`查看

看下chrome 浏览器的 xpath 路径 `//*[@id="post-113568"]/div[1]/h1`

如果想要获取 值 而不要标签, 需要调用 `/text()` 函数
`//*[@id="post-113568"]/div[1]/h1/text()`

这样就只取到值了
> 注意:页面结构可能是运行了所有js动态产生的页面结构源码,所以 div 的层级可能不相同,就取不到值了
xpath用法 用 id 是最为准确的
scrapy 通过 shell 脚本来调试学习会更为的快速
在命令行 `scrapy shell ` 是对这个 url 进行调试


输出 title 可以看出是一个 `Selector` 对象,可以使用 `extract()` 函数获取,返回一个数组,访问数组里的第一个值
xpath 返回的是 `Selector` 是可以继续执行的的, 而 `extract()` 返回的就是一个数组了

继续爬取时间
`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()`

继续获取称赞数、收藏数、评论数

这里有 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()值

```
>>> 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)
```
提取正文内容

可以看到所有正文内容是放在 `
` 里的

`response.xpath("//div[@class='entry']").extract()`
*这里需要说明的是获取到正文,如何获取正文的内容呢?一种简单的方式是把 entry 中所有的 div,或者说 div 下面的所有内容包括标签 全部保存。因为关于一片文章的正文分析实际上比较复杂的工作,因为不同的网站,它的正文内容排版是不同的,所以说怎么从这里面提取出它原来的样式,实际上是比较的课题,我们本课程不做这个课题的研究,所以我们只管将它的源码 html 保存下来,以后如果需要再对这里面继续做进一步的提取,和做样式的修改的话,就可以继续使用这一步的提取*
`content = response.xpath("//div[@class='entry']").extract()[0]`
下面继续获取 标签 tag

`tag_list = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/a/text()').extract()`

使用列表生成式来删除重复的评论,完成过滤
`[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

点赞数量
`>>> response.css(".vote-post-up h10::text").extract()[0] '1'`
收藏数量

`>>> 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)
```
继续提取评论数量

通过a标签href的值来确定是哪一个标签

`>>> 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]`

继续提取标签
```
>>> 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)
```
这里调试,需要取消正则表达式匹配的非贪婪模式

```
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/']`

我们是需要

**这里注意要取到 a 属性 href 在css 选择器中应该这样写 `a::attr(href)`**
所以就是 `response.css(".floated-thumb .post-thumb a::attr(href)")`

这里可以用 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/`
所以我们还要进一步做限定

所以在写代码的时候应该经常调试,这样可以避免许多问题,这里改写 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()函数,可以查看源代码

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 进行下载,首先让我们来先分析下页面的结构

这里有 `下一页 »`
我们通过 `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 进行调试

这里面提取所有 post_urls 出来了,文章的具体链接

继续走下去异步调用 callback 函数 response就是具体的一个文章的url 了
然后看 评论和收藏的数据都有问题

这里我们先查看这个网页的具体内容

因为这里只有评论和收藏这两个字,没有数字,既然没有数字,我们来分析下我们的代码,既然没有取到就设置为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 ,因为这是个异步调用

**我们这里就要用到`Request`另一个变量 `meta`,这里很关键,因为以后极有可能会用到很多,`meat={}`传递过来的是字典**
接下来我们分析如何从列表页提取出封面图


之前我们获取过这个 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

这里可以看出 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 中

这样看就传递到了 pipeline 中了,然后就可以操作数据了,比如 drop 掉,或者保存到数据库中,pipeline 主要做数据存储,我们先不介绍它,先继续做完善工作
**我们继续分析下 item ,前面我们已经获取到了 封面url `front_image_url`,如果这个时候我们想将 url 图片下载下来,并保存到我们本地,如何来做?**
Scrapy 给我们提供了一种自动下载图片的机制,只需要在`settings.py`中配置好,只需要在`ITEM_PIPELINES`中添加`imagepapeline`就可以了,我们可以在源码中找到它

```
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')`

##### 4-12 items设计 - 3
这样就设置好了图片的存放路径,图片还需要安装第三方库`pip install pillow`
`pip install -i https://pypi.douban.com/simple pillow`
安装成功我们来运行下

这样就把图片成功保存在了本地,图片已经保存起来了,我们是否可以提取出本地图片的路径,把本地图片路径和 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
```

这些都是可以设置的参数
学习下提供的方法 `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类型就不行

通过传递给`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 是什么结构的,我们应该先打断点调试
首先我们把之前下载好的图片给删除

然后设置`settings.py`,使用我们重载的 `ArticleImagePipeline`

```
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`

这里可以看到 `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 还要处理**

debug 可以看到文件的路径已经保存进来了 `front_image_path`,所以到这个`ArticlespiderPipeline` pipeline的时候需要获取的字段值都已经处理差不多了,所以下一步就在这和数据库或者文件系统通信,可以保存到本地文件,保存到`mysql`、`monogodb`或者发送到`elasticsearch`
现在还有一个字段 `url_object_id`,通过对`url`做`md5`处理,把`url`处理成唯一的且长度固定的值,所以我们可以在 `jobbole.py`里做`md5`,我们新建一个 `utils` 文件夹来存放一些常用的函数,这 `utils` 文件夹中新建一个 `common.py` 文件来先写`md5`函数

注意这里说`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`文件

这是我们的`pipelines.py`文件

```
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`

我们可以看下源码,`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,`
然后再运行下看能不能导出

可以看到最后它给我们加了`[]`这里看源码

`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中的字段是一致的

数据库和上图 `item`的关系就相当于`django`里`model`和`form`的关系,是一一对应的,我们可以通过 `navicate` 来建表需要注意选择 `Character Set: utf8`和`Collation: utf8_general_ci`


这里`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看下有没有成功

现在回数据库继续设计数据表

这里 `content` 是 `longtext` 类型,对字符长度没有做限制,可以添加任意长度的字符。再把`url_object_id`设置成主键。
再将数据保存到数据库中,需要先安装驱动`pip install mysqlclient`

在 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)`
还有其他的参数可以看源代码

获取了 `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()`

这样就完成了[自定义 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调试一下

可以看到我们刚才定义的值都在里面
```
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,`设置进来
注意这里有个错误

因为在 `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()的实例化对象

可以看见有两个重要参数,一个是 `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下

这里调试可以看见返回的`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")`

所以说这个`title`运行了我们的 `lambda` 函数
##### 4-17 scrapy item loader机制- 2

这里尝试两个预处理函数也没有问题,连续调用两个函数,从左到右,依次调用了这两个函数。可以处理时间的转换问题

这样实际变成了`date`对象,不再是 `string`对象,所以之前的正则表达式提取也是可以改变的。这样这个问题就解决了,还有一个问题就是传递进来的是一个数组,只要数组中的第一个值,怎么设置,需要使用到`Scrapy`给我们提供的另一个函数`from scrapy.loader.processors import MapCompose, TakeFirst`
有了`TakeFirst`之后我们就可以使用了`output_processor = TakeFirst()`这样就只取`list` 中的第一个值

这里就只有 date 对象的一个值了,说明我们的`output_processor = TakeFirst()`已经有效了,现在就可以来完善代码了,如果有100个字段都需要取第一个,那是不是需要调用`output_processor = TakeFirst()`100次呢?
为了完成所有的字段都只取第一个,我们可以自己定义一个 `item_loader`
首先要继承Scrapy的一个类`from scrapy.loader import ItemLoader`
我们重载这个类`class ArticleItemLoader(ItemLoader):`看下这个类`ItemLoader`源代码,

可以设置`default_output_processor = Identity()`
```
class ArticleItemLoader(ItemLoader):
# 自定义itemLoader
default_output_processor = TakeFirst()
```
这样就可以调用`TakeFirst()`,这样在用`loader`的时候就不能在用`ItemLoader`了,就得使用我们自定义的`ArticleItemLoader`
`item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)`

这样就都是一个值是`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
```
点赞数、收藏数、评论数,都是可以重复使用的

好,下面还需要对 `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(","))`

我们不需要这里出现评论,所以如果出现我们移除就可以了
```
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`的模式,所以这种判断也是有效的
修改完成之后做一下调试

[使用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/)