碎碎念 其实学scrapy已经有一段时间了,前些日子写论文的时候,要用到这一块知识点,所以就顺便捡起来再记下来。写的只是一部分,更加全面的要移步去官方那里噢。
官方文档传送门
scrapy文档
简介 Scrapy 是一个快速的高级网页抓取和网页抓取框架,用于抓取网站并从其页面中提取结构化数据。 它可用于广泛的用途,从数据挖掘到监控和自动化测试。
基本架构 Scrapy Engine(引擎):负责Spider、Downloader、Scheduler、Item Pipeline的通讯、数据传递。 Spider(爬虫):负责处理Response并从中提取数据,然后将提取的URL交给引擎。 Scheduler(调度器):接收引擎发送过来的request请求,并将其进行排列入队。 Downloader(下载器):下载引擎发送的request请求,并将获得的response交给引擎 Item Pipeline(管道):处理从spider中获取的item(过滤、存储) Downloader Middlewares(下载中间件):可扩展的下载组件 Spider Middlewares(爬虫中间件):可扩展的下载组件 工作流程 画的可能有点花,不要介意
1.爬虫(起始的url构造成request对象)→爬虫中间件→引擎→调度器
2.调度器(把request排序入队,将处理的request)→引擎→下载中间件→下载器
3.下载器(发送请求并将获取的response响应)→下载中间件→引擎→爬虫中间件→爬虫
4.爬虫(提取url,组成新的request对象)→爬虫中间件→引擎→调度器(这里将重复步骤2)
5.爬虫(提取数据)→引擎→管道(处理和保存数据)
结构使用 Item Pipeline 当爬虫的数据存储到item之后就会被送到Item Pipeline,通过一些组件依次处理Item。
每个Item Pipeline组件是一个实现简单方法的python类,它们接收到Item并通过它执行一些行为,同时决定它是否继续通过pipeline或者被丢弃不再执行处理。
Item Pipeline的一些典型应用:
清理HTML数据 验证爬取的数据(检查items容器字段) 查重(并丢弃) 存储爬取的数据并保存到数据库中 编写Item Pipeline
每个Item Pipeline组件时一个独立的python类,同时必须实现以下方法:
这个方法当spider被执行时调用
当spider被关闭时调用
Logging 日志功能,它分为五个层次的logging级别:
CRITICAL - 严重错误 ERROR - 一般错误 WARNING - 警告信息 INFO - 一般信息 DEBUG - 调试信息 Items 首先先定义我想要抓取的内容
1
2
3
4
5
6
7
import scrapy
class Product (scrapy. Item):
# 商品名称
name = scrapy. Field()
#商品价格
price = scrapy. Field()
Field()类
Field对象指明每个字段的元数据。**注意:用来声明item的Field对象并没有被赋值class属性,不能通过item.attr去访问,但可以用item.fields属性进行访问。**即,Field类只不过是内置字典类(dict)的一个别名,并没有提供额外的方法和属性。
settings Scrapy设置允许自定义所有Scrapy组件,包括核心,插件,pipeline和spider。
settings的参数分为五个级别(从高到低排序):
命令行选项(最高级) 单独爬虫设定模块(即每个spider的Settings) 项目设定模块(项目的Settings) 命令默认设定 全局默认设定(默认的全局Settings) 1.命令行选项
使用命令行听过的参数拥有最高的优先级,会覆盖所有其他方式设置的Settings选项。你可以用**-s或者 –set**来明确指定覆盖一个或多个Settings
1
scrapy crawl myspider - s LOG_FILE= scrapy. log
2.每个spider的Settings
在每个spider中,可以定义这个spider所特有的settings。在spider类中定义suctom_settings属性
当一个项目有多个spider,每个spider需要设置的参数是不一样的,举个例子,spider1要用pipeline1将数据放到MySQLl中,spider2用pipeline2将数据放到MongoDB中,这时我们要对两个spider分开进行pipeline配置,此时在spider中添加custom_settings属性
1
2
3
4
5
class MySpider (scrapy. Spider):
name = 'spider1'
custom_settings = {
'ITEM_PIPELINES' : {'myproject.pipelines.pipeline1' : 301 },
}
1
2
3
4
5
class MySpider (scrapy. Spider):
name = 'spider2'
custom_settings = {
'ITEM_PIPELINES' : {'myproject.pipelines.pipeline2' : 301 },
}
3.项目的Settings
项目的Settings是Scrapy项目的标准配置文件,在settings.py
这个文件中添加或者修改配置的字段。
4.命令默认设定
每个 Scrapy tool 命令拥有其默认设定,并覆盖了全局默认的设定。 这些设定在命令的类的 default_settings 属性中指定。
5.全局默认设定
默认的全局变量设定在scrapy.settings.default_settings模块里。
常用的settings参数
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
CONCURRENT_REQUESTS = 16 # 全局最大并发数
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 单个域名最大并发数,如果下一个参数设置非0,此参数无效
CONCURRENT_REQUESTS_PER_IP = 0 # 单个ip最大并发数
COOKIES_ENABLED = True # 默认启用cookie
DEFAULT_REQUEST_HEADERS = { # 设置默认请求头
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' ,
'Accept-Language' : 'en' ,
}
DOWNLOAD_DELAY = 0 # 下载延时,高并发采集时设为0
DOWNLOAD_TIMEOUT = 180 # 超时时间设置,一般设置在10-30之间
LOG_ENABLED = True # 启用日志
LOG_STDOUT = False # 将进程所有的标准输出(及错误)重定向到log中,默认False。如果开启,在项目中的print方法也会以log的形式输出
LOG_LEVEL = 'DEBUG' # 日志输出级别,上线后至少使用info级别
LOG_FILE = None # 将日志输出到文件中
LOGSTATS_INTERVAL = 60.0 # 吞吐量输出间隔,就是输出每分钟下载多少个页面、捕获多少个item的那个,默认每分钟输出一次,自主配置
REDIRECT_ENABLED = True # 默认开启页面跳转,一般选择关闭
RETRY_ENABLED = True # 默认开启失败重试,一般关闭
RETRY_TIMES = 2 # 失败后重试次数,默认两次
RETRY_HTTP_CODES = [500 , 502 , 503 , 504 , 522 , 524 , 408 ] # 碰到这些验证码,才开启重试
ROBOTSTXT_OBEY = False # 遵守网站robot协议,一般不遵守
DOWNLOADER_MIDDLEWARES = { # 下载中间件
'myproject.middlewares.MyDownloaderMiddleware' : 543 ,
}
ITEM_PIPELINES = { # 数据处理、存储pipeline
'myproject.pipelines.MyPipeline' : 300 ,
}
实战 简单的实战一下,这里拿来练习的网站是苏宁图书
创建项目 win+R 进入cmd后输入以下命令:
1
scrapy startproject suningBook
将会看到这样子的结果(表示项目创建成功):
1
2
3
4
5
6
7
D:\Scrapy Project> scrapy startproject suningBook
New Scrapy project 'suningBook' , ..... (此处省略) created in :
D:\Scrapy Project\suningBook
You can start your first spider with :
cd suningBook
scrapy genspider example example. com
创建虫子 项目创建之后再创建一只虫子(spider):
命令:
1
scrapy genspider [spider文件命名] [爬取的网站域名]
过程如下:
1
2
3
4
5
D:\Scrapy Project> cd suningBook
D:\Scrapy Project\suningBook> scrapy genspider books book. suning. com
Created spider 'books' using template 'basic' in module:
suningBook. spiders. books
Settings设置 1
2
3
4
5
6
7
8
9
#在settings.py编辑
# 不遵循Robot协议
ROBOTSTXT_OBEY = False
# 设置请求头
DEFAULT_REQUEST_HEADERS = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
# 输出的结果只显示要print的内容
LOG_LEVEL = 'WARNING'
明确需求 首先要明确想要爬取的数据是哪些然后再去分析网页,不然需求都不清楚就跑去分析网页,就是瞎搞。
那么我想爬取的数据是一个分类 页面下的所有书本信息 。
分析网页 这个网站存二级分类,一级如下图的”文学艺术“,二级为”小说“、”散文随笔“等
在网站的首页右键→检查
一级二级分类这些元素都在一个div标签中class="ment-item"
里面,一级分类的内容在标签里面,二级分类内容在标签里面,只要明确了最外层装着这些我们想要的内容,顺藤摸瓜就能找到目标数据的定位。
定位一级分类 1
2
3
4
5
6
7
8
9
#在books.py这个爬虫文件里面
def parse (self, response):
# 一级分类(L1表示Lecel 1)
L1_list = response. xpath('//div[@class="menu-list"]/div/dl' )
# 获取大分类
for L1 in L1_list:
item = {}
item['L1_cate' ] = dl. xpath('./dt/h3/a/text()' ). extract_first()
print (item)
这时候执行启动爬虫
1
2
# 执行命令
scrapy crawl [爬虫文件名称]
结果如下:
1
2
3
4
5
6
7
8
9
10
D:\S crapy Project\s uningBook>scrapy crawl books
{ 'L1_cate' : '文学艺术' }
{ 'L1_cate' : '少儿' }
{ 'L1_cate' : '人文社科' }
{ 'L1_cate' : '经管励志' }
{ 'L1_cate' : '健康生活' }
{ 'L1_cate' : '考试教育' }
{ 'L1_cate' : '科技' }
{ 'L1_cate' : '进口原版书' }
{ 'L1_cate' : '期刊杂志' }
定位二级分类 既然一级分类已经获取到了,那么自然二级分类也是用同样的方法去解决
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
#在books.py这个爬虫文件里面
def parse (self, response):
# 一级分类(L1表示Lecel 1)
L1_list = response. xpath('//div[@class="menu-list"]/div/dl' )
# 获取大分类
for L1 in L1_list:
item = {}
item['L1_cate' ] = L1. xpath('./dt/h3/a/text()' ). extract_first()
#从这里接着写
# 获取二级分类
L2_list = L1. xpath('./dd/a' )
for L2 in L2_list:
# 二级分类url
item['L2_href' ] = L2. xpath('./@href' ). extract_first()
# 二级分类名称
item['L2_name' ] = L2. xpath('./text()' ). extract_first()
yield scrapy. Request(
url= item['L2_href' ],
callback= self. parse_book_list,#这行代码是进入二级URL的
meta= {'item' : deepcopy(item)}
)
# 进入二级URL
def parse_book_list (self, response):
item = response. meta. get('item' )
print (item)
运行结果
1
2
3
4
5
{'L1_cate' : '文学艺术' , 'L2_href' : 'https://list.suning.com/1-502320-0.html' , 'L2_name' :'小说' }
{'L1_cate' : '文学艺术' , 'L2_href' : 'https://search.suning.com/ %E 6 %95% A3 %E 6 %96% 87 %E 9%9A %8F%E 7%AC%94/' , 'L2_name' : '散文随笔' }
{'L1_cate' : '文学艺术' , 'L2_href' : 'https://list.suning.com/1-502348-0.html' , 'L2_name' :'青春文学' }
.....
#以下省略
拿到了二级分类的url和名称,这样就让虫子爬取二级分类的url,恰好书籍信息在二级url的页面里。类似无限的取出套娃。
书籍列表页 进入二级URL之后的工作就是分析书籍列表页面,并获取每一本书的信息。先分析页面,发现:
每本书的信息在
标签里,而这些 标签又全都在这个标签里,那么获取每本书的连接,详情,图片连接的代码可以这样写 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def parse_book_list (self, response):
item = response. meta. get('item' )
#获取每一本书的信息
books_list = response. xpath('//ul[@class="clearfix"]/li' )
for book in books_list:
#书籍详情URL
item['book_href' ] = book. xpath('//div[@class="res-img"]/div/a/@href' ). extract_first()
# 书籍名字
item['book_name' ] = book. xpath('//div[@class="res-img"]/div/a/img/@alt' ). extract_first()
# 书籍图片
item['src' ] = book. xpath('//div[@class="res-img"]/div/a/img/@src2' ). extract_first()
print (item)
yield scrapy. Request(
url= item['book_href ' ],
callback= self. parse_book_detail,
meta= {'item' : deepcopy(item)}
)
结果如下:
1
2
3
{'L1_cate' : '文学艺术' , 'L2_href' : 'https://search.suning.com/ %E 7%A7 %91% E5%B9%BB/' , 'L2_name' : '科幻' , 'book_href' :'//product.suning.com/0070129646/12181637004.html' , 'book_name' : '绿野仙踪正版书小学生正版 阅读励志7-9-12岁青少年版儿童文学书籍名著三年级四年级课外书书读物' ,'src' :'//imgservice3.suning.cn/uimg1/b2c/image/ZbF_iVDMfL1hCj6diWLkOg.jpg_220w_220h_4e' }
#省略
......
书本详情页面 每一本书的详情URL拿到了,那就从URL入手,拿到书籍的详情信息(包括作者,出版社)
1
2
3
4
5
6
7
8
9
def parse_book_detail (self, response):
item = response. meta. get('item' )
# 作者
item['author' ] = response. xpath('//ul[@class="bk-publish clearfix"]/li[1]/text()' ). extract_first()
# 出版社
item['publish' ] = response. xpath('//ul[@class="bk-publish clearfix"]/li[2]/text()' ). extract_first()
# 数据处理,采集到的数据有些多余的空格,需要去除
item['author' ] = item['author' ]. replace(' \n ' , '' ). replace(' \t ' , '' ). replace('</span>' , '' )
item['publish' ] = item['publish' ]. replace(' \n ' , '' ). replace(' \t ' , '' ). replace('</span>' , '' )
ok,至此暂时算是能够把书籍信息采集到,但是问题是,数据量有点少,还有我们只采集当前页面,并没有对书籍列表页面进行翻页。那接下来要做的就是解决翻页问题
翻页 翻页的时候,观察url变化规律:
1
2
3
4
5
6
7
# 第一页
https:// list . suning. com/ 1 - 502687 - 0. html
# 第二页
https:// list . suning. com/ 1 - 502687 - 0. html#second-filter
# 第三页
https:// list . suning. com/ 1 - 502687 - 0. html#second-filter
....
发现并没有什么规律,那只能去看看network了
每翻一页,showProductList文件中的Request URl值都不一样,如下:
1
2
3
4
5
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 0 & il= 0 & iy= 0 & adNumber= 0 & n= 1 & ch= 4 & prune= 0 & sesab= ACBAABC& id = IDENTIFYING& paging= 1 & sub= 0
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 1 & il= 0 & iy= 0 & adNumber= 0 & n= 1 & ch= 4 & prune= 0 & sesab= ACBAABC& id = IDENTIFYING& cc= 020
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 2 & il= 0 & iy= 0 & adNumber= 0 & n= 1 & ch= 4 & prune= 0 & sesab= ACBAABC& id = IDENTIFYING& cc= 020
好像很乱还看不出什么规律,试试将一些参数删除掉会不会影响页面访问
整理如下:
1
2
3
4
5
6
# 第一页
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 0
# 第二页
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 1
# 第三页
https:// list . suning. com/ emall/ showProductList. do?ci= 502320 & pg= 03 & cp= 2
规律出来了! 观察出ci代表分类代号,cp后面跟着的就是页码。ci后面的参数可以在parse方法中获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 一级分类(L1表示Lecel 1)
L1_list = response. xpath('//div[@class="menu-list"]/div/dl' )
# 获取大分类
for L1 in L1_list:
item = {}
item['L1_cate' ] = L1. xpath('./dt/h3/a/text()' ). extract_first()
# 获取二级分类
L2_list = L1. xpath('./dd/a' )
for L2 in L2_list:
# 二级分类url
item['L2_href' ] = L2. xpath('./@href' ). extract_first()
# 二级分类名称
item['L2_name' ] = L2. xpath('./text()' ). extract_first()
# 获取ci
item['ci' ] = item['L2_href' ][26 :32 ]
# 翻页url
item['next_url' ] = 'https://list.suning.com/emall/showProductList.do?ci=' + item['ci' ] + '&pg=03&cp= {} '
yield scrapy. Request(
url= item['L2_href' ],
callback= self. parse_book_list,
meta= {'item' : deepcopy(item)}
)
在parse_book_list里面执行翻页代码
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
def parse_book_list (self, response):
item = response. meta. get('item' )
#获取每一本书的信息
books_list = response. xpath('//ul[@class="clearfix"]/li' )
for book in books_list:
#书籍详情URL
item['book_href' ] = 'https:' + book. xpath('//div[@class="res-img"]/div/a/@href' ). extract_first()
# 书籍名字
item['book_name' ] = book. xpath('//div[@class="res-img"]/div/a/img/@alt' ). extract_first()
# 书籍图片
item['src' ] = 'https:' + book. xpath('//div[@class="res-img"]/div/a/img/@src2' ). extract_first()
yield scrapy. Request(
url= item['book_href' ],
callback= self. parse_book_detail,
meta= {'item' : deepcopy(item)}
)
# 翻页
# 这里的页数是写死的,也可以抓取网页的pageNumber(总页数)
for i in range (5 ):
next_url = item['next_url' ]. format(i+ 1 )
yield scrapy. Request(
url= next_url,
callback= self. parse_book_list,
meta= {'item' : deepcopy(item)}
)
这里差不多就写完了,我这里没有增加数据额外的数据清洗和数据处理(懒)
存储数据 把数据存到MySQl里面,代码就不必多讲了,看注释就能看得懂
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
38
39
40
41
42
43
44
45
46
47
48
#编辑pipelines.py文件
from itemadapter import ItemAdapter
import pymysql
import sshtunnel
class SuningbookPipeline :
server = sshtunnel. SSHTunnelForwarder(
('xxx.xxx.xxx.xxx' , 22 ), # 填写你服务器的IP
ssh_username= 'xxx' , # 连接的用户名
ssh_password= 'xxxx' , # 密码
remote_bind_address= ('xx.xx.xx.xx' , 3306 ), #也是服务器IP
local_bind_address= ('127.0.0.1' , 3306 )
)
server. start()
print ('SSH连接成功' )
def __init__(self):
self. connect = pymysql. connect(
host= '127.0.0.1' ,
port= 3306 , #MySQL端口
user= 'xxx' , #数据库用户名
database= 'xxx' , #数据库名
password= 'xxx' , #数据库密码
charset= 'utf8'
)
print ('mysql数据库连接成功' )
self. cursor = self. connect. cursor()
print ('游标获取成功' )
def process_item (self, item, spider):
info = """INSERT INTO Book(BookcName,Author,Publish) VALUES (' %s ',' %s ',' %s ')""" \
% (
item['book_name' ],
item['author' ],
item['publish' ],
)
self. cursor. execute(info)
self. connect. commit()
print ('insert succeed' )
return item
def close_spider (self,spider):
self. cursor. close()
self. connect. close()
实战就到这了,多动手写多观察,总是能学会的。
具体代码在这里👉传送门
结尾 不知不觉写了好多字。。。。