如何抓取汽车之家的车型库

赞助

如果你觉得我写得还行,并且愿意付费,那么我会更有动力写下去。

实际上,关于「如何抓取汽车之家的车型库」,我已经在「使用 Mitmproxy 分析接口」一文中给出了方法,不过那篇文章里讲的是利用 API 接口来抓取数据,一般来说,因为接口不会频繁改动,相对 WEB 页面而言更稳定,所以通常这是数据抓取的最佳选择,不过利用 API 接口来抓取数据有一些缺点,比如有的数据没有 API 接口,亦可能虽然有 API 接口,但是数据使用了加密格式,此时只能通过 WEB 页面来抓取数据。

既然要通过 WEB 页面来抓取数据,那么就不得不提到 Scrapy,它可以说是爬虫之王,我曾经听说有人用 Scrapy,以有限的硬件资源在几天的时间里把淘宝商品数据从头到尾撸了一遍,如此看来,本文用 Scrapy 来抓取汽车之家的车型库应该是绰绰有余的了。

在抓取汽车之家的车型库之前,我们应该对其结构有一个大致的了解,按照百科中的描述,其大致分为四个级别,分别是品牌、厂商、车系、车型。本文主要关注车系和车型两个级别的数据。在抓取前我们要确定从哪个页面开始抓取,比较好的选择有两个,分别是产品库品牌找车,选择哪个都可以,本文选择的是品牌找车,不过因为品牌找车页面使用了 js 来按字母来加载数据,所以直接使用它的话可能会有点不必要的麻烦,好在我们可以直接使用从 AZ 的字母页面。

假设你已经有了 Scrapy 的运行环境(注:本文代码以 Python3 版本为准):

shell> scrapy startproject autohome
shell> cd autohome
shell> scrapy genspider automobile www.autohome.com.cn -t crawl

如此就生成了一个基本的蜘蛛骨架,需要说明的是 Scrapy 有两种蜘蛛,分别是 spider 和 crawl,其中 spider 主要用于简单的抓取,而 crawl 则可以用来实现复杂的抓取,复杂在哪里呢?主要是指蜘蛛可以根据规则萃取需要的链接,并且可以逐级自动抓取。就抓取汽车之家的车型库这个任务而言,使用 spider 就可以实现,不过鉴于 crawl 在功能上更强大,本文选择 crawl 来实现,其工作流程大致如下:通过 start_urls 设置起始页,通过 rules 设置处理哪些链接,一旦遇到匹配的链接地址,那么就会触发对应的 callback,在 callback 中可以使用 xpath/css 选择器来选择数据,并且通过 item loader 来加载 item:

车系

车系

车型

车型

文件:autohome/items.py:

# -*- coding: utf-8 -*-

import scrapy
from scrapy.loader.processors import MapCompose, TakeFirst

class SeriesItem(scrapy.Item):
    series_id = scrapy.Field(
        input_processor=MapCompose(lambda v: v.strip("/")),
        output_processor=TakeFirst()
    )
    series_name = scrapy.Field(output_processor=TakeFirst())

class ModelItem(scrapy.Item):
    model_id = scrapy.Field(
        input_processor=MapCompose(lambda v: v[6:v.find("#")-1]),
        output_processor=TakeFirst()
    )
    model_name = scrapy.Field(output_processor=TakeFirst())
    series_id = scrapy.Field(output_processor=TakeFirst())

文件:autohome/autohome/spiders/automobile.py:

# -*- coding: utf-8 -*-

import json
import string
from scrapy import Request
from scrapy.http import HtmlResponse
from scrapy.linkextractors import LinkExtractor
from scrapy.loader import ItemLoader
from scrapy.spiders import CrawlSpider, Rule
from urllib.parse import parse_qs, urlencode, urlparse
from autohome.items import ModelItem, SeriesItem

