如何抓取汽車之家的車型庫

jxsh2011 7年前發布 | 18K 次閱讀 Scrapy 網絡爬蟲

實際上,關于「如何抓取汽車之家的車型庫」,我已經在「 使用 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):
        data = 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(data["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 不僅僅是一個庫,更是一個平臺,本文涉及的內容只能算是管中窺豹,有興趣的讀者不妨多看看官方文檔,此外,網上也有很多 例子 可供參考。

 

來自:http://huoding.com/2017/02/01/601

 

 本文由用戶 jxsh2011 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!