Scrapy框架 什么是scrapy Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,我们只需要实现少量的代码,就能够快速的抓取。
Scrapy使用了Twisted异步网络框架,可以加快我们的下载速度。(Twisted(中文翻译)扭曲)
入门文档:https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html
异步和非阻塞的区别.png
补充: 阻塞和非阻塞指的是拿到结果之前的状态 异步和同步指的是整个过程
功能
解释
scrapy
Scrapy Engine(引擎)
总指挥:负责数据和信号在不同模块的传递
scrapy已经实现
Scheduler(调度器)
一个队列,存放引擎发过来的request请求
scrapy已经实现
Downloader(下载器)
下载把引擎发过来的requests请求,并返回引擎
scrapy已经实现
Spider(爬虫)
处理引擎发来的response,提取数据,提取url,并交给引擎
需要手写
Item Pipeline(管道)
处理引擎传过来的数据,比如存储
需要手写
Downloader Middlewares(下载中间件)
可以自定义的下载扩展,比如设置代理
一般不用手写
Spider MiddlewaresSpider(中间件)
可以自定义requests请求和进行response过滤
一般不用手写
文件目录 1 2 3 4 5 6 7 scrapy.cfg items.py middlewares pipelines.py settings.py spiders itcast.py
爬虫步骤:
1.创建一个scrapy项目
1 scrapy startproject mySpider
2.生成一个爬虫
1 scrapy genspider itcast itcast .cn #itcast 是爬虫名字,"itcast .cn "限制爬虫地址,防止爬到其他网站
3.提取数据
3.保存数据
启动爬虫
启动爬虫不打印日志
1 scrapy crawl 爬虫名字 --nolog
run.py启动爬虫
1 2 from scrapy import cmdline cmdline.execute("scrapy crawl lagou" .split ())
Scrapy运行流程
spider内容 spider翻页 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import scrapyfrom tencent.items import TencentItemclass ItcastSpider (scrapy.Spider ): name = 'itcast' allowed_domains = ['tencent.com' ] start_urls = ['https://hr.tencent.com/position.php' ] def parse (self, response ): li_list = response.xpath("//table[@class=" tablelist"]/tr" )[1 :-1 ] for li in li_list: item = TencentItem() item["name" ] = li.xpath(".//h3/text()" ).extract_first() item["title" ] = li.xpath(".//h4/text()" ).extract_first() yield item next_url = response.xpath('//a[@id="next"]/@href' ).extract_first() if next_url != "javascript:;" : next_url = "https://hr.tencent.com/" + next_url yield scrapy.Request(next_url,callback=self.parse)
提取数据 response.xpath(‘//a[@id=”next”]/@href’)
body = response.text.replace(‘\n’, ‘’).replace(‘\r’, ‘’).replace(‘\t’, ‘’) re.findall(‘<a title=”.?” href=”(. ?)”‘, body)
从选择器中提取字符串:
extract() 返回一个包含有字符串数据的列表
extract_first()返回列表中的第一个字符串
注意:
spider中的parse方法名不能修改
需要爬取的url地址必须要属于allow_domain(允许_域)下的连接
respone.xpath()返回的是一个含有selector对象的列表
为什么要使用yield? 让整个函数变成一个生成器,变成generator(生成器)有什么好处? 每次遍历的时候挨个读到内存中,不会导致内存的占用量瞬间变>高python3中range和python2中的xrange同理
scrapy.Request常用参数为 callback = xxx:指定传入的url交给那个解析函数去处理 meta={“xxx”:”xxx”}:实现在不同的解析函数中传递数据,配合callback用 dont_filter=False:让scrapy的去重不会过滤当前url,默认开启url去重 headers:请求头 cookies:cookies,不能放在headers中,独立写出来 method = “GET”:请求方式,(GET和POST)
爬取详细页和翻页 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import scrapyfrom yangguang.items import YangguangItemclass YgSpider (scrapy.Spider ): name = 'yg' allowed_domains = ['sun0769.com' ] start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=0' ] def parse (self, response ): tr_list = response.xpath("//div[@class='greyframe']/table[2]/tr/td/table/tr" ) for tr in tr_list: item = YangguangItem() item["title" ] = tr.xpath("./td[2]/a[@class='news14']/@title" ).extract_first() item["href" ] = tr.xpath("./td[2]/a[@class='news14']/@href" ).extract_first() item["publish_date" ] = tr.xpath("./td[last()]/text()" ).extract_first() yield scrapy.Request(item["href" ],callback=self.parse_detail,meta={"item" :item}) next_url = response.xpath("//a[text()='>']/@href" ).extract_first() if next_url is not None : yield scrapy.Request(next_url,callback=self.parse) def parse_detail (self,response ): item = response.meta["item" ] item["content" ] = response.xpath("//div[@class='c1 text14_2']//text()" ).extract() item["content_img" ] = response.xpath("//div[@class='c1 text14_2']//img/@src" ).extract() item["content_img" ] = ["http://wz.sun0769.com" + i for i in item["content_img" ]] yield item
items(存储爬取字段) 1 2 3 4 5 6 import scrapyclass TencentItem (scrapy.Item ):url = scrapy.Field() name = scrapy.Field()
使用pipeline(管道) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from demo1 import settingsimport pymongoclass Demo1Pipeline (object ): def __init__ (self ): client = pymongo.MongoClient(host=settings.MONGODB_HOST, port=settings.MONGODB_PORT) self.db = client[settings.MONGODB_DBNAME][settings.MONGODB_DOCNAME] def process_item (self, item, spider ): data = dict(item) self.db.insert(data) ITEM_PIPELINES = { 'mySpider.pipelines.MyspiderPipeline' : 300 , } MONGODB_HOST = '127.0.0.1' MONGODB_PORT = 27017 MONGODB_DBNAME = 'DouBan' MONGODB_DOCNAME = 'DouBanMovies'
第二种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class MyspiderPipeline (object ):def __init__ (self ): client = pymongo.MongoClient(host=settings.MONGODB_HOST, port=settings.MONGODB_PORT) self.db = client[settings.MONGODB_DBNAME] def process_item (self, item, spider ): table = '' if spider.name == "itcast" : table = self.db.表名 elif spider.name == "itcast2" : table = self.db.表名 table.insert(dict(item)) table = '' if isinstance(item, 爬虫名字): table = self.db.表名 table.insert(dict(item))
mysql存储 1 2 3 4 5 6 7 8 9 10 11 12 13 from pymysql import connectimport pymysqlclass TxPipeline (object ): def __init__ (self ): self.conn=connect(host='localhost' ,port=3306 ,db='txzp' ,user='root' ,passwd='root' ,charset='utf8' ) self.cc = self.conn.cursor() def process_item (self, item, spider ): print(item["title" ],item["href" ],item["number" ],item["time" ],item["duty" ]) aa = (item["title" ],item["href" ],item["number" ],item["time" ],item["duty" ],item["requirement" ]) sql = '''insert into tx values (0,"%s","%s","%s","%s","%s","%s")''' self.cc.execute(sql%aa) self.conn.commit()
注意
pipeline中process_item方法名不能修改,修改会报错
pipeline(管道)可以有多个
设置了pipelines必须开启管道,权重越小优先级越高
为什么需要多个pipeline:
可能会有多个spider,不同的pipeline处理不同的item的内容
一个spider的内容可能要做不同的操作,比如存入不同的数据库中
简单设置LOG(日志)
为了让我们自己希望输出到终端的内容能容易看一些: 我们可以在setting中设置log级别 在setting中添加一行(全部大写):
默认终端显示的是debug级别的log信息
logging模块的使用 scrapy中使用logging
1 2 3 LOG_LEVEL=“WARNING” LOG_FILE="./a.log"
1 2 3 4 5 6 7 8 9 import logginglogging = logging.getLogger(__name__) logging.warning(item) 2018 -10 -31 15 :25 :57 [mySpider.pipelines] WARNING: {'name' : '胡老师' , 'title' : '高级讲师' }
普通项目中使用logging 具体参数信息:https://www.cnblogs.com/bjdxy/archive/2013/04/12/3016820.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import logginglogging.basicConfig(level=logging.INFO, format= '%(asctime)s' ' %(levelname)s [%(filename)s : %(lineno)d ]' ' : %(message)s' , datefmt='[%Y/%m/%d %H:%M:%S]' ) logging=logging.getLogger(__name__) if __name__ == '__main__' : logging.info("this is a info log" )
b.py文件使用a.py文件的logging(日志)
1 2 3 4 5 6 7 from a import logging if __name__ == '__main__' :logger.warning("this is log b" )
日志级别:
debug #调试
info #正常信息
warning #警告
error #错误
如果设置日志级别为info,warning级别比info大,warning也可以打印,debug比info小不可以打印 如果设置日志级别为warning,info和debug都比warning小,不可以打印
把数据保存到mongodb中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pymongo import MongoClientclient = MongoClient(host='127.0.0.1' ,port = 27017 ) collection = client["tencent" ]["hr" ] class TencentPipeline (object ): def process_item (self, item, spider ): item = dict(item) collection.insert(item) print(item) return item
scrapy shell
Scrapy shell是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath表达式
使用方法:
1 2 命令行输入: scrapy shell http:
常用参数:
1 2 3 4 5 6 7 response.url:当前响应的url地址 response.request.url:当前响应对应的请求的url地址 response.headers:响应头 response.body:响应体,也就是html代码,默认是byte类型 response.body.decode():变为字符串类型 response.request.headers:当前响应的请求头 response.xpath("//h3/text()" ).extract():调试xpath,查看xpath可不可以取出数据
setting设置文件
为什么需要配置文件:
配置文件存放一些公共的变量(比如数据库的地址,账号密码等)
方便自己和别人修改
一般用全大写字母命名变量名SQL_HOST=’192.168.0.1’
参考地址:https://blog.csdn.net/u011781521/article/details/70188171
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 BOT_NAME = 'yangguang' SPIDER_MODULES = ['yangguang.spiders' ] NEWSPIDER_MODULE = 'yangguang.spiders' ROBOTSTXT_OBEY = True DOWNLOAD_DELAY = 3 DEFAULT_REQUEST_HEADERS = { 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' , 'Accept-Language' : 'en' , "Cookie" : "rfnl=https://www.guazi.com/sjz/dazhong/; antipas=2192r893U97623019B485050817" , } ITEM_PIPELINES = { 'yangguang.pipelines.YangguangPipeline' : 300 , }
spiders文件使用settings的配置属性
1 2 3 4 self.settings["MONGO_HOST" ] self.settings.get("MONGO_HOST" )
pipelines文件使用settings的配置属性
1 spider.settings.get("MONGO_HOST" )
Scrapy中CrawlSpider类 深度爬虫
1 2 scrapy genspider -t crawl cf gov.cn
第一种用法:提取内容页和翻页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import scrapyfrom scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Rulefrom tengxun.items import TengxunItemclass TxSpider (CrawlSpider ): name = 'tx' allowed_domains = ['hr.tencent.com' ] start_urls = ['https://hr.tencent.com/position.php' ] rules = ( Rule(LinkExtractor(allow=r'position_detail\.php\?id=\d+&keywords=&tid=0&lid=0' ), callback='parse_item' ), Rule(LinkExtractor(allow=r'position\.php\?&start=\d+#a' ), follow=True ), ) def parse_item (self, response ): item = TengxunItem() item["bt" ] = response.xpath('//td[@id="sharetitle"]/text()' ).extract_first() item["gzyq" ] = response.xpath('//div[text()="工作要求:"]/../ul/li/text()' ).extract() yield item
第二种用法:提取标题页,内容,翻页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import scrapyfrom scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Rulefrom tengxun.items import TengxunItemclass Tx2Spider (CrawlSpider ): name = 'tx2' allowed_domains = ['hr.tencent.com' ] start_urls = ['https://hr.tencent.com/position.php' ] rules = ( Rule(LinkExtractor(allow=r'position\.php\?&start=\d+#a' ), callback='parse_item' , follow=True ), ) def parse_item (self, response ): tr_list = response.xpath('//table[@class="tablelist"]/tr' )[1 :-1 ] for tr in tr_list: item = TengxunItem() item['bt' ] = tr.xpath('./td/a/text()' ).extract_first() item['url' ] = tr.xpath('./td/a/@href' ).extract_first() item['url' ] = "https://hr.tencent.com/" + item['url' ] yield scrapy.Request( item['url' ], callback=self.parse_detail, meta={"item" :item} ) def parse_detail (self,response ): item = response.meta['item' ] item['gzyq' ] = response.xpath('//div[text()="工作要求:"]/../ul/li/text()' ).extract() yield item
Rule的匹配细节
1 2 3 Rule(LinkExtractor(allow=r'position_detail\.php\?id=\d+&keywords=&tid=0&lid=0' ), callback='parse_item' ),
注意点:
用命令创建一个crawlspider的模板:scrapy genspider-t crawl 爬虫名字 域名,也可以手动创建
CrawiSpider中不能再有以parse为名字的数据提取方法,这个方法被CrawlSpider用来实现基础url提取等功能)
一个Rule对象接收很多参数,首先第一个是包含url规则的LinkExtractor对象,常用的还有calback(制定满足规则的url的解析函数的字符串)和follow(response中提取的链接是否需要跟进)
不指定callback函数的请求下,如果follow为True,满足该rule的url还会继续被请求
如果多个Rule都满足某一个url,会从rules中选择第一个满足的进行操作
CrawlSpider补充(了解)
1 2 3 4 5 6 7 8 9 LinkExtractor更多常用链接 LinkExtractor(allow=r'/web/site0/tab5240/info\d+\.htm' ) allow:满足括号中"正则表达式" 的URL会被提取,如果为空,则全部匹配. deny:满足括号中"正则表达式" 的URL一定不提取(优先级高于allow). allow_domains:会被提取的链接的domains. deny_domains:一定不会被提取链接的domains. restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接,级xpath满足范围内的uri地址会被提取
1 2 3 4 5 6 7 8 9 rule常见参数: Rule(LinkExtractor(allow=r'/web/site0/tab5240/info\d+\.htm' ), callback='parse_item' , follow=False ), LinkExtractor:是一个Link Extractor对象,用于定义需要提取的链接. callback:从link_extractor中每获取到链接时,参数所指定的值作为回调函数 follow:是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进.如果callback为None ,follow 默认设置为True ,否则默认为False . process_links:指定该spider中哪个的函数将会被调用,从link_extractor中获取到链接列表时将会调用该函数,该方法主要用来过滤url. process_request:指定该spider中哪个的函数将会被调用,该规则提取到每个request时都会调用该函数,用来过滤request
Scrapy分布式爬虫
Scrapy分布式爬虫流程
Scrapy_redis之domz domz相比于之前的spider多了持久化和request去重的功能 domz就是Crawlspider去重和持久化版本 不能分布式 可以分布式的是RedisSpider和RedisCrawlspider
Scrapy redis在scrapy的基础上实现了更多,更强大的功能,具体体现在:reqeust去重,爬虫持久化,和轻松实现分布式 官方站点:https://github.com/rmax/scrapy-redis
1 2 3 爬虫内容和自己写的CrawlSpider没有任何区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 settings.py DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER = "scrapy_redis.scheduler.Scheduler" SCHEDULER_PERSIST = True ITEM_PIPELINES = { } REDIS_URL = 'redis://127.0.0.1:6379'
我们执行domz的爬虫,会发现redis中多了一下三个键:
dmoz:requests (zset类型)(待爬取) Scheduler队列,存放的待请求的request对象,获取的过程是pop操作,即获取一个会去除一个
dmoz:dupefilter (set)(已爬取) 指纹集合,存放的是已经进入scheduler队列的request对象的指纹,指纹默认由请求方法,url和请求体组成
dmoz:items (list类型)(item信息) 存放的获取到的item信息,在pipeline中开启RedisPipeline才会存入
Scrapy_redis之RedisSpider 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from scrapy_redis.spiders import RedisSpiderclass MySpider (RedisSpider ):name='myspider_redis' redis_key ='myspider:start_urls' allow_doamin=["dmoztools.net" ] def parse (self, response ): ...
启动
1 2 3 4 5 scrapy runspider myspider 或(2选1) scrapy runspider myspider.py
redis运行
1 2 #redis 添加 键:值 "爬取的网址" redis-c1i lpush guazi:start_urls "http://ww.guazi.com/sjz/dazhong/"
Scrapy_redis之RedisCrawlSpider 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from scrapy.spiders import Rulefrom scrapy.linkextractors import LinkExtractorfrom scrapy_redis.spiders import RedisCrawlSpiderclass MyCrawler (RedisCrawlSpider ):name='mycrawler_redis' redis_key='mycrawler:start_urls' allow_domains=["dmoztools.net" ] rules=( Rule(LinkExtractor(),callback='parse_page' ,follow=True )
启动
1 2 3 4 5 scrapy runspider myspider 或(2选1) scrapy runspider myspider.py
redis运行
1 2 #redis 添加 键:值 "爬取的网址" redis-c1i lpush guazi:start_urls "http://ww.guazi.com/sjz/dazhong/"
快速启动爬虫 1 2 3 4 5 6 7 8 from scrapy import cmdlinecmdline.execute("scrapy crawl guazicrawl" .split())
其他参数
如果抓取的url不完整,没前面的url,可以使用下面方法
1 2 3 4 5 6 7 8 import urllib a = http: b = ?456 b = urllib.parse.urljoin(a,b) print ("b=%s" %b)
存多个url或其他东西,可以用列表存储 1 2 3 4 item["img_list" ] =[] item["img_list" ].extend(response.xpath('//img[@class="BDE_Image"]/@src' ).extract())
数据保存方式(包含:图片、文件的下载)