<

作者:互联网

积分:7

  • 文章37
  • 阅读0
  • 评论0

2018.2 最新 - Scrapy+elasticSearch+Django 打造搜索引擎直至部署上线 (四)

编辑:互联网来源:互联网更新时间:2018-02-05 08:50:12

最终项目上线演示地址: http://search.mtianyan.cn

第四节: 开工啦, 开工啦这节我们爬点数据试试手, 并且存起来存到 json,mysql 下载图片通通试一试, 通通试一试

Github 地址: https://github.com/mtianyan/ArticleSpider (欢迎先点个 star 后上车)

伯乐在线爬取所有文章

scrapy 框架介绍及网站分析

scrapy 百度百科:

Scrapy,Python 开发的一个快速, 高层次的屏幕抓取和 web 抓取框架, 用于抓取 web 站点并从页面中提取结构化的数据 Scrapy 用途广泛, 可以用于数据挖掘监测和自动化测试

确定要爬取的网站: 伯乐在线

对于 url 结构进行了解, 以及我们需要哪些数据

/all-posts / 的所有文章内容

前提: 如果网站直接提供抓取所有文章内容的 url 我们要首先考虑直接用

观察列表分页中第二页 url 变化:

url = http://blog.jobbole.com/all-posts/page/2/

因此我们可以通过只更换 url 中的 page 参数来实现爬取所有

但是当它的文章更多的时候我们就得去改源码

改良版: 获取下一页 url 形式来爬取所有内容只要有下一页就提取下一页

基础环境 python3.5.3

配置所需虚拟环境

mkvirtualenv articlespider3

创建虚拟环境

workon articlespider3

直接进入虚拟环境

deactivate

退出激活状态

workon

知道有哪些虚拟环境

虚拟环境内安装 scrapy

自行官网下载 py35 对应得 whl 文件进行 pip 离线安装

创建 scrapy 项目, 目录结构分析

命令行创建 scrapy 项目

scrapy startproject ArticleSpider

mark

这里可以看到, 我们应用的是系统自带的模板 scrapy 是可以自定义模板的

scrapy 目录结构

通过 pycharm 打开 scrapy 的项目

mark

可以看到 scrapy 的目录结构如上图

scrapy 借鉴了 django 的项目思想

scrapy.cfg: 配置文件 & setings.py: 设置

Setting 文件包含了很多 scrapy 相关的设置:

  1. BOT_NAME = 'ArticleSpider' # 工程名
  2. SPIDER_MODULES = ['ArticleSpider.spiders'] # 存放 spider 的路径
  3. NEWSPIDER_MODULE = 'ArticleSpider.spiders'
  4. pipelines.py:

做跟数据存储相关的东西

middilewares.py:

自己定义的 middlewares 定义方法, 处理响应的 IO 操作

items.py(类似于 django 中的 form):

定义数据保存的格式比 django 的 form 简单, 因为字段类型单一

定义我们所要爬取的信息的相关属性 Item 对象是种类似于表单, 用来保存获取到的数据

spider 文件夹:

存放我们具体的爬虫

创建具体的 spider

  1. cd ArticleSpider
  2. scrapy genspider jobbole blog.jobbole.com

mark

使用基础模板为我们创建了具体的 spider

  1. # -*- coding: utf-8 -*-
  2. import scrapy
  3. class JobboleSpider(scrapy.Spider):
  4. name = "jobbole"
  5. allowed_domains = ["blog.jobbole.com"]
  6. # start_urls 是一个待爬取的列表.
  7. # spider 会为我们请求下载网页, 直接到 parse 阶段
  8. start_urls = ['http://blog.jobbole.com/']
  9. def parse(self, response):
  10. pass

比如我们可以直接把 url 拼凑 500 个出来, 直接放进列表中

mark

可以看到 start_requests 是对于 start_urls 进行遍历, 然后交给

make_requests_from_url

来进行处理

make_requests_from_url

会 return 一个 request 会被 yield 命令直接交给 scrapy 的下载器

下载器会去根据 request 下载东西下载完成之后, 会跑到 Parse 中

scrapy 启动 spider,main 文件调试:

scrapy crawl jobbole

在 windows 报出错误:

  1. ImportError: No module named 'win32api'
  2. pip install pypiwin32# 解决

