简介:面向Python爬虫与数据分析学习者的完整实践项目,集成信息爬取、LSTM时序预测与机器学习分析,适合课程设计、毕业设计、项目立项演示或作为实战入门参考。压缩包共472个文件,源码以Python脚本和Jupyter Notebook为主,另有大量pkl文件保存模型权重与中间结果、sdi数据存储文件、CSV结果表及项目说明文档,整体约174MB,目录结构清晰,便于按模块检索。目前已有63人学习/下载。项目不仅提供可运行的爬虫和预测分析代码,还附带模型训练日志、损失记录、可视化CSV结果与设计文档,覆盖数据采集、清洗、特征处理、模型搭建、训练评估到报告生成各个环节,可帮助读者理解一个真实数据分析项目的完整组织方式,也可在此基础上进行二次开发,完成其他数据场景的扩展应用。
1. 一个zip三种活:这套爬虫与数据分析实践到底解决什么问题
拿到“爬虫与数据分析实践-信息爬取+LSTM预测+机器学习分析(含源码+项目说明+可视化报告).zip”这个标题时,先别急着解压。它其实是一条完整的流水线:先用 Python 爬虫把外部信息抓下来,再交给机器学习做常规分析,最后用 LSTM 对时间序列做预测,全程配好源码、说明文档和可视化报告。对刚接触 python爬虫和数据分析的从业者来说,这是少有的能把“采集、建模、预测、出报告”一次跑通的样例项目;对熟手来说,它更大的价值是参数怎么设、坑在哪,而不是源码本身多玄乎。这篇笔记就按我自己的落地顺序,把每个环节拆开讲,包含可直接复制的代码、推荐参数和踩过的坑。
2. 信息爬取模块:requests 加 SQLAlchemy,把网页数据变成可训练样本
2.1 先用 requests 把目标页面拉下来,别急着上 Scrapy
常见做法是先写一个最简爬虫验证数据可得性,再考虑并发和框架。一个 requests 脚本足够应付中小型站点,结构清晰,调试也方便。下面这段是我最常用的起始骨架,目标 URL 换成你要采集的页面即可。
import requests import time HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_html(url: str, retry: int = 3) -> str: for attempt in range(retry): try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.RequestException as e: print(f"[{attempt + 1}/{retry}] 请求失败: {e}") time.sleep(2 ** attempt) # 指数退避 raise RuntimeError(f"连续 {retry} 次请求失败: {url}") html = fetch_html("https://example.com/data") print(len(html))这段代码有几个值得注意的地方。apparent_encoding根据页面内容推断编码,能减少中文乱码问题;超时设为 10 秒,避免单个页面拖死整个采集;指数退避让重试间隔呈 2 秒、4 秒、8 秒递增。对反爬比较敏感的站点,我一般再把time.sleep(1)加到每次请求之间,控制 QPS 在 1 左右。源码包里如果带了headers配置,注意核对 Cookie 和 Referer,有些站点缺了 Referer 直接返回 403。
2.2 解析与清洗:正则和 BeautifulSoup 怎么选,字段怎么对齐
页面拉到本地后,下一步是提取字段。标题和正文用 BeautifulSoup 更直观,但列表页里夹杂的 JSON 数据用正则更快。这个项目标题指向“信息爬取”,没有限定具体站点,所以解析层要做得通用一点:先把 HTML 转成树,再对每个目标字段写独立的提取函数。
from bs4 import BeautifulSoup import re import json def extract_records(html: str): soup = BeautifulSoup(html, "html.parser") records = [] for item in soup.select("div.item"): title_node = item.select_one("h2 a") title = title_node.get_text(strip=True) if title_node else "" # 部分页面把时间藏在 data 属性里,正则兜底 time_match = re.search(r"data-time=['\"](\d{4}-\d{2}-\d{2})['\"]", str(item)) publish_time = time_match.group(1) if time_match else None # 有些字段以 JSON 形式嵌在 script 标签中 script_text = item.find("script").string or "" if script_text: try: extra = json.loads(re.search(r"var data = ({.*?});", script_text).group(1)) value = extra.get("value") except (AttributeError, json.JSONDecodeError): value = None records.append({ "title": title, "publish_time": publish_time, "value": value, }) return records字段对齐是整个环节里最容易被低估的一步。爬虫拿到的是字符串,后续 LSTM 和机器学习要的是数值,所以清洗函数要返回严格的类型:时间转成datetime.date,数值字段转成float,空值统一用None而不是空字符串。源码里的项目说明如果提到“数据字典”,那大概率就是用来约定这些字段类型的,跑模型前先对一遍能省不少事。
2.3 SQLAlchemy 建表与入库:为什么不用 CSV 一步到位
很多入门教程喜欢to_csv一把梭,但一旦采集量上去,CSV 的三个问题就暴露了:类型会丢、断点续采麻烦、多文件合并乱。这个项目既然带了“数据分析”环节,我更推荐把数据落到 SQLite,后面用 SQL 做聚合和抽样都很方便。
from sqlalchemy import create_engine, Column, Integer, String, Date, Float from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class Record(Base): __tablename__ = "records" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(500)) publish_time = Column(Date, index=True) value = Column(Float) source_url = Column(String(1000), unique=True) # 去重用 engine = create_engine("sqlite:///data.db") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) def save_records(records: list[dict]): session = Session() try: for rec in records: session.merge(Record( title=rec["title"], publish_time=rec["publish_time"], value=rec["value"], source_url=rec["source_url"], )) session.commit() except Exception: session.rollback() raise finally: session.close()核心是session.merge:它按主键判断记录是否存在,存在就更新,不存在就插入。source_url加了唯一索引,天然实现 URL 级别的去重,重复采集同一页面时不会产生脏数据。我习惯把日期字段加索引,因为后面 LSTM 取训练集时常按时间范围过滤,没索引的表在几十万行时会明显变慢。这里用 SQLAlchemy 不是炫技,是为了让爬虫和数据分析共用同一个 ORM 模型,后续做特征查询时少写一半胶水代码。
3. LSTM 预测:窗口、归一化和 PyTorch 实现,一个都不能少
3.1 window_size 和 horizon:先想清楚要预测什么,再谈模型
LSTM 时间序列预测里最容易犯的错,是不区分“用过去多少步预测未来多少步”。常见做法是固定一个 horizon(预测目标长度),再根据数据周期选 window_size。做日频数据预测时,我一般先试 window_size=30,horizon=1;如果数据带周周期,window_size 至少取 7 的倍数。源码里的 LSTM 预测模块如果给了参数配置文件,留意里面seq_len和pred_len这两个键,它们对应的就是这里说的窗口和预测步长。
import numpy as np from sklearn.preprocessing import MinMaxScaler def make_sequences(data: np.ndarray, window_size: int = 30, pred_len: int = 1): """ 把一维时间序列切成 (X, y) 监督学习样本。 X 形状: (样本数, window_size, 1) y 形状: (样本数, pred_len) """ X, y = [], [] for i in range(len(data) - window_size - pred_len + 1): X.append(data[i: i + window_size]) y.append(data[i + window_size: i + window_size + pred_len]) return np.array(X), np.array(y) scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(values.reshape(-1, 1)).flatten() X, y = make_sequences(scaled, window_size=30, pred_len=1) split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:]切窗口的代码要注意两点。第一,pred_len大于 1 时,输出层神经元数量要相应改成pred_len,损失函数用 MSE 即可,不需要改结构;第二,切完样本后必须按时间顺序划分训练集和测试集,不能用train_test_split随机打乱,否则时间顺序被破坏,模型等于偷看了未来数据。测试集比例我常用 0.2,样本量少于 500 时建议改成 0.15,给训练集多留一点数据。
3.2 归一化必须和逆变换成对出现,否则预测数字没法看
LSTM 对输入尺度敏感,tanh 和 sigmoid 激活函数在输入绝对值过大时梯度容易饱和。常见做法是 MinMaxScaler 或 StandardScaler,前者适合已知上下界的平稳序列,后者适合分布相对对称的数据。这里的关键不是选哪个,而是逆变换时别忘把预测结果还原回原始量纲。
pred_scaled = model.predict(X_test) # 假设已经训练好 pred_original = scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten()一个隐蔽的坑是:训练时如果对整条序列做 MinMaxScaler,再用滑动窗口切样本,测试集里会混进整体最大值和最小值的信息。严格做法是先切分原始序列,再分别对训练段和测试段做归一化。这个项目里如果只提供了一个 scaler 对象,那大概率是整体归一化,能做但不严谨;如果你要拿结果说服别人,建议改成两段分别 fit。逆变换后的负值或超出合理区间的数字,不代表模型错了,先检查是不是归一化边界对不上。
3.3 PyTorch 实现最小可训练 LSTM:从定义到训练的完整代码
现在很多 python 数据分析项目已经转向 PyTorch,因为动态图调试方便,断点打印也更直观。下面是一个能直接跑的 LSTM 回归模型,输入是单变量时间序列,输出是标量预测。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=1): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 if num_layers > 1 else 0, ) self.fc = nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(last) model = LSTMPredictor(input_size=1, hidden_size=64, num_layers=2, pred_len=1) optimizer = torch.optim.Adam(model.parameters(), lr=0.001) criterion = nn.MSELoss() X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) for epoch in range(100): model.train() optimizer.zero_grad() y_pred = model(X_train_t) loss = criterion(y_pred, y_train_t) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() if (epoch + 1) % 20 == 0: print(f"epoch {epoch + 1:03d}, loss {loss.item():.6f}")batch_first=True让输入张量形状保持(batch, seq_len, input_size),和之前make_sequences的输出形状直接对齐,省去 transpose。取最后一个时间步的隐状态作为全连接层的输入,是单步预测的常见做法;如果要预测多步,可以改为把所有隐状态输出都接给fc,或者用 teacher forcing 逐步预测。clip_grad_norm_防止梯度爆炸,学习率 0.001 是 Adam 的默认推荐值,但数据噪声大时可以降到 0.0005,让损失曲线更平滑。
训练完成后,验证环节不能只看 loss。要做一次“平移对比”:把测试集里真实曲线和预测曲线画在同一张图上,重点看趋势方向和峰值是否错位。LSTM 对单变量序列经常有滞后效应,预测峰值比真实晚一个周期是正常表现,如果连趋势都反向,那问题多半出在窗口长度或归一化上,而不是网络结构。
4. 机器学习分析:用 sklearn 补上 LSTM 看不透的维度
4.1 不只是再跑一个模型:相关性和聚类才是分析报告的主菜
LSTM 擅长拟合时间序列,但它给不出“哪些特征对结果影响最大”这类解释。这个项目标题里把“机器学习分析”和“LSTM 预测”并列,说明作者希望用传统机器学习补充可解释性。我一般会做三件事:先算特征相关性矩阵,再做一个随机森林回归看特征重要性,最后对样本做 KMeans 聚类观察数据是否天然分成几群。
import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.cluster import KMeans df = pd.read_sql("SELECT * FROM records", engine) df["value_lag1"] = df["value"].shift(1) df["value_lag7"] = df["value"].shift(7) df["weekday"] = pd.to_datetime(df["publish_time"]).dt.weekday df = df.dropna() features = ["value_lag1", "value_lag7", "weekday"] X = df[features] y = df["value"] rf = RandomForestRegressor(n_estimators=200, max_depth=6, random_state=42) rf.fit(X, y) importance = pd.Series(rf.feature_importances_, index=features).sort_values(ascending=False) print(importance) kmeans = KMeans(n_clusters=3, n_init=10, random_state=42) df["cluster"] = kmeans.fit_predict(X)随机森林的max_depth我是有意限制的,因为这类样本维度不高,树太深只会记住噪声。n_estimators=200在速度和精度之间比较均衡,超过 300 后收益递减。聚类数n_clusters=3不是拍脑袋,建议先跑一下轮廓系数,看 2 到 6 的区间里哪个值最合理;源码里的分析脚本如果写死了n_clusters,你至少要知道怎么去验证它。这一步的意义是:LSTM 给的是“接下来涨还是跌”,聚类给的是“当前数据属于哪一类场景”,两种结论放一起,报告才完整。
4.2 可视化报告:matplotlib + pandas,图要能直接进 PPT
项目标题最后挂着“可视化报告”,说明不是让自己看一眼,而是要交付给别人。matplotlib 是主力,但有两个细节十有八九会翻车:中文字体显示成方块,以及时间轴 X 轴标签拥挤。下面这段代码把这两个问题一起处理。
import matplotlib.pyplot as plt import matplotlib.dates as mdates plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(2, 2, figsize=(14, 8)) # 1. 真实值 vs LSTM 预测值 axes[0, 0].plot(test_dates, y_test_original, label="真实值", linewidth=1.5) axes[0, 0].plot(test_dates, pred_original, label="LSTM预测", linewidth=1.5, alpha=0.8) axes[0, 0].set_title("LSTM 预测效果对比") axes[0, 0].legend() # 2. 特征重要性 axes[0, 1].barh(importance.index, importance.values) axes[0, 1].set_title("特征重要性") # 3. 聚类散点 axes[1, 0].scatter(df["value_lag1"], df["value_lag7"], c=df["cluster"], cmap="viridis", s=8) axes[1, 0].set_xlabel("lag1") axes[1, 0].set_ylabel("lag7") # 4. 数据分布直方图 axes[1, 1].hist(df["value"], bins=50, alpha=0.7) axes[1, 1].set_title("数值分布") fig.autofmt_xdate(rotation=30) plt.tight_layout() plt.savefig("report.png", dpi=150) plt.close()font.sans-serif前两项是 Windows 和 macOS 常见的字体,在 Linux 服务器上要改成系统实际安装的中文字体,否则依然方块。fig.autofmt_xdate自动旋转日期标签,比手动set_xticklabels省事。保存图时用plt.close()释放内存,我在批量生成多张图的血泪经验是:不 close 的话,循环一百次后内存直接翻倍,且不好排查。
4.3 报告页怎么组织:从数据概况到可执行结论
可视化报告不是图越多越好,核心要传递三张图。第一张是数据时间线,让读者一眼看到整体趋势和异常点;第二张是预测对比,证明 LSTM 不是黑匣子;第三张是特征和聚类结果,回答“数据为什么呈现这个规律”。我一般用 pandas 的describe()生成基础统计表放在报告开头,再把上面四张图按“概况、预测、解释”的顺序排列。
summary = df[["value", "value_lag1", "value_lag7"]].describe().T summary.to_csv("summary_stats.csv", encoding="utf-8-sig")导出 CSV 时用utf-8-sig而不是utf-8,这样用 Excel 打开不会乱码。报告最后一定要写一段结论文字,哪怕只是三句话:预测周期是什么,预测结果相对历史均值偏离多少,聚类中哪个群的样本最值得关注。这个项目说明文档如果写了结论文案,可以直接复用;如果没写,这段文字是最能拉开报告质量差距的地方。
5. 避坑:让这套源码跑起来时最常见的五个翻车点
5.1 反爬导致数据量不足,LSTM 训练集不够用
现象:爬虫跑完只采到几百条记录,LSTM 训练出来的曲线严重过拟合,测试集 loss 明显大于训练集。 原因:目标站点限制了访问频次,或请求头缺 Cookie,大量请求返回 403 导致有效数据锐减。 解决:先检查日志里 403 的比例。如果是频次问题,把每次请求间隔提高到 2 至 3 秒,并开启指数退避;如果站点要求登录,用requests.Session先登录再采集。数据量低于 1000 条时,LSTM 的时序预测价值有限,不如先用 ARIMA 或随机森林做基线,至少结果可解释。
5.2 SQLAlchemy 入库后字段类型对不上,日期变成字符串
现象:入库时报TypeError,或者从库里读出的publish_time是字符串,导致dt.weekday报错。 原因:解析层提取日期时返回了str,ORM 里声明Date类型,SQLite 做了隐式转换,读出时没转回datetime.date。 解决:在save_records入口统一校验字段类型,例如datetime.date.fromisoformat(value)强制转换。读取时再写一次类型转换兜底:pd.to_datetime(df["publish_time"]),这比在 ORM 层面死磕更实际。
5.3 PyTorch 训练时维度对不上,报 “shape mismatch”
现象:y_pred和y_train的形状不同,常见报错是Expected input batch_size to match target size。 原因:make_sequences里pred_len=1,但全连接层输出维度写成hidden_size,或者y的形状是(batch, 1),而预测结果是(batch,)。 解决:输出层维度统一为pred_len;如果y是二维,在损失函数前加.squeeze()或.unsqueeze(1)。debug 时打印三行:X_train.shape、y_train.shape、model(X_train).shape,一眼就能定位问题。
5.4 图表中文全部变成方块,报告直接没法用
现象:生成 PNG 后标题和坐标轴标签全是方块,英文正常。 原因:当前环境没有可用的中文字体,或者字体配置在运行时被覆盖。 解决:先执行fc-list :lang=zh(Linux)检查系统字体,Windows 直接使用 SimHei,macOS 使用 PingFang SC。最稳的做法是在代码开头同时设置font.sans-serif和axes.unicode_minus,不要在画图函数内部反复修改,全局统一配置一次,后面所有图都不会再出问题。
5.5 训练和预测用了不同的 scaler,预测结果严重偏移
现象:训练集 loss 很低,但预测值整体比真实值高或低一个固定倍数。 原因:训练时对全量数据做了一次fit_transform,测试阶段又重新fit,scaler 的 min/max 变了。 解决:只允许对训练集fit,然后用同一个 scaler 对测试集transform。如果必须整体归一化,把 scaler 用joblib.dump保存下来,预测脚本里只load不重新fit。这个问题非常隐蔽,因为 loss 正常人都不怪,但预测数字一眼就能看出不对。
6. 进阶用法:把单机脚本变成定时采集与预测任务
跑通这套源码之后,下一步是让它每天自动更新数据、重新训练、出报告,而不是每次都手动执行。常见做法是用 APScheduler 写一个最简单的定时任务,把爬虫、训练、绘图的三个模块串起来。
from apscheduler.schedulers.blocking import BlockingScheduler def job(): html = fetch_html(SOURCE_URL) records = extract_records(html) save_records(records) run_lstm_training() # 这里封装整个训练流程 generate_report() # 生成可视化报告 scheduler = BlockingScheduler() scheduler.add_job(job, 'cron', hour=8, minute=30) scheduler.start()定时任务不要选在整点,整点是很多网站更新数据和执行批处理的拥挤时段,8:30 这类非整时间请求成功率更高。训练频率也需要克制:日频数据一周重训一次已经足够,每次全量重训既费时间,又可能让模型在短期波动里反复震荡。我自己的习惯是保留最近三次训练好的权重文件,每次新权重生成后,先跑一次测试集对比再决定是否替换,不给模型留后悔药。这里有个容易被忽略的验证技巧:用滚动窗口做回测,每次只拿最近 30 个点重训,不断向前平移,累计误差比单次训练更有说服力。
这个 zip 项目最值得投入的地方,不在于某一段代码多漂亮,而在于它提供了一条完整的、可以扩展的链路。把爬虫换成你自己的数据源,把 LSTM 换成别的时序模型,把可视化报告改成定时发送的 PDF,整套骨架依然成立。希望这篇文章能帮你少走几步弯路,尤其第五章那五个坑,哪怕只避开其中一个,也值回你读这篇笔记的时间了。
本文还有配套的精品资源,点击获取