
1. 项目背景与核心价值去年接手公司二手车价格监控项目时我曾面临一个典型的数据采集困境每天需要从多个平台抓取数万条车辆信息初期直接将数据写入CSV文件但随着数据量增长频繁的IO操作导致脚本运行效率直线下降。更糟的是某次脚本意外中断导致当天采集的3000多条数据全部丢失。正是这次教训让我意识到一个完整的工程化数据采集方案必须包含高效存储和容错机制。本方案将展示如何用Python构建从数据采集到持久化存储的完整链路重点解决三个核心问题如何高效采集动态加载的二手车列表数据如何设计兼顾查询效率和可靠性的存储方案如何实现采集数据的快速导出与分析2. 环境配置与工具选型2.1 基础环境搭建推荐使用Python 3.8版本这个版本在异步IO和类型提示方面已经成熟稳定。通过以下命令创建虚拟环境python -m venv car_spider source car_spider/bin/activate # Linux/Mac car_spider\Scripts\activate # Windows安装核心依赖库pip install requests beautifulsoup4 aiohttp sqlalchemy pandas特别说明选型理由requestsBeautifulSoup适合静态页面的快速开发学习曲线平缓aiohttp当需要并发采集时提供异步IO支持sqlalchemyORM工具方便切换不同数据库后端pandas数据清洗和分析的瑞士军刀2.2 反爬策略应对方案根据实测经验二手车平台常见的反爬手段及应对策略反爬类型特征解决方案UA检测返回403状态码使用fake-useragent轮换UAIP限制连续请求失败1. 降低请求频率 2. 使用代理IP池行为验证弹出验证码1. 模拟鼠标移动 2. 使用打码平台建议在代码中添加基础防护from fake_useragent import UserAgent headers { User-Agent: UserAgent().random, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.che168.com/ }3. 数据采集实战3.1 页面结构分析以某二手车平台为例列表页关键元素定位div classcar-list div classcar-card !-- 单条车辆信息 -- a classcar-title2020款 宝马3系 320Li/a span classprice23.58万/span ul classcar-info li3.2万公里/li li2020-05上牌/li /ul /div !-- 更多车辆... -- /div3.2 单页采集实现使用CSS选择器定位关键字段def parse_car_list(html): soup BeautifulSoup(html, html.parser) cars [] for item in soup.select(.car-card): try: title item.select_one(.car-title).text.strip() price float(item.select_one(.price).text.replace(万, )) info_items [li.text for li in item.select(.car-info li)] car_data { title: title, price: price, mileage: info_items[0], register_date: info_items[1], crawl_time: datetime.now().strftime(%Y-%m-%d %H:%M:%S) } cars.append(car_data) except Exception as e: print(f解析异常: {e}) continue return cars3.3 分页采集策略发现分页规律后可以使用并行请求加速采集import concurrent.futures def crawl_pages(base_url, max_page10): with concurrent.futures.ThreadPoolExecutor(max_workers5) as executor: futures [] for page in range(1, max_page1): url f{base_url}?page{page} futures.append(executor.submit(fetch_page, url)) results [] for future in concurrent.futures.as_completed(futures): results.extend(future.result()) return results重要提示实测某平台在并发超过5个请求时会触发封禁建议添加随机延迟import random time.sleep(random.uniform(0.5, 1.5))4. 数据存储方案设计4.1 数据库表结构设计针对二手车数据特点设计SQLite表结构CREATE TABLE IF NOT EXISTS used_cars ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, brand TEXT GENERATED ALWAYS AS (SUBSTR(title, 1, INSTR(title, )-1)) VIRTUAL, price REAL CHECK(price 0), mileage TEXT, register_date TEXT, source_platform TEXT DEFAULT che168, crawl_time TIMESTAMP, is_deleted INTEGER DEFAULT 0, UNIQUE(title, register_date) ON CONFLICT IGNORE ); CREATE INDEX idx_car_brand ON used_cars(brand); CREATE INDEX idx_car_price ON used_cars(price);设计亮点使用虚拟列自动提取品牌信息唯一约束避免重复数据软删除标记保留数据完整性4.2 使用SQLAlchemy实现ORM定义数据模型类from sqlalchemy import create_engine, Column, Integer, String, Float, TIMESTAMP from sqlalchemy.ext.declarative import declarative_base Base declarative_base() class UsedCar(Base): __tablename__ used_cars id Column(Integer, primary_keyTrue) title Column(String(100), nullableFalse) price Column(Float, nullableFalse) mileage Column(String(20)) register_date Column(String(10)) crawl_time Column(TIMESTAMP) def __repr__(self): return fUsedCar(title{self.title}, price{self.price})数据库操作封装from sqlalchemy.orm import sessionmaker class CarDB: def __init__(self, db_pathcars.db): self.engine create_engine(fsqlite:///{db_path}) Base.metadata.create_all(self.engine) self.Session sessionmaker(bindself.engine) def batch_save(self, car_data_list): session self.Session() try: cars [UsedCar(**data) for data in car_data_list] session.bulk_save_objects(cars) session.commit() return len(car_data_list) except Exception as e: session.rollback() print(f批量保存失败: {e}) return 0 finally: session.close()5. 数据导出与分析5.1 CSV导出实现使用pandas直接导出def export_to_csv(db_path, output_file): import pandas as pd from sqlalchemy import create_engine engine create_engine(fsqlite:///{db_path}) df pd.read_sql_table(used_cars, engine) # 数据清洗 df[price] df[price].astype(float) df[crawl_date] pd.to_datetime(df[crawl_time]).dt.date # 按品牌统计 brand_stats df.groupby(brand)[price].agg([count, mean, max]) # 保存结果 with pd.ExcelWriter(output_file) as writer: df.to_excel(writer, sheet_name原始数据, indexFalse) brand_stats.to_excel(writer, sheet_name品牌统计) print(f导出成功文件已保存至 {output_file})5.2 数据分析示例生成价格分布直方图import matplotlib.pyplot as plt def plot_price_distribution(df): plt.figure(figsize(10, 6)) df[price].plot.hist(bins20, edgecolorblack) plt.title(二手车价格分布, fontsize15) plt.xlabel(价格(万元), fontsize12) plt.ylabel(数量, fontsize12) plt.grid(axisy, alpha0.5) plt.savefig(price_distribution.png) plt.close()6. 工程化改进建议6.1 异常处理机制增强采集脚本的健壮性from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min4, max10)) def fetch_page(url): try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() return response.text except Exception as e: print(f请求失败: {e}) raise6.2 日志记录配置import logging from logging.handlers import RotatingFileHandler def setup_logger(name): logger logging.getLogger(name) logger.setLevel(logging.INFO) formatter logging.Formatter( %(asctime)s - %(name)s - %(levelname)s - %(message)s ) # 控制台输出 console logging.StreamHandler() console.setFormatter(formatter) # 文件输出(自动轮转) file RotatingFileHandler( car_spider.log, maxBytes10*1024*1024, backupCount5 ) file.setFormatter(formatter) logger.addHandler(console) logger.addHandler(file) return logger6.3 部署建议对于长期运行的采集任务建议使用APScheduler实现定时采集from apscheduler.schedulers.blocking import BlockingScheduler scheduler BlockingScheduler() scheduler.scheduled_job(cron, hour2, minute30) def daily_crawl(): # 执行采集任务 pass scheduler.start()使用Docker容器化部署FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, main.py]7. 项目扩展方向本方案可以进一步扩展为价格监控预警系统当特定车型价格低于阈值时触发通知车源对比分析横向比较不同平台的报价差异保值率计算模型基于历史数据预测车辆贬值曲线我曾在一个实际项目中通过持续采集半年的二手车数据帮助客户建立了品牌保值率评估模型最终将库存周转率提升了27%。这充分证明了数据采集和持久化的商业价值。