爬虫开发与机器学习模型部署的结合,正在成为数据处理领域最有性价比的技术组合。单纯会写爬虫,只能算掌握了数据的入口;把模型部署接入采集流程,才能让数据在采集的同时就完成清洗、分类和判断。这篇文章按照从零到部署的顺序,把整条链路的实践方法完整梳理一遍,适合有一定Python基础、想搭建数据智能化流水线的开发者。

一、搭建稳定的爬虫采集层
爬虫是整条流水线的地基,地基不稳,后面的模型再好也白搭。推荐使用Scrapy框架作为主力采集工具,它自带并发调度、去重过滤和中间件机制,比手写requests循环稳定得多。下面是一个基础的爬虫骨架:
import scrapy
class ArticleSpider(scrapy.Spider):
name = 'article'
start_urls = ['https://ipipp.com/news']
def parse(self, response):
for item in response.css('div.article-item'):
yield {
'title': item.css('h2::text').get(),
'content': item.css('p.content::text').getall(),
'url': response.urljoin(item.css('a::attr(href)').get())
}
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
采集层要重点解决三个问题:稳定性、数据质量和反爬应对。稳定性方面,建议配置自动重试和下载延迟,避免请求过快触发封禁。数据质量方面,尽量在爬虫内部就完成字段抽取和初步校验,不要把脏数据留到后面。反爬方面,除了常见的User-Agent轮换和代理池,还可以借助Scrapy的中间件机制做统一处理,把反爬逻辑和业务解析逻辑解耦。
另外要养成一个习惯:所有爬取下来的原始数据先落库再处理,哪怕只是存一份JSON备份。采集到的原始数据是不可再生的,一旦解析逻辑写错还能回溯重跑,直接在内存里边爬边解析的做法风险很高。
二、数据清洗与特征工程
原始网页数据噪声很大,直接拿去训练模型效果通常很差。这一步的目标是把非结构化文本转成模型能消化的特征向量。常用的处理包括:去除HTML标签残留、过滤特殊字符、中文分词、去停用词、以及文本向量化。pandas配合jieba和scikit-learn可以快速搭建这条清洗管道:
import pandas as pd
import jieba
from sklearn.feature_extraction.text import TfidfVectorizer
def clean_text(text):
words = jieba.lcut(text)
return ' '.join(w for w in words if len(w) > 1)
df = pd.read_json('raw_data.json').dropna(subset=['content'])
df['clean'] = df['content'].apply(clean_text)
vectorizer = TfidfVectorizer(max_features=5000)
X = vectorizer.fit_transform(df['clean'])
特征工程阶段有几个容易踩的坑。第一是TF-IDF向量的维度不要开太大,五千到一万的特征量级对中文文本分类已经够用,维度过高反而会让模型过拟合。第二是训练集和线上数据必须用同一个vectorizer实例做转换,很多新手会把vectorizer重新fit一遍,导致线上特征和训练特征对不上,模型输出全是乱的。第三是清洗逻辑要封装成独立函数并保存下来,因为后面部署时还要用一模一样的逻辑处理实时爬取的数据。
清洗完成后的数据要划分训练集和测试集,用简单的逻辑回归或者LightGBM先跑一个基线模型。基线模型的作用不是追求精度,而是给后续优化提供一个参照点,避免陷入无方向的调参。
三、模型服务化:用FastAPI包装推理接口
模型训练好之后,怎么让它进入爬虫流程?最常见的做法是把模型包装成HTTP接口,爬虫在采集到数据后立即调用接口完成实时判断。相比Flask,FastAPI自带异步支持和自动文档,更适合高并发的爬虫场景:
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
app = FastAPI()
model = joblib.load('text_classifier.pkl')
vectorizer = joblib.load('tfidf_vectorizer.pkl')
class Item(Base_model = None
class Item(BaseModel):
text: str
@app.post('/predict')
def predict(item: Item):
features = vectorizer.transform([item.text])
label = model.predict(features)[0]
return {'label': int(label), 'score': float(model.predict_proba(features).max())}
注意上面代码中vectorizer和model都是从磁盘加载的全局对象,服务启动后常驻内存,避免每个请求都重复加载,这是接口性能的关键。爬虫侧的调用也很简单,在Scrapy的pipeline里发一个HTTP请求即可:
import requests
class MLPipeline:
def process_item(self, item, spider):
resp = requests.post('http://127.0.0.1:8000/predict',
json={'text': item['content']},
timeout=5).json()
item['category'] = resp['label']
if resp['score'] < 0.6:
item['need_review'] = True
return item
这套架构的好处是解耦彻底:模型升级时只需要重启接口服务,爬虫代码一行都不用改;接口还可以同时服务其他业务方。如果爬虫和模型服务部署在同一台机器上,也可以直接在pipeline里import模型对象,省去网络开销,适合对延迟要求极高的场景。两种方式按实际需求选择即可。
四、容器化部署与任务调度
单机跑通之后,生产环境需要考虑部署问题。推荐用Docker把爬虫和模型服务分别打包成容器,用docker-compose统一编排。模型服务的Dockerfile可以这样写:
FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
容器化的最大价值在于环境一致性。模型依赖的库版本和系统环境经常和爬虫冲突,各自打包成独立镜像后互不干扰,迁移服务器也只需要拉取镜像即可。对于周期性采集任务,配合APScheduler或者crontab做定时调度,让爬虫每天固定时间运行,采集到的增量数据自动流入模型接口完成分类入库。
最后不要忘了监控环节。建议在接口层记录每次预测的置信度分布,当置信度整体下降时往往意味着线上数据分布发生了漂移,这时候就该考虑用新采集的数据重新训练模型了。爬虫、模型、部署形成闭环之后,这条流水线就具备了自我迭代的能力,这也是从会用爬虫到精通爬虫的真正分水岭。