@TOC
Scrapy简介
较为流行的python爬虫框架。
本文着重将记录本人入门Scrapy时的所有精炼总结(除了一些书、官方文档,同时也会借鉴一些比较好的blog的内容,因为书写的太生涩,而官方文档又搞得和过家家一样,乱的不行,根本没法看)。希望能给大家带来帮助,抛砖引玉。
如果爬下来的数据还不会分析,建议先看本人上一篇博文《BeautifulSoup总结及contents内容分析》
Scrapy架构
架构如下图所示:
图中绿色线条代表了数据流向。其他几个则是其组件。
//以下是我个人对这些组件的理解,并非官方文档解释
- 引擎(Scrapy Engine):负责整个数据流走向
- 调度器(scheduler):负责将Request入队,并在需要时提供给引擎
- 下载器(DownLoader): 负责提交Request,并获得对应网站的Response,将其提交给spider下一步处理。(可以根据用户定义的<kbd>下载中间件</kbd>中的配置进行自定义下载)
- 蜘蛛(Spider):负责处理网站返回的Response,提取Item 或者是 需要继续跟进的URL
- 数据管道(Item pipeline): 去重、过滤、加工和存储Item
- 下载中间件(Downloader middlewares):自定义扩展下载功能的组件
- Spider 中间件 (Spider middlewares):自定义扩展Engine和Spider中间通信的功能组件
Scrapy运作流程
1 引擎:Hi!Spider, 你要处理哪一个网站?
2 Spider:老大要我处理xxxx.com。
3 引擎:你把第一个需要处理的URL给我吧。
4 Spider:给你,第一个URL是xxxxxxx.com。
5 引擎:Hi!调度器,我这有request请求你帮我排序入队一下。
6 调度器:好的,正在处理你等一下。
7 引擎:Hi!调度器,把你处理好的request请求给我。
8 调度器:给你,这是我处理好的request
9 引擎:Hi!下载器,你按照老大的下载中间件的设置帮我下载一下这个request请求
10 下载器:好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载)
11 引擎:Hi!Spider,这是下载好的东西,并且已经按照老大的下载中间件处理过了,你自己处理一下(注意!这儿responses默认是交给def parse()这个函数处理的)
12 Spider:(处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。
13 引擎:Hi !管道 我这儿有个item你帮我处理一下!调度器!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。
14 管道``调度器:好的,现在就做!
参考: https://segmentfault.com/a/1190000013178839
项目文件目录结构
在命令行中,执行 scrapy startproject <项目名称>即会在所在当前目录下创建项目目录以及相关文件。
项目名
|—— 项目名
| |—— _init_.py #包定义
| |—— items.py #模型定义
| |—— middlewares.py #中间件定义
| |—— pipelines.py #管道定义
| |—— settings.py #配置文件。编程方式控制的配置文件
| |—— spider
| |—— _init_.py #默认蜘蛛代码文件
|——— scrapy.cfg #运行配置文件。该文件存放的目录为根目录。模块名的字段定义了项目的设置
最基本的Scrapy爬虫制作流程
1、新建项目
2、明确目标:主要是编写Item.py
3、制作爬虫:主要是编写spider.py
4、存储内容:主要是编写pipelines.py
实战
环境安装
//建议直接安装,不要用conda创建一个环境再安装。
//因为scrapy命令需要在全局使用,这样才能在任何文件夹轻松调用。
pip install Scrapy
//验证是否安装成功。运行该命令出现图中内容即为成功。
scrapy
1、新建项目
项目介绍:从中新网爬取新闻供稿的标题、链接、内容和日期,并以json形式保存到本地。
// 需要先cd到你想要存放该项目的路径下
scrapy startproject chinanews_crawler
2、明确目标
先查看以下目标网站内容:
中新网:http://www.chinanews.com/rss/
然后用chrome的开发者工具,查看以下我们需要的链接的位置。
我们发现,那些链接是被放在一个
<iframe>
元素内,也就是当前展示的其实是两个页面组成的,直接爬取该网站是拿不到链接的。于是,查看<ifreame>
元素,发现src="http://www.chinanews.com/rss/rss_2.html"
。于是,打开该网页,找到真正的链接所在。我们继续F12查看其元素。
发现所有的新闻频道链接都在一个
<a>
标签中。并且打开这些频道后,就是即时新闻的内容了,也就是到达了我们的目标。如下图:然后,我们要的是title,link,description,pubDate。
最后,我们可以开始编写代码了。
文件:items.py
from scrapy.item import Item, Field
class ChinaNewsItem(Item):
title = Field() # 标题
link = Field() # 详情链接
desc = Field() # 新闻综述
pub_date = Field() # 发布日期
3、制作爬虫
可以直接在spider/文件夹下,编辑那个 init 文件,或者新建一个py文件,再者在命令行中,<kbd>scrapy genspider 爬虫名 目标网站。</kbd>
无论那种方式,其实都是生成一个爬虫的类。
执行:scrapy genspider newsCrawler chinanews.com
打开新生成的spider/newsCrawler.py文件,内容如下:
# -*- coding: utf-8 -*-
from scrapy.spider import Spider
class EasyNewsCrawlerItem(Spider):
name = 'newsCrawler' # 爬虫名,启动时需要用到,scrapy crawl newsCrawler,就是启动该爬虫(对照运行流程中第一步中,引擎:Hi,spider,你要处理哪个网站。
allowed_domains = ['chinanews.com'] # 允许爬虫搜索的域名范围
start_urls = ['http://chinanews.com/'] # 起始爬取位置
def parse(self, response):
"""解析函数。response就是下载器下载好的页面内容,爬虫就在该网站中,提取所需的Item"""
pass
根据<kbd>2、明确目标</kbd>中,Item的内容,我们发现,直接爬取并不能实现。因为,http://www.chinanews.com/rss/rss_2.html 这个页面只有链接,要打开这些链接,才能看到正真的内容。于是,我们需要再写一个解析的函数,处理真正网站的内容。
def parse_feed(self, response):
"""二次解析,本次目标:解析出最后结果,包装成Item"""
rss_page = BeautifulSoup(response.body, "lxml")
items = rss_page.find_all('item')
for item in items:
newsItem = EasyNewsCrawlerItem()
newsItem['title'] = item.title.text
# link是个自闭和标签,不能用item.link.text,原因可以看我上一篇博文
newsItem['link'] = item.contents[2]
newsItem['description'] = item.title.text
newsItem['pubDate'] = item.pubdate.text
yield newsItem
最后,我们稍微处理一下第一个解析函数,将二次解析定位转一下就行了。下面给出spider的完整代码。
# -*- coding: utf-8 -*-
from scrapy.spider import Spider
from scrapy.http import Request
from bs4 import BeautifulSoup
from ..items import EasyNewsCrawlerItem
class NewscrawlerSpider(Spider):
name = 'newsCrawler' # 爬虫名,启动时需要用到,scrapy crawl newsCrawler,就是启动该爬虫(对照运行流程中第一步中,引擎:Hi,spider,你要处理哪个网站。
allowed_domains = ['chinanews.com'] # 允许爬虫搜索的域名范围
start_urls = ['http://www.chinanews.com/rss/rss_2.html'] # 起始爬取位置
def parse(self, response):
"""解析函数。response就是下载器下载好的页面内容,爬虫就在该网站中,提取所需的Item"""
"""本次目标:解析出href中的链接,然后留给下一个解析函数继续解析"""
# 不熟悉BeautifulSoup的可以看我上一个博文
rss_page = BeautifulSoup(response.body, "lxml")
"""拿到该网站后,先找到所有<a>标签,然后把其中的href的内容保存起来。"""
rss_links = set([item['href'] for item in rss_page.find_all("a")]) # 用set是为了滤掉重复链接
for link in rss_links:
yield Request(url=link, callback=self.parse_feed)
def parse_feed(self, response):
"""二次解析,本次目标:解析出最后结果,包装成Item"""
rss_page = BeautifulSoup(response.body, "lxml")
items = rss_page.find_all('item')
for item in items:
newsItem = EasyNewsCrawlerItem()
newsItem['title'] = item.title.text
# link是个自闭和标签,不能用item.link.text,原因可以看我上一篇博文
newsItem['link'] = item.contents[2]
newsItem['description'] = item.title.text
newsItem['pubDate'] = item.pubdate.text
yield newsItem
"""如果是item.pubDate.text会失败并报错,然后我查了一下文档,发现:"""
"""
如果同样的代码在不同环境下结果不同,可能是因为两个环境下使用不同的解析器造成的.
例如这个环境中安装了lxml,而另一个环境中只有html5lib, 解析器之间的区别中说明了原因.
修复方法是在 BeautifulSoup 的构造方法中中指定解析器
因为HTML标签是 大小写敏感 的,所以3种解析器再出来文档时都将tag和属性转换成小写.
"""
"""结论:beautifulSoup会将tag统一变成小写"""
4、 存储内容
存储数据的方法有以下几种:
- 通过pipeline(管道)存储
- 全局性指定。setting.py文件中配置存储选项
- 动态指定。命令行启动时添加-o参数
<kbd>方法1:管道存储</kbd>
为了加深对管道的理解,体现其功能,这里写了三个管道,分别对应过滤功能,加工功能,存储功能。每个管道都是一个拥有process_item方法的类。
同时,管道写好了后,要在setting.py文件中将管道配置一下,主要是控制 Item经过管道的顺序,可以取值为0-1000,值越小优先级越高。
文件:pipelines.py
# -*- coding: utf-8 -*-
from scrapy.exceptions import DropItem
import time
from bs4 import BeautifulSoup
import json
class PreservationPipeline(object):
"""过滤性管道。只通过最近一个小时以内的新闻"""
def process_item(self, item, spider):
# <pubDate>2018-12-06 16:09:03<pubDate>
# 先将字符串转为时间戳
newsTime = time.mktime(time.strptime(item['pub_date'], '%Y-%m-%d %H:%M:%S'))
# 获取当前时间戳
nowTime = time.time()
if (nowTime - newsTime) / (60 * 60) > 1:
raise DropItem("%s ,Not Fresh!" % item) # 超过一个小时,丢弃
return item
class CleanPipeline(object):
"""加工性管道。删除掉所有的\r\n符号"""
def process_item(self, item, spider):
def clear_html(text):
html = BeautifulSoup(text)
return html.get_text().replace('\n', '')
item['desc'] = clear_html(item['desc'])
return item
class JsonFeedPipeline(object):
"""存储管道。存储到指定的Json文件中去"""
def __init__(self):
self.json_file = open('pipResult.json', 'w')
self.json_file.write("[\n")
def process_item(self, item, spider):
line = json.dumps(dict(item)) + ",\n"
# BeautifulSoup会统一为Unicode编码,需要重新编码一下
self.json_file.write(line.encode('utf-8').decode("unicode_escape"))
return item
def close_spider(self, spider):
self.json_file.write("\n]")
self.json_file.close()
文件: setting.py
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'chinanews_crawler.pipelines.PreservationPipeline': 300,
'chinanews_crawler.pipelines.CleanPipeline': 301,
'chinanews_crawler.pipelines.JsonFeedPipeline': 302,
}
这样,整个爬虫系统就写好了,直接在项目根目录下,运行命令 scrapy crawl newsCrawler,即可看到pipResult.json文件。
<kbd>方法2:全局性指定</kbd>
在setting.py文件中,直接加上以下几项。
FEED_URI = "result.json" # 保存文件名
FEED_FORMAT = "json" # 保存文件格式
FEED_EXPORT_ENCODING = 'utf-8' # 保存文件的编码
这样,整个爬虫系统就写好了,直接在项目根目录下,运行命令 scrapy crawl newsCrawler,即可看到result.json文件。
<kbd>方法3:动态指定</kbd>
在Scrapy命令中加入-o的输出参数即可。(本人觉得还不如方法2,每次命令都得加,然后还不利于后人查看代码。因此此方法本人并未尝试)
本项目仅供学习参考,所有步骤与本人遇到的坑,本人给予了解释注释 。因此,请不要都测试同一网站,以免引起不必要的麻烦,谢谢。
@copyright Dawn
编辑于:2018/12/6
网友评论