class AutomobileSpider(CrawlSpider):
    name = "automobile"
    allowed_domains = ["www.autohome.com.cn"]

    start_urls = [
        "http://www.autohome.com.cn/grade/carhtml/" + x + ".html"
        for x in string.ascii_uppercase if x not in "EIUV"
    ]

    rules = (
        Rule(LinkExtractor(allow=("/\d+/#",)), callback="parse_item"),
    )

    def parse(self,response):
        params = {
            "url": response.url,
            "status": response.status,
            "headers": response.headers,
            "body": response.body,
        }

        response = HtmlResponse(**params)

        return super().parse(response)

    def parse_item(self, response):
        sel = response.css("div.path")

        loader = ItemLoader(item=SeriesItem(), selector=sel)
        loader.add_css("series_id", "a:last-child::attr(href)")
        loader.add_css("series_name", "a:last-child::text")

        series = loader.load_item()

        # 即将销售 & 在售
        for sel in response.css("div.interval01-list-cars-infor"):
            loader = ItemLoader(item=ModelItem(), selector=sel)
            loader.add_css("model_id", "a::attr(href)")
            loader.add_css("model_name", "a::text")
            loader.add_value("series_id", series['series_id'])

            yield loader.load_item()

        # 停售
        url = "http://www.autohome.com.cn/ashx/series_allspec.ashx"

        years = response.css(".dropdown-content a::attr(data)")

        for year in years.extract():
            qs = {
                "y": year,
                "s": series["series_id"]
            }

            yield Request(url + "?" + urlencode(qs), self.stop_sale)

    def stop_sale(self, response):
        qs = parse_qs(urlparse(response.url).query)

        body = json.loads(response.body_as_unicode())

        for spec in body["Spec"]:
            yield {
                "model_id": str(spec["Id"]),
                "model_name": str(spec["Name"]),
                "series_id": str(qs["s"][0]),
            }

把如上两段源代码拷贝到对应的文件里,下面我们就可以让蜘蛛爬起来了:

shell> scrapy crawl automobile -o autohome.csv

抓取的结果会保存到 autohome.csv 里。当然也可以保存为 json 格式,如果发现输出的是 unicode 编码,那么可以通过设置 FEED_EXPORT_ENCODING 来解决,如果想保存到数据库中,那么可以使用 Scrapy 的 pipeline 来实现。

如果你完整读过 Scrapy 的文档,那么可能会记得在 spiders 一章中有如下描述:

When writing crawl spider rules, avoid using parse as callback, since the CrawlSpider uses the parse method itself to implement its logic. So if you override the parse method, the crawl spider will no longer work.

意思是说,在使用 crawl 的时候,应该避免覆盖 parse 方法,不过本文的源代码中恰恰重写了 parse 方法,究其原因是因为汽车之家的字母页存在不规范的地方:

shell> curl -I http://www.autohome.com.cn/grade/carhtml/A.html

HTTP/1.1 200 OK
Date: ...
Server: ...
Content-Type: text/html, text/html; charset=gb2312
Content-Length: ...
Last-Modified: ...
Accept-Ranges: ...
X-IP: ...
Powerd-By-Scs: ...
X-Cache: ...
X-Via: ...
Connection: ...

乍看上去好像没什么问题,不过仔细一看就会发现在 Content-Type 中 text/html 存在重复,此问题导致 Scrapy 在判断页面是否是 html 页面时失败。为了修正此问题,我重写了 parse 方法,把原本是 TextResponse 的对象重新包装为 HtmlResponse 对象。通过抓取竟然还帮助汽车之家找到一个 BUG,真是醉了。

有时候,为了避免蜘蛛被对方屏蔽,我们需要伪装 User-Agent,甚至通过一些代理服务来伪装自己的 IP,本文篇幅所限,就不多说了,实际上,Scrapy 不仅仅是一个库,更是一个平台,本文涉及的内容只能算是管中窥豹,有兴趣的读者不妨多看看官方文档,此外,网上也有很多例子可供参考。

如何抓取汽车之家的车型库》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注