Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其可以应用在数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。
Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下:
Scrapy主要包括了以下组件(结合上图):
- 引擎(Scrapy)
用来处理整个系统的数据流处理, 触发事务(框架核心) - 调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址 - 下载器(Downloader)
用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的) - 爬虫( Spiders )
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面 - 项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。 - 下载器中间件(Downloader Middlewares)
位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。 - 爬虫中间件(Spider Middlewares)
介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。 - 调度中间件(Scheduler Middewares)
介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。
Scrapy运行流程大概如下(结合上图):
- 每个 Spiders 都会有一个初始start_urls。(第一次运行)
引擎第一次运行时把 Spiders 里的start_urls全放到调度器里 - 然后引擎从调度器中取出一个链接(URL)用于接下来的抓取
- 引擎把URL封装成一个请求(Request)传给下载器
- 下载器把资源下载下来,并封装成应答包(Response)
- 爬虫解析Response
- 解析出实体(Item),则交给实体管道进行进一步的处理
- 解析出的是链接(URL),则把URL交给调度器等待抓取
Scrapy 安装
Linux
pip3 install scrapy
Windows
a. pip3 install wheel
b. 下载twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. 进入下载目录,执行 pip3 install Twisted‑17.1.0‑cp35‑cp35m‑win_amd64.whl
d. pip3 install scrapy
e. 下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/
Scrapy 基本使用
基本命令(cmd):
1. scrapy startproject 项目名称
- 在当前目录中创建中创建一个项目文件(类似于Django)
2. scrapy genspider [-t template] <name> <domain>
- 创建爬虫应用
如:
scrapy gensipider -t basic oldboy oldboy.com
scrapy gensipider -t xmlfeed autohome autohome.com.cn
PS:
查看所有命令:scrapy gensipider -l
查看模板命令:scrapy gensipider -d 模板名称
3. scrapy list
- 展示爬虫应用列表
4. scrapy crawl 爬虫应用名称
- 运行单独爬虫应用
如果想在通过main.py运行,则新建main.py然后
from scrapy import cmdline
cmdline.execute("scrapy crawl chouti --nolog".split())
项目结构以及爬虫应用简介:
project_name/
scrapy.cfg
project_name/
__init__.py
items.py
pipelines.py
settings.py
spiders/
__init__.py
爬虫1.py
爬虫2.py
爬虫3.py
文件说明:
- scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
- items.py 设置数据存储模板,用于结构化数据,如:Django的Model
- pipelines 数据处理行为,如:一般结构化的数据持久化
- settings.py 配置文件,如:递归的层数、并发数,延迟下载等
- spiders 爬虫目录,如:创建文件,编写爬虫规则
运行 scrapy genspider zhihu zhihu.com 后生成的.py文件
import scrapy
class ZhiHuSpider(scrapy.spiders.Spider):
name = "zhihu" # 爬虫名称 *****
allowed_domains = ["zhihu.com"] # 允许的域名
start_urls = [
"https://www.zhihu.com/", # 起始 URL
]
def parse(self, response):
# 访问起始URL并获取结果后的回调函数
# 如果在Windows里遇到编码问题,输入下面代码
import sys,os
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
xpath语法
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。
如:
//book 选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
/div[@id="i1"] 儿子中id="i1"的div
/div[@id="i1"]/text() 获取当前标签(div)的文本
/div[@id="i1"]/@href 获取当前标签(div)的href属性
obj.extract() 列表中的每一个对象转换成字符串,返回一个列表
obj.extract_first() 列表中的每一个对象转换成字符串,返回列表第一个元素
# a[starts-with(@属性(href), "link"),获取以link开头的
hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]')
# 也可以用正则表达式a[re:test(@href, "...")]
hxs = Selector(response=response).xpath('//a[re:test(@href, "/all/hot/recent/\d+")]')
# contains代表包含link就行
# hxs = Selector(response=response).xpath('//a[contains(@href, "link")]')
低级去重方式
我们通过爬虫获取到的 url 肯定会有重复的,所以要去重,假如我们用集合去重(低级方式):
# 定义静态字段
visited_urls = set()
...
def parse(self, response):
for url in hxs:
if url in self.visited_urls:
print('已经存在', url)
else:
print(url)
self.visited_urls.add(url)
这样就去重了,但还有问题,当我们存储时(无论存在数据库还是内存/缓存中),如果url的长度过大,就会导致内存的浪费,所以我们需要通过md5或者其他加密算法,将url加密,这样就固定了存储url的长度。
首先定义加密方法:
def md5(self, url):
import hashlib
obj = hashlib.md5()
obj.update(bytes(url, enco
return obj.hexdigest()
然后将上面的改成:
hxs = Selector(response=response).xpath('//a[starts-with(@href, "/all/hot/recent/")]')
for url in hxs:
md5_url = self.md5(url)
if md5_url in self.visited_urls:
print('已经存在', url)
else:
self.visited_urls.add(md5_url)
real_url = "http://dig.chouti.com/%s"%(url)
print(real_url)
运用 Request 发送请求
获取当前页的所有页码已经完成了,这时需求改了,要获取所有的页码怎么做?
获取所有页,就是从当前页跳转,然后再到跳转的页继续找,重复这个动作,这样就像是递归,而在Scrapy里,为我们提供了一个Request对象。用来请求数据(跳转就是请求新的页码)。
from scrapy.http import Request
...
else:
self.visited_urls.add(md5_url)
real_url = "http://dig.chouti.com%s"%(url)
print(real_url)
# 将新要访问的url添加到调度器
# 必须写yield,写了yield引擎才能把Request发给调度器
yield Request(url=real_url, callback=self.parse)
这样就能找到所有的页码,如果我们不想要这么多,就可以在settings.py里设置递归深度,如
DEPTH_LIMIT = 2
# DEPTH_LIMIT = 0 表示无限制
数据持久化:
Scrapy里数据的持久化是在Pipeline和item里做的。
首先在items.py里,将爬取的东西作为一个对象
class ChoutiItem(scrapy.Item):
title = scrapy.Field()
href = scrapy.Field()
然后chouti.py爬的时候存数据:
xs_item_list = Selector(response=response).xpath('//div[@id="content-list"]/div[@class="item"]')
for item in hxs_item_list:
title = item.xpath('.//a[@class="show-content color-chag"]/text()').extract_first().strip()
href = item.xpath('.//a[@class="show-content color-chag"]/@href').extract_first().strip()
# 存数据
item = ChoutiItem(title=title, href=href)
# 通过yield 发送给Pipeline
yield item
然后再Pipeline里进行持久化:
pieplines.py
class ScrapyLearnPipeline(object):
# process 处理
def process_item(self, item, spider):
# 因为所有爬虫爬的数据都放在pipeline里做持久化,所以可以根据spider.name来做区分
if spider.name == 'chouti':
# 从item里取数据通过item['xxx']
tmp = "%s\n%s\n\n"%(item['title'], item['href'])
with open('data.json', 'a') as f:
f.write(tmp)
还需要注意的是,要在settings.py中注册Pipeline:
ITEM_PIPELINES = {
'scrapy_learn.pipelines.ScrapyLearnPipeline': 300,
}
300是权重,权重大,则先持久化
去重的新方式
- 自定义类去重
- 用scrapy自带的类(RFPDupeFilter),他是存在文件中,然后通过文件来判断的。(
from scrapy.dupefilters import RFPDupeFilter
)
自定义类:
新建duplication.py
class RepeatFilter(object):
def __init__(self):
# 自定义的类可以存在内存,缓存,数据库,文件
self.visited_set = set()
@classmethod
def from_settings(cls, settings):
# 创建RepeatFilter对象 - cls()
return cls()
# 检查是否访问过
def request_seen(self, request):
if request.url in self.visited_set:
return True
self.visited_set.add(request.url)
return False
def open(self): # can return deferred
# print('open') 开始爬取
pass
def close(self, reason): # can return a deferred
# print('close') 结束爬取
pass
def log(self, request, spider): # log that a request has been filtered
# print('log....') 打印日志
pass
执行顺序是 from_settings -> __init__ -> open -> request_seen -> close
自定义类还需要更改下配置文件:
DUPEFILTER_CLASS = "day96.duplication.RepeatFilter"
// 默认是 DUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter",即scrapy自带的去重
Pipeline深入分析
Pipeline除了process_item()
还有from_crawler,open_spider,close_spider
方法。
from_crawler
类似自定义去重类中的from_settings
,也是一个类方法。
class ScrapyLearnPipeline(object):
def __init__(self,conn_str):
"""
初始化数据
"""
self.conn_str = conn_str
@classmethod
def from_crawler(cls, crawler):
"""
初始化时调用,用于创建pipeline对象,得到配置文件
还可以进行数据库配置(数据库配置都是在settings.py里进行的)
"""
conn_str = crawler.settings.get('DB') # 注意配置文件中的变量都要大写
return cls(conn_str)
def open_spider(self,spider):
"""
爬虫开始执行时,调用,如果存储用的是文件/数据库等可以在里面打开文件,这样就实现了打开一次文件,写入完后再关闭文件
"""
self.conn = open(self.conn_str, 'a')
def close_spider(self,spider):
"""
爬虫关闭时,被调用
"""
self.conn.close()
def process_item(self, item, spider):
"""
每当数据需要持久化时,就会被调用
"""
# if spider.name == 'chouti'
tpl = "%s\n%s\n\n" %(item['title'],item['href'])
self.conn.write(tpl)
# 注意如果有多个方法,一定要return,这样才能交给下一个pipeline处理
return item
# 当不需要交个下一个pipeline处理时,请务必这样写(from scrapy.exceptions import DropItem)
# raise DropItem()
Cookie问题
当爬取的网站需要登录时,需要携带请求头,请求体,Cookie。
登录流程:
访问当前页面,拿到返回的GPSD,然后带着GPSD去登录,然后服务器就会将我们的GPSD授权,授权后GPSD就可以一直用了。具体操作看实例。
实例
自动给抽屉点赞
/spiders/chouti.py
import scrapy
from scrapy.selector import Selector, HtmlXPathSelector
from scrapy.http import Request
from scrapy.http.cookies import CookieJar
class ChoutiSpider(scrapy.Spider):
name = 'chouti'
# allowed_domains = ['chouti.com/']
start_urls = ['http://dig.chouti.com/']
# 存储cookie
cookie_dict = None
def parse(self, response):
# 由于有start_urls的存在,所以会自动访问一次页面
# 创建cookie对象,现在里面还什么都没有,只是一个容器
cookie_obj = CookieJar()
# extract_cookies()需要两个参数,response和request,而reponse里包含request,这一步得到授权的cookie
cookie_obj.extract_cookies(response, response.request)
# cookie_obj._cookies 从cookie对象中拿到cookie
cookie_dict = cookie_obj._cookies
yield Request(
# 发起登录请求
url="http://dig.chouti.com/login",
method="POST",
headers={'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
# 请求体写法:= 和 &
body="phone=86xxx&password=xxx&oneMonth=1",
cookies=self.cookie_dict,
callback=check_login
)
def check_login(self, response):
yield Request(url='http://dig.chouti.com/', callback=self.like)
# 点赞
def like(self, response):
# 拿到赞的id列表
id_list = Selector(response).xpath('//div[@share-linkid]/@share-linkid').extract()
for nid in id_list:
# 每个赞的url
url - 'http://dig.chouti.com/link/vote?linksId=%s'%(nid)
yield Request(
url=url,
method='POST',
# 点赞需要携带cookie
cookies=self.cookie_dict,
callback=self.success
)
# 查找下一页
page_urls = Selector(response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
for page in page_urls:
url = 'http://dig.chouti.com%s'%(page)
yield Request(url=url, callback=self.like)
def success(self, response):
print(response.text)
具体源码:猛戳这里