scrapy解析数据、配置文件、整站爬取cnblogs=》爬取详情=》数据传递、持久化、爬虫中间件和下载中间件、scrapy继承selenium、源码去重规则(布隆过滤器)、分布式爬虫
scrapy解析数据
##### 运行爬虫 scrapy crawl cnblogs ##### 可以项目目录下写个main.py from scrapy.cmdline import execute execute(['scrapy','crawl','cnblogs','--nolog']) #### 重点 1 response对象有css方法和xpath方法 -css中写css选择器 response.css('') -xpath中写xpath选择 response.xpath('') 2 重点1: -xpath取文本内容 './/a[contains(@class,"link-title")]/text()' -xpath取属性 './/a[contains(@class,"link-title")]/@href' -css取文本 'a.link-title::text' -css取属性 'img.image-scale::attr(src)' 3 重点2: .extract_first() 取一个 .extract() 取所有
##### 使用css选择器解析数据 def parse(self, response): article_list = response.css('article.post-item') for article in article_list: name = article.css('a.post-item-title::text').extract_first() author = article.css('a.post-item-author>span::text').extract_first() url = article.css('a.post-item-title::attr(href)').extract_first() img = article.css('img.avatar::attr(src)').extract_first() desc = article.css('p.post-item-summary::text').extract() # 文本内容可能放在第二个位置 desc_content=desc[0].replace('\n', '').replace(' ', '') if not desc_content: desc_content = desc[1].replace('\n', '').replace(' ', '') print(''' 文章标题:%s 文章作者:%s 文章地址:%s 头像:%s 摘要:%s ''' % (name, author, url, img, desc_content)) #### xpath 解析数据 def parse(self, response): article_list = response.xpath('//article[@class="post-item"]') for article in article_list: name = article.xpath('.//a[@class="post-item-title"]/text()').extract_first() # name = article.xpath('./section/div/a/text()').extract_first() author = article.xpath('.//a[@class="post-item-author"]/span/text()').extract_first() url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first() img = article.xpath('./section/div/p/a/img/@src').extract_first() desc = article.xpath('./section/div/p/text()').extract() # 文本内容可能放在第二个位置 desc_content = desc[0].replace('\n', '').replace(' ', '') if not desc_content: desc_content = desc[1].replace('\n', '').replace(' ', '') print(''' 文章标题:%s 文章作者:%s 文章地址:%s 头像:%s 摘要:%s ''' % (name, author, url, img, desc_content))
配置文件
#### 基础配置 # 项目名 BOT_NAME = "scrapy_demo" # 爬虫所在路径 SPIDER_MODULES = ["scrapy_demo.spiders"] NEWSPIDER_MODULE = "scrapy_demo.spiders" # 记住 日志级别 LOG_LEVEL='ERROR' # 请求头中的 USER_AGENT USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" # 是否遵循爬虫协议 ROBOTSTXT_OBEY = False # 默认请求头 #DEFAULT_REQUEST_HEADERS = { # "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", # "Accept-Language": "en", #} #爬虫中间件 #SPIDER_MIDDLEWARES = { # "scrapy_demo.middlewares.ScrapyDemoSpiderMiddleware": 543, #} # 下载中间件 #DOWNLOADER_MIDDLEWARES = { # "scrapy_demo.middlewares.ScrapyDemoDownloaderMiddleware": 543, #} # 持久化相关 #ITEM_PIPELINES = { # "scrapy_demo.pipelines.ScrapyDemoPipeline": 300, #} ### 高级配置(提高爬取效率) #1 增加并发:默认16 默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改 CONCURRENT_REQUESTS = 100 值为100,并发设置成了为100 #2 提高日志级别: 在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写: LOG_LEVEL = 'INFO' # 3 禁止cookie: 如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写: COOKIES_ENABLED = False # 4 禁止重试: 对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写: RETRY_ENABLED = False # 5 减少下载超时: 如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写: DOWNLOAD_TIMEOUT = 10 超时时间为10s
整站爬取cnblogs==》爬取详情==》数据传递
# 整站爬取: 爬取所有页 -解析出下一页 yield Request(url=next, callback=self.parse) 爬取文章详情 -解析出详情地址:yield Request(url=url, callback=self.detail_parser) 多个Request之间数据传递 yield Request(url=url,meta={'item':item}) 在解析的 response中 response.meta.get('item')
def parse(self, response): article_list = response.xpath('//article[@class="post-item"]') for article in article_list: name = article.xpath('.//a[@class="post-item-title"]/text()').extract_first() # name = article.xpath('./section/div/a/text()').extract_first() author = article.xpath('.//a[@class="post-item-author"]/span/text()').extract_first() url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first() img = article.xpath('./section/div/p/a/img/@src').extract_first() desc = article.xpath('./section/div/p/text()').extract() # 文本内容可能放在第二个位置 desc_content = desc[0].replace('\n', '').replace(' ', '') if not desc_content: desc_content = desc[1].replace('\n', '').replace(' ', '') # print(''' # 文章标题:%s # 文章作者:%s # 文章地址:%s # 头像:%s # 摘要:%s # ''' % (name, author, url, img, desc_content)) # 详情地址:url ----》想继续爬取详情 item={'name':name,'url':url,'img':img,'text':None} yield Request(url=url, callback=self.detail_parser,meta={'item':item}) #### 继续爬取下一页 # next='https://www.cnblogs.com'+response.css('div.pager>a:last-child::attr(href)').extract_first() next = 'https://www.cnblogs.com' + response.xpath('//div[@class="pager"]/a[last()]/@href').extract_first() print(next) yield Request(url=next, callback=self.parse) # 逻辑---》起始地址:https://www.cnblogs.com---》回到了parse---》自己解析了(打印数据,继续爬取的地址)---》yield Request对象---》第二页---》爬完后又回到parser解析 def detail_parser(self, response): print(len(response.text)) item=response.meta.get('item') text=response.css('#cnblogs_post_body').extract_first() item['text']=text # 我们想把:上一个请求解析出来的 标题,摘要,图片 和这个请求解析出来的 文本合并到一起 # 这个text 无法和 上面 parse解析出的文章标题对应上 print(item)
持久化
# 持久化---》把数据保存到磁盘上:文件,mysql -管道 -使用步骤 -1 写个类:items.py,里面写字段 class CnblogItem(scrapy.Item): name = scrapy.Field() author = scrapy.Field() url = scrapy.Field() img = scrapy.Field() desc_content = scrapy.Field() # 文本详情 text = scrapy.Field() -2 配置文件配置(管道,配置多个,存在多个位置) ITEM_PIPELINES = { "scrapy_demo.pipelines.CnblogPipeline": 300, "scrapy_demo.pipelines.CnblogMysqlPipeline": 200, } -3 爬虫解析中:yield item -4 pipelines.py中写类:open_spider,close_spider,process_item
爬虫中间件和下载中间件
爬虫中间件(一般不用)
# 第一步:写个爬虫中间件类 class ScrapyDemoSpiderMiddleware: @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 # 走架构图第1步,会触发这里 def process_spider_input(self, response, spider): # Called for each response that goes through the spider # middleware and into the spider. # Should return None or raise an exception. return None # 架构图,第1,7步走这里 def process_spider_output(self, response, result, spider): # Called with the results returned from the Spider, after # it has processed the response. # Must return an iterable of Request, 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 Request 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) # 2 配置文件配置 SPIDER_MIDDLEWARES = { "scrapy_demo.middlewares.ScrapyDemoSpiderMiddleware": 543, }
下载中间件
class ScrapyDemoDownloaderMiddleware: @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): # - return None: 继续执行当次请求,继续走下一个中间件---》如果中间件没了---》执行下载 # - return Response :直接把Response返回给引擎,引擎交给爬虫去解析 # - return Request :把request返回给引擎,引擎给调度器,等待下一次被爬取 # - 直接抛异常: 触发process_exception执行 return None def process_response(self, request, response, spider): # Must either; # - return Response:正常爬取完---》给引擎---》引擎给爬虫去解析 # - return Request: 爬取失败--》给引擎--》引擎给调度器--》等待下次爬取 # - 抛异常 :走到process_exception return response def process_exception(self, request, exception, spider): # 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)
加请求头(加到请求对象中)
# faker 模块 :随机生成假数据 # pip install fake_useragent:随机生成请求头 ### 加referer,加token 加 user-agent def process_request(self, request, spider): #### 加请求头 print(request.headers) request.headers['referer'] = 'http://www.lagou.com' request.headers['token'] = 'asdfasdf.asdfads.asfdasfd' # user-agent--->写死了---》想随机请求头 from fake_useragent import UserAgent ua = UserAgent() request.headers['User-Agent'] = str(ua.random) print(request.headers) return None
加cookie
def process_request(self, request, spider): print(request.cookies) request.cookies['name']='lqz' return None
加代理
# 在下载中间件的def process_request(self, request, spider):写代码 # 第一步: -在下载中间件写process_request方法 def get_proxy(self): import requests res = requests.get('http://127.0.0.1:5010/get/').json() if res.get('https'): return 'https://' + res.get('proxy') else: return 'http://' + res.get('proxy') def process_request(self, request, spider): #request.meta['proxy'] = self.get_proxy() request.meta['proxy'] = 'http://192.168.11.11:8888' return None # 第二步:代理可能不能用,会触发process_exception,在里面写 def process_exception(self, request, exception, spider): print('-----',request.url) # 这个地址没有爬 return request
scrapy集成selenium
# 使用scrapy默认下载器---》类似于requests模块发送请求,不能执行js,有的页面拿回来数据不完整 # 想在scrapy中集成selenium,获取数据更完整,获取完后,自己组装成 Response对象,就会进爬虫解析,现在解析的是使用selenium拿回来的页面,数据更完整
# 集成selenium 因为有的页面,是执行完js后才渲染完,必须使用selenium去爬取数据才完整 # 保证整个爬虫中,只有一个浏览器器 # 只要爬取 下一页这种地址,使用selenium,爬取详情,继续使用原来的 # 第一步:在爬虫类中写 from selenium import webdriver class CnblogsSpider(scrapy.Spider): bro = webdriver.Chrome() # 使用无头 bro.implicitly_wait(10) def close(spider, reason): spider.bro.close() #浏览器关掉 # 第二步:在中间件中 def process_request(self, request, spider): # 爬取下一页这种地址---》用selenium,但是文章详情,就用原来的 if 'sitehome/p' in request.url: spider.bro.get(request.url) from scrapy.http.response.html import HtmlResponse response = HtmlResponse(url=request.url, body=bytes(spider.bro.page_source, encoding='utf-8')) return response else: return None
源码去重规则(布隆过滤器)
# 如果爬取过的地址,就不会再爬了,scrapy 自带去重 # 调度器可以去重,研究一下,如何去重的---》使用了集合 # 要爬取的Request对象,在进入到scheduler调度器排队之前,先执行enqueue_request,它如果return False,这个Request就丢弃掉,不爬了----》如何判断这个Request要不要丢弃掉,执行了self.df.request_seen(request),它来决定的-----》RFPDupeFilter类中的方法----》request_seen---》会返回True或False----》如果这个request在集合中,说明爬过了,就return True,如果不在集合中,就加入到集合中,然后返回False # 调度器源码 from scrapy.core.scheduler import Scheduler # 这个方法如果return True表示这个request要爬取,如果return False表示这个网址就不爬了(已经爬过了) def enqueue_request(self, request: Request) -> bool: # request当次要爬取的地址对象 if self.df.request_seen(request): # 有的请情况,在爬虫中解析出来的网址,不想爬了,就就可以指定 # yield Request(url=url, callback=self.detail_parse, meta={'item': item},dont_filter=True) # 如果符合这个条件,表示这个网址已经爬过了 return False return True # self.df 去重类 是去重类的对象 RFPDupeFilter--》配置文件配置的 -在配置文件中如果配置了:DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'表示,使用它作为去重类,按照它的规则做去重 -RFPDupeFilter的request_seen def request_seen(self, request: Request) -> bool: # request_fingerprint 生成指纹 fp = self.request_fingerprint(request) #request当次要爬取的地址对象 #判断 fp 在不在集合中,如果在,return True if fp in self.fingerprints: return True #如果不在,加入到集合,return False self.fingerprints.add(fp) return False # 传进来是个request对象,生成的是指纹 -爬取的网址:https://www.cnblogs.com/teach/p/17238610.html?name=lqz&age=19 -和 https://www.cnblogs.com/teach/p/17238610.html?age=19&name=lqz -它俩是一样的,返回的数据都是一样的,就应该是一条url,就只会爬取一次 -所以 request_fingerprint 就是来把它们做成一样的(核心原理是把查询条件排序,再拼接到后面) -生成指纹,指纹是什么? 生成的指纹放到集合中去重 -www.cnblogs.com?name=lqz&age=19 -www.cnblogs.com?age=19&name=lqz -上面的两种地址生成的指纹是一样的 # 测试指纹 from scrapy.utils.request import RequestFingerprinter from scrapy import Request fingerprinter = RequestFingerprinter() request1 = Request(url='http://www.cnblogs.com?name=lqz&age=20') request2 = Request(url='http://www.cnblogs.com?age=20&name=lqz') res1 = fingerprinter.fingerprint(request1).hex() res2 = fingerprinter.fingerprint(request2).hex() print(res1) print(res2) # 集合去重,集合中放 # a一个bytes # 假设爬了1亿条url,放在内存中,占空间非常大 a6af0a0ffa18a9b2432550e1914361b6bffcff1a a6af0a0ffa18a9b2432550e191361b6bffc34f1a # 想一种方式,极小内存实现去重---》布隆过滤器 https://zhuanlan.zhihu.com/p/94668361
布隆过滤器
# 极小内存实现去重: # 应用场景:爬虫去重,避免缓存穿透,垃圾邮件过滤 # bloomfilter:是一个通过多哈希函数映射到一张表的数据结构,能够快速的判断一个元素在一个集合内是否存在,具有很好的空间和时间效率。(典型例子,爬虫url去重)
布隆案例
# from pybloom_live import ScalableBloomFilter # bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH) # url = "www.cnblogs.com" # url2 = "www.liuqingzheng.top" # bloom.add(url) # print(url in bloom) # print(url2 in bloom) from pybloom_live import BloomFilter bf = BloomFilter(capacity=1000) url='www.baidu.com' bf.add(url) print(url in bf) print("www.liuqingzheng.top" in bf)
from scrapy.dupefilters import BaseDupeFilter from scrapy.utils.request import RequestFingerprinter from pybloom_live import ScalableBloomFilter class MyPDupeFilter(BaseDupeFilter): fingerprints = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH) fingerprinter = RequestFingerprinter() def request_seen(self, request): print('zoule') fp = self.request_fingerprint(request) if fp in self.fingerprints: return True self.fingerprints.add(fp) return False def request_fingerprint(self, request) -> str: return self.fingerprinter.fingerprint(request).hex()
分布式爬虫
# 原来scrapy的Scheduler维护的是本机的任务队列(待爬取的地址)+本机的去重队列(放在集合中)---》在本机内存中 # 如果把scrapy项目,部署到多台机器上,多台机器爬取的内容是重复的 # 所以实现分布式爬取的关键就是,找一台专门的主机上运行一个共享的队列比如Redis, 然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求,所以总结下来,实现分布式的关键就是三点: #1、多台机器共享队列 #2、重写Scheduler,让其无论是去重还是任务都去访问共享队列 #3、为Scheduler定制去重规则(利用redis的集合类型) # scrapy-redis实现分布式爬虫 -公共的去重 -公共的待爬取地址队列 # 使用步骤 0 下载:pip2 install scrapy-redis 1 把之前爬虫类,继承class CnblogsSpider(RedisSpider): 2 去掉起始爬取的地址,加入一个类属性 redis_key = 'myspider:start_urls' # redis列表的key,后期我们需要手动插入起始地址 3 配置文件中配置 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # scrapy redis去重类,使用redis的集合去重 # 不使用原生的调度器了,使用scrapy_redis提供的调度器,它就是使用了redis的列表 SCHEDULER = "scrapy_redis.scheduler.Scheduler" REDIS_HOST = 'localhost' # 主机名 REDIS_PORT = 6379 # 端口 ITEM_PIPELINES = { # 'mysfirstscrapy.pipelines.MyCnblogsPipeline': 300, 'mysfirstscrapy.pipelines.MyCnblogsMySqlPipeline': 301, 'scrapy_redis.pipelines.RedisPipeline': 400, } 4 再不同多台机器上运行scrapy的爬虫,就实现了分布式爬虫 5 写入到redis的列表中起始爬取的地址:列表key:myspider:start_urls rpush myspider:start_urls https://www.cnblogs.com