爬虫实战|Scrapy+Selenium 批量爬取汽车之家海量车型外观图(附完整源码)二
本文介绍了一个使用Scrapy+Selenium批量爬取汽车之家车型外观图的爬虫项目。项目通过Scrapy框架进行高效调度,结合Selenium解决动态页面加载问题,实现了对汽车之家全车型外观图的自动化采集。核心功能包括:1) 定义数据模型存储车型信息;2) 使用Selenium进行动态交互(点击外观标签、展开图片);3) 支持批量爬取和断点续传;4) 优化反爬策略(UA伪装、无头模式)。该项目可
大家好,我是你们的桃子叔叔!今天给大家带来一个超实用的爬虫项目——批量爬取汽车之家全车型外观图,结合 Scrapy 的高效调度和 Selenium 的动态页面交互能力,完美解决汽车之家的动态加载、反爬限制等问题。
不管你是做汽车数据分析、设计参考,还是AI训练数据收集,这个项目都能直接复用!全程从环境搭建到代码解析,再到运行测试,一步步带你吃透 Scrapy+Selenium 的组合玩法,新手也能轻松上手~
上文爬虫实战|Scrapy+Selenium 批量爬取汽车之家海量车型外观图(附完整源码)一
我们完成了整个项目的搭建,接下来我们完善所有核心代码并运行这个项目
六、核心模块开发详解(附代码解析)
1. 数据模型:items.py(定义爬取字段)
首先定义要存储的数据结构,明确需要抓取哪些信息:
import scrapy
class CarsfetchItem(scrapy.Item):
type = scrapy.Field() # 车型(如"宝马X5 2025款")
img_name = scrapy.Field() # 图片名称(如"正前方视角")
img_src = scrapy.Field() # 图片高清URL
images = scrapy.Field() # 下载后的图片本地路径
car_info = scrapy.Field() # 完整车型信息(品牌、ID等)
- 字段设计原则:最小冗余+最大可用,car_info 存储完整车型信息,方便后续扩展分析。
2. 主爬虫:spiders/car.py(核心中的核心)
这是项目的灵魂,整合了「数据加载→Selenium动态交互→图片解析→数据提交」全流程,分模块讲解:
(1)初始化与环境配置
import scrapy
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 selenium.webdriver.chrome.options import Options
# 其他导入...
class CarSpider(scrapy.Spider):
name = "car"
allowed_domains = ["www.autohome.com.cn"]
start_urls = [] # 从JSON文件动态获取
def __init__(self):
# 加载车型数据(从parsed_cars.json读取)
self.cars_data = self.load_cars_data()
self.current_batch = [] # 当前批次待爬数据
# Selenium Chrome配置(关键反爬+性能优化)
chrome_options = Options()
chrome_options.add_argument("--headless") # 无头模式(不显示浏览器)
chrome_options.add_argument("--disable-gpu") # 禁用GPU加速
chrome_options.add_argument("--no-sandbox") # 规避系统权限问题
chrome_options.add_argument("--disable-dev-shm-usage") # 解决资源限制
chrome_options.add_argument("--user-agent=你的UA") # 模拟真实浏览器
# 初始化浏览器和等待对象
self.driver = webdriver.Chrome(options=chrome_options)
self.wait = WebDriverWait(self.driver, 15) # 最长等待15秒
self.actions = ActionChains(self.driver)
- 关键优化:无头模式提高效率,UA伪装避免被识别为爬虫,禁用不必要功能减少报错。
(2)数据加载与批量处理
从 JSON 文件读取车型数据,支持批量爬取和断点续传:
def load_cars_data(self):
"""从parsed_cars.json加载车型数据(ID、品牌、名称等)"""
json_path = os.path.join(os.path.dirname(__file__), '../docs/parsed_cars.json')
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.logger.info(f"成功加载 {len(data)} 个车型数据")
return data
except Exception as e:
self.logger.error(f"加载数据失败: {e}")
return []
def get_next_batch(self, batch_size=1500):
"""获取未下载的车型批次(支持断点续传)"""
undownloaded = [car for car in self.cars_data if not car.get('download', False)]
batch = undownloaded[:batch_size]
self.current_batch = batch
return batch
def update_batch_status(self):
"""更新车型下载状态(避免重复爬取)"""
json_path = os.path.join(os.path.dirname(__file__), '../docs/parsed_cars.json')
try:
with open(json_path, 'r', encoding='utf-8') as f:
all_data = json.load(f)
# 标记已下载的车型
for car in self.current_batch:
for item in all_data:
if item['id'] == car['id']:
item['download'] = True
# 写回文件
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(all_data, f, ensure_ascii=False, indent=2)
except Exception as e:
self.logger.error(f"更新状态失败: {e}")
- 核心亮点:批量爬取(一次1500个车型)+ 断点续传(通过
download字段标记状态),避免程序中断后重复劳动。
(3)Selenium 动态交互(解决动态页面)
这是最关键的部分!处理「点击外观标签→展开所有图片→触发懒加载」的交互流程:
def _precise_interact_with_page(self):
"""动态交互核心逻辑"""
try:
# 1. 点击「外观」标签(切换到外观图页面)
appearance_element = self.wait.until(
EC.element_to_be_clickable((By.XPATH, "//h2[contains(text(), '外观')]"))
)
self.driver.execute_script("arguments[0].scrollIntoView(true);", appearance_element)
appearance_element.click()
time.sleep(2)
# 2. 滚动+点击「x张外观图」按钮(展开隐藏图片)
clicked_button_texts = set() # 记录已点击按钮,避免重复
scroll_step = 400 # 每次滚动距离
last_height = self.driver.execute_script("return document.body.scrollHeight")
current_scroll = 0
while current_scroll < last_height:
# 重新获取按钮(解决StaleElementReferenceException)
pic_buttons = self.driver.find_elements(By.XPATH, '//div[@class="tw-mb-1"]')
for i in range(len(pic_buttons)):
try:
buttons = self.driver.find_elements(By.XPATH, '//div[@class="tw-mb-1"]')
button = buttons[i]
button_text = button.text
if button_text in clicked_button_texts:
continue # 跳过已点击按钮
# 滚动到按钮位置并点击
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
if button.is_displayed() and button.is_enabled():
button.click()
clicked_button_texts.add(button_text)
time.sleep(1)
except StaleElementReferenceException:
self.logger.warning("元素失效,跳过当前按钮")
continue
# 继续滚动
current_scroll += scroll_step
self.driver.execute_script(f"window.scrollTo(0, {current_scroll});")
time.sleep(1)
# 更新页面高度(懒加载可能新增内容)
last_height = self.driver.execute_script("return document.body.scrollHeight")
# 3. 触发所有懒加载图片(滚动到顶部→底部→顶部)
self.driver.execute_script("window.scrollTo(0, 0);")
time.sleep(1)
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
except Exception as e:
self.logger.error(f"交互失败: {e}")
- 踩坑重点:
StaleElementReferenceException:元素失效问题,通过「重新查找元素」解决- 滚动加载:分步骤滚动,动态更新页面高度,确保所有按钮可见
- 重复点击:用集合记录已点击按钮,避免重复操作
(4)图片解析与高清URL转换
汽车之家返回的是缩略图,需要转换为高清图:
def convert_thumbnail_to_large(self, thumbnail_url):
"""汽车之家缩略图→高清图转换(核心技巧!)"""
try:
# 1. 修正域名:g.autoimg.cn/carX/ → carX.autoimg.cn/
pattern1 = r'g\.autoimg\.cn/@img/(car\d+)/'
replacement1 = r'\1.autoimg.cn/'
# 2. 替换尺寸:560x420_c42 → 1400x1050(高清尺寸)
pattern2 = r'(\d+x\d+)(?:_c\d+)?'
replacement2 = '1400x1050'
large_url = re.sub(pattern1, replacement1, thumbnail_url)
large_url = re.sub(pattern2, replacement2, large_url)
# 补全HTTPS协议
if not large_url.startswith('http'):
large_url = 'https://' + large_url
return large_url
except Exception as e:
self.logger.error(f"URL转换失败: {e}")
return thumbnail_url # 失败则返回原URL
def parse(self, response):
"""解析页面,提取图片数据"""
car_info = response.meta.get('car_info', {})
img_elements = response.xpath('//section[@class="tw-my-6"]') # 图片容器
for imgs in img_elements:
title = imgs.xpath('.//span[@class="SpecPrice_carModelInfoText__vMhYU"]/text()').get()
if not title:
continue # 无标题则跳过
# 提取图片URL并转换为高清图
img_urls = imgs.xpath('.//img/@src').getall()
real_img_urls = [url for url in img_urls if url and not url.startswith('data:image')] # 过滤base64占位图
for img_url in real_img_urls:
high_def_url = self.convert_thumbnail_to_large(img_url)
# 生成Item并提交
yield self.getImageItem(
type=car_info.get('name', ''),
title=title,
src=high_def_url,
car_info=car_info
)
- 核心技巧:通过正则解析汽车之家的URL规则,将缩略图(560x420)转换为1400x1050高清图,实用性拉满!
3. 数据处理管道:pipelines.py(下载+保存)
设计了两个管道,分工明确,优先级通过数字控制(越小越先执行):
(1)CarsImagePipeline:图片下载
基于 Scrapy 内置的 ImagesPipeline,处理图片下载、路径生成、格式识别:
from scrapy.pipelines.images import ImagesPipeline
import hashlib
class CarsImagePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
"""发起图片下载请求(添加请求头反爬)"""
img_url = item.get('img_src')
if img_url:
headers = {
'Referer': 'https://www.autohome.com.cn/', # 关键!添加Referer避免403
'User-Agent': '你的UA'
}
yield scrapy.Request(
img_url, headers=headers, meta={'item': item}, dont_filter=True
)
def file_path(self, request, response=None, info=None, *, item=None):
"""自定义图片保存路径:车型文件夹/图片名称_hash.格式"""
car_info = item.get('car_info', {})
car_name = self.sanitize_filename(car_info.get('name', 'unknown'))
img_name = self.sanitize_filename(item.get('img_name', 'unknown'))
url_hash = hashlib.md5(request.url.encode()).hexdigest()[:8] # 哈希去重
# 自动识别图片格式(从响应头或URL提取)
if response and 'Content-Type' in response.headers:
content_type = response.headers['Content-Type'].decode().lower()
ext = 'jpg' if 'jpeg' in content_type else content_type.split('/')[-1]
else:
ext = self._get_extension_from_url(request.url)
# 生成路径:downloaded_images/车型名称/图片名称_hash.格式
return f"{car_name}/{img_name}_{url_hash}.{ext}"
def sanitize_filename(self, filename):
"""清理非法文件名字符(避免路径错误)"""
return re.sub(r'[<>:"/\\|?*]', '_', filename).replace(' ', '_')
- 关键优化:
- 添加 Referer 头:解决汽车之家图片防盗链(403 Forbidden)
- 哈希去重:避免重复图片下载
- 自动识别格式:支持 jpg/png/webp 等格式
(2)CarsfetchPipeline:数据结构化保存
将爬取的图片信息(名称、URL、下载状态)按车型保存为 JSON 文件,方便后续使用:
import json
from pathlib import Path
class CarsfetchPipeline:
def __init__(self):
self.items_by_car = {} # 按车型ID分组
def open_spider(self, spider):
"""创建输出目录"""
self.output_dir = Path('output')
self.output_dir.mkdir(exist_ok=True)
def process_item(self, item, spider):
"""收集数据(按车型分组)"""
car_id = item.get('car_info', {}).get('id')
if car_id not in self.items_by_car:
self.items_by_car[car_id] = {
'car_info': item.get('car_info', {}),
'items': []
}
# 记录图片信息和下载状态
self.items_by_car[car_id]['items'].append({
'type': item.get('type'),
'img_name': item.get('img_name'),
'img_src': item.get('img_src'),
'download_status': 'success' if item.get('images') else 'failed'
})
return item
def close_spider(self, spider):
"""保存JSON文件(按车型分文件)"""
for car_id, car_data in self.items_by_car.items():
car_name = self.sanitize_filename(car_data['car_info'].get('name', f'car_{car_id}'))
car_dir = self.output_dir / car_name
car_dir.mkdir(exist_ok=True)
# 保存JSON
with open(car_dir / f'{car_name}.json', 'w', encoding='utf-8') as f:
json.dump({
'car_info': car_data['car_info'],
'images': car_data['items']
}, f, ensure_ascii=False, indent=2)
# 打印统计信息
success_count = sum(1 for i in car_data['items'] if i['download_status'] == 'success')
spider.logger.info(f"车型 {car_name}:成功下载 {success_count}/{len(car_data['items'])} 张图片")
- 输出效果:每个车型一个文件夹,包含「图片文件+JSON数据文件」,结构清晰,便于后续使用。
4. 项目配置:settings.py(反爬+性能优化)
关键配置项逐一说明,避免踩坑:
# 项目名称
BOT_NAME = "carsfetch"
# 爬虫模块路径
SPIDER_MODULES = ["carsfetch.spiders"]
NEWSPIDER_MODULE = "carsfetch.spiders"
# 反爬核心配置
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
ROBOTSTXT_OBEY = False # 忽略robots.txt(汽车之家禁止爬虫,需关闭)
# 禁用SSL验证(解决公司网络/代理导致的证书错误)
DOWNLOAD_HANDLERS = {
'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
}
import os
os.environ['CURL_CA_BUNDLE'] = ''
os.environ['REQUESTS_CA_BUNDLE'] = ''
# 限速配置(避免被封IP)
CONCURRENT_REQUESTS = 16 # 并发请求数
CONCURRENT_REQUESTS_PER_DOMAIN = 1 # 单域名并发数(关键!汽车之家反爬严格)
DOWNLOAD_DELAY = 1 # 下载延迟
AUTOTHROTTLE_ENABLED = True # 自动限速(根据响应速度调整延迟)
AUTOTHROTTLE_START_DELAY = 3
AUTOTHROTTLE_MAX_DELAY = 60
# 管道配置(优先级:100→200,先下载图片再保存JSON)
ITEM_PIPELINES = {
'carsfetch.pipelines.CarsImagePipeline': 100,
'carsfetch.pipelines.CarsfetchPipeline': 200,
}
# 图片存储路径
IMAGES_STORE = 'downloaded_images'
IMAGES_EXPIRES = 90 # 图片过期时间(90天)
# 下载超时
DOWNLOAD_TIMEOUT = 30
- 反爬关键:单域名并发数=1、自动限速、UA伪装、禁用SSL验证,这四项缺一不可!
七、项目运行与测试
1. 准备工作
- 在
docs文件夹下创建parsed_cars.json,格式如下(至少包含id、brand、name字段):[ {"id": "6643", "brand": "宝马", "name": "宝马X5 2025款", "download": false}, {"id": "xxx", "brand": "奔驰", "name": "奔驰GLC 2025款", "download": false} ]
这里待优化
2. 运行爬虫
在项目根目录执行命令:
scrapy crawl car
3. 查看结果
- 图片:保存到
downloaded_images/车型名称/目录下 - 数据:保存到
output/车型名称/车型名称.json,包含完整的图片信息和下载状态
八、避坑指南(实战总结)
- ChromeDriver版本不匹配:下载与Chrome浏览器版本一致的驱动,或用 webdriver-manager 自动管理
- StaleElementReferenceException:元素失效问题,通过「重新查找元素」而非复用旧元素解决
- 图片403 Forbidden:必须添加 Referer 头,值为
https://www.autohome.com.cn/ - SSL证书错误:禁用 SSL 验证(settings.py 中的相关配置)
- 爬取速度过快被封IP:降低并发数、增加延迟,开启自动限速
- 动态加载图片漏爬:多轮滚动触发懒加载,确保所有图片都被渲染
九、项目扩展方向
- 多线程优化:用
scrapy-redis实现分布式爬取,提高效率 - 代理池集成:添加代理池中间件,解决IP被封问题
- GUI界面:用 PyQt 或 Streamlit 开发可视化界面,方便非技术人员使用
- 图片去重:基于图片哈希(如 dHash)实现相似图片去重
- 增量爬取:定期检查车型是否有新增图片,只爬取更新内容
总结
这个项目完美结合了 Scrapy 的高效调度和 Selenium 的动态交互能力,解决了汽车之家这类动态网站的爬取难题。从数据加载、动态交互、图片解析到结构化保存,全流程覆盖了工业级爬虫的核心需求,而且代码可复用性极强,稍作修改就能适配其他图片类网站。
如果你需要批量收集汽车图片,或者想学习 Scrapy+Selenium 的组合用法,这个项目绝对值得动手实践!完整源码已经同步到 GitHub(文末附地址),欢迎 Star 收藏~
如果在实践中遇到问题,欢迎在评论区留言,我会第一时间解答!祝大家爬取顺利,数据满满~
更多推荐


所有评论(0)