你所需要的,不仅仅是一个好用的代理。
既然要通过 WEB 页面来抓取数据,那么就不得不提到 Scrapy ,它可以说是爬虫之王,我曾经听说有人用 Scrapy,以有限的硬件资源在几天的时间里把淘宝商品数据从头到尾撸了一遍,如此看来,本文用 Scrapy 来抓取汽车之家的车型库应该是绰绰有余的了。
在抓取汽车之家的车型库之前,我们应该对其结构有一个大致的了解,按照 百科 中的描述,其大致分为四个级别,分别是品牌、厂商、车系、车型。本文主要关注车系和车型两个级别的数据。在抓取前我们要确定从哪个页面开始抓取,比较好的选择有两个,分别是 产品库 和 品牌找车 ,选择哪个都可以,本文选择的是品牌找车,不过因为品牌找车页面使用了 js 来按字母来加载数据,所以直接使用它的话可能会有点不必要的麻烦,好在我们可以直接使用从 A 到 Z 的字母页面。
假设你已经有了 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 不仅仅是一个库,更是一个平台,本文涉及的内容只能算是管中窥豹,有兴趣的读者不妨多看看官方文档,此外,网上也有很多 例子可供参考。