1. 懂车帝二手车数据爬取的核心挑战
懂车帝作为国内领先的汽车垂直平台,其二手车频道包含丰富的车源信息和交易数据。这些数据对于二手车市场分析、价格预测等场景具有重要价值。然而,平台采用了多种反爬机制保护数据,其中最具代表性的是字体反爬和动态渲染技术。
字体反爬(Font Anti-Scraping)是近年来流行的反爬手段。平台会动态生成自定义字体文件,将关键数据(如价格、里程数)映射到特殊字符。爬虫获取的HTML中显示的是这些特殊字符,而非真实数据。例如,数字"5"可能被替换为Unicode中的""字符,只有在加载了特定字体文件后才能正确显示。
动态渲染则增加了数据获取的复杂度。懂车帝大量使用JavaScript动态加载内容,传统的requests库直接获取的HTML往往不包含完整数据。需要模拟浏览器行为或解析接口才能获取真实数据。
提示:字体反爬的识别特征是页面显示正常但爬取到的数据为乱码或特殊符号,而动态渲染的典型表现是浏览器查看有数据但requests获取为空。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术方案设计与工具选型
2.1 整体架构设计
针对上述挑战,我们采用分层处理的方案:
- 请求层:使用selenium模拟浏览器行为,解决动态渲染问题
- 解析层:提取字体文件并建立映射关系,破解字体反爬
- 存储层:将清洗后的结构化数据存入数据库
python复制# 架构示意图代码表示
class Crawler:
def __init__(self):
self.driver = webdriver.Chrome()
self.font_mapper = FontMapper()
def crawl(self, url):
# 处理动态渲染
html = self._get_dynamic_page(url)
# 处理字体反爬
data = self._parse_with_font(html)
return data
2.2 关键工具选型理由
-
Selenium:相比requests能完整执行页面JS,虽然速度较慢但稳定性高。ChromeDriver是最佳选择,因为:
- 对现代Web标准支持最完善
- 调试工具丰富(可复用Chrome DevTools)
- 无头模式节省资源
-
FontTools:专业的Python字体处理库,能解析woff/ttf字体文件:
bash复制
pip install fonttools其核心功能包括:
- 提取字形(Glyph)到Unicode的映射关系
- 解析字体文件的cmap表
- 支持多种字体格式转换
-
Requests-HTML:作为备用方案,它内置了简易的JS执行引擎,适合轻量级动态页面:
python复制from requests_html import HTMLSession session = HTMLSession() r = session.get(url) r.html.render() # 执行JS
3. 动态渲染破解实战
3.1 Selenium环境配置
推荐使用conda管理环境:
bash复制conda create -n car_spider python=3.8
conda activate car_spider
pip install selenium webdriver-manager
Chromedriver自动管理方案:
python复制from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(ChromeDriverManager().install())
3.2 关键等待策略
动态内容加载需要科学的等待方式,避免硬性sleep:
- 显式等待(推荐):
python复制from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "list-item"))
)
- 文档就位检测:
python复制WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
3.3 反检测技巧
懂车帝会检测自动化工具特征,需进行伪装:
- 修改webdriver属性:
python复制driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
})
- 设置正常User-Agent:
python复制options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
- 随机化操作间隔:
python复制import random
time.sleep(random.uniform(0.5, 1.5))
4. 字体反爬破解深度解析
4.1 字体映射原理
懂车帝的字体反爬流程:
- 每次请求生成唯一的字体文件(.woff)
- 关键数据使用字体中的特殊编码显示
- HTML中通过CSS指定使用的字体
示例被加密的价格显示:
html复制<span class="price"></span>
4.2 字体文件提取步骤
- 定位字体CSS链接:
python复制font_url = driver.find_element(By.XPATH, '//style[contains(text(),"@font-face")]').get_attribute("textContent")
font_url = re.search(r"url\('(.*?)'\)", font_url).group(1)
- 下载字体文件:
python复制import requests
font_data = requests.get(font_url).content
with open("current.woff", "wb") as f:
f.write(font_data)
- 使用FontTools解析:
python复制from fontTools.ttLib import TTFont
font = TTFont("current.woff")
cmap = font.getBestCmap() # 获取编码映射
glyph_order = font.getGlyphOrder() # 获取字形顺序
4.3 建立映射关系
通过分析发现懂车帝的字体规律:
- 数字0-9对应glyph00010-glyph00019
- 价格、里程等数字使用相同编码体系
映射表示例:
python复制{
59381: '0', # Unicode编码59381对应数字0
59382: '1',
...
59390: '9'
}
动态更新映射的方法:
python复制class FontMapper:
def __init__(self):
self.mapping = {}
def update(self, font_path):
font = TTFont(font_path)
for k, v in font.getBestCmap().items():
if v.startswith('glyph000'):
num = int(v[-2:]) - 10
if 0 <= num <= 9:
self.mapping[k] = str(num)
5. 数据解析与存储
5.1 页面结构分析
懂车帝二手车列表项结构特征:
html复制<div class="list-item">
<div class="title">2020款 宝马5系 525Li 豪华套装</div>
<div class="price">.万</div>
<div class="mileage">.万公里</div>
<div class="info">
<span>2019-12上牌</span>
<span>2.0T/184马力</span>
</div>
</div>
5.2 数据清洗流程
- 提取原始文本:
python复制items = driver.find_elements(By.CLASS_NAME, "list-item")
for item in items:
title = item.find_element(By.CLASS_NAME, "title").text
price_code = item.find_element(By.CLASS_NAME, "price").text
- 解码字体加密:
python复制def decode_text(encoded_text, mapper):
return ''.join([mapper.get(ord(c), c) for c in encoded_text])
price = decode_text(price_code, font_mapper.mapping)
- 结构化处理:
python复制import re
def parse_info(info_text):
data = {}
# 解析上牌时间
match = re.search(r'(\d{4}-\d{2})', info_text)
if match:
data['reg_date'] = match.group(1)
# 解析排量
match = re.search(r'(\d+\.\d+)T/(\d+)马力', info_text)
if match:
data['displacement'] = float(match.group(1))
data['horsepower'] = int(match.group(2))
return data
5.3 存储方案
推荐使用MongoDB存储非结构化数据:
python复制from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017/')
db = client['car_data']
collection = db['used_cars']
car_data = {
'title': title,
'price': float(price.replace('万', '')),
'mileage': float(mileage.replace('万公里', '')),
**parse_info(info_text)
}
collection.insert_one(car_data)
如需要结构化存储,可使用Pandas处理后存入CSV或MySQL:
python复制import pandas as pd
df = pd.DataFrame(data_list)
df.to_csv('cars.csv', index=False)
6. 完整源码解析
6.1 核心爬虫类实现
python复制import re
import time
import random
from fontTools.ttLib import TTFont
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from pymongo import MongoClient
class DCDCarSpider:
def __init__(self):
self.driver = self._init_driver()
self.font_mapper = FontMapper()
self.client = MongoClient('mongodb://localhost:27017/')
def _init_driver(self):
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=options)
# 反检测设置
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
})
return driver
def crawl_page(self, url):
self.driver.get(url)
# 等待关键元素加载
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "list-item"))
)
# 更新字体映射
self._update_font_mapping()
# 解析数据
return self._parse_data()
def _update_font_mapping(self):
style_text = self.driver.find_element(
By.XPATH, '//style[contains(text(),"@font-face")]'
).get_attribute("textContent")
font_url = re.search(r"url\('(.*?)'\)", style_text).group(1)
self.font_mapper.update_from_url(font_url)
def _parse_data(self):
items = self.driver.find_elements(By.CLASS_NAME, "list-item")
data_list = []
for item in items:
try:
data = {
'title': item.find_element(By.CLASS_NAME, "title").text,
'price': self._parse_price(item),
'mileage': self._parse_mileage(item),
**self._parse_info(item)
}
data_list.append(data)
except Exception as e:
print(f"解析失败: {e}")
continue
return data_list
def _parse_price(self, item):
price_code = item.find_element(By.CLASS_NAME, "price").text
return float(self.font_mapper.decode(price_code).replace('万', ''))
def _parse_mileage(self, item):
mileage_code = item.find_element(By.CLASS_NAME, "mileage").text
return float(self.font_mapper.decode(mileage_code).replace('万公里', ''))
def _parse_info(self, item):
info_text = item.find_element(By.CLASS_NAME, "info").text
# 解析逻辑同前
...
def close(self):
self.driver.quit()
self.client.close()
6.2 字体映射工具类
python复制import requests
from fontTools.ttLib import TTFont
from io import BytesIO
class FontMapper:
def __init__(self):
self.mapping = {}
def update_from_url(self, font_url):
response = requests.get(font_url)
font_data = BytesIO(response.content)
self._parse_font(font_data)
def _parse_font(self, font_data):
font = TTFont(font_data)
cmap = font.getBestCmap()
for code, name in cmap.items():
if name.startswith('glyph000'):
num = int(name[-2:]) - 10
if 0 <= num <= 9:
self.mapping[code] = str(num)
def decode(self, encoded_text):
return ''.join([self.mapping.get(ord(c), c) for c in encoded_text])
6.3 使用示例
python复制if __name__ == "__main__":
spider = DCDCarSpider()
try:
data = spider.crawl_page("https://www.dongchedi.com/usedcar")
# 存储到MongoDB
db = spider.client['car_data']
db['used_cars'].insert_many(data)
print(f"成功爬取{len(data)}条数据")
finally:
spider.close()
7. 高级技巧与优化方案
7.1 分布式爬取架构
对于大规模爬取,建议采用:
- Scrapy-Redis:实现分布式调度
- Selenium Grid:管理多个浏览器实例
- IP代理池:避免IP封锁
架构示意图:
code复制Master节点(Scheduler) → Redis队列 → Worker节点(Selenium+Parser) → MongoDB集群
7.2 智能限速算法
根据服务器响应动态调整请求频率:
python复制class AdaptiveDelayer:
def __init__(self, base_delay=1.0):
self.base_delay = base_delay
self.last_response_time = None
def get_delay(self):
if self.last_response_time:
# 根据上次响应时间调整
adjust = random.uniform(0.8, 1.2) * (self.last_response_time / 2)
return max(self.base_delay, adjust)
return self.base_delay
7.3 验证码处理策略
遇到验证码时的解决方案:
- 使用第三方打码平台(如超级鹰)
- 人工介入模式
- 降低频率+更换IP
集成示例:
python复制def handle_captcha(image_element):
image_bytes = image_element.screenshot_as_png
# 调用打码平台API
captcha_text = submit_to_decoder(image_bytes)
input_box = driver.find_element(By.ID, 'captcha-input')
input_box.send_keys(captcha_text)
8. 法律合规与道德考量
8.1 robots.txt检查
爬取前务必检查:
python复制import requests
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://www.dongchedi.com/robots.txt")
rp.read()
if not rp.can_fetch("*", target_url):
raise Exception("此页面不允许爬取")
8.2 合理使用原则
建议遵守:
- 限制爬取频率(≥5秒/请求)
- 仅爬取公开数据,不绕过登录
- 不进行商业性大规模爬取
- 设置明显的User-Agent标识
合规Header示例:
python复制headers = {
'User-Agent': 'AcademicResearchBot/1.0 (+https://example.edu)',
'From': 'research@example.edu'
}
8.3 数据使用建议
获取的数据应仅用于:
- 学术研究
- 个人学习
- 市场分析(不涉及商业竞争)
避免:
- 直接复制展示
- 用于商业竞争
- 侵犯用户隐私
