很多开发者的爬虫生涯,都是从一段几十行的脚本 开始的。能跑就行、逻辑直出、一把梭哈,快速拿到数据是第一目标。但随着需求迭代:网站改版、反爬升级、字段增多、多站点复用、定时调度、日志排查...... 原本顺滑的小脚本,很快会变成难读、难改、难扩展的 "屎山"。
真正的生产环境,需要的不是 "能用",而是可维护、可扩展、可监控 的爬虫项目。本文就带你完成一次关键蜕变:从一次性脚本,到工程化、可维护的爬虫项目。
一、先认清:脚本式爬虫的致命问题
一段典型的 "脚本爬虫" 长这样:
- 所有代码塞在一个
.py文件里 - 请求、解析、存储、逻辑硬编码混在一起
- 配置写死在代码中,改个 URL 要翻遍全文
- 无异常处理,一报错直接崩,不知道死在哪
- 无日志、无重试、无去重,数据质量不可控
- 换个网站 / 换个页面,只能复制粘贴改代码
这种写法在小需求、临时任务里没问题,但一旦进入长期维护,成本会指数级上升。
二、重构目标:可维护项目的核心标准
重构不是 "重写",而是让代码变得更好。一个合格的可维护爬虫项目,应满足:
- 结构清晰:职责分离,一眼看懂流程
- 配置与代码分离:不改代码就能适配站点变化
- 健壮稳定:自动重试、异常捕获、降级处理
- 易于扩展:新增站点 / 字段只加代码不改旧逻辑
- 可观测:日志清晰、进度可见、方便排查
- 可测试:核心逻辑可单独运行验证
三、实战重构:从脚本到项目的标准路径
我们以最常见的 Python requests + parsel/lxml 为例,一步步拆解。
1. 项目结构规范化
抛弃单文件,建立标准目录结构:
plaintext
spider_project/
├── config/ # 配置文件
│ ├── settings.py # 全局配置:请求头、超时、重试、路径
│ └── sites.py # 各站点规则配置:URL、XPath/选择器
├── core/ # 核心通用模块
│ ├── http.py # 请求封装:重试、代理、请求头
│ ├── parser.py # 解析封装:通用提取方法
│ └── storage.py # 存储封装:MySQL/CSV/JSON/ES
├── spiders/ # 具体爬虫
│ ├── __init__.py
│ ├── blog_spider.py
│ └── product_spider.py
├── utils/ # 工具类
│ ├── logger.py # 日志统一配置
│ ├── time_util.py # 时间处理
│ └── validate.py # 数据校验
├── main.py # 项目入口
└── requirements.txt # 依赖清单
好处:新人接手也能快速定位功能,不用在几千行里大海捞针。
2. 配置与代码解耦
把所有可变内容抽到配置里:
- URL、列表页 / 详情页规则
- 请求头、Cookie、超时时间
- 数据库连接信息
- 重试次数、并发数、间隔时间
python
运行
# config/settings.py
BASE_HEADERS = {
"User-Agent": "Mozilla/5.0 ...",
"Accept-Language": "zh-CN,zh;q=0.9"
}
MAX_RETRY = 3
TIMEOUT = 10
python
运行
# config/sites.py
BLOG_SITE = {
"list_url": "https://xxx.com/page/{}",
"total_page": 10,
"title_xpath": "//h1/text()",
"content_xpath": "//div[@class='content']//text()"
}
以后网站改了 XPath,只改配置,不动逻辑代码。
3. 封装通用请求模块(核心!)
脚本里最常见的就是满屏重复的 requests.get()。统一封装 HTTP 请求,内置:
- 自动重试
- 随机请求头
- 代理切换
- 状态码判断
- 统一异常捕获
python
运行
# core/http.py
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_session():
session = requests.Session()
retry = Retry(total=3, backoff_factor=1)
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def fetch(url, headers=None):
# 统一请求逻辑
...
所有爬虫共用这一套,改一次,全项目生效。
4. 抽取通用解析函数
把 XPath/CSS 提取、文本清洗、去空格、去换行、时间格式化,抽成通用方法:
python
运行
# core/parser.py
def extract_text(selector, xpath):
result = selector.xpath(xpath).extract_first()
return result.strip() if result else ""
爬虫里只写:
python
运行
title = extract_text(selector, site_config["title_xpath"])
解析规则统一,避免每个人写得五花八门。
5. 统一存储层
不要在爬虫里直接写 INSERT INTO。封装存储接口,支持切换存储介质:
- CSV/JSON(本地测试)
- MySQL/PostgreSQL(生产)
- Elasticsearch(搜索)
- Kafka/MQ(数据流)
python
运行
# core/storage.py
def save_item(item):
# 统一去重、校验、入库
...
以后换存储引擎,爬虫代码完全不用改。
6. 标准化日志系统
用日志代替 print(),记录:
- 当前爬取 URL
- 响应状态
- 解析失败字段
- 异常信息
- 爬取速度、总量
python
运行
# utils/logger.py
import logging
def get_logger(name):
# 按文件/控制台输出,按级别切割
...
线上出问题,看日志就能定位,不用复现现场。
7. 爬虫业务只写 "业务"
经过封装,最终爬虫文件会极度干净:
python
运行
# spiders/blog_spider.py
from config.sites import BLOG_SITE
from core.http import fetch
from core.parser import extract_text
from core.storage import save_item
from utils.logger import get_logger
logger = get_logger(__name__)
def run():
for page in range(1, BLOG_SITE["total_page"] + 1):
url = BLOG_SITE["list_url"].format(page)
resp = fetch(url)
# 解析
items = parse_list(resp.text)
for item in items:
save_item(item)
logger.info(f"页面 {page} 完成")
def parse_list(html):
# 只写当前页面专属解析逻辑
...
只关注业务,不关注底层细节。
8. 统一入口与可执行性
提供一个干净的入口:
python
运行
# main.py
from spiders.blog_spider import run as run_blog
if __name__ == "__main__":
run_blog()
配合 argparse,可以支持:
plaintext
python main.py --spider blog --page 5
四、重构后的进阶能力
当你把爬虫改成项目结构,很多能力会自然解锁:
- 单元测试:请求、解析、存储可单独测试
- 并发 / 异步改造:只改 core/http.py,不影响业务
- 接入调度:配合 Celery/Apscheduler 定时任务
- 监控告警:统一统计失败率、响应时间
- 多环境切换:开发 / 测试 / 生产配置分离
- 代码评审与协作:多人开发不互相覆盖
五、什么时候该重构?
满足任意一条,就别犹豫了:
- 单个爬虫文件超过 300 行
- 修改一个字段要改 N 个地方
- 报错要花半小时定位
- 不敢删代码,怕崩
- 新同事看不懂你的逻辑
能跑就行 ≠ 可以上线。
六、总结
从脚本到可维护项目,本质是三件事:
- 职责分离:请求、解析、存储、日志互不干扰
- 通用下沉:重复代码封装成底层模块
- 可变上浮:把配置抽离,不写死在逻辑里
重构不会让爬虫 "跑得更快",但会让你维护更轻松、排错更快速、扩展更无痛。
如果你是从 "脚本小子" 走向工程化的开发者,这套思路,适用于:普通 requests 爬虫、Playwright 爬虫、Scrapy 项目、甚至分布式爬虫架构。
先写对,再写快;先能用,再好用;先可维护,再谈高性能。