一、爬虫工程化
在之前的爬虫学习中基本已经掌握了爬虫这门技术的大多数技术点,但是我们现在写的代码还很流程化,很难进行商用,想要爬虫达到商用级别,必须要对我们现在编写的爬虫进行大刀阔斧式的重组,以达到工程化的爬虫,所谓工程化,就是让程序更加有体系,有逻辑,更加模块化。
爬虫工程化:对爬虫的功能进行模块化的开发,并达到可以批量生产的效果(不论开发还是数据产出)
二、scrapy简介
scrapy是一个用python编写的开源网络爬虫框架,用于高效地从网站上抓取信息并提取结构化数据。
特点:速度快、简单、可扩展性强。
三、scrapy的工作流程
引擎:scrapy的核心,所有模块的衔接,数据流程处理。
调度器:本质上这东西可以看成是一个队列,里面存放着一堆我们即将要发送的请求。可以看成是一个url的容器,它决定了下一步要爬取哪一个url,在这里可以对url进行去重操作。
下载器:它的本质就是一个发送请求的模块,返回的是response对象。
爬虫:这是我们要写的的一个个部分的内容,负责解析下载器返回的response对象,从中提取我们需要的数据。
管道:这是我们要写的第二部分的内容,主要负责数据的存储和各种持久化操作。
工作流程:
1、爬虫中起始的url构成的request对象,并传递给调度器。
2、引擎从调度器中获取到request对象,然后交给下载器
3、由下载器来获取到网页源代码,并封装成response对象,并回馈给引擎
4、引擎将获取到的response对象传递给spider,由spider对数据进行解析(parse),并回馈给引擎。
5、引擎将将数据传递给pipeline进行数据持久化保存或进一步的数据处理
6、在此期间如果spider中提取到的并不是数据,而是子页面url,可以进一步提交给调度器,进而重复步骤。
四、scrapy的安装
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple scrapy
查看是否安装成,可以在终端输入:
scrapy version
五、scrapy的使用
接下来,我们用scrapy来完成一个超级简单的爬虫,目标:深入理解scrapy的工作流程,以及各个模块之间是如何搭配工作的。
1、创建项目:
scrapy startproject 项目名称
示例:
scrapy startproject spider_test
创建好项目后,我们可以在pycharm中观察到scrapy帮我们创建了一个文件夹,结构如下:
以爬取4399小游戏的名称、类型为例子
# 首先进入项目
cd spider_test
# 创建爬虫项目
scrapy genspider xiao 4399.com # xaio代表名称后面跟的是域名
然后spiders文件夹下会生成一个xiao.py的文件,内容为:
import scrapyclass XiaoSpider(scrapy.Spider):name = "xiao" # 爬虫的名字allowed_domains = ["4399.com"] # 允许抓取的域名start_urls = ["https://www.4399.com/flash/"] # 起始页面urldef parse(self, response):# 该方法是用来处理解析的print(response)
运行该程序:
scrapy crawl xiao
但会发现有相当多的内容日志,我们可以在setting.py加上这段程序:
LOG_LEVEL = "WARNING"
# 日志的级别:DEBUG INFO WARNING ERROR CRITICAL(由低到高)设置成warning代表warning及以上的信息才能被打印
运行程序后,信息如下:
<200 https://www.4399.com/flash/>
(.venv) PS D:python学习python_studypythonProjectspider_test> import scrapyclass XiaoSpider(scrapy.Spider):name = "xiao" # 爬虫的名字allowed_domains = ["4399.com"] # 允许抓取的域名start_urls = ["https://www.4399.com/flash/"] # 起始页面urldef parse(self, response):# 该方法是用来处理解析的# print(response)# 拿到源代码# print(response.text)# 提取数据# text = response.xpath('//ul[@class="n-game cf"]/li/a/b/text()').extract()# print(text)# 分块解析数据li_list= response.xpath('//ul[@class="n-game cf"]/li')for li in li_list:name = li.xpath('a/b/text()').extract_first()category = li.xpath('em/a/text()').extract_first()time = li.xpath('em/text()').extract_first()dic = {"name":name,"category":category,"time":time}
对数据进行解析后,下一步就是对数据进行存储,这一步是在pipeline管道中进行的,需要用到yield将数据传递个管道。
yield dic # 可以节省内存,是因青睐调用的
因为管道默认是不生效的,需要在setting里手动开启管道
ITEM_PIPELINES = {# key就是管道的路径# value是管道的优先级,数值越小,优先级越高"spider_test.pipelines.SpiderTestPipeline": 300,
}
管道pipeline.py文件中打印数据和爬虫的名称:
class SpiderTestPipeline:def process_item(self, item, spider): # 处理数据的专用方法,item;数据,spider:爬虫print(item)print(spider.name)return item
对url进行代理,cookie以及UA等操作可以在爬虫和引擎之间或者引擎和下载器之间处理。
六、scrapy shell
scrapy shell是scrapy终端,是一个交互终端,可以在未启动spider的情况下尝试及调试爬取的代码。其本意是是用来测试提取数据的代码,不过可以将其是为正常的python终端,在上面测试任何的python代码。该终端是用来测试xpath或css表达式,查找他们的工作及爬取的网页中提取的数据,在编写spider时,该终端提供了交互性测试表达式代码的功能,免去了每次修改后运行spider的麻烦。
python_159">1、安装ipython
pip install ipython
ipython终端与其他相比更为强大,提供智能的自动补全,高亮输出,及其他特性。
2、应用
在ipython终端直接输入:
scrapy shell www.baidu.com
3、语法
(1)response对象
response.body(二进制文本)
response.text
response.url
response.stayus
(2)response解析
response.xpath()
使用xpath路径查询特定元素,返回selector列表对象
response.css()(bs4语法)
获取内容:response.css(‘#su::text’).extract_first()
获取属性:response.css(‘#su::attr(“value”)’).extract_first()
(3)selector对象
extract()
提取selector对象的值,如果提取不到,则会报错
使用xpath请求到的对象是一个selector对象,需要使用extract()方法拆包
extract_first()
提取selector列表中的第一个值,若提取不到返回空值
以获取百度网页百度一下为例:
scrapy shell www.baidu.com
response.xpath('//input[@id="su"]/@value').extract_first()
直接在终端输入即可,不需要输入ipython。
七、yield
1、带有yeild的函数不再是一个普通函数,而是一个生成器generator,用于迭代。
2、yield是一个类似于return的关键字,迭代一次遇到yield时就返回后面的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码开始执行。
简要理解:yield就是return一个返回值,并记住这个返回的位置,下次迭代就从这个位置后开始。
案例:当当网(1)yield (2)管道封装(3)多条管道下载(4)多页数据下载
首先需要在终端创建项目以及爬虫名称
scrapy startproject scrapy_dangdang
cd scrapy_dangdang
scrapy genspider dangdang www.dangdang.com
因为要爬取书的名称、图片以及价格,可以在items.py中自定义数据结构。
class ScrapyDangdangItem(scrapy.Item):# define the fields for your item here like:# name = scrapy.Field()# 图名src = scrapy.Field()# 名称name = scrapy.Field()# 价格price = scrapy.Field()
在dangdang.py中去执行解析数据并发送url给引擎
import scrapy
from scrapy_dangdang.items import ScrapyDangdangItemclass DangdangSpider(scrapy.Spider):name = "dangdang"allowed_domains = ["www.dangdang.com"]start_urls = ["https://category.dangdang.com/cp01.01.02.00.00.00.html"]def parse(self, response):# src = //ul[@id="component_59"]/li//img/@src# name = //ul[@id="component_59"]/li//img/@alt# price = //ul[@id="component_59"]/li//p[@class="price"]/span[1]/text()# 所有的seletor的对象,都可以再次调用xpath方法li_list = response.xpath('//ul[@id="component_59"]/li')for li in li_list:src = li.xpath('.//img/@data-original').extract_first() # 这里需要注意:除了第一条数据没有懒加载,其它均有懒加载if src:src = srcelse:src = li.xpath('.//img/@src').extract_first()name = li.xpath('.//img/@alt').extract_first()price = li.xpath('.//p[@class="price"]/span[1]/text()').extract_first()book = ScrapyDangdangItem(src=src, name=name, price=price)yield book # 需要在setting中手动开启pipeline管道
然后就需要保存数据,这个时候就用到了管道pipeline,我们需要在setting中手动打开管道。
ITEM_PIPELINES = {"scrapy_dangdang.pipelines.ScrapyDangdangPipeline": 300,
}
key就是管道的路径,value是管道的优先级,数值越小,优先级越高。
在pipeline中保存数据:
from itemadapter import ItemAdapterclass ScrapyDangdangPipeline:# 在爬虫文件执行之前,就执行的方法:def open_spider(self, spider):self.fp = open("book.json","w",encoding="utf-8") # 打开文件但并未关闭,所以w的模式写入def process_item(self, item, spider):# item就是我们的数据# with open("book.json", "a", encoding="utf-8") as f: # 这里需要采用追加的形式,因为数据是一条条传递来的,每一个对象都打开一次文件,从而导致数据被覆盖# f.write(str(item))# 但上述方法并不是最优解,因为我们对文件的操作过于频繁,,会有频繁的IO操作。这个时候我们通过两个方法来改进代码self.fp.write(str(item) )return item# 爬虫文件执行后,执行的方法。def close_spider(self,spider):self.fp.close()
多条管道下载,实现一边下载图片,一边下载json数据。
# setting文件中需要加上这条管道。因为图片下载慢,因此优先级就低一些
ITEM_PIPELINES = {"scrapy_dangdang.pipelines.ScrapyDangdangPipeline": 300,"scrapy_dangdang.pipelines.DangDangDownloadImg":301
}
# pipelines文件
import urllib.request
class DangDangDownloadImg:def process_item(self, item, spider):url = "http:" + item.get('src') # 这里需要注意的是:下载下来的路径前面无http,需要拼接filename = './books/'+item.get("name")+'.jpg'urllib.request.urlretrieve(url=url,filename=filename)return item
完成当当网的多页数据下载,因为每一页爬取的业务都是一样的,所以我们只需要将执行的那个页的请求再次调用parse方法即可。
import scrapy
from scrapy_dangdang.items import ScrapyDangdangItem
from scrapy.http import Request
class DangdangSpider(scrapy.Spider):name = "dangdang"# 如果十多页下载的话,那么必须要调整的是allowed_domins的范围,一般情况只写域名allowed_domains = ["category.dangdang.com"]start_urls = ["https://category.dangdang.com/pg1-cp01.01.02.00.00.00.html"]base_url = "https://category.dangdang.com/pg"page = 1def parse(self, response):# src = //ul[@id="component_59"]/li//img/@src# name = //ul[@id="component_59"]/li//img/@alt# price = //ul[@id="component_59"]/li//p[@class="price"]/span[1]/text()# 所有的seletor的对象,都可以再次调用xpath方法li_list = response.xpath('//ul[@id="component_59"]/li')for li in li_list:src = li.xpath('.//img/@data-original').extract_first() # 这里需要注意:除了第一条数据没有懒加载,其它均有懒加载if src:src = srcelse:src = li.xpath('.//img/@src').extract_first()name = li.xpath('.//img/@alt').extract_first()price = li.xpath('.//p[@class="price"]/span[1]/text()').extract_first()book = ScrapyDangdangItem(src=src, name=name, price=price)yield book # 需要在setting中手动开启pipeline管道if self.page < 100:self.page = self.page + 1url = self.base_url + str(self.page) + "-cp01.01.02.00.00.00.html"# 调用parse方法,callback表示要执行的函数yield scrapy.Request(url=url, callback=self.parse)
八、CrawlSpider
CrawlSpider可以定义规则,在解析html内容的时候,可以根据连接规则提出指定的链接,然后再向这些链接发送请求。所以,如果有需要跟进链接的需求,意思就是爬取了网页之后,需要提取链接再次爬取,使用CrawlSpider是非常合适的。
1、提取链接
提取链接器,在这里就可以写规则提取指定连接
scrapy.linkectractors.LinkExtractor(
allow = (), # 正则表达式,提取符合正则的链接
dengy = (), # (不用)正则表达式 不提取符合正则的链接
allow_domins = (), # (不用)允许的域名
restrict_xpaths = (), # xpath,提取符合xpath规则的链接
restrict_css = (), # 提取符合选择器规则的链接
2、模拟使用
正则用法:links = LinkExtractor(allow=r"list_23d+.html")
xpath语法:links = LinkExtractor(restrict_xpaths=r"//div[@class=‘x’]")
css语法:links = LinkExtractor(restrict_css=“.x”)
3、提取连接
link.extract_links(response)
案例:以爬取多页读书网数据为例(每一页数据的结构是相似的),并存储到数据库中。
scrapy shell https://www.dushu.com/book/1107.html
# 导包
In [1]: from scrapy.linkextractors import LinkExtractor
# 通过re正则表达式提取数据
In [2]: link = LinkExtractor(allow=r'/book/1107_d+.html')
# 提取连接
In [3]: link.extract_links(response)
注意事项:
1、callback只能写函数名字符串,callback=“parse_item”
2、在基本的spider中,如果重新发送请求,那么callback写的是 callback=self.parse_item follow=true 是否跟进就是按照提取连接规则进行提取
代码复现:
# 在终端创建项目
scrapy stratproject scrapy_readbook
# 进入根目录
scrapy startproject scrapy_readbook
# 创建爬虫文件这里是有所不同的,需要注意
scrapy genspider -t crawl read www.dushu.com/book/1107.html
上面是原始的read爬虫文件,还是有所不同的。
把数据存储在MySQL中,这里可能需要一些MySQL以及pymysql的知识。可以根据情况进行学习。这里我们只需要在pipelines文件中写主要的存储逻辑,并且在settings文件中进行多管道下载:
# settings文件
DB_HOST = "localhost"
DB_PORT = 3306
DB_USER = "root"
DB_PASSWORD = "0219423"
ITEM_PIPELINES = {"scrapy_readbook.pipelines.ScrapyReadbookPipeline": 300,"scrapy_readbook.pipelines.PyMysqlPipeline":301
}
# pipelines文件
# 加载settings文件
from scrapy.utils.project import get_project_settings
from pymysql import Connection
class PyMysqlPipeline:def open_spider(self, spider):settings = get_project_settings()self.host = settings["DB_HOST"]self.port = settings["DB_PORT"]self.user = settings["DB_USER"]self.password = settings["DB_PASSWORD"]self.connect()def connect(self):self.conn = Connection(host=self.host,port=self.port,user=self.user,password=self.password)self.cursor = self.conn.cursor()self.conn.select_db("test")def process_item(self, item, spider):sql = 'insert into spider(name,src) values("{}","{}")'.format(item['name'], item['src'])self.cursor.execute(sql)self.conn.commit()return itemdef close_spider(self, spider):self.cursor.close()self.conn.close()
九、scrapy的post请求
我们还是以百度翻译为例,在这里我就只写出逻辑执行的文件:
import scrapy
import jsonclass PostSpider(scrapy.Spider):name = "post"allowed_domains = ["fanyi.baidu.com"]# post请求 如果没有参数 那么这个请求将没有任何意义,所以start_urls也就没有用了,从而parse方法也无用# start_urls = ["https://fanyi.baidu.com/sug"]# def parse(self, response):# passdef start_requests(self):url = "https://fanyi.baidu.com/sug"data={"kw": "spider"}yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse_second)def parse_second(self, response):content = response.textobj = json.loads(content)print(obj)