【代码】Python3|Scrapy框架初探(汽车之家大连市二手车车辆数据爬取、清洗与可视化)

本篇主要是整个项目的介绍,没提到太多琐碎的技术细节,以后有空的话会整理一下 Scrapy 和原生爬虫的差异,还有它坑人的一些地方,单发出来。

开源地址:https://github.com/shandianchengzi/car_home_spider

使用说明:切换到autohome_spider目录下,运行run.bat

文章目录

汽车之家大连市二手车车辆数据爬取、清洗与可视化

一、项目简介

本项目旨在通过网络爬虫技术,从"汽车之家"网站的二手车频道自动抓取各类汽车的详细信息,包括品牌、车型、价格、公里数、上牌时间等,然后利用Pandas库对数据进行清洗和可视化分析,为用户提供直观的数据展示和分析结果。通过本项目,用户可以了解到大连市二手车市场的价格分布、常购品牌等信息,为购车提供参考依据。

二、项目架构

1)运行环境

  1. 硬件系统:Intel Core i7, 16GB RAM, NVIDIA GeForce GTX 1050 Ti
  2. 操作系统:Windows 10, Version 20H2
  3. 软件版本:Python 3.8.5, SQLite 3.31.1, Visual Studio Code 1.57.1
  4. 运行时刻:2024年7月13日 23:00

3)系统架构图

4)数据库设计

数据存入csv文件,包含字段:品牌、上市年份、车型、表显里程(公里)、上牌时间(年)、价格(万)、原厂保修时间、所属城市、链接。

三、项目核心代码说明

1)Scrapy框架数据爬取

爬取之前,先分析网站信息。

  1. 确定网页:在网上汽车之家能找到两个网页是卖二手车的,一个在产品库[1](#1)里、另一个在二手车严选[2](#2)上。结果显示,2024年7月13日,产品库中的车源更多,有2714条;而二手车严选里只有1934条。因此,本项目选择产品库中的结果。

  2. 确定请求方式:打开开发者工具并刷新,然后搜索页面上的任意车辆的名称,检查返回的结果对应的请求。结果表明,请求方式为Get,请求的就是网站的链接,同时,没有传递任何Get参数。

  3. 确定传参模式:

    1. 筛选机制:注意到网页本身拥有排序功能,这样之后就不需要额外排序。它的排序选项不在参数中,在链接中,比较小众。比如a0_0msdgscncgpi1ltocsp2ex/

      对应默认排序,a0_0msdgscncgpi1lto2cspex/

      对应价格降序。

    2. 翻页机制:产品库的二手车页面中,页码也在链接中。比如a0_0msdgscncgpi1lto2csp1ex/对应第一页,a0_0msdgscncgpi1lto2csp2ex/对应第二页。

  4. 确定结果格式:由于他返回的就是网页,没有json之类的额外信息,所以爬取的结果就是网页上能看到的结果。分析页面内容,可知包含的信息为"品牌"、"上市年份"、"车型"、"实物图"、"表显里程"、"上牌时间"、"价格"、"原厂保修时间"、"所属城市"和详细信息的访问链接。观察到图片采用了懒加载技术,需要额外请求别的链接才能返回,加之数据分析时并不需要图片,因此干脆去掉实物图这一个信息。

  5. 额外注意事项:特别坑的一点是,页数超过100页时会自动重定向回到100页,而不是返回空,这样代码中加空判断就无法真正停止下来。这里得额外写一个100页的判断。而且也因为这个,最多返回结果数量2400条。考虑到总共也只有2714条,可以认为该数量是可以接受的,因此不更改筛选条件重新跑了。

分析好了这些内容之后,再开始用Scrapy写爬虫,代码如下:

  1. Spider中数据爬取解析:
python 复制代码
class AutohomeSpider(scrapy.Spider):
    name = 'autohome_spider'
    allowed_domains = ['autohome.com.cn']
    base_url = 'https://car.autohome.com.cn/2sc/dalian/a0_0msdgscncgpi1lto2csp{}ex/'
    page = 1
    start_urls = ['https://car.autohome.com.cn/2sc/dalian/a0_0msdgscncgpi1lto2csp1ex/']

    def parse(self, response):
        xml = lxml.etree.HTML(response.text)
        piclist = xml.xpath('//div[@class="piclist"]/ul/li')
        if len(piclist) == 0 or self.page > 100: # max page 100
            return
        for car in piclist:
            try:
                l = ItemLoader(item=AutohomeItem())
                title = car.xpath('div[@class="title"]/a/text()')[0]
                title_href = car.xpath('div[@class="title"]/a/@href')[0]
                somethings = title.split(' ', 2)
                if len(somethings) != 3:
                    with open('error.log', 'a') as f:
                        f.write(title + '\n')
                    continue
                # 获得icon_list里所有a标签的title属性并拼接非空的为字符串
                icon_list = car.xpath('div[@class="icon-list"]/a')
                city = car.xpath('div[@class="icon-list"]/span/span/text()')[0]
                icons_info = []
                for icon in icon_list:
                    icon_info = icon.xpath('@title')
                    if icon_info:
                        icons_info.extend(icon_info)
                icons_info = ', '.join(icons_info)
                # 用ItemLoader加载数据
                l.add_value('brand', somethings[0])
                l.add_value('year', somethings[1])
                l.add_value('model', somethings[2])
                l.add_value('mileage', getNumberAndFloat(car.xpath('*/div[@class="detail-l"]/p[1]/text()')[0]))
                l.add_value('registration_time', getNumberAndFloat(car.xpath('*/div[@class="detail-l"]/p[2]/text()')[0]))
                l.add_value('price', car.xpath('*/div[@class="detail-r"]/span/text()'))
                l.add_value('warranty_time', icons_info)
                l.add_value('city', city)
                l.add_value('link', "https:" + title_href)
                yield l.load_item()
            except Exception as e:
                # skip no full information car
                pass

        # 下一页
        self.page += 1
        new_url = self.base_url.format(self.page)
        yield scrapy.Request(new_url, callback=self.parse)
  1. Pipeline中做数据存储:
python 复制代码
class AutohomeSpiderPipeline:
    def open_spider(self, spider):
        self.file = open('autohome.csv', 'w', newline='', encoding='utf-8')
        self.writer = csv.DictWriter(self.file, fieldnames=['brand', 'year', 'model', 'mileage', 'registration_time', 'price', 'warranty_time', 'city', 'link'])
        # ['品牌', '上市年份', '车型', '表显里程(公里)', '上牌时间(年)', '价格(万)', '原厂保修时间', '所属城市', '链接']
        self.writer.writeheader()

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        # let item: {field: [value]} to {field: value}
        item = {k: v[0] for k, v in item.items()}
        self.writer.writerow(item)
        return item

2)Pandas数据清洗与可视化