在项目根目录里创建 main.py 作为调试工具文件

  1. # encoding: utf-8
  2. __author__ = 'mtianyan'
  3. __date__ = '2018/1/17 0017 19:50'
  4. from scrapy.cmdline import execute
  5. import sys
  6. import os
  7. # 将项目根目录加入系统环境变量中国
  8. # os.path.abspath(__file__)为当前文件所在绝对路径
  9. # os.path.dirname() 获取文件的父目录
  10. sys.path.append(os.path.dirname(os.path.abspath(__file__)))
  11. print(os.path.abspath(__file__))
  12. # D:\CodeSpace\PythonProject\ArticleSpider\main.py
  13. print(os.path.dirname(os.path.abspath(__file__)))
  14. # D:\CodeSpace\PythonProject\ArticleSpider
  15. # 调用 execute 函数执行 scrapy 命令, 相当于在控制台 cmd 输入该命令
  16. # 可以传递一个数组参数进来:
  17. execute(["scrapy", "crawl" , "jobbole"])

不遵守 reboots 协议设置

ROBOTSTXT_OBEY = False

在 jobble.py 打上断点:

  1. def parse(self, response):
  2. pass

可以看到他返回的 htmlresponse 对象:

(HtmlResponse)对象内部:

url: 'http://blog.jobbole.com/'

body: 网页内容

  1. _DEFAULT_ENCODING = 'ascii'
  2. encoding= 'utf-8'

可以看出 scrapy 已经为我们做到了将网页下载下来而且编码也进行了转换

xpath 提取伯乐在线网页内容

提取目标伯乐在线的一篇文章的标题, 日期, 评论, 正文

xpath 简介及语法

xpath 简介

xpath 术语

xpath 使用路径表达式在 xml 和 html 中进行导航

xpath 包含有一个标准函数库

xpath 是一个 w3c 的标准

xpath 节点关系:

html 中被尖括号包起来的被称为一个节点

父节点 上一层节点

子节点 下一层节点

兄弟节点 同胞节点

先辈节点

父节节点, 爷爷节点

后代节点

儿子节点, 孙子节点

xpath 语法:

表达式 说明
article 选取所有 article 元素的所有子节点
/article 选取根元素 article(html 中根元素都是 html;xml 可以自定义根节点)
article/a 选取所有属于 article 的子元素的 a 元素
//div 选取所有 div 元素(不管出现在文档里的任何地方)
article//div 选取所有属于 article 元素的后代的 div 元素,不管它出现在 article 之下的任何位置
//@class 选取所有名为 class 的属性

xpath 语法 - 谓语:

