大家好,我是你们的桃子叔叔!今天给大家带来一个超实用的爬虫项目——批量爬取汽车之家全车型外观图,结合 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,格式如下(至少包含 idbrandname 字段):
    [
      {"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,包含完整的图片信息和下载状态

八、避坑指南(实战总结)

  1. ChromeDriver版本不匹配:下载与Chrome浏览器版本一致的驱动,或用 webdriver-manager 自动管理
  2. StaleElementReferenceException:元素失效问题,通过「重新查找元素」而非复用旧元素解决
  3. 图片403 Forbidden:必须添加 Referer 头,值为 https://www.autohome.com.cn/
  4. SSL证书错误:禁用 SSL 验证(settings.py 中的相关配置)
  5. 爬取速度过快被封IP:降低并发数、增加延迟,开启自动限速
  6. 动态加载图片漏爬:多轮滚动触发懒加载,确保所有图片都被渲染

九、项目扩展方向

  1. 多线程优化:用 scrapy-redis 实现分布式爬取,提高效率
  2. 代理池集成:添加代理池中间件,解决IP被封问题
  3. GUI界面:用 PyQt 或 Streamlit 开发可视化界面,方便非技术人员使用
  4. 图片去重:基于图片哈希(如 dHash)实现相似图片去重
  5. 增量爬取:定期检查车型是否有新增图片,只爬取更新内容

总结

这个项目完美结合了 Scrapy 的高效调度和 Selenium 的动态交互能力,解决了汽车之家这类动态网站的爬取难题。从数据加载、动态交互、图片解析到结构化保存,全流程覆盖了工业级爬虫的核心需求,而且代码可复用性极强,稍作修改就能适配其他图片类网站。

如果你需要批量收集汽车图片,或者想学习 Scrapy+Selenium 的组合用法,这个项目绝对值得动手实践!完整源码已经同步到 GitHub(文末附地址),欢迎 Star 收藏~

如果在实践中遇到问题,欢迎在评论区留言,我会第一时间解答!祝大家爬取顺利,数据满满~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