扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
讲师博客:https://www.cnblogs.com/wupeiqi/p/6229292.html
中文资料(有示例参考):http://www.scrapyd.cn/doc/
站在用户的角度思考问题,与客户深入沟通,找到托克逊网站设计与托克逊网站推广的解决方案,凭借多年的经验,让设计与互联网技术结合,创造个性化、用户体验好的作品,建站类型包括:做网站、网站建设、企业官网、英文网站、手机端网站、网站推广、域名申请、网络空间、企业邮箱。业务覆盖托克逊地区。
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。使用之前有一个类似django的创建项目以及目录结构的过程。
使用pip安装(windows会有问题):
pip3 install scrapy
装不上主要是因为依赖的模块Twisted安装不上,所以得先安装Twisted,并且不能用pip直接下载安装。先去下载Twisted的whl安装文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
然后使用pip本地安装:
pip install E:\Downloads\Twisted-18.9.0-cp36-cp36m-win_amd64.whl
pip install -i https://mirrors.aliyun.com/pypi/simple/ scrapy
pip install -i https://mirrors.aliyun.com/pypi/simple/ pywin32
Scrapy主要包括了以下组件:
工作流程:
绿线是数据流向,引擎是整个程序的入口。首先从初始 URL 开始(这步大概是引擎把初始URL加到调度器),Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,Spider 分析出来的结果有两种:一种是需要进一步抓取的链接,例如“下一页”的链接,这些东西会被传回 Scheduler ;另一种是需要保存的数据,它们则被送到 Item Pipeline 那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。
另外,引擎和其他3个组件直接有通道。在数据流动的通道里还可以安装各种中间件,进行必要的处理。
启动项目
打开终端进入想要存储 Scrapy 项目的目录,然后运行 scrapy startproject (project name)。创建一个项目:
> scrapy startproject PeppaScrapy
执行完成后,会生成如下的文件结构:
ProjectName/
├── ProjectName
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
文件说明
关于配置文件,需要的时候可以先去下面的地址查,版本不是最新的,不过是中文。
https://www.jianshu.com/p/df9c0d1e9087
创建爬虫应用
先切换到项目目录,在执行grnspider命令 scrapy genspider [-t template] (name) (domain) 。比如:
> cd PeppaScrapy
> scrapy genspider spider_lab lab.scrapyd.cn
效果就是在spiders目录下,创建了一个spider_lab.py的文件。这里没有用-t参数指定模板,就是用默认模板创建的。其实不用命令也行了,自己建空文件,然后自己写也是一样的。
可以使用-l参数,查看有哪些模板:
> scrapy genspider -l
Available templates:
basic
crawl
csvfeed
xmlfeed
然后再用-d参数,加上上面查到的模板名,查看模板的内容:
> scrapy genspider -d basic
# -*- coding: utf-8 -*-
import scrapy
class $classname(scrapy.Spider):
name = '$name'
allowed_domains = ['$domain']
start_urls = ['http://$domain/']
def parse(self, response):
pass
把之前的创建的应用的文件修改一下,简单完善一下parse方法:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'spider_lab'
allowed_domains = ['lab.scrapyd.cn']
start_urls = ['http://lab.scrapyd.cn/']
def parse(self, response):
print(response.url)
print(response.body.decode())
查看应用列表:
> scrapy list
spider_lab
运行单独爬虫应用,这里加上了--nolog参数,避免打印日志的干扰:
> scrapy crawl spider_lab --nolog
每次都去命令行打一遍命令也很麻烦,也是可以直接写python代码,执行python来启动的。把下面的代码加到引用文件的最后:
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
其实就是提供了在python里调用命令行执行命令的方法。之后,还可以写一个main.py放到项目根目录下,写上启动整个项目的命令。
有可能会遇到编码问题,不过我的windows没问题,如果遇到了,试一下下面的方法:
import io
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
Robots协议就是每个网站对于来到的爬虫所提出的要求。并非强制要求遵守的协议,只是一种建议。
默认scrapy遵守robot协议。我在爬 http://dig.chouti.com/ 的时候遇到了这个问题。把 --nolog 参数去掉,查看错误日志,有如下的信息:
[scrapy.core.engine] DEBUG: Crawled (200) (referer: None)
[scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt:
先去下载robots.txt文件,然后根据文件的建议,就禁止继续爬取了。可以直接浏览器输入连接查看文件内容:
User-agent: *
Allow: /link/
Disallow: /?
Disallow: /*?
Disallow: /user
Disallow: /link/*/comments
Disallow: /admin/login
# Sitemap files
Sitemap: https://dig.chouti.com/sitemap.xml
你要守规矩的的话,就只能爬 https://dig.chouti.com/link/xxxxxxxx
这样的url,一个帖子一个帖子爬下来。
如果可以选择不遵守协议,那么就在爬的时候把这个设置设为False。全局的设置在settings.py文件里:
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
也可以只对一个应用修改设置:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
custom_settings = {'ROBOTSTXT_OBEY': False}
def parse(self, response):
print(response.url)
print(response.encoding)
print(response.text)
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
上面踩坑的过程中,一度以为是请求头有问题,已定义请求头的方法也是设置settings.py文件,里面有一个剩下的默认配置:
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
#}
默认都注释掉了,你可以在这里为全局加上自定义的请求头,当然也可以只为单独的应用配置:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'test'
allowed_domains = ['chouti.cn']
start_urls = ['http://dig.chouti.com/']
# 这个网站会屏蔽User-Agent里包含python的请求
custom_settings = {'ROBOTSTXT_OBEY': False,
'DEFAULT_REQUEST_HEADERS': {'User-Agent': 'python'},
}
def parse(self, response):
print(response.request.headers) # 这个是请求头
print(response.headers) # 这个是响应头
if __name__ == '__main__':
from scrapy import cmdline
log_level = ''
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
使用xpaht选择器可以提取数据,另外还有CSS选择器也可以用。
XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。对 XPath 的理解是很多高级 XML 应用的基础。
解析页面内容会用到Selector这个类,下面只贴出parse回调函数里的代码:
from scrapy.selector import Selector
def parse(self, response):
title1 = response.xpath('//title')
print('title1', title1)
title2 = Selector(response).xpath('//title')
print('title2', title2)
上面的两种用法是一样的,通过response对象也可以直接调用xpath方法。这里说明了xpath方法是Selector这个类提供的。另外用方法二还有一个好处,就是因为之后需要调用Selector类里的方法,这样显示的声明Selector类之后,编辑器可以找到类似的方法,给出各种提示。直接用response调用,就没有这种便利了。
另外还有一个XmlXPathSelector类,作用和Selector类差不多,可能是就版本使用的类。
常用的表达式:
提取属性
提取属性的话,也是先定位到标签的范围,然后最后@属性名称,拿到所有对应的属性。另外@*可以拿到所有的属性。要当某个标签下的属性,就在标签名之后/@就好了:
Selector(response).xpath('//@href') # 提取所有的href属性
Selector(response).xpath('//ol[@class="page-navigator"]//@href') # ol.page-navigator下的所有的href属性
Selector(response).xpath('//head/meta/@*').extract() # head>meta 标签了所有的属性
Selector(response).xpath('//*[@id="body"]/div/@class') # id为body的标签的下一级标签里的class属性
查找标签,限定属性
使用这样的表达式:标签[@属性名='属性值'] ,另外还能用not(),注意要用小括号把取反的内容包起来:
Selector(response).xpath('//div[@id="body"]//span[@class="text"]') # 只要 span.text 的span标签
Selector(response).xpath('//div[@id="body"]//span[not(@class="text")]') # 没有text这个class的span标签
Selector(response).xpath('//meta[@name]') # 有name属性的meta
Selector(response).xpath('//meta[not(@name)]') # 没有name属性meta
提取值
xpath方法返回的是个对象,这个对象还可以无限次的再调用xpath方法。拿到最终的对象之后,我们需要获取值,这里有 extract() 和 extract_first() 这两个方法。因为查找的结果可能是多个值,extract方法返回列表,而extract_first方法直接返回值,但是是列表是第一个元素的值。
提取文字
表达式:/text() 可以把文字提取出来:
def parse(self, response):
tags = Selector(response).xpath('//ul[@class="tags-list"]//a/text()').extract()
print(tags) # 这样打印效果不是很好
for tag in tags:
print(tag.strip())
还有个方法,可以提取整段文字拼到一起。表达式:string() :
Selector(response).xpath('string(//ul[@class="tags-list"]//a)').extract() # 这样没拿全
Selector(response).xpath('string(//ul[@class="tags-list"])').extract() # 这样才拿全了
上面第一次没拿全,某个a标签下的文字就是一段。string()表达式看来值接收一个值,如果传的是个列表,可能就只操作第一个元素。
在我们商品详情、小说内容的时候可能会比较好用。
匹配class的问题
xpath中没有提供对class的原生查找方法。因为class里是可以包含多个值的。比如下面的这个标签:
Test
下面的表达式是无法匹配到的:
response.xpath('//div[@class="test"]')
要匹配到,你得写死:
response.xpath('//div[@class="test main"]')
但是这样显然是不能接受的,如果还有其他test但是没出main的标签就匹配不上了。
contains 函数 (XPath),检查第一个参数字符串是否包含第二个参数字符串。用这个函数就能做好了
response.xpath('//div[contains(@class, "test")]')
这样又有新问题了,如果有别的class名字比如:test1、mytest,这种也都会被上面的方法匹配上。
concat 函数 (XPath),返回参数的串联。就是字符串拼接,contains的两个参数的两边都加上空格,就能解决上面的问题。之所以要引入concat函数时因为,后面的字符串可以手动在两边加上空格,但是@class是变量,这个也不能用加号,就要用这个函数做拼接:
response.xpath('//div[contains(concat(" ", @class, " "), " test ")]')
normalize-space 函数 (XPath),返回去掉了前导、尾随和重复的空白的参数字符串。上面已经没问题了。不过还不够完美。在拼接@class之前,先把两边可能会出现的其他空白字符给去掉,可能会有某些操作需要改变一下class,但是又不要对这个class有任何实际的影响。总之这个是最终的解决方案:
response.xpath('//div[contains(concat(" ", normalize-space(@class), " "), " test ")]')
这里已经引出了好几个函数了,还有更多别的函数,需要的时候再查吧。
正则匹配
xpath也是可以用正则匹配的,用法很简单 re:test(x, y)
。第一个参数用@属性比较多,否则就是正则匹配标签了,就和纯的正则匹配似乎没什么差别了。
Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')
https://www.cnblogs.com/tina-cherish/p/7127812.html
xpath很强大,但是不支持原生的class,不过上面已经给了比较严谨的解决方案了。
css有部分功能无法实现。比如不能向上找,只能匹配当前层级,要通过判断子元素来确定当前元素是否匹配就不行。这种情况使用xpath的话,中括号里可以在嵌套中括号的。
不过css感觉更直观,也已经没什么学习成本了。
登录抽屉并点赞。边一步一步实现,边补充用到的知识点。
import scrapy
from scrapy.selector import Selector
class SpiderLabSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
custom_settings = {'ROBOTSTXT_OBEY': False}
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
这里爬取的只是首页的内容
现在要获取所有分页的url,然后继续爬取。下面就是在parse回调函数后面增加了一点代码是做好了。不过现在的代码还不完善,会无休止的爬取下去,先不要运行,之后还要再改:
import urllib.parse
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
# 不找下一页,而是找全部的页,这样会有去重的问题,就是要这个效果
pages = Selector(response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
print(pages)
url_parse = urllib.parse.urlparse(response.url)
for page in pages:
url = "%s://%s%s" % (url_parse.scheme, url_parse.hostname, page)
yield scrapy.Request(url=url)
这里做的事情就是当从前也分析了分页的信息,把分页信息生成新的url,然后再给调度器继续爬取。
这里用的 scrapy.Request()
,实际上是应该要通过 from scrapy.http import Request
导入再用的。不过这里并不需要导入,并且只能能在scrapy下调用。因为在 scrapy/__init__.py 里有导入这个模块了。并且这里已经不是系统第一次调用这个类了,程序启动的时候,其实就是跑了下面的代码把 start_urls 的地址开始爬取网页了:
for url in self.start_urls:
yield Request(url, dont_filter=True)
这段代码就是在当前类的父类 scrapy.Spider 里的 start_requests 方法里面。
爬取深度,允许抓取任何网站的最大深度。如果为零,则不施加限制。
这个是可以在配置文件里设置的。默认的配置里没有写这条,并且默认值是0,就是爬取深度没有限制。所以就会永不停止的爬取下去。实际上不会无休止,似乎默认就有去重的功能,爬过的页面不会重复爬取。所以不设置爬取深度,就能把所有的页面都爬下来了
这里要讲的是爬取深度的设置,所以和其他设置一样,可以全局的在settings.py里设置。也可以现在类的公用属性 custom_settings 这个字典里:
custom_settings = {
'ROBOTSTXT_OBEY': False,
'DEPTH_LIMIT': 1,
}
这个深度可以在返回的response参数里找到,在meta这个字典里:response.meta['depth']
默认有下面2条配置:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
DUPEFILTER_DEBUG = False
去重的功能默认就是在 'scrapy.dupefilters.RFPDupeFilter' 这个类里做的。这个类有个父类 BaseDupeFilter 帮我们定义好了接口,我们可以写一个自己的类自定义去重规则,继承 BaseDupeFilter 实现里面的方法:
from scrapy.dupefilters import BaseDupeFilter
class MyFilter(BaseDupeFilter):
def __init__(self):
# 去重可以用上集合
# 在request_seen方法里判断这个set,操作这个set
self.visited_url = set()
@classmethod
def from_settings(cls, settings):
"""初始化时调用的方法
返回一个实例,作用就是可以调用配置的信息生成实例
实例化时使用:obj = MyFilter.from_settings()
所以不要这样实例化:obj = MyFilter()
什么都不写,上面两重方法生成的实例是一样的
"""
return cls()
def request_seen(self, request):
"""过滤规则
检测当前请求是否需要过滤(去重)
返回True表示需要过滤,返回False表示不用过滤
"""
return False
def open(self): # can return deferred
"""开始爬虫时,调用一次
比如要记录到文件的,在这里检查和重建记录文件
"""
pass
def close(self, reason): # can return a deferred
"""结束爬虫时,调用一次
这里可以把之前的记录文件close掉
"""
pass
def log(self, request, spider): # log that a request has been filtered
"""日志消息的记录或打印可以写在这里"""
pass
现在知道了,默认就是有去重规则的。所以上面爬取所有页面的代码并并不会无休止的执行下去,而是可以把所有页面都爬完的。
程序启动后,首先会调用父类 scrapy.Spider 里的 start_requests 方法。我们也可以不设置 start_urls 属性,然后自己重构 start_requests 方法。启动的效果是一样的:
# start_urls = ['http://lab.scrapyd.cn/']
def start_requests(self):
urls = ['http://lab.scrapyd.cn/']
for url in urls:
yield scrapy.Request(url=url, dont_filter=True)
另外就是这个 scrapy.Request 类,回调函数 parse 方法最后也是调用这个方法类。这里还有一个重要的参数 callback 。默认不设置时 callback=parse
,所以可以手动设置callback参数,使用别的回调函数。或者准备多个回调函数,每次调度的时候设置不同额callback。比如第一次用默认的,之后在 parse 方法里再调用的时候,设置 callback=func
使用另外的回调函数。
默认就是开启Cookie的,所以其实我们并不需要操作什么。
配置的 COOKIES_ENABLED 选项一旦关闭,则不会有Cookie了,别处再怎么设置也没用。
可以用meta参数,为请求单独设置cookie:
yield scrapy.Request(url, self.login, meta={'cookiejar': True})
不过如果要为请求单独设置的话,就得为每个请求都显示的声明。否则不写,就是认为是不要cookie。meta可以有如下设置:
meta={'cookiejar': True} # 使用Cookie
meta={'cookiejar': False} # 不使用Cookie,也就写在第一个请求里。之后的请求不设置就是不使用Cookie
meta={'cookiejar': response.meta['cookiejar']} # 使用上一次的cookie,上一次必须是True或者这个,否则会有问题
手动设置cookie值
Request 实例化的时候有 cookies 参数,直接传字典进去就可以了。
获取cookie的值
并没有cookie这个专门的属性。本质上cookie就是headers里的一个键值对,用下面的方法去headers里获取:
response.request.headers.getlist('Cookie') # 请求的Cookie
response.headers.getlist('Set-Cookie') # 响应的Cookie
最后就是综合应用了。登录需要Cookies的操作。不过其实什么都不做就可以了,默认方法就能把Cookies操作好。
然后就是从打开页面、完成登录、到最后点赞,需要发多次的请求,然后每次请求返回后所需要做的操作也是不一样的,这里就需要准备多个回调函数,并且再发起请求的时候指定回调函数。代码如下:
import scrapy
from scrapy.selector import Selector
from utils.base64p import b64decode_str # 自己写的从文件读密码的方法,不是重点
class SpiderLabSpider(scrapy.Spider):
name = 'chouti_favor'
custom_settings = {
'ROBOTSTXT_OBEY': False,
}
def start_requests(self):
url = 'http://dig.chouti.com/'
yield scrapy.Request(url, self.login)
def login(self, response):
# 避免把密码公开出来,去文件里拿,并且做了转码,这不是这里的重点
with open('../../utils/password') as f:
auth = f.read()
auth = auth.split('\n')
post_dict = {
'phone': '86%s' % auth[0], # 从请求正文里发现,会在手机号前加上86
'password': b64decode_str(auth[1]), # 直接填明文的用户名和密码也行的
}
yield scrapy.FormRequest(
url='http://dig.chouti.com/login',
formdata=post_dict,
callback=self.check_login,
)
def check_login(self, response):
print(response.request.headers.getlist('Cookie'))
print(response.headers.getlist('Set-Cookie'))
print(response.text)
yield scrapy.Request(
url='http://dig.chouti.com/',
dont_filter=True, # 这页之前爬过了,如果不关掉过滤,就不会再爬了
)
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
do_favor = True
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
# 点赞,做个判断,只赞第一条
if do_favor:
do_favor = False
linkid = item.xpath('./div[@class="news-content"]/div[@share-linkid]/@share-linkid').extract_first()
yield scrapy.Request(
url='https://dig.chouti.com/link/vote?linksId=%s' % linkid,
method='POST',
callback=self.favor,
)
def favor(self, response):
print("点赞", response.text)
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
注意:首页的地址 http://dig.chouti.com 一共访问了两次。第二次如果不把 dont_filter 设为True,关闭过滤,就不会再去爬了。当然也可以第一次爬完之后,就保存在变量里,等登录后再从这个返回开始之后的处理。
上面的POST请求,用到了 FormRequest 这个类。这个类继承的是 Request 。里面主要就是把字典拼接成请求体,设置一下请求头的 Content-Type ,默认再帮我们把 method 设为 POST 。也是可以继续用 Request 的,就是把上面的3个步骤自己做了。主要是请求体,大概是按下面这样拼接一下传给body参数:
body='phone=86151xxxxxxxx&password=123456&oneMonth=1',
之前只是简单的处理,所以在parse方法中直接处理。对于想要获取更多的数据处理,则可以利用Scrapy的items将数据格式化,然后统一交由pipelines来处理。
回顾一下 Scrapy 组件和工作流程,项目管道(Pipeline) 组件负责这个工作。
先要编辑一下 items.py 里的类,默认会帮我们生成一个类,并有简单的注释。必须要处理2个数据 title 和 href ,则改写 items.py 如下:
import scrapy
class PeppascrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
href = scrapy.Field()
然后去修改之前的 parse 方法,导入上面的类,把要处理的数据传递进去生成实例,然后 yield :
from PeppaScrapy.items import PeppascrapyItem
class SpiderLabSpider(scrapy.Spider):
name = 'spider_lab'
allowed_domains = ['lab.scrapyd.cn']
start_urls = ['http://lab.scrapyd.cn/']
def parse(self, response):
print(response.url)
items = Selector(response).xpath(
'//div[@id="body"]//div[@id="main"]/div[@class="quote post"]')
for item in items:
title = item.xpath('./span[@class="text"]/text()').extract_first()
href = item.xpath('./span/a/@href').extract_first()
yield PeppascrapyItem(title=title, href=href)
上面这段代码,只需要注意最后3行。把要保存的数据用items.py里的类实例化后,yield返回。
回顾下流程,之前yield返回给 scrapy.Request ,就是把数据返回给调度器继续继续爬取
这里yield返回给 scrapy.Item ,就是 Item Pipeline 里的 Item 进入数据的处理。
在 Item 里只是把数据传递出来,数据的处理则在 Pipeline 里。
如果有多处数据要返回,则可以自定义多个 scrapy.Item 类,来做数据的格式化处理。
还有一个 pipelines.py 文件,默认里面只有一个 return ,但是传入2个参数 item 和 spider,先打印看看:
class PeppascrapyPipeline(object):
def process_item(self, item, spider):
print(item)
print(spider)
return item
只是编写处理方法还不够,这个方法需要注册。在settings.py文件里,默认写好了注册的方法,只需要把注释去掉。ITEM_PIPELINES 的 key 就是要注册的方法,而 value 则是优先级。理论上字典没有顺序,优先级小的方法先执行:
ITEM_PIPELINES = {
'PeppaScrapy.pipelines.PeppascrapyPipeline': 300,
}
最后返回的item是个字典,我们报错的变量名是key,值就是value。而spider则是这个爬虫 scrapy.Spider 对象。
执行多个操作
这里一个类就是执行一个操作,如果对返回的数据要有多次操作,也可以多定义几个类,然后注册上即可。
每次操作的item,就是上一次操作最后 return item 传递下来的。第一次操作的item则是从 scrapy.Item 传过来的。所以也可以对item进行处理,然后之后的操作就是在上一次操作对item的修改之上进行的。所以也可以想return什么就return什么,就是给下一个操作处理的数据。
绑定特定的爬虫
Pipline并没有和特定的爬虫进行绑定,也就是所有的爬虫都会依次执行所有的Pipline。对于特定爬虫要做得特定的操作,可以在process_item方法里通过参数spider的spider.name进行判断。
接着讲上面的执行多个操作。如果在某个地方要终止之后所有的操作,则可以用 DropItem 。用法如下:
from scrapy.exceptions import DropItem
class PeppascrapyPipeline(object):
def process_item(self, item, spider):
print(item)
raise DropItem()
这样对这组数据的操作就终止了。一般应该把这句放在某个条件的分支里。
Pipeline 这个类里,还可以定义更多方法。除了上面的处理方法,还有另外3个方法,其中一个是类方法。所有的方法名都不能修改,具体如下:
class PeppascrapyPipeline(object):
def __init__(self, value):
self.value = value
def process_item(self, item, spider):
"""操作并进行持久化"""
print(item)
# 表示将item丢弃,不会被后续pipeline处理
raise DropItem()
# print(spider)
# return item 给后续的pipeline继续处理
# return item
@classmethod
def from_crawler(cls, crawler):
"""初始化时候,用于创建pipeline对象"""
val = crawler.settings.get('BOT_NAME')
# getint 方法可以直接获取 int 参数
# val = crawler.settings.getint('DEPTH_LIMIT')
return cls(val)
def open_spider(self,spider):
"""爬虫开始执行时,调用"""
print('START')
def close_spider(self,spider):
"""爬虫关闭时,被调用"""
print('OVER')
类方法 from_crawler 是用于创建pipeline对象的。主要是接收了crawler参数,可以获取到settings里的参数然后传给构造方法。比如这里获取了settings.py里的值传给了对象。
另外2个方法 open_spider 和 close_spider ,是在爬虫开始和关闭时执行的。即使爬虫有多次返回,处理方法要调用多次,但是这2个方法都只会调用一次。这2个方法是在爬虫 scrapy.Spider 开始和关闭的时候各执行一次的。而不是第一次返回数据处理和最后一次数据处理完毕。
打开文件的操作
以写入文件为例,写入一段数据需要3步:打开文件,写入,关闭文件。如果把这3不都写在 process_item 方法里,则会有多次的打开和关闭操作。正确的做法是,打开文件在 open_spider 方法里执行,写入还是在 process_item 方法里每次返回都可以写入,最后在 close_spider 方法里关闭文件。
默认有一个 middlewares.py 文件,里面默认创建了2个类,分别是爬虫中间件和下载中间件
class PeppascrapySpiderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
"""下载完成,执行,然后交给parse处理"""
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
"""spider处理完成,返回时调用
返回Request或者Item(字典也行,Item本身也是个字典)
Request就是给调度器继续处理
Item就是给项目管道保存
"""
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, dict or Item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
"""异常调用"""
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Response, dict
# or Item objects.
pass
def process_start_requests(self, start_requests, spider):
"""爬虫启动时调用"""
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
爬虫中间件这里要注意下 process_spider_output() 返回的内容之后是要交给调度器继续爬取的,或者是交给项目管道做保存操作。所以返回的可以是 Request 或者是 Item 。
class PeppascrapyDownloaderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
"""请求需要被下载时,经过所有下载器中间件的process_request调用"""
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
def process_response(self, request, response, spider):
"""spider处理完成,返回时调用"""
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
def process_exception(self, request, exception, spider):
"""异常处理
当下载处理器(download handler)
或 process_request() (下载中间件)抛出异常时执行
"""
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
process_request方法
对不同的返回值,回有不同的效果:
一般返回None,继续后面的中间件或者下载。这里可以修改一下请求头信息。比如,在请求头里添加代理的设置,然后再让后续的操作来执行。
返回Response,下载器就是要去下载生成Response。这里直接返回Response就相当于已经下载完成了。所以之后不再是执行下载了,而是返回给中间件里的process_response方法,执行下载完成后的操作。比如,可以不用默认的下载器来下载。到这里自己用Request模块写段代码去下载,然后创建一个scrap.http.Eesponse对象,把内容填进去返回。
返回Request,调度器就是生成一个个的Request,然后调度执行。如果这里返回了Request,就会停止这次的执行,把Request放回调度器,等待下一次被调度执行。在process_response方法里返回Request也是一样的效果,只是这里是在下载前要重新调度,那个是在下载后。
自定制命令
一、在spiders同级创建任意目录,如:commands
二、在目录里创建 crawlall.py 文件,名字任意取,这个文件名将来就是执行这段代码的命令
下面是一个启动spiders里所有爬虫的代码:
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings
class Command(ScrapyCommand):
requires_project = True
def syntax(self):
return '[options]'
def short_desc(self):
return 'Runs all of the spiders'
def run(self, args, opts):
spider_list = self.crawler_process.spiders.list()
for name in spider_list:
self.crawler_process.crawl(name, **opts.__dict__)
self.crawler_process.start()
三、在 settings.py 中添加配置 COMMANDS_MODULE = '项目名称.目录名称' ,比如:
COMMANDS_MODULE = "PeppaScrapy.commands"
四、执行命令: scrapy crawlall
利用信号在指定位置注册制定操作。
自定义的型号要写在写一类,然后在settings里注册。默认的配置文件里是有EXTENSIONS的,注释掉了,这里就放开注释然后改一下:
# Enable or disable extensions
# See https://doc.scrapy.org/en/latest/topics/extensions.html
EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
'PeppaScrapy.extensions.MyExtension': 100
}
根据上面的操作,就是创建 extensions.py 文件,然后写一个 MyExtension 的类:
# PeppaScrapy/extensions.py 文件
from scrapy import signals
class MyExtension(object):
def __init__(self, value):
self.value = value
@classmethod
def from_crawler(cls, crawler):
val = crawler.settings.get('BOT_NAME')
ext = cls(val)
# 注册你的方法和信息
crawler.signals.connect(ext.spider_start, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_stop, signal=signals.spider_closed)
return ext
# 写你要执行的方法
def spider_start(self, spider):
print('open')
def spider_stop(self, spider):
print('close')
所有的信号
上面的例子里用到了 spider_opened 和 spider_closed 这2个信号。
在 scrapy/signals.py 里可以查到所有的信号:
engine_started = object()
engine_stopped = object()
spider_opened = object()
spider_idle = object()
spider_closed = object()
spider_error = object()
request_scheduled = object()
request_dropped = object()
response_received = object()
response_downloaded = object()
item_scraped = object()
item_dropped = object()
# -*- coding: utf-8 -*-
# Scrapy settings for step8_king project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
# 1. 爬虫名称
BOT_NAME = 'step8_king'
# 2. 爬虫应用路径
SPIDER_MODULES = ['step8_king.spiders']
NEWSPIDER_MODULE = 'step8_king.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 3. 客户端 user-agent请求头
# USER_AGENT = 'step8_king (+http://www.yourdomain.com)'
# Obey robots.txt rules
# 4. 禁止爬虫配置
# ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 5. 并发请求数
# CONCURRENT_REQUESTS = 4
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 6. 延迟下载秒数
# DOWNLOAD_DELAY = 2
# The download delay setting will honor only one of:
# 7. 单域名访问并发数,并且延迟下次秒数也应用在每个域名
# CONCURRENT_REQUESTS_PER_DOMAIN = 2
# 单IP访问并发数,如果有值则忽略:CONCURRENT_REQUESTS_PER_DOMAIN,并且延迟下次秒数也应用在每个IP
# CONCURRENT_REQUESTS_PER_IP = 3
# Disable cookies (enabled by default)
# 8. 是否支持cookie,cookiejar进行操作cookie
# COOKIES_ENABLED = True
# COOKIES_DEBUG = True
# Disable Telnet Console (enabled by default)
# 9. Telnet用于查看当前爬虫的信息,操作爬虫等...
# 使用telnet ip port ,然后通过命令操作
# TELNETCONSOLE_ENABLED = True
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
# 10. 默认请求头
# Override the default request headers:
# DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# }
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
# 11. 定义pipeline处理请求
# ITEM_PIPELINES = {
# 'step8_king.pipelines.JsonPipeline': 700,
# 'step8_king.pipelines.FilePipeline': 500,
# }
# 12. 自定义扩展,基于信号进行调用
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
# EXTENSIONS = {
# # 'step8_king.extensions.MyExtension': 500,
# }
# 13. 爬虫允许的最大深度,可以通过meta查看当前深度;0表示无深度
# DEPTH_LIMIT = 3
# 14. 爬取时,0表示深度优先Lifo(默认);1表示广度优先FiFo
# 后进先出,深度优先
# DEPTH_PRIORITY = 0
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
# 先进先出,广度优先
# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'
# 15. 调度器队列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler'
# from scrapy.core.scheduler import Scheduler
# 16. 访问URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'
# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
"""
17. 自动限速算法
from scrapy.contrib.throttle import AutoThrottle
自动限速设置
1. 获取最小延迟 DOWNLOAD_DELAY
2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间
5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY
target_delay = latency / self.target_concurrency
new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延迟时间
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay
"""
# 开始自动限速
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# 初始下载延迟
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# 最大下载延迟
# AUTOTHROTTLE_MAX_DELAY = 10
# The average number of requests Scrapy should be sending in parallel to each remote server
# 平均每秒并发数
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# 是否显示
# AUTOTHROTTLE_DEBUG = True
# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
"""
18. 启用缓存
目的用于将已经发送的请求或相应缓存下来,以便以后使用
from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否启用缓存策略
# HTTPCACHE_ENABLED = True
# 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
# 缓存超时时间
# HTTPCACHE_EXPIRATION_SECS = 0
# 缓存保存路径
# HTTPCACHE_DIR = 'httpcache'
# 缓存忽略的Http状态码
# HTTPCACHE_IGNORE_HTTP_CODES = []
# 缓存存储的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
"""
19. 代理,需要在环境变量中设置
from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware
方式一:使用默认
os.environ
{
http_proxy:http://root:woshiniba@192.168.11.11:9999/
https_proxy:http://192.168.11.11:9999/
}
方式二:使用自定义下载中间件
def to_bytes(text, encoding=None, errors='strict'):
if isinstance(text, bytes):
return text
if not isinstance(text, six.string_types):
raise TypeError('to_bytes must receive a unicode, str or bytes '
'object, got %s' % type(text).__name__)
if encoding is None:
encoding = 'utf-8'
return text.encode(encoding, errors)
class ProxyMiddleware(object):
def process_request(self, request, spider):
PROXIES = [
{'ip_port': '111.11.228.75:80', 'user_pass': ''},
{'ip_port': '120.198.243.22:80', 'user_pass': ''},
{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
{'ip_port': '101.71.27.120:80', 'user_pass': ''},
{'ip_port': '122.96.59.104:80', 'user_pass': ''},
{'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]
proxy = random.choice(PROXIES)
if proxy['user_pass'] is not None:
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
print "**************ProxyMiddleware have pass************" + proxy['ip_port']
else:
print "**************ProxyMiddleware no pass************" + proxy['ip_port']
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}
"""
"""
20. Https访问
Https访问时有两种情况:
1. 要爬取网站使用的可信任证书(默认支持)
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"
2. 要爬取网站使用的自定义证书
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory"
# https.py
from scrapy.core.downloader.contextfactory import ScrapyClientContextFactory
from twisted.internet.ssl import (optionsForClientTLS, CertificateOptions, PrivateCertificate)
class MySSLFactory(ScrapyClientContextFactory):
def getCertificateOptions(self):
from OpenSSL import crypto
v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
return CertificateOptions(
privateKey=v1, # pKey对象
certificate=v2, # X509对象
verify=False,
method=getattr(self, 'method', getattr(self, '_ssl_method', None))
)
其他:
相关类
scrapy.core.downloader.handlers.http.HttpDownloadHandler
scrapy.core.downloader.webclient.ScrapyHTTPClientFactory
scrapy.core.downloader.contextfactory.ScrapyClientContextFactory
相关配置
DOWNLOADER_HTTPCLIENTFACTORY
DOWNLOADER_CLIENTCONTEXTFACTORY
"""
"""
21. 爬虫中间件
class SpiderMiddleware(object):
def process_spider_input(self,response, spider):
'''
下载完成,执行,然后交给parse处理
:param response:
:param spider:
:return:
'''
pass
def process_spider_output(self,response, result, spider):
'''
spider处理完成,返回时调用
:param response:
:param result:
:param spider:
:return: 必须返回包含 Request 或 Item 对象的可迭代对象(iterable)
'''
return result
def process_spider_exception(self,response, exception, spider):
'''
异常调用
:param response:
:param exception:
:param spider:
:return: None,继续交给后续中间件处理异常;含 Response 或 Item 的可迭代对象(iterable),交给调度器或pipeline
'''
return None
def process_start_requests(self,start_requests, spider):
'''
爬虫启动时调用
:param start_requests:
:param spider:
:return: 包含 Request 对象的可迭代对象
'''
return start_requests
内置爬虫中间件:
'scrapy.contrib.spidermiddleware.httperror.HttpErrorMiddleware': 50,
'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': 500,
'scrapy.contrib.spidermiddleware.referer.RefererMiddleware': 700,
'scrapy.contrib.spidermiddleware.urllength.UrlLengthMiddleware': 800,
'scrapy.contrib.spidermiddleware.depth.DepthMiddleware': 900,
"""
# from scrapy.contrib.spidermiddleware.referer import RefererMiddleware
# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
# 'step8_king.middlewares.SpiderMiddleware': 543,
}
"""
22. 下载中间件
class DownMiddleware1(object):
def process_request(self, request, spider):
'''
请求需要被下载时,经过所有下载器中间件的process_request调用
:param request:
:param spider:
:return:
None,继续后续中间件去下载;
Response对象,停止process_request的执行,开始执行process_response
Request对象,停止中间件的执行,将Request重新调度器
raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
'''
pass
def process_response(self, request, response, spider):
'''
spider处理完成,返回时调用
:param response:
:param result:
:param spider:
:return:
Response 对象:转交给其他中间件process_response
Request 对象:停止中间件,request会被重新调度下载
raise IgnoreRequest 异常:调用Request.errback
'''
print('response1')
return response
def process_exception(self, request, exception, spider):
'''
当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
:param response:
:param exception:
:param spider:
:return:
None:继续交给后续中间件处理异常;
Response对象:停止后续process_exception方法
Request对象:停止中间件,request将会被重新调用下载
'''
return None
默认下载中间件
{
'scrapy.contrib.downloadermiddleware.robotstxt.RobotsTxtMiddleware': 100,
'scrapy.contrib.downloadermiddleware.httpauth.HttpAuthMiddleware': 300,
'scrapy.contrib.downloadermiddleware.downloadtimeout.DownloadTimeoutMiddleware': 350,
'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': 400,
'scrapy.contrib.downloadermiddleware.retry.RetryMiddleware': 500,
'scrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware': 550,
'scrapy.contrib.downloadermiddleware.redirect.MetaRefreshMiddleware': 580,
'scrapy.contrib.downloadermiddleware.httpcompression.HttpCompressionMiddleware': 590,
'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600,
'scrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware': 700,
'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 750,
'scrapy.contrib.downloadermiddleware.chunked.ChunkedTransferMiddleware': 830,
'scrapy.contrib.downloadermiddleware.stats.DownloaderStats': 850,
'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900,
}
"""
# from scrapy.contrib.downloadermiddleware.httpauth import HttpAuthMiddleware
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
# 'step8_king.middlewares.DownMiddleware1': 100,
# 'step8_king.middlewares.DownMiddleware2': 500,
# }
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流