表达式 说明
/article/div[1 选取属于 article 子元素的第一个 div 元素
/article/div[last()] 选取属于 article 子元素的最后一个 div 元素
/article/div[last()-1] 选取属于 article 子元素的倒数第二个 div 元素
//div[@color] 选取所有拥有 color 属性的 div 元素
//div[@color='red'] 选取所有 color 属性值为 red 的 div 元素

xpath 语法:

表达式 说明
/div/* 选取属于 div 元素的所有子节点
//* 选取所有元素
//div[@*] 选取所有带属性的 div 元素
//div/a 丨 //div/p 选取所有 div 元素的 a 和 p 元素
//span 丨 //ul 选取文档中的 span 和 ul 元素
article/div/p 丨 //span 选取所有属于 article 元素的 div 元素的 p 元素以及文档中所有的 span 元素

firebugs 插件

取某一个网页上元素的 xpath 地址

如: http://blog.jobbole.com/110287/

手写: /html/body/div[3]/div[3]/div[1]/div[1]/h1

在标题处右键使用 Firefox 浏览器最新开发版查看元素

然后在

<h1>2016 腾讯软件开发面试题(部分)</h1>

右键查看 xpath

复制出来的 xpath:

  1. /html/body / div[3] / div[3] / div[1] / div[1] / h1
  2. # -*- coding: utf-8 -*-
  3. import scrapy
  4. class JobboleSpider(scrapy.Spider):
  5. name = "jobbole"
  6. allowed_domains = ["blog.jobbole.com"]
  7. start_urls = ['http://blog.jobbole.com/110287/']
  8. def parse(self, response):
  9. re_selector = response.xpath("/html/body/div[3]/div[3]/div[1]/div[1]/h1")
  10. print(re_selector)
  11. pass

调试 debug 可以看到

re_selector = (selectorlist)[]

可以看到返回的是一个空列表, 列表是为了如果我们当前的 xpath 路径下还有层级目录

可以进行进一步的后续选取

空说明没取到值, 我们可以来 chorme 里观察一下

chorme 取到的值

//*[@id="post-110287"]/div[1]/h1

html 中全局 id 唯一

chorme 版代码:

  1. # -*- coding: utf-8 -*-
  2. import scrapy
  3. class JobboleSpider(scrapy.Spider):
  4. name = "jobbole"
  5. allowed_domains = ["blog.jobbole.com"]
  6. start_urls = ['http://blog.jobbole.com/110287/']
  7. def parse(self, response):
  8. # 取任何节点 id 等于 post-110287.
  9. re_selector = response.xpath('//*[@id="post-110287"]/div[1]/h1')
  10. # print(re_selector)
  11. pass

可以看出此时可以取到值, 如果想取到里面的值直接 h1/text()

我们右键检查是页面上的所有 JavaScript 等都运行完了之后的页面

真实爬虫 get 到的内容是

查看网页源代码中内容

分析页面, 可以发现页面内有一部 html 是通过 JavaScript ajax 交互来生成的, 因此在 f12 检查元素时的页面结构里有, 而 xpath 不对

xpath 是基于 html 源代码文件结构来找的

xpath 可以有多种多样的写法:

  1. re_selector = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()")
  2. re2_selector = response.xpath('//*[@id="post-110287"]/div[1]/h1/text()')
  3. re3_selector = response.xpath('//div[@class="entry-header"]/h1/text()')

推荐使用 id 型因为页面 id 唯一

推荐使用 class 型, 因为后期循环爬取可扩展通用性强

通过了解了这些此时我们已经可以抓取到页面的标题, 此时可以使用 xpath 利器照猫画虎抓取任何内容只需要点击右键查看 xpath

cmd 下运行 python 命令中文乱码解决方案

chcp 65001

scrapy shell 运用

scrapy shell http://blog.jobbole.com/110287/

完整的 xpath 提取伯乐在线字段代码

  1. # -*- coding: utf-8 -*-
  2. import scrapy
  3. import re
  4. class JobboleSpider(scrapy.Spider):
  5. name = "jobbole"
  6. allowed_domains = ["blog.jobbole.com"]
  7. start_urls = ['http://blog.jobbole.com/110287/']
  8. def parse(self, response):
  9. #提取文章的具体字段
  10. title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first("")
  11. # '2016 腾讯软件开发面试题(部分)'
  12. create_date = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace(".","").strip()
  13. # 2017/02/18
  14. praise_nums = response.xpath('//div[@class="post-adds"]/span/h10/text()').extract_first("")
  15. fav_nums = response.xpath("//span[contains(@class,'bookmark-btn')]/text()").extract()[0]
  16. match_re = re.match(".*?(\d+).*", fav_nums)
  17. if match_re:
  18. fav_nums = match_re.group(1)
  19. comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract()[0]
  20. match_re = re.match(".*?(\d+).*", comment_nums)
  21. if match_re:
  22. comment_nums = match_re.group(1)
  23. content = response.xpath("//div[@class='entry']").extract()[0]
  24. tag_list = response.xpath("//p[@class='entry-meta-hide-on-mobile']/a/text()").extract()
  25. tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
  26. tags = ",".join(tag_list)
  27. pass

xpath/text()获取到的是一个 selector 通过 extract()获取一个内容值的 list 而

extract_first("") | extract()[0]

则是获取 list 中第一个元素

xpath 返回的是一个可以继续执行 xpath 的对象而 extract 之后就是数组了

获取 p 标签里面值, 会过滤掉标签

strip()去除无效字符 replace()方法把我们不想要的字符替换成其他无效字符, 然后 strip()掉

span[contains(@class, 'vote-post-up')]

有多个 class 属性值时选取其中一个的写法

[element for element in tag_list if not element.strip().endswith("评论")]

使用列表生成式去掉以评论结尾的

join 方法将数组变成字符串便于数据库一个字段保存

css 选择器的使用:

渲染样式时: 通过 css 选择器选择元素, 为其添加样式

class="container" 与之匹配的 css 选择器. container

表达式 说明
* 选择所有节点
#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[color="red"] 选取所有 color 属性为 red 值的 a
a[href*="jobbole"] 选取所有 href 属性包含 jobbole 的 a 元素
a[href^="http"] 选取所有 href 属性值以 http 开头的 a 元素
a[hre$=".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
  1. # 通过 css 选择器提取字段
  2. # front_image_url = response.meta.get("front_image_url", "") #文章封面图
  3. title = response.css(".entry-header h1::text").extract_first()
  4. create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace(".","").strip()
  5. praise_nums = response.css(".vote-post-up h10::text").extract()[0]
  6. fav_nums = response.css(".bookmark-btn::text").extract()[0]
  7. match_re = re.match(".*?(\d+).*", fav_nums)
  8. if match_re:
  9. fav_nums = int(match_re.group(1))
  10. else:
  11. fav_nums = 0
  12. comment_nums = response.css("a[href='#article-comment'] span::text").extract()[0]
  13. match_re = re.match(".*?(\d+).*", comment_nums)
  14. if match_re:
  15. comment_nums = int(match_re.group(1))
  16. else:
  17. comment_nums = 0
  18. content = response.css("div.entry").extract()[0]
  19. tag_list = response.css("p.entry-meta-hide-on-mobile a::text").extract()
  20. tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
  21. tags = ",".join(tag_list)
  22. pass

extract_first()让我们免去做异常处理数组下标有可能没有

爬取所有文章

我们需要获取下一页的 url 并交给 scrapy 进行下载

yield 关键字

使用 request 下载详情页面, 下载一篇文章的详情完成后回调方法 parse_detail()提取文章内容中的字段

yield Request(url = parse.urljoin(response.url, post_url), callback = self.parse_detail)

scrapy.http: Request 下载网页

  1. from scrapy.http import Request
  2. Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

parse.urljoin 拼接网址应对 herf 内有可能网址不全

  1. from urllib import parse
  2. url=parse.urljoin(response.url,post_url)
  3. parse.urljoin("http://blog.jobbole.com/all-posts/","http://blog.jobbole.com/111535/")
  4. # 结果为 http://blog.jobbole.com/111535/

class 选择器空格 & 不加空格

  1. next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
  2. # 如果. next .pagenumber 是指两个 class 为层级关系而不加空格为同一个标签

twist 异步机制

Scrapy 使用了 Twisted 作为框架, Twisted 有些特殊的地方是它是事件驱动的, 并且比较适合异步的代码在任何情况下, 都不要写阻塞的代码阻塞的代码包括:

访问文件数据库或者 Web

产生新的进程并需要处理新进程的输出, 如运行 shell 命令

执行系统层次操作的代码, 如等待系统队列

实现全部文章字段下载的代码:

  1. def parse(self, response):
  2. """

1. 获取文章列表页中的文章 url 交给 scrapy 下载并进行解析

2. 获取下一页的 url 并交给 scrapy 进行下载, 下载完成后交给 parse

  1. """
  2. # 解析列表页中的所有文章 url 并交给 scrapy 下载后并进行解析
  3. # 不使用 extra 成值的 list 可以进行二次筛选
  4. post_urls = response.css(
  5. "#archive.floated - thumb.post - thumb a: :attr(href)").extract()
  6. # post_url 是我们每一页的具体的文章 url
  7. for post_url in post_urls:
  8. # 下面这个 request 是文章详情页面. 使用回调函数每下载完一篇就 callback 进行这一篇的具体解析
  9. # 我们现在获取到的是完整的地址可以直接进行调用如果不是完整地址: 根据 response.url + post_url
  10. # def urljoin(base, url)完成 url 的拼接
  11. # 初始化好的 Request 如何交给 scrapy 进行下载: yield 关键字
  12. yield Request(url=parse.urljoin(response.url, post_url),callback=self.parse_detail)
  13. # Requ est(url=post_url, callback=self.parse_detail)
  14. # 提取下一页并交给 scrapy 进行下载
  15. next_url = response.css(".next.page - numbers: :attr(href)").extract_first("")
  16. # 如果. next .pagenumber 是指两个 class 为层级关系而不加空格为同一个标签
  17. if next_url:
  18. # 如果还有 next url 就调用下载下一页, 回调 parse 函数找出下一页的 url
  19. yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)"

parse_detail 方法就是我们上一节实现的字段提取

全部文章的逻辑流程图

所有文章流程图

要点: 当 next_url 还存在也就是还有下一页时我们需要下载的是 next_url 这个页面

scrapy 的 items 整合字段

数据爬取的任务就是从非结构的数据中提取出结构性的数据

items 可以让我们自定义自己的字段(类似于字典, 但比字典的功能更齐全)

博主个人认为就是一个个 item 对象每个字段是 item 对象的一个属性

通过爬虫爬过来的数据通过 item 进行实例化, 当我们对 item 进行实例化之后我们在我们的 spider 中对它做 yield 的时候

比如我们通过 parse_detail 生成到了一个 item 对象时

我们直接把这个类 yield,scrapy 识别到这是一个 item 的实例时

会直接将 item 路由到 pipeline 中

优点: 可以在 pipeline 中做一个集中的数据保存, 去重

Request 使用的细节: 解决图片问题

上一节中我们并没有进行图片字段的抓取获取列表页的封面图

目标: 希望获取到图片的 url, 并把图片的 url 放到 request 里面

通过 request, 在它进行 callback 回调 parse_detail 时我们能获取这个值保存下来

给 request 添加能传递的参数, meta={}传递一个字典过来

实现: 在下载列表页时将这个封面 url 获取到, 并通过 meta 将他发送出去在 callback 的回调函数中接收该值

原始写法: extract 之后则生成 list 列表, 无法进行二次筛选

所以我们将 extract 延后便于我们对于列表每个单项文章既记录文章 url, 又记录图片地址

将我们以前的代码:

post_urls = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()

改为如下:

  1. post_nodes = response.css("#archive .floated-thumb .post-thumb a")
  2. for post_node in post_nodes:
  3. #获取封面图的 url
  4. image_url = post_node.css("img::attr(src)").extract_first("")
  5. post_url = post_node.css("::attr(href)").extract_first("")

在列表页的时候把获取到的封面图的 url 传给 parse_detail 的 response

  1. # 发送
  2. yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)
  3. # 接收
  4. def parse_detail(self, response):
  5. front_image_url = response.meta.get("front_image_url", "")

urljoin 的好处:

如果你没有域名, 我就从 response 里取出来, 如果你有域名则我对你起不了作用了

图片的 url 有两种格式, 全地址和早期的不全地址这里 urljoin 就可以实现

对两者的兼容

上面代码中我们将图片 url 放进了 Request 的 meta 信息中, 然后在 response 中获取 meta 进行接收

编写 Jobbole 的 items 类

  1. class JobBoleArticleItem(scrapy.Item):
  2. title = scrapy.Field()
  3. create_date = scrapy.Field()
  4. url = scrapy.Field()
  5. url_object_id = scrapy.Field()
  6. front_image_url = scrapy.Field()
  7. front_image_path = scrapy.Field()
  8. praise_nums = scrapy.Field()
  9. comment_nums = scrapy.Field()
  10. fav_nums = scrapy.Field()
  11. content = scrapy.Field()
  12. tags = scrapy.Field()

这里可以看出 items 比 django 的 models form 弱在类型单一 Field

但是强在无论什么类型都可以接收

实例化 items 并为其填充数据

import 之后实例化, 实例化之后填充:

  1. from ArticleSpider.items import JobBoleArticleItem
  2. article_item = JobBoleArticleItem()
  3. article_item["title"] = title
  4. article_item["url"] = response.url
  5. article_item["create_date"] = create_date
  6. article_item["front_image_url"] = [front_image_url]
  7. article_item["praise_nums"] = praise_nums
  8. article_item["comment_nums"] = comment_nums
  9. article_item["fav_nums"] = fav_nums
  10. article_item["tags"] = tags
  11. article_item["content"] = content

yield article_item 将这个 item 传送到 pipelines

yield article_item

pipelines 可以接收到传送过来的 item

将 setting.py 中的 pipeline 配置取消注释

  1. # Configure item pipelines
  2. # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
  3. ITEM_PIPELINES = {
  4. 'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
  5. }

此时 debug 可以发现, 我们的 item 被传输到 pipeline. 我们可以将其进行存储到数据库等工作

调试小技巧; 将 next_url 换为 post_url 让它只爬一页

setting 设置下载图片 pipeline

添加 scrapy 自带的 imagepipeline

  1. ITEM_PIPELINES = {
  2. 'scrapy.pipelines.images.ImagesPipeline': 1,
  3. }
  4. D: /CodeSpace/PythonEnvs / articlespider3 / Lib / site - packages / scrapy / pipelines

里面有三个 scrapy 默认提供的 pipeline: 分别提供了文件, 图片, 媒体的下载

ITEM_PIPELINES 是一个数据管道的登记表, 每一项具体的数字代表它的优先级, 数字越小, 越早进入

数据在每个被声明的 pipeline 中流动, 被处理之后流向下一个

setting 设置哪个字段是图片

  1. import os
  2. IMAGES_URLS_FIELD = "front_image_url"
  3. project_dir = os.path.abspath(os.path.dirname(__file__))
  4. IMAGES_STORE = os.path.join(project_dir, 'images')

设置哪个字段是图片, imagePipeline 会在传来的 url 中自动寻找

设置下载的路径: 新建文件夹 images, 放在项目目录下

图片必备: 安装 PIL 库

pip install pillow

报错处理:

  1. raise ValueError('Missing scheme in request url: %s' % self._url)
  2. ValueError: Missing scheme in request url: h

原因:

如果我们在 setting 中配置了 IMAGES_URLS_FIELD, 这个值在传递到 pipeline 时

会被当成 list 处理而我们只传了一个值进来

article_item["front_image_url"] = [front_image_url]

定制自己的 pipeline 使其下载图片后能保存下它的本地路径

目标: 将图片的本地路径保存起来

imagesPipeline 中有很多可以设置的参数

  1. # IMAGES_MIN_HEIGHT = 100
  2. # IMAGES_MIN_WIDTH = 100

设置下载图片的最小高度, 宽度可以帮我们过滤掉一部分小图片

重要函数; 转换, 过滤图片.

get_media_requests()

接收一个迭代器对象 (我们之前设置的 field) 进行 for 循环下载图片

item_completed 可以获取到图片的实际下载存放地址

继承 ImagesPipeline 并重写 item_completed()

  1. from scrapy.pipelines.images import ImagesPipeline
  2. class ArticleImagePipeline(ImagesPipeline):
  3. # 重写该方法可从 result 中获取到图片的实际下载地址
  4. def item_completed(self, results, item, info):
  5. for ok, value in results:
  6. image_file_path = value["path"]
  7. item["front_image_path"] = image_file_path
  8. return item

result 是一个 tuple, 而 dict 里面的 path 存放了文件的路径

获取文件路径, 并填充进 front_image_path

一定要把 item return 出去, 因为下一个 pipeline 还要接收

setting 中设置使用我们自定义的 pipeline, 而不是系统自带的

  1. ITEM_PIPELINES = {
  2. 'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
  3. #'scrapy.pipelines.images.ImagesPipeline': 1,
  4. 'ArticleSpider.pipelines.ArticleImagePipeline': 1,
  5. }

自定义图片 pipeline 的调试信息

上图通过调试来看 results 里面放着啥: 可以看到第一个值是一个布尔值, 表示有没有成功

第二个值是一个字典: checksum path 原始 url 路径都存放在里面

保存下来的本地地址

图片 url 的 md5 处理

新建 package: utils

  1. import hashlib
  2. def get_md5(url):
  3. m = hashlib.md5()
  4. m.update(url)
  5. return m.hexdigest()
  6. if __name__ == "__main__":
  7. print(get_md5("http://jobbole.com".encode("utf-8")))

进阶版: 如果不确定用户传入的是不是 Unicode

  1. def get_md5(url):
  2. # str 就是 unicode 了
  3. if isinstance(url, str):
  4. url = url.encode("utf-8")
  5. m = hashlib.md5()
  6. m.update(url)
  7. return m.hexdigest()

在 jobbole.py 中将 url 的 md5 保存下来

  1. from ArticleSpider.utils.common import get_md5
  2. article_item["url_object_id"] = get_md5(response.url)

数据保存到本地文件以及 mysql 中

保存到本地 json 文件

import codecs 避免打开文件时的一些编码问题

自定义

JsonWithEncodingPipeline

实现 json 本地保存

  1. # 自定义的将伯乐在线内容保存到本地 json 的 pipeline
  2. class JsonWithEncodingPipeline(object):
  3. # 自定义 json 文件的导出
  4. def __init__(self):
  5. # 使用 codecs 打开避免一些编码问题
  6. self.file = codecs.open('article.json', 'w', encoding="utf-8")
  7. def process_item(self, item, spider):
  8. # 将 item 转换为 dict, 然后调用 dumps 方法生成 json 对象, false 避免中文出错
  9. lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
  10. self.file.write(lines)
  11. return item
  12. # 当 spider 关闭的时候: 这是一个 spider_closed 的信号量
  13. def spider_closed(self, spider):
  14. self.file.close()

setting.py 注册 JsonWithEncodingPipeline

  1. ITEM_PIPELINES = {
  2. 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
  3. #'scrapy.pipelines.images.ImagesPipeline': 1,
  4. 'ArticleSpider.pipelines.ArticleImagePipeline': 1,
  5. }

scrapy exporters: JsonItemExporter 导出

scrapy/exporters.py

scrapy 自带的导出:

  1. 'CsvItemExporter',
  2. 'XmlItemExporter',
  3. 'JsonItemExporter'
  4. from scrapy.exporters import JsonItemExporter
  5. class JsonExporterPipleline(object):
  6. # 调用 scrapy 提供的 json export 导出 json 文件
  7. def __init__(self):
  8. self.file = open('articleexport.json', 'wb')
  9. self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
  10. self.exporter.start_exporting()
  11. def close_spider(self, spider):
  12. self.exporter.finish_exporting()
  13. self.file.close()
  14. def process_item(self, item, spider):
  15. self.exporter.export_item(item)
  16. return item

设置 setting.py 注册该 pipeline

'ArticleSpider.pipelines.JsonExporterPipeline': 2

注意: 注意: 注意: 在 setting 中注册 pipeline 时千万不能加空格

保存到数据库(mysql)

数据库设计数据表, 表的内容字段是和 item 中应该是一致的

数据库与 item 的关系类似于 django 中 model 与 form 的关系

日期的转换, 将字符串转换为 datetime

  1. import datetime
  2. try:
  3. create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date()
  4. except Exception as e:
  5. create_date = datetime.datetime.now().date()

数据库表设计

mark

三个 num 字段均设置不能为空, 然后默认 0.

content 设置为 longtext

主键设置为 url_object_id: 后面会利用这个主键进行更新

pip install mysqlclient

Linux 报错解决方案:

  1. ubuntu:
  2. sudo apt-get install libmysqlclient-dev
  3. centos
  4. sudo yum install python-devel mysql-devel

保存到数据库 pipeline(同步)编写

  1. import MySQLdb
  2. class MysqlPipeline(object):
  3. #采用同步的机制写入 mysql
  4. def __init__(self):
  5. self.conn = MySQLdb.connect('127.0.0.1', 'root', 'password', 'articlespider', charset="utf8", use_unicode=True)
  6. self.cursor = self.conn.cursor()
  7. def process_item(self, item, spider):
  8. insert_sql = """
  9. insert into jobbole_article(title, url, create_date, fav_nums)
  10. VALUES (%s, %s, %s, %s)
  11. """self.cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))
  12. self.conn.commit()

如果插入出现异常将不为 null 都暂时去掉

保存到数据库的 (异步 Twisted) 编写[通用版]

因为我们的爬取速度很有肯大于数据库存储的速度

插入速度是跟不上我们的爬取速度的

提供连接池使我们的 mysql 插入变为异步操作

设置可配置参数 seeting.py 设置:

  1. MYSQL_HOST = "127.0.0.1"
  2. MYSQL_DBNAME = "articlespider"
  3. MYSQL_USER = "root"
  4. MYSQL_PASSWORD = "123456"

代码中获取到设置的可配置参数, 实现 twisted 异步:

  1. # 异步操作 mysql 插入
  2. class MysqlTwistedPipeline(object):
  3. def __init__(self, dbpool):
  4. self.dbpool = dbpool
  5. @classmethod
  6. # 自定义组件或扩展很有用的方法: 这个方法名字固定, 是会被 scrapy 调用的
  7. # 这里传入的 cls 是指当前的 MysqlTwistedPipline class
  8. def from_settings(cls, settings):
  9. # setting 值可以当做字典来取值
  10. dbparms = dict(
  11. host = settings["MYSQL_HOST"],
  12. db = settings["MYSQL_DBNAME"],
  13. user = settings["MYSQL_USER"],
  14. passwd = settings["MYSQL_PASSWORD"],
  15. charset='utf8',
  16. cursorclass=MySQLdb.cursors.DictCursor,
  17. use_unicode=True,
  18. )
  19. # 连接池 ConnectionPool
  20. # def __init__(self, dbapiName, *connargs, **connkw):
  21. dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
  22. # 此处相当于实例化 pipeline, 要在 init 中接收
  23. return cls(dbpool)
  24. def process_item(self, item, spider):
  25. # 使用 twisted 将 mysql 插入变成异步执行: 参数 1: 我们自定义一个函数, 里面可以写我们的插入逻辑
  26. query = self.dbpool.runInteraction(self.do_insert, item)
  27. # 添加自己的处理异常的函数
  28. query.addErrback(self.handle_error, item, spider)
  29. def do_insert(self, cursor, item):
  30. insert_sql = """
  31. insert into jobbole_article(title, url, create_date, fav_nums)
  32. VALUES (%s, %s, %s, %s)
  33. """
  34. # 使用 VALUES 实现传值
  35. cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))
  36. def handle_error(self, failure, item, spider):
  37. # 处理异步插入的异常
  38. print (failure)

通过关键的方法: @classmethod

def from_settings(cls, settings) :

scrapy 提供的是一个异步的容器: 到底用哪个库连接 mysql 是我们可以指明的

adbapi 可以将我们 mysql 的操作变成异步操作

adbapi.ConnectionPool

升级通用版后面会讲到:

  1. # 根据不同的 item 构建不同的 sql 语句并插入到 mysql 中
  2. insert_sql, params = item.get_insert_sql()
  3. cursor.execute(insert_sql, params)

将 django 的 orm 集成进来: django.items

https://github.com/scrapy-plugins/scrapy-djangoitem

可以让我们保存的 item 直接变成 django 的 models.

itemloader 来维护字段提取代码

itemloadr 提供了一个容器, 让我们配置某一个字段该使用哪种规则

三个常用的方法: add_css add_value add_xpath

可以让我们维护自己的字段提取代码: 并且能够有复用的可能

  1. from scrapy.loader import ItemLoader
  2. # 通过 item loader 加载 item
  3. front_image_url = response.meta.get("front_image_url", "") # 文章封面图
  4. item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)
  5. item_loader.add_css("title", ".entry-header h1::text")
  6. item_loader.add_value("url", response.url)
  7. item_loader.add_value("url_object_id", get_md5(response.url))
  8. item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
  9. item_loader.add_value("front_image_url", [front_image_url])
  10. item_loader.add_css("praise_nums", ".vote-post-up h10::text")
  11. item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
  12. item_loader.add_css("fav_nums", ".bookmark-btn::text")
  13. item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
  14. item_loader.add_css("content", "div.entry")
  15. #调用这个方法来对规则进行解析生成 item 对象
  16. article_item = item_loader.load_item()

mark

所有值变成了 list

对于这些值都没有经过后期处理函数处理

但是 itemloader 可以让代码变整洁, 甚至可以把这些规则存在数据库

item.py 中编写 input_processor 处理函数

MapCompose 可以传入函数对于该字段进行处理, 而且可以传入多个函数

  1. from scrapy.loader.processors import MapCompose
  2. def add_mtianyan(value):
  3. return value+"-mtianyan"
  4. title = scrapy.Field(
  5. input_processor=MapCompose(lambda x:x+"mtianyan",add_mtianyan),
  6. )

注意: 此处的自定义方法一定要写在代码前面

  1. # 时间转换
  2. def date_convert(value):
  3. try:
  4. create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
  5. except Exception as e:
  6. create_date = datetime.datetime.now().date()
  7. return create_date
  8. create_date = scrapy.Field(
  9. input_processor=MapCompose(date_convert),
  10. output_processor=TakeFirst()
  11. )

TakeFirst 实现只取 list 中的第一个值不需要每个都加上 output

自定义 itemloader 实现默认提取第一个

  1. class ArticleItemLoader(ItemLoader):
  2. #自定义 itemloader 实现默认提取第一个
  3. default_output_processor = TakeFirst()

将 jobbole.py 中的 itemloader 替换为我们自己的

item_loader = ArticleItemLoader(item = JobBoleArticleItem(), response = response)

list 保存原值

  1. def return_value(value):
  2. return value
  3. front_image_url = scrapy.Field(
  4. output_processor=MapCompose(return_value)
  5. )

对于字段的预处理进行单独方法的提取, 做到代码重用

  1. get_nums & remove_comment_tags
  2. def get_nums(value):
  3. match_re = re.match(".*?(\d+).*", value)
  4. if match_re:
  5. nums = int(match_re.group(1))
  6. else:
  7. nums = 0
  8. return nums
  9. # 去除标签中提取的评论方法
  10. def remove_comment_tags(value):
  11. if "评论" in value:
  12. return ""
  13. else:
  14. return value

下载图片 pipeline 增加 if 增强通用性

这里我们的 pipeline 是会重用的可能后面的爬虫的 items 也会经过这个 pipeline

这就需要我们做出判断: items 中有这个字段再执行

  1. class ArticleImagePipeline(ImagesPipeline):
  2. #重写该方法可从 result 中获取到图片的实际下载地址
  3. def item_completed(self, results, item, info):
  4. if "front_image_url" in item:
  5. for ok, value in results:
  6. image_file_path = value["path"]
  7. item["front_image_path"] = image_file_path
  8. return item

自定义的 item 带处理函数的完整代码 **

  1. class JobBoleArticleItem(scrapy.Item):
  2. title = scrapy.Field()
  3. create_date = scrapy.Field(
  4. input_processor=MapCompose(date_convert),
  5. )
  6. url = scrapy.Field()
  7. url_object_id = scrapy.Field()
  8. front_image_url = scrapy.Field(
  9. output_processor=MapCompose(return_value)
  10. )
  11. front_image_path = scrapy.Field()
  12. praise_nums = scrapy.Field(
  13. input_processor=MapCompose(get_nums)
  14. )
  15. comment_nums = scrapy.Field(
  16. input_processor=MapCompose(get_nums)
  17. )
  18. fav_nums = scrapy.Field(
  19. input_processor=MapCompose(get_nums)
  20. )
  21. #因为 tag 本身是 list, 所以要重写
  22. tags = scrapy.Field(
  23. input_processor=MapCompose(remove_comment_tags),
  24. output_processor=Join(",")
  25. )
  26. content = scrapy.Field()