本文共 39229 字,大约阅读时间需要 130 分钟。
讲师博客:
中文资料(有示例参考):Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。使用之前有一个类似django的创建项目以及目录结构的过程。
使用pip安装(windows会有问题):
pip3 install scrapy
装不上主要是因为依赖的模块Twisted安装不上,所以得先安装Twisted,并且不能用pip直接下载安装。先去下载Twisted的whl安装文件:
然后使用pip本地安装:pip install E:\Downloads\Twisted-18.9.0-cp36-cp36m-win_amd64.whlpip install -i https://mirrors.aliyun.com/pypi/simple/ scrapypip 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
文件说明
关于配置文件,需要的时候可以先去下面的地址查,版本不是最新的,不过是中文。
创建爬虫应用
先切换到项目目录,在执行grnspider命令 scrapy genspider [-t template] (name) (domain) 。比如:> cd PeppaScrapy> scrapy genspider spider_lab lab.scrapyd.cn
效果就是在spiders目录下,创建了一个spider_lab.py的文件。这里没有用-t参数指定模板,就是用默认模板创建的。其实不用命令也行了,自己建空文件,然后自己写也是一样的。
可以使用-l参数,查看有哪些模板:> scrapy genspider -lAvailable templates: basic crawl csvfeed xmlfeed
然后再用-d参数,加上上面查到的模板名,查看模板的内容:
> scrapy genspider -d basic# -*- coding: utf-8 -*-import scrapyclass $classname(scrapy.Spider): name = '$name' allowed_domains = ['$domain'] start_urls = ['http://$domain/'] def parse(self, response): pass
把之前的创建的应用的文件修改一下,简单完善一下parse方法:
import scrapyclass 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 listspider_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 ioimport syssys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
Robots协议就是每个网站对于来到的爬虫所提出的要求。并非强制要求遵守的协议,只是一种建议。
默认scrapy遵守robot协议。我在爬 的时候遇到了这个问题。把 --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: /userDisallow: /link/*/commentsDisallow: /admin/login# Sitemap filesSitemap: https://dig.chouti.com/sitemap.xml
你要守规矩的的话,就只能爬 https://dig.chouti.com/link/xxxxxxxx
这样的url,一个帖子一个帖子爬下来。
# Obey robots.txt rulesROBOTSTXT_OBEY = True
也可以只对一个应用修改设置:
import scrapyclass 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 scrapyclass 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属性的metaSelector(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+")]')
登录抽屉并点赞。边一步一步实现,边补充用到的知识点。
import scrapyfrom scrapy.selector import Selectorclass 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 BaseDupeFilterclass 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} # 使用Cookiemeta={'cookiejar': False} # 不使用Cookie,也就写在第一个请求里。之后的请求不设置就是不使用Cookiemeta={'cookiejar': response.meta['cookiejar']} # 使用上一次的cookie,上一次必须是True或者这个,否则会有问题
手动设置cookie值
Request 实例化的时候有 cookies 参数,直接传字典进去就可以了。获取cookie的值
并没有cookie这个专门的属性。本质上cookie就是headers里的一个键值对,用下面的方法去headers里获取:response.request.headers.getlist('Cookie') # 请求的Cookieresponse.headers.getlist('Set-Cookie') # 响应的Cookie
最后就是综合应用了。登录需要Cookies的操作。不过其实什么都不做就可以了,默认方法就能把Cookies操作好。
然后就是从打开页面、完成登录、到最后点赞,需要发多次的请求,然后每次请求返回后所需要做的操作也是不一样的,这里就需要准备多个回调函数,并且再发起请求的时候指定回调函数。代码如下:import scrapyfrom scrapy.selector import Selectorfrom 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())
注意:首页的地址 一共访问了两次。第二次如果不把 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 scrapyclass 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 PeppascrapyItemclass 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 DropItemclass 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 ScrapyCommandfrom scrapy.utils.project import get_project_settingsclass 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.htmlEXTENSIONS = { # 'scrapy.extensions.telnet.TelnetConsole': None, 'PeppaScrapy.extensions.MyExtension': 100}
根据上面的操作,就是创建 extensions.py 文件,然后写一个 MyExtension 的类:
# PeppaScrapy/extensions.py 文件from scrapy import signalsclass 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.htmlSPIDER_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,# }
转载于:https://blog.51cto.com/steed/2315113