数据清洗主要是将错误的数据修改正确或者删除,以便进一步分析。我在爬虫代码的解析过程中,对品牌的解析有误,有一些品牌的名称,本身就带空格,我用空格做分割,导致该列被解析到下一列中。代码如下:

python 复制代码
# 数据清洗
df.drop_duplicates(inplace=True) # 去重
df.dropna(subset=['brand'], inplace=True) # 去掉品牌为空的数据
# 合并前三列的数据形成字符串,用正则重新解析,查找"xxxx款",该字符串前面的是brand,后面的是model,中间的是year
df['brand_model_year'] = df['brand'] + df['year'] + df['model'] # 辅助列
df['brand'] = df['brand_model_year'].str.extract(r'^(.*?)(\d{4}款)(.*)')[0]
df['year'] = df['brand_model_year'].str.extract(r'^(.*?)(\d{4}款)(.*)')[1]
df['model'] = df['brand_model_year'].str.extract(r'^(.*?)(\d{4}款)(.*)')[2]
df.drop(columns=['brand_model_year'], inplace=True)
df['year'] = df['year'].replace('款', '', regex=True)
# 如果原厂保修时间为空,填充为0
df['warranty_time'].fillna('0', inplace=True)
df.to_csv(result_path, index=False, encoding='gbk') # for windows excel

3)Matplotlib数据可视化

多角度可视化代码如下:

python 复制代码
# 数据可视化
df.columns = ['品牌', '上市年份', '车型', '表显里程(公里)', '上牌时间(年)', '价格(万)', '原厂保修时间', '所属城市', '链接']
# 设置字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 各种数据的占比统计,全画在一张图上
plt.figure(figsize=(20, 10))
# 1.1 价格分布(只做0~200万的)
plt.subplot(2, 2, 1)
sns.histplot(df['price'], bins=20, kde=True)
plt.xlim(0, 200)
plt.title('价格分布')
# 1.2 里程分布
plt.subplot(2, 2, 2)
sns.histplot(df['mileage'], bins=20, kde=True)
plt.title('里程分布')
# 1.3 品牌分布(前20,扇形图,不要x和y的label)
plt.subplot(2, 2, 3)
df['brand'].value_counts().head(20).plot.pie(autopct='%1.1f%%')
plt.ylabel('')
plt.title('品牌数量分布(前20)')
# 1.4 车型分布(前10,扇形图,不要x和y的label)
plt.subplot(2, 2, 4)
df['model'].value_counts().head(10).plot.pie(autopct='%1.1f%%')
plt.ylabel('')
plt.title('车型数量分布(前10)')
plt.savefig(os.path.join(imgs_dir, '各种数据的占比统计.png'))
# 2 时间分析
plt.figure(figsize=(20, 10))
# 2.1 上牌时间分布
plt.subplot(1, 2, 1)
sns.histplot(df['registration_time'], bins=20, kde=True)
plt.title('上牌时间分布')
# 2.2 上市年份分布
plt.subplot(1, 2, 2)
sns.histplot(df['year'], bins=20, kde=True)
plt.title('上市年份分布')
plt.savefig(os.path.join(imgs_dir, '时间分析.png'))
# 3. 保修存在性与其他数据的关系,标好图例
plt.figure(figsize=(20, 10))
# 3.1 保修存在占比(1表示存在,0表示不存在,扇形图)
plt.subplot(2, 2, 1)
df['warranty_time_exist'].value_counts().plot.pie(autopct='%1.1f%%')
plt.legend(['无保修', '有保修'])
plt.ylabel('')
plt.title('保修存在占比')
# 3.2 保修存在与价格的关系
plt.subplot(2, 2, 2)
sns.boxplot(x='warranty_time_exist', y='price', data=df)
plt.xticks([0, 1], ['无保修', '有保修'])
plt.title('保修存在与价格的关系')
# 3.3 保修存在与里程的关系
plt.subplot(2, 2, 3)
sns.boxplot(x='warranty_time_exist', y='mileage', data=df)
plt.xticks([0, 1], ['无保修', '有保修'])
plt.title('保修存在与里程的关系')
# 3.4 保修存在与上牌时间的关系
plt.subplot(2, 2, 4)
sns.boxplot(x='warranty_time_exist', y='registration_time', data=df)
plt.xticks([0, 1], ['无保修', '有保修'])
plt.title('保修存在与上牌时间的关系')
plt.savefig(os.path.join(imgs_dir, '保修是否存在与其他数据的关系.png'))

四、项目运行截图

1) 爬虫结果截图

2) 持久化存储结果截图

3) 可视化分析图形截图

价格、里程、品牌和车型数量分布可视化

上牌时间和上市时间分布

保修的存在性与价格、里程和上牌时间的关联

五、总结

通过本项目,我学习并掌握了网络爬虫的基本原理和方法,以及如何利用Python进行数据处理和可视化分析。在项目中,我使用Scrapy框架爬取了汽车之家网站的二手车信息,然后利用Pandas库对数据进行清洗和整理,最后使用Matplotlib库对数据进行可视化分析。通过这个项目,我不仅提高了自己的数据处理和分析能力,还为大连市二手车市场的价格分布、常购品牌等信息提供了直观的展示和分析结果,为购车提供了参考依据。

以上都是套话,下附真实的总结:

这次发现 scrapy 自带 pipeline 挺不错的,比我自己直接写一个强远了,而且报错日志什么的也很方便。不过这个网站反爬太少,header啥的根本不用加,这块我就还没学到。框架的坏处也有,很多东西一个 requests 改改参数能解决这个偏偏是类的函数,得诡异地设置一些东西去传递参数,还难写结束条件。

六、参考资料


  1. https://car.autohome.com.cn/2sc/dalian/a0_0msdgscncgpi1lto2csp1ex/ ↩︎

  2. https://www.che168.com/dalian/list/#pvareaid=100945 ↩︎

相关推荐
笨鸟先飞,勤能补拙1 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能2 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂3 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study3 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境4 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境4 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1234 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy12393102165 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb005 小时前
一个项目带你入门AI应用开发01
python
0566465 小时前
Python康复训练——常用标准库
开发语言·python·学习