基于Python Scrapy框架车型库爬虫系统的设计与实现
网络与继续教育学院 计算机科学与技术专业
(xx 19220190xx 指导教师:廖晓东)
【摘要】本文主要是对汽车消费网的汽车型号库按照A-Z的字母顺序对汽车品牌以及型号款式价格进行爬取并保存到mysql数据库,其中利用了python对js代码的编译读取保存所有汽车类型的ID,继而使用scrapy爬虫框架对没有分页也没有任何拼接规律的网站全站抓取提供了一种思路,有一定的参考价值。
【关键词】Scrapy爬虫框架;车型库;Python爬虫;
1 概述(summary)
现如今汽车品牌众多,人们通过搜索引擎可以检索具体的车型号进行了解具体详情,本文主要通过python的scrapy框架对汽车的型号库通过汽车消费网进行抓取,原本打算使用requests库对网站进行数据抓取,分析了一番才发现此网站并没有所谓的分页,索引目录也是一个js文件定义的对象动态加载,这引起了我的极大兴趣,众所周知scrapy在有分页以及可以有规律的拼接url进行数据采集是十分方便的,但对于这种没有规则的网站应该怎样去采集信息呢,本着知难而上的态度特意花了一点时间来了解此网站的架构流程,解决问题的思路很重要,这是我以scrapy技术栈来写这篇文章的来由,就是为了去分析对于一些没有规律的网站应该怎样用scrapy去做数据采集。
2 目标网站分析(Target website analysis)
这里以汽车消费网为例进行分析,目标网站 http://www.315che.com/
2.1 网站架构流程
网站进入可以看到有关于汽车的各种品牌以及类型展示,进入到具体的详情页可以查看到左侧网页有按字母显示的车型库索引。如图1所示:
图1 按字母索引的web元素
通过点击不同汽车品牌可以查看到具体更细分的当前品牌子类,点击子类可以查看当前分类汽车的型号以及款式,但是没有分页信息,只能通过左侧的字母索引表来进行点击查询。点击索引的最内层车系列的名字可以查看到具体的类型信息。如果我们按照从A-Z汽车品牌的方式对网页链接遍历就可以得到我们想要的所有数据。
2.2 网站源码分析
我们查看源码发现字母索引的列表并不存在,通过chrome的开发者工具可以查看到网页的html元素以及加载进来的各种资源,最终我们发现一个叫brandcar_price.js产生了这些品牌索引,如果我们想要使用scrapy对这些数据进行爬取,首先就必须把这个js返回的对象列表拿到。Js文件中定义了一个变量对象,大致的格式如图2所示
图2 brandcar_price.js文件内容
结合js文件和chrome开发者工具查看对应的链接可以得知对应的模版各个key、value所充当的作用含义。
这里列举一个子对象作为剖析,67783是当前汽车品牌的id,北汽瑞丽是对应汽车的品牌,而品牌名字对应的value是一个列表对象,保存了当前品牌生产的有哪些系列的汽车,当中几个key的含义我们多看几个页面即可简单的知道,CD字段是当前系列的id,CE字段是当前系列的名称,CN代表了当前系列的分类(比如哈佛H4,H6,H7,H9都属于同一个CN),所有品牌的某个品牌的某个系列都可以通过http://price.315che.com/series/系列ID-0-0.htm 这种拼接URL的方式去获取详情。在详情页当中我们同样可以获取到汽车品牌、分类、系列、某年的什么款式、变速箱类型、价格等字段信息。
到这里,我们基本就可以得出我们可以通过提取js文件当中所有汽车系列的id去拼接URL然后通过scrapy去爬取我们需要的字段信息了。
3 系统设计思路与数据表结构(System design idea and data table structure)
通过我们的简单分析即可得知想要使用scrapy进行爬取首先是要提取js中的字段,拿到其中最重要的CD字段,也就是当前系列的id用来拼接url进行爬取,保存这些字段数据的方式有很多,可以直接访问url直接for循环遍历这些列表,也可以保存到redis这种非关系型数据库中,我这里为了后期扩展某些功能选择了保存到MySQL中,因此需要先设计一下需要数据表的字段。
3.1 数据表的设计
这里一共需要设计两张数据表,其中一张保存js对象中各个汽车系列各个字段信息的表,我们可以简单的命名为car。另外一张数据表我们用来保存scrapy爬虫爬取的某款汽车的详细信息,我们把数据表定义为carinfo。
每张表的数据结构详情如下:
字段名 | 类型 | 说明 |
Id | Int(11) | 主键ID |
Cd | Varchar(20) | 汽车系列ID |
Ce | Varchar(255) | 具体型号名字 |
Cn | Varchar(40) | 汽车系列分类 |
Car_category | Varchar(255) | 汽车分类 |
Parent_id | Varchar(40) | 汽车品牌ID |
cs | Varchar(30) | 未知 |
other | Varchar(255) | 备注信息 |
表1 car数据表结构
字段名 | 类型 | 说明 |
Id | Int(11) | 主键ID |
Brand | Varchar(80) | 汽车品牌 |
Brand_type | Varchar(150) | 品牌子分类 |
Car_name | Varchar(100) | 汽车型号 |
Car_series | Varchar(255) | 型号哪一个系列 |
gearbox | Varchar(100) | 变速箱类型 |
Guide_price | Varchar(50) | 指导价 |
Reference_price | Varchar(50) | 参考价 |
Source_id | Varchar(30) | 来源id |
表2 carinfo数据表结构
3.2 scrapy爬虫系统框架的设计
我们定义好了数据表就可以用来存放我们需要的数据了,下一步我们需要先把js当中的数据保存到mysql数据库中,做好了这些前期的准备才能为我们scrapy爬虫提供循环遍历的数据。
3.2.1 使用python的execjs库来执行js读取数据到mysql
在做爬虫系统之前我们需要先把js数据保存到数据库,这就需要我们了解一下python怎么处理js代码了。我们需要execjs库来编译执行js代码,需要先pip install execjs来安装并使用,使用很简单,将js代码的变量brandcarObj读取到python代码中使用变量data引用,并循环遍历其中的每条数据将其保存到数据库中,核心代码如下:
# 引入requests来访问js文件,execjs用来编译读取js代码
import requests,execjs
# sql是自己封装的一个数据库类接口
from sql import *
# 中间省去了获取js代码的步骤,这里以直接读取js文件为例
with open(“car.js”,encoding=”utf-8″) as f:
ss=f.read()
# 将读取的js文件内容ss进行编译运行
js=execjs.compile(ss)
# 读取js环境中的brandcarObj对象,并赋值给data引用
data=js.eval(“brandcarObj”)
k={}
# 删除无用子元素brandmap
brandmap=data.pop(“brandmap”)
# 初始化数据库连接
k=Sql(“car”)
for key1,val1 in data.items():
#最外层key是id,val是某个品牌对象
for key2,val2 in val1.items():
#这里的val1的key2是汽车品牌名字,val2是数组对象包含所有自分类
for val3 in val2:
#这里的val2的key3是汽车型号名字比如xx新能源,val3是具体的车分组数组信息
# 添加车的父id等信息到字典
val3[“car_category”]=key2
val3[“parent_id”]=key1
sql=”insert into car (cd,ce,cn,car_category,parent_id,cs) values (%s,%s,%s,%s,%s,%s);”
# 保存到mysql中
k.execute(sql,(val3[“CD”],val3[“CE”],val3[“CN”],val3[“car_category”],val3[“parent_id”],val3[“CS”]))
print(key2)
k.close()
print(“数据添加成功”)
代码执行完成就完成了所有汽车型号信息的提取,共此部分中要给出系统的条汽车系列型号。如图3所示
图3 car汽车分类表倒序预览
现在所有汽车品牌的每一个系列的汽车关键数据我们都有了,之后做爬虫只用把cd字段值拿出来做url拼接即可进行爬取,下一步我们开始做scrapy爬虫系统设计。
3.2.2 scrapy爬虫系统的思路分析
此刻我们拥有了所有的汽车id,但是我们知道scrapy的爬虫是异步方式,并且我们的目标网站并没有下一页来给我们分析,我们只能通过目前car数据表中的cd字段进行爬取。
试想一下,我们使用scrapy创建一个项目并新建了一个爬虫,假设我们从数据库中的第一条信息开始爬取,那么在scrapy的self.parse解析函数中解析完了第一条数据怎么才能让爬虫自动调度去yield 异步访问下一条url呢。可以明确的说不能采用数据表加字段判断是否scrapy爬虫访问过就改状态这种方式,因为scrapy基于Twisted异步网络的特性,判断字段状态是否访问往往会出现数据延时导致的数据混乱,我们需要通过一种新的思路去考虑怎么去解决。
俗话说,解决问题的方式有很多种,这里我提供了一种思路。就是使用redis缓存一个key用来记录下一个将要访问的汽车系列id,如果访问到当前页面,就根据当前汽车系列id查询到car数据表对应的表主键id,获取到主键id对其+1接着反查下一个对应的汽车系列id,并将值保存到redis的缓存key中,如此就解决了scrapy没有分页也可以进行循环遍历的方式了。
梳理清楚以上思路,基本上我们的爬虫系统就成型了,下一步,创建scrapy车型库爬虫系统!
4 scrapy爬虫系统的开发与实现(Development and implementation of the crawler system)
4.1 创建scrapy项目和爬虫
我们首先安装scrapy,执行是pip install scrapy。如果安装失败需要升pip版本,pip install –upgrade pip。 再安装scrapy框架,pip install scrapy[1]。
我们使用Scrapy startproject cartype来创建一个项目,名字为cartype
命令执行完成之后会生成一个cartype目录,通过cd命令进入cartype目录执行scrapy genspider car “315che.com”,来创建一个名字为car的爬虫并指定域范围为315che.com。
执行完成上面之后,项目的基本架构就创建好了,如图4所示
图4 项目cartype的文件布局
我们首先根据之前分析我们要采集的数据字段在items中定义响应的字段类型,方便在scrapy中传递给管道,items.py代码如下:
class CartypeItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
brand=scrapy.Field()
brand_type=scrapy.Field()
car_name=scrapy.Field()
car_series=scrapy.Field()
gearbox=scrapy.Field()
guide_price=scrapy.Field()
reference_price=scrapy.Field()
source_id=scrapy.Field()
在cartype/cartype/spiders目录中,sql.py是我们引入pymysql用来操作数据库用来封装的接口文件,car.py是我们爬虫的核心文件。
基于我们上篇的思路分析,car.py爬虫的代码如下:
import scrapy
from cartype.items import CartypeItem
from cartype.spiders.sql import Sql
import redis
pool = redis.ConnectionPool(host=’localhost’, port=6379, decode_responses=True)
r = redis.Redis(host=’localhost’, port=6379, decode_responses=True)
firstid=”72330″
# 设置redis的curcarid为72330
r.set(“curcarid”,firstid)
def nextid(curcarid):
”’根据传入的curcarid来返回下一个要请求的carid 初始72330”’
k=Sql(“car”)
sql=”select * from car where cd=%s” % curcarid
data=k.query(sql)
curid=data[0][“id”]
#得到curid主键查看下一个主键id的对应carid
sql=”select * from car where id=%s” % str(curid+1)
# print(sql)
nextcarid=k.query(sql)[0][“cd”]
if not nextcarid:
return None
return nextcarid
class CarSpider(scrapy.Spider):
name = ‘car’
allowed_domains = [‘315che.com’]
start_urls = [‘http://price.315che.com/series/’+firstid+’-0-0.htm’]
def parse(self, response):
if response.status==200:
item=CartypeItem()
item[“brand”]=response.xpath(‘//div[@class=”breadnav mini-breadnav”]//a[3]/text()’).extract_first()
item[“brand_type”]=response.xpath(‘//div[@class=”breadnav mini-breadnav”]//a[4]/text()’).extract_first()
item[“car_name”]=response.xpath(‘//div[@class=”breadnav mini-breadnav”]//span[last()]/text()’).extract_first()
# 下面一个车型有很多款式需要做循环了
list_series=response.xpath(‘//div[@class=”series-type-list mb60″]//dl/dd’)
curcarid=r.get(“curcarid”) #当前网页的汽车id
if list_series:#如果不为空就遍历
for one_series in list_series:
# 一个款式的提取数据
# 具体哪一款的名字
item[“car_series”]=one_series.xpath(‘.//div[@class=”col1″]/a/text()’).extract_first()
# 变速箱的类型
item[“gearbox”]=one_series.xpath(‘.//div[@class=”col2″]//text()’).extract_first()
# 官方指导价
item[“guide_price”]=one_series.xpath(‘.//div[@class=”col3″]//text()’).extract_first()
# 市场参考价
item[“reference_price”]=one_series.xpath(‘.//div[@class=”col4″]/span//text()’).extract_first()
# 上面数据已经提取完毕,可以进行yield
item[“source_id”]=curcarid
yield item
# 开始遍历到下一个车型id
nextcarid=nextid(curcarid)
print(nextcarid)
if nextcarid:
nexturl=’http://price.315che.com/series/’+nextcarid+’-0-0.htm’
r.set(“curcarid”,nextcarid)
yield scrapy.Request(nexturl,callback=self.parse)
else:
print(curcarid)
print(“数据采集完毕,已经到最后一个”)
return “ok”
在car.py的文件中,parse解析函数解析出每一个系列的款式汽车详情数据,将其不中断返回到pipelines.py管道文件中,由管道文件负责将数据保存到carinfo数据表中,在parse函数的最后,如果redis中缓存的key还有值,那么就往下一个主键id继续遍历,直到所有的数据全部爬取完成。
pipelines.py管道文件负责将传送过来的文件保存到mysql数据库中,代码如下:
from itemadapter import ItemAdapter
import pymysql
class CartypePipeline:
def __init__(self):
self.db = pymysql.connect(host=”127.0.0.1″, user=”root”, password=”root”, database=”benty”, charset=’utf8′ )
def process_item(self, item, spider):
data=dict(item)#将传来的item字段转为字典
print(data)
cursor=self.db.cursor()
sql=”INSERT INTO `carinfo` (`id`, `brand`, `brand_type`, `car_name`, `car_series`, `gearbox`, `guide_price`, `reference_price`,`source_id`) VALUES (NULL, %s, %s, %s, %s, %s, %s, %s,%s)”
try:
cursor.execute(sql,[data[‘brand’],data[‘brand_type’],data[‘car_name’],data[‘car_series’],data[‘gearbox’],data[‘guide_price’],data[‘reference_price’],data[‘source_id’]])
self.db.commit()
print(“添加到数据库成功”)
except:
self.db.rollback()
print(sql)
print(“sql错误——————————————“)
cursor.close()
#添加的url的最后id
# r.sadd(spider.name,re.findall(r’jobId=(\d+)’,data[‘source_url’])[0])
return item
def close_spider(self,spider):
self.db.close()
以上我们所有的爬虫都已经准备完成,再执行爬虫之前,我们需要在settings.py 中定义请求头等相关配置,启用管道。
接下来,让我们开始执行爬虫 scrapy crawl car
5 执行爬虫获取结果并分析(Execute crawler to get results and analyze)
我们执行上面爬虫之后就开始等待程序完成,并对结果进行分析,这里特别说一下settings.py中很重要且容易忽略几个配置项的作用说明。
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS这个参数是指爬虫最大执行的并发数,也就是最多一次同时爬取16个链接,再多就会添加到队列中进行等候,有空闲就按照一定的优先级进行出栈进行爬取。
DEPTH_PRIORITY = 1
DEPTH_PRIORITY配置项是scrapy爬取的爬取策略调整。Scrapy默认使用的是后进先出队列,基本可以看成是深度优先(DFO),当DEPTH_PRIORITY为正值时越靠广度优先,负值则越靠深度优先,默认值为0。说白话就是此项配合CONCURRENT_REQUESTS会影响爬虫爬取的顺序,如果爬虫起始页是一个列表想要一页爬取完成再去爬取下一页则需要配置DEPTH_PRIORITY为正数,这称为广度优先算法。
5.1 爬虫结果分析
爬取爬取完成之后会返回一些配置信息,结果如下:
2021-07-17 14:56:44 [scrapy.core.engine] INFO: Closing spider (finished)
2021-07-17 14:56:44 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{‘downloader/request_bytes’: 609754,
‘downloader/request_count’: 1667,
‘downloader/request_method_count/GET’: 1667,
‘downloader/response_bytes’: 18707140,
‘downloader/response_count’: 1667,
‘downloader/response_status_count/200’: 1667,
‘elapsed_time_seconds’: 747.314428,
‘finish_reason’: ‘finished’,
‘finish_time’: datetime.datetime(2021, 7, 17, 6, 56, 44, 322073),
‘httpcompression/response_bytes’: 84061849,
‘httpcompression/response_count’: 1667,
‘item_scraped_count’: 11662,
‘log_count/DEBUG’: 13329,
‘log_count/ERROR’: 1,
‘log_count/INFO’: 22,
‘log_count/WARNING’: 1,
‘request_depth_max’: 1666,
‘response_received_count’: 1667,
‘scheduler/dequeued’: 1667,
‘scheduler/dequeued/memory’: 1667,
‘scheduler/enqueued’: 1667,
‘scheduler/enqueued/memory’: 1667,
‘spider_exceptions/IndexError’: 1,
‘start_time’: datetime.datetime(2021, 7, 17, 6, 44, 17, 7645)}
2021-07-17 14:56:44 [scrapy.core.engine] INFO: Spider closed (finished)
从上面信息我们可以得知,爬虫耗时747秒,一共请求了1667个请求,响应数量1667个,提交到管道的item字段有11662条信息,我们到数据库中查看car汽车类型有1667条数据,carinfo有11662条汽车详情信息,结果与我们预期的一致,说明我们成功的爬取了所有的信息。如图5所示
图5 数据表记录详情
6 结束语(Concluding remarks)
到这里我们的爬虫系统就完成了,篇幅所限可能个别细节没有讲到,但是最重要的是一种解决问题的思路,只要我们思路梳理清楚,那么就按照既定思路一点一点的去实现其实也是很简单的。
6.1 总结
本文可能没有过多的介绍scrapy的基本使用,所以阅读本文需要有了解过scrapy且使用redis这类非关系型数据库的读者观看会更轻松一点,不过从根本底层上去分析其实还是scrapy的基本使用,只是目标网站没有下一页或直接可以拼接的方式可能想要爬取会费点功夫,但是只要捋顺了网站的基本逻辑基本上还是没有难度的。毕竟网站没有进行js混淆加密对爬虫爬取策略也没有进行频率限制以及没有验证码设置等障碍,总体来说难易度还是没有那么大的。
6.2 展望
本文爬取的数据目前没有做到断点续爬,也就是如果中间数据出现意外情况导致爬虫终止了会导致下次爬取会重复爬取,如果想要防止这种情况可以对数据库字段做文本可为空类型最大限度的进行兼容保存到数据库以防止出错,之后再进行数据清洗过滤。另外在保存数据库的时候也可以根据汽车类型的source_id来源id来判断进行去重,想要做到中断下次中断的地方继续运行也可以在redis中记录一下当前正在爬取的数据主键,到哪里中断下次从缓存中直接跳到这里继续爬取。由于本次爬取的数据量没有那么大也没有做这些复杂的东西,但是,我们要明白,只要我们有解决问题的思路或者说能力,那么就可以说自己所掌握的东西是有意义的。
参考文献
[1] 欧阳元东.基于 Scrapy 框架的网站数据抓爬的技术实现[J].电子制作. 2020年Z2期,49.
转载请注明:稻香的博客 » 基于Python Scrapy框架车型库爬虫系统的设计与实现