官方文档:https://docs.scrapy.org/en/latest/topics/architecture.html
Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。整体架构大致如下
Components:
引擎(EGINE)
引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。有关详细信息,请参见上面的数据流部分。
调度器(SCHEDULER)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
队列:先进先出,实现深度优先
栈:后进先出,实现广度优先
优先级队列:实现优先级
下载器(DOWLOADER)
用于下载网页内容, 并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的
爬虫(SPIDERS)
SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求
项目管道(ITEM PIPLINES)
在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
下载器中间件(Downloader Middlewares)
位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,你可用该中间件做以下几件事
1、process a request just before it is sent to the Downloader (i.e. right before Scrapy sends the request to the website);
2、change received response before passing it to a spider;
3、send a new Request instead of passing received response to a spider;
4、pass response to a spider without fetching a web page;
silently drop some requests.
爬虫中间件(Spider Middlewares)
位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)
pip3 install scrapy
Windows平台如果安装不了则需要如下配置
6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
8、pip3 install scrapy
#Linux平台
1、pip3 install scrapy
pip3 install wheel # 安装后,便支持通过wheel文件安装软件,wheel文件官网https://www.lfd.uci.edu/~gohlke/pythonlibs
pip3 install lxml
pip3 install pyopenssl
python3 -m pip debug --verbose
pip3 install scrapy
安装完成后在解释器的 Scripts 目录下会多出 scrapy.exe 文件,相当于 django 的 django_admin.exe 文件。
scrapy startproject 项目名字
创建完成如下图所示
firstscrapy # 项目名
firstscrapy # 项目同名文件夹
spiders # 文件夹,存放一个个爬虫
cnblogs.py # 其中一个爬虫,重点写代码的地方(解析数据,发起请求)
items.py # 类比 djagno 的 models,表模型(类)
middlewares.py # 中间件,爬虫中间件和下载中间件都在里面
pipelines.py # 管道,做持久化需要在这写代码
settings.py # 配置文件
scrapy.cfg # 上线配置,开发阶段不用
scrapy genspider 爬虫名字 爬取的初始网站
scrapy genspider cnblogs cnblogs.com
scrapy crawl 爬虫名字
# --nolog 表示不打印日志
scrapy crawl 爬虫名字 --nolog
除了使用命令,还可以在 pycharm 中项目的根目录下创建 main.py 文件(文件名随意),添加如下内容
from scrapy.cmdline import execute
# execute(['scrapy','crawl','爬虫名','--nolog']) --nolog 表示不打印日志
execute(['scrapy','crawl','cnblogs','--nolog'])
第一个请求定义在 start_requests() 方法内默认从 start_urls 列表中获得 url 地址来生成 Request请求,默认的回调函数是 parse 方法。回调函数在下载完成返回 response 时自动触发,在回调函数中,可以解析 response 并且返回值
解析的方式通常使用 Scrapy 自带的 Selectors,也可以使用 Beutifulsoup,lxml或其他。最后,针对返回的Items对象将会被持久化到 Mysql、redis 或者文件中。
Spiders总共提供了五种类:
导入语句: from scrapy.spiders import Spider,CrawlSpider,XMLFeedSpider,CSVFeedSpider,SitemapSpider
1、scrapy.spiders.Spider #scrapy.Spider等同于scrapy.spiders.Spider
2、scrapy.spiders.CrawlSpider
3、scrapy.spiders.XMLFeedSpider
4、scrapy.spiders.CSVFeedSpider
5、scrapy.spiders.SitemapSpider
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
def parse(self, response):
print(response.text)
response 对象有css方法和xpath方法,在css中使用css选择器,在xpath中写xpath选择器
xpath取文本内容
'.//a[contains(@class,"link-title")]/text()'
xpath取属性
'.//a[contains(@class,"link-title")]/@href'
css取文本 ::text
'a.link-title::text'
css取属性 ::attr()
'img.image-scale::attr(src)'
.extract_first() 取一个
.extract() 取所有
属性方法介绍
定义爬虫名,scrapy 会根据该值定位爬虫程序,所以它必须要有且必须唯一
定义允许爬取的域名,如果OffsiteMiddleware启动(默认就启动),那么不属于该列表的域名及其子域名
都不允许爬取如果爬取的网址为:https://www.example.com/1.html,那就添加'example.com'到列表.
如果没有指定 url,就从该列表中读取 url 来生成第一个请求
值为一个字典,定义一些配置信息,在运行爬虫程序时,这些配置会覆盖项目级别的配置,所以
custom_settings必须被定义成一个类属性,由于settings会在类实例化前被加载
通过 self.settings['配置项的名字'] 可以访问 settings.py 中的配置,如果优先使用自己定义的
custom_settings 配置
日志名默认为spider的名字
self.logger.debug('=============>%s' %self.settings['BOT_NAME'])
该方法用来发起第一个Requests请求,且必须返回一个可迭代的对象。它在爬虫程序打开时就被Scrapy调用
Scrapy只调用它一次。默认从start_urls里取出每个url来生成Request(url, dont_filter=True)
#针对参数dont_filter,请看自定义去重规则
如果你想要改变起始爬取的Requests,你就需要覆盖这个方法,例如你想要起始发送一个POST请求,如下
class MySpider(scrapy.Spider):
name = 'myspider'
def start_requests(self):
return [scrapy.FormRequest("http://www.example.com/login",
formdata={'user': 'xxx', 'pass': '123'},
callback=self.logged_in)]
def logged_in(self, response):
# here you would extract links to follow and return Requests for
# each of them, with another callback
pass
这是默认的回调函数,所有的回调函数必须返回Request和/或dicts或Item对象的可迭代对象。
爬虫程序结束时自动触发
解析博客园文章数据
import scrapy
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
custom_settings = {}
def parse(self, response):
article_list = response.css('article.post-item')
for article in article_list:
title = article.css('a.post-item-title::text').extract_first()
# title = article.xpath('.//a/text()').extract_first()
desc = article.css('p.post-item-summary::text').extract()
# desc = article.xpath('./section/div/p/text()').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if not real_desc:
real_desc = desc[1].replace('\n', '').replace(' ', '')
pub_time = article.css('span.post-meta-item>span::text').extract_first()
# pub_time=article.xpath('.//footer/span/span/text()').extract_first()
author = article.css('footer.post-item-foot span::text').extract_first()
# author=article.xpath('.//footer/a/span/text()').extract_first()
url = article.css('a.post-item-title::attr(href)').extract_first()
# url = article.xpath('./section/div/a/@href').extract_first()
配置 | 值 | 说明 |
---|---|---|
ROBOTSTXT_OBEY | True / False | 是否遵循爬虫协议 |
LOG_LEVEL | ‘ERROR’ / ‘INFO’ /… | 日志级别 |
USER_AGENT | Mozilla/5.0 (Windows NT 10.0;… | 用户代理 |
DEFAULT_REQUEST_HEADERS | 如下所示 | 默认请求头 |
SPIDER_MIDDLEWARES | 如下所示 | 爬虫中间件 |
DOWNLOADER_MIDDLEWARES | 如下所示 | 下载中间件 |
ITEM_PIPELINES | 如下所示 | 持久化配置 |
CONCURRENT_REQUESTS | 默认 16 | 配置Scrapy执行的最大并发请求 |
COOKIES_ENABLED | 默认 True | 是否禁止 cookie |
RETRY_ENABLED | True / False | 是否重试 |
DOWNLOAD_TIMEOUT | 数字 | 超时等待时间 |
# 默认请求头
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
# 爬虫中间件
SPIDER_MIDDLEWARES = {
'myfirstscrapy.middlewares.MyfirstscrapySpiderMiddleware': 543,
}
# 持久化配置
ITEM_PIPELINES = {
'myfirstscrapy.pipelines.MyfirstscrapyPipeline': 300,
}
CONCURRENT_REQUESTS = 100
默认 scrapy 开启的并发线程为 16 个,可以适当进行增加。在 settings 配置文件中如上修改为100。
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为ERROR。
在配置文件中编写: LOG_LEVEL = 'ERROR'
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。
在配置文件中编写:COOKIES_ENABLED = False
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
RETRY_ENABLED = False
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。
在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 # 超时时间为10s
解析好的数据需要进行存储,存储的形式可以是文件、redis、MySQL等等。
一般不使用该方法
1. 解析函数parse返回的格式需要如下形式:
return [{},{},{}]
2. 运行如下命令
scrapy crawl cnblogs -o 文件名(json,pkl,csv结尾)
class CnBlogItem(scrapy.Item):
title = scrapy.Field()
author = scrapy.Field()
real_desc = scrapy.Field()
pub_time = scrapy.Field()
url = scrapy.Field()
content = scrapy.Field()
import scrapy
from myfirstscrapy.items import CnBlogItem
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
def parse(self, response):
for article in article_list:
item = CnBlogItem()
...
item['title'] = title
item['real_desc'] = real_desc
item['pub_time'] = pub_time
item['author'] = author
item['url'] = url
yield item
ITEM_PIPELINES = {
'myfirstscrapy.pipelines.CnBlogPipeline': 300,
}
class CnBlogPipeline:
def open_spider(self, spider):
# 数据初始化,打开文件,打开数据库链接
# 打开文件,链接数据库
self.f = open('cnblogs.txt', 'w', encoding='utf-8')
def process_item(self, item, spider):
# 储存数据,必须返回 item 给后续的 pipline 继续使用
self.f.write('标题:%s,作者:%s\n' % (item['title'], item['author']))
return item
def close_spider(self, spider):
# 销毁资源,关闭文件,关闭数据库链接
# 关闭文件,关闭数据库
self.f.close()
import scrapy
from myfirstscrapy.items import CnBlogItem
from scrapy import Request
class CnblogsSpider(scrapy.Spider):
# 爬虫名字
name = 'cnblogs'
# 域名
allowed_domains = ['cnblogs.com']
# 爬取地址
start_urls = ['http://cnblogs.com/']
# 回调函数
def parse(self, response):
# 获取一页文章内容
article_list = response.css('article.post-item')
for article in article_list:
# 实例化 Items.py 下的类
item = CnBlogItem()
title = article.css('a.post-item-title::text').extract_first()
desc = article.css('p.post-item-summary::text').extract()
real_desc = desc[0].replace('\n', '').replace(' ', '')
if not real_desc:
real_desc = desc[1].replace('\n', '').replace(' ', '')
pub_time = article.css('span.post-meta-item>span::text').extract_first()
author = article.css('footer.post-item-foot span::text').extract_first()
url = article.css('a.post-item-title::attr(href)').extract_first()
# 给 item 设置文章内容
item['title'] = title
item['desc'] = real_desc
item['pub_time'] = pub_time
item['author'] = author
item['url'] = url
# 发起请求,获取文章详情(另一个页面),回调函数设置为 parser_detail,并通过 meta 实现请求对象和响应对象通信
yield Request(url=url, callback=self.parser_detail, meta={'item': item})
# 获取下一页元素链接属性
next = response.css('div.pager>a:last-child::attr(href)').extract_first()
# 拼接地址
next = 'https://www.cnblogs.com' + next
print(next)
# 发起请求,设置回调函数为 parse,当然默认也是 parse
yield Request(url=next, callback=self.parse)
def parser_detail(self, response):
# 获取传递的 item,用于数据持久化
item = response.meta.get('item')
# 获取文章内容
content = response.css('#cnblogs_post_body').extract_first()
# 添加属性
item['content'] = content
yield item
import scrapy
class CnBlogItem(scrapy.Item):
title = scrapy.Field()
author = scrapy.Field()
desc = scrapy.Field()
pub_time = scrapy.Field()
url = scrapy.Field()
content = scrapy.Field()
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
ITEM_PIPELINES = {
'myfirstscrapy.pipelines.CnBlogPipeline': 300,
}
import pymysql
class CnBlogPipeline:
def open_spider(self, spider):
# 数据初始化,使用 pymysql 打开数据库链接
self.conn = pymysql.connect(user='root',
password="123123",
host='127.0.0.1',
port=3306,
database='cnblogs')
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
# 编写 sql 语句,例如 desc 这种本身属于关键词的可以使用 `` 包裹,编写的占位符使用 %s
sql = 'insert into cnblog (title,`desc`,url,pub_time,author,content) values (%s,%s,%s,%s,%s,%s)'
# 传入参数
self.cursor.execute(sql, args=[item['title'], item['desc'], item['url'], item['pub_time'], item['author'],
item['content']])
# 提交确认
self.conn.commit()
# 储存数据,必须返回 item 给后续的 pipline 继续使用
return item
def close_spider(self, spider):
# 销毁资源,关闭文件,关闭数据库链接
self.cursor.close()
self.conn.close()
中间件中 SPIDER_MIDDLEWARES 爬虫中间件 (了解即可,用的少),DOWNLOADER_MIDDLEWARES 下载中间件(用的多)。尤其是下载中间件,里面的两个方法
process_request、process_response
class CnBlogDownloaderMiddleware:
@classmethod
def from_crawler(cls, crawler):
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# 请求来的时候执行
# return None 继续处理此请求
# return a Response object 返回给 engin,去解析
# return a Request object 返回给 engin,继续去请求
# raise IgnoreRequest: 执行 process_exception 方法
return None
def process_response(self, request, response, spider):
# 请求走的时候执行
# return a Response object 继续走下一个中间件的process_response,给engin,去解析
# return a Request object 给engin,进入调度器,等待下一次爬取
# raise IgnoreRequest 抛出异常
return response
def process_exception(self, request, exception, spider):
# 出现异常时执行
pass
def spider_opened(self, spider):
# 爬虫运行时执行
spider.logger.info('Spider opened: %s' % spider.name)
需要在 settings.py 中配置:
DOWNLOADER_MIDDLEWARES = {
'myfirstscrapy.middlewares.CnBlogDownloaderMiddleware': 543,
}
在下载中间件的 process_request 方法中如下配置:
...
def process_request(self, request, spider):
# 这里是固定的,最好搭建一个代理池,每次随机取出一个使用
request.meta['proxy'] = 'http://221.6.215.202:9091'
return None
...
在下载中间件的 process_request 方法中如下配置:
...
def process_request(self, request, spider):
request.cookies['name'] = 'xwx'
request.cookies = {}
return None
...
在下载中间件的 process_request 方法中如下配置:
...
def process_request(self, request, spider):
# cookie 也可以写在请求头
request.headers['Auth'] = 'asdfasdfasdfasdf'
request.headers['USER-AGENT'] = 'ssss'
return None
...
需要使用 fake_useragent 模块
pip3 install fake-useragent
在下载中间件的 process_request 方法中如下配置:
...
def process_request(self, request, spider):
from fake_useragent import UserAgent
ua = UserAgent()
print(ua.ie) # 随机打印ie浏览器任意版本
print(ua.firefox) # 随机打印firefox浏览器任意版本
print(ua.chrome) # 随机打印chrome浏览器任意版本
print(ua.random) # 随机打印任意厂家的浏览器
request.headers['USER-AGENT'] = ua.chrome
return None
...
...
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
...
chrome = webdriver.Chrome()
...
def close(spider, reason):
# 关闭浏览器
spider.chrome.close()
...
def process_request(self, request, spider):
from scrapy.http import HtmlResponse
url = request.url
if 'sitehome' in url:
spider.chrome.get(url=url)
response = HtmlResponse(url=request.url, body=spider.chrome.page_source.encode('utf-8'), request=request)
return response
else:
return None
...
scrapy 使用集合实现了去重,也就是爬过的网址不会再爬了。
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
# from scrapy.dupefilters import RFPDupeFilter
class RFPDupeFilter(BaseDupeFilter)
def request_seen(self, request: Request) -> bool:
# 把request生成指纹,如果request对象的url一样,指纹就一样
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + '\n')
return False
www.cnblogs.com?name=xwx&age=19
www.cnblogs.com?age=19&name=xwx
测试生成指纹
from scrapy.utils.request import request_fingerprint
from scrapy import Request
ur1=Request(url='http://www.cnblogs.com?name=xwx&age=19')
ur2=Request(url='http://www.cnblogs.com?age=20&name=xwx')
print(request_fingerprint(ur1))
print(request_fingerprint(ur2))
集合的方式如果爬取的网址少还行,如果特别多 ,则会占非常大的内存空间。可以使用 布隆过滤器 来实现极小空间实现去重
bloomfilter:是一个通过多哈希函数映射到一张表的数据结构,能够快速的判断一个元素在一个集合内是否存在,具有很好的空间和时间效率。(典型例子,爬虫url去重)
原理: BloomFilter 会开辟一个m位的bitArray(位数组),开始所有数据全部置 0 。当一个元素过来时,能过多个哈希函数(h1,h2,h3…)计算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。
关于多个哈希函数,它们计算出来的值必须 [0,m) 之中。
假设长度为 20的bitArray,通过 3 个哈希函数求值。如下图:
另外说明一下,当来查找对应的值时,同样通过哈希函数求值,再去寻找数组的下标,如果所有下标都为1时,元素存在。当然也存在错误率。(如:当数组全部为1时,那么查找什么都是存在的),但是这个错误率的大小,取决于数组的位数和哈希函数的个数。
安装:pip3 install pybloom_live
# pybloom_live 依赖这个包,直接安装 pybloom_live 会自动下载依赖的包
pip3 install bitarray-0.8.1-cp36-cp36m-win_amd64.whl
# ScalableBloomFilter 可以自动扩容
from pybloom_live import ScalableBloomFilter
# 错误达到 0.001 自动扩容
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)
# BloomFilter 是定长的
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 pybloom_live import ScalableBloomFilter
from scrapy.dupefilters import RFPDupeFilter
class MyRFPDupeFilter(RFPDupeFilter):
bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
fingerprints = bloom
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + '\n')
return False
DUPEFILTER_CLASS = 'myfirstscrapy.my_filter.MyRFPDupeFilter'
pip3 install scrapy-redis
from scrapy_redis.spiders import RedisSpider
class CnblogSpider(RedisSpider):
name = 'cnblog_redis'
allowed_domains = ['cnblogs.com']
# 写一个key:redis列表的key,起始爬取的地址
redis_key = 'myspider:start_urls'
# 分布式爬虫配置
# 去重规则使用redis
REDIS_HOST = 'localhost' # 主机名
REDIS_PORT = 6379 # 端口
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化:文件,mysql,redis
ITEM_PIPELINES = {
'cnblogs.pipelines.CnblogsFilePipeline': 300,
'cnblogs.pipelines.CnblogsMysqlPipeline': 100,
'scrapy_redis.pipelines.RedisPipeline': 400,
}
lpush myspider:start_urls value http://www.cnblogs.com/