ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

爬虫与LSTM预测:一套完整的数据采集-存储-分析实战源码解析

爬虫与LSTM预测:一套完整的数据采集-存储-分析实战源码解析 简介这是一份面向高校计算机相关专业学生的爬虫与数据分析综合实践项目包融合网络信息爬取、LSTM时序预测与机器学习分析三大模块可直接用于课程设计、毕业设计或项目初期展示。包内共472个文件以pkl数据文件、Python源码、ipynb分析笔记、SQL/CSV数据文件及Docker部署配置为主并附带README与项目说明文档便于快速理解与复现。压缩包整体约174MB内置训练日志与可视化结果文件可直观对比模型效果。项目代码经过严格测试功能完整适合人工智能、电子信息、自动化、物联网等专业的学生学习进阶。目前已有63人学习对基础薄弱者还提供远程运行指导遇到配置问题可交流求助。下载后可获得完整源码、项目说明、数据文件及可视化报告方便在此基础上二次开发。1. 爬虫与数据分析实践一份能完整跑通「采集-预测-分析」的源码包需要爬虫、数据库、LSTM 预测、机器学习分析和说明文档全部配套的项目这份值得拆开看。它不是单独一段爬虫脚本也不是只给训练代码的 LSTM notebook而是把信息抓取、MySQL 落库、时序预测、特征分析和可视化报告串成了一条完整链路爬虫拿到的数据先写进数据库清洗后导出特征表再滑窗喂给 LSTM 做预测预测结果落成 vresult.csv最后用 sklearn 算指标、用 matplotlib 出对比图。比起单点教程它更像一个能直接交作业的工程样板。适合正在做课程设计、毕业设计或比赛初版的人也适合想完整跑一遍「采集-存储-预测-分析」全流程的学习者。新手照着改 URL 和字段就能运行熟手可以拿它当地基去换数据源、换预测目标。下面我按数据流向把爬虫、入库、LSTM 训练到分析报告每个环节怎么改、参数怎么设、坑在哪逐一拆开。2. 项目结构与数据链路MySQL 中转、CSV 输出、TensorBoard 日志各司其职拿到压缩包别急着跑先按文件清单过一遍。这个包除了源码和项目说明还能看到几类特殊文件events.out.tfevents.*、vresult.csv、*_loss_log.csv以及binlog.000012、general_log、slow_log这类 MySQL 相关文件。第一次拆这类项目的人容易懵其实它们各有各的用途有的必须看懂有的可以直接忽略。2.1 包里的文件在项目里分别干什么文件/文件类型在项目里的角色处理建议events.out.tfevents.*TensorBoard 训练日志记录 loss 曲线和指标训练时自动生成可用tensorboard --logdir查看*_loss_log.csv每轮训练损失CSVLogger 回调输出分析收敛情况时优先读它vresult.csv预测结果表一般含真实值、预测值、时间戳机器学习分析和画图的主入口binlog.000012/general_log.*/slow_log.*MySQL 运行日志和系统文件打包时残留的运行痕迹不用管建议忽略源码 项目说明 可视化报告项目主体按说明文档里的顺序阅读看到events.out.tfevents.*基本能判断技术栈是 TensorFlow 那套配合*_loss_log.csv说明训练时同时开了 TensorBoard 和 CSVLogger 两种记录方式。vresult.csv是整个项目的出口后面机器学习分析、可视化报告都从这张表取数。至于binlog、general_log、slow_log是开发环境 MySQL 数据目录里的日志文件被一起打进了压缩包跑代码用不到它们。有同学下载后直接拿这些目录当自己的数据文件用容易踩各种玄学问题建议一律忽略自己新建库。2.2 数据链路为什么这样设计MySQL 当中间层CSV 当接口整个项目的核心不是某个单个模型而是这条数据管道。数据流按顺序走四个节点requests 抓取页面并解析字段SQLAlchemy 写入 MySQL 做去重和增量查询结果导出成特征 CSV再滑窗构造成 LSTM 输入。预测完成写出vresult.csv最后用 sklearn 评估、matplotlib 出报告。很多人图省事直接 requests → pandas → CSV → LSTM小 demo 没问题但一旦要断点续爬、按日期增量CSV 的痛点就出来了去重要自己维护、并发写容易冲突、字段类型不严格。MySQL 做中转层唯一键去重由数据库保证爬虫崩了重跑不会重复入库。输出端再统一查成 CSV 给训练脚本这样训练代码不依赖数据库环境换台机器只要 CSV 就能复现实验。我一般会把这个链路想成「数据管道」而不是「爬虫加模型」管道里每一段的输出都要能被下一段直接消费管道里的任一环节崩了只重跑这一段就行。项目里vresult.csv单独存而不是直接写回 MySQL也是这个道理——分析脚本只读文件不碰数据库环境依赖降到最低。如果只在一台机器上跑把 MySQL 换成 SQLite 也可以代码层面只需要改 SQLAlchemy 的连接串。2.3 运行前环境准备与最小配置跑通这个项目需要 Python 3.8、MySQL 5.7以及 requests、sqlalchemy、pymysql、pandas、numpy、tensorflow、scikit-learn、matplotlib 这几个库。tensorflow 装 CPU 版就够序列量级不大时 CPU 和 GPU 的差距体现不出来。建库时记住两点字符集用utf8mb4排序规则选utf8mb4_unicode_ci连接串里带上charsetutf8mb4否则中文入库很容易乱码。CREATE DATABASE IF NOT EXISTS spider DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS articles ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, url VARCHAR(500) NOT NULL, publish_time DATETIME NULL, pv INT DEFAULT 0, category VARCHAR(64) DEFAULT unknown, UNIQUE KEY uq_article_url (url) ) ENGINEInnoDB CHARSETutf8mb4;这张表和后面 SQLAlchemy 的 ORM 模型是一一对应的URL 上的唯一键uq_article_url是去重的关键。建表时没有加唯一键的话后面爬虫重复请求会把同一篇文章插好几遍清洗阶段还得再花力气去重。DDL 里的publish_time允许为空是为了应对上游数据缺失时间字段的情况宁可留空也别让整条记录报废。3. 信息爬取落地requests 请求、字段清洗与 SQLAlchemy 入库参数爬虫部分是整条链路的入口这里的代码质量直接决定后面数据库和模型能不能省心。很多项目死在爬虫上不是因为反爬多厉害而是请求层和入库层写得不够稳。下面这三段代码是项目里最核心的部分按请求、解析、入库三个层次拆开。3.1 请求层超时、重试、UA 伪装一个都不能少import requests import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept: application/json, text/html, */*, } def fetch(url, paramsNone, max_retries3, timeout8): session requests.Session() retry Retry(totalmax_retries, backoff_factor0.8, status_forcelist[500, 502, 503, 504]) session.mount(https://, HTTPAdapter(max_retriesretry)) session.mount(http://, HTTPAdapter(max_retriesretry)) for attempt in range(max_retries): try: resp session.get(url, paramsparams, headersHEADERS, timeouttimeout) if resp.status_code 200: return resp elif resp.status_code in (403, 429): print(f[blocked] status{resp.status_code}, sleep {2 * (attempt 1)}s) time.sleep(2 * (attempt 1)) except requests.RequestException as exc: print(f[retry] {attempt 1}/{max_retries}: {exc}) time.sleep(1.5 * (attempt 1)) return None这段代码把请求层该处理的三个问题都堵上了。Retry里的status_forcelist指定哪些状态码触发重试500、502、503、504 是服务端临时故障重试合理backoff_factor0.8表示重试间隔按 0.8 秒、1.6 秒、3.2 秒递增避免刚恢复的服务又被一波请求压垮。timeout8是给每个请求设置硬超时没有它某个连接卡住会让整个爬虫挂在那不动。403 和 429 是反爬信号这里没有走重试逻辑而是指数退避后继续下一次循环。我跑采集任务时见过不少人一遇到 403 就换代理换 IP其实先降频率、把 UA 和必要的 Headers 补齐大部分静态站点就能放行。requests 爬虫最容易被封的就是缺 UA 和固定频率这两个血泪教训一定要记住。3.2 字段解析与清洗把杂乱响应变成规整记录import pandas as pd def parse_item(raw, sourceapi): if source api: data raw[data][list] else: data raw # 表格页面时raw 是 pandas 读出的 DataFrame records [] for item in data: rows { title: str(item.get(title, )).strip(), url: item.get(url, ), publish_time: pd.to_datetime(item.get(time), units, errorscoerce), pv: int(item.get(pv, 0) or 0), category: item.get(category, unknown), } if rows[title] and rows[url]: records.append(rows) return pd.DataFrame(records)解析函数的核心是「每个字段都带默认值兜底」。item.get(title, )防止 KeyError.strip()去掉前后空格pd.to_datetime(..., errorscoerce)会把无法解析的时间转成 NaTint(item.get(pv, 0) or 0)这个写法是同时处理None和空字符串的情况避免类型转换报错。最后强制要求 title 和 url 非空才保留缺关键字段的记录直接丢弃。清洗的原则是上游字段永远可能是脏的解析阶段宁可丢数据也不要让脏数据进库。实际跑的时候这个函数返回的 DataFrame 列名要和数据库表字段严格一致SQLAlchemy 这边才能直接用**row.to_dict()插入否则会出现字段对不上导致的插入失败。如果爬的是 HTML 页面而不是 JSON API把source换成html分支用 pandas 的read_html读表格也很省事。3.3 SQLAlchemy 入库ORM 模型、唯一键去重与增量更新from sqlalchemy import create_engine, Column, String, Integer, DateTime, UniqueConstraint from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class Article(Base): __tablename__ articles __table_args__ (UniqueConstraint(url, nameuq_article_url),) id Column(Integer, primary_keyTrue, autoincrementTrue) title Column(String(255), nullableFalse) url Column(String(500), nullableFalse) publish_time Column(DateTime, nullableTrue) pv Column(Integer, default0) category Column(String(64), defaultunknown) engine create_engine( mysqlpymysql://root:passwordlocalhost:3306/spider?charsetutf8mb4, echoFalse ) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session() def insert_ignore_duplicate(df): added skipped 0 for _, row in df.iterrows(): exists session.query(Article).filter_by(urlrow[url]).first() if exists: skipped 1 continue session.add(Article(**row.to_dict())) added 1 if added % 50 0: session.commit() session.commit() print(finserted{added}, skipped{skipped})ORM 模型里的UniqueConstraint(url)和 2.3 节 DDL 里的唯一键是同一件事保证同一 URL 只入库一次。insert_ignore_duplicate用先查再插的方式做增量写入每次批量跑完打印 inserted 和 skipped 计数方便确认爬虫到底新增了多少数据。注意这里没有用INSERT ... ON DUPLICATE KEY UPDATE做 upsert原因是这个场景里重复数据直接跳过就行不需要更新已有记录。如果业务上要求重复时刷新pv字段那再考虑用 MySQL dialect 的 upsert 写法。另一个细节是每 50 条 commit 一次爬虫脚本跑几个小时是常态如果最后一次性 commit中途断电会全部回滚分批提交能把失败代价控制在最近 50 条以内。4. LSTM 预测实战滑窗构建、归一化、训练回调与损失解读LSTM 部分是这个项目里最容易被玄学化的一段。其实时序预测的代码套路很固定先构造监督学习样本再归一化再搭网络训练。踩坑的地方往往不在模型结构而在数据切分和归一化这两个环节。下面按项目实际顺序走一遍。4.1 从 CSV 构造监督样本look_back 与步长的取舍import numpy as np import pandas as pd def build_dataset(series, look_back12, step1): X, y [], [] for i in range(0, len(series) - look_back - 1, step): X.append(series[i : i look_back]) y.append(series[i look_back]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) df pd.read_csv(input_series.csv, parse_dates[date]).sort_values(date) values df[value].values.reshape(-1, 1) split_idx int(len(values) * 0.8) train_raw, test_raw values[:split_idx], values[split_idx:] X_train, y_train build_dataset(train_raw, look_back12) X_test, y_test build_dataset(test_raw, look_back12) print(fX_train.shape{X_train.shape}, X_test.shape{X_test.shape})look_back12表示用过去 12 个时间点预测下一个点这个值要根据数据节奏调日粒度数据可以试 7、14、30小时粒度数据可以试 24、48。我的习惯是先看数据的周期性没有一个万能值。step参数在数据量大时用来隔点采样比如step2表示每两个样本取一个能减少训练量但会丢失部分信息默认 1 就好。这段代码最关键的是split_idx的位置时序切分必须按时间顺序从时间轴第 80% 处硬切前面做训练集、后面做测试集。很多新手在这里用了train_test_split的默认随机切分导致训练集里混进测试时间段的数据指标虚高得离谱这个问题在 5.2 节专门讲。4.2 MinMaxScaler 归一化与 Keras LSTM 模型搭建from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)) test_scaled scaler.transform(test_raw.reshape(-1, 1)) X_train, y_train build_dataset(train_scaled.flatten(), look_back12) X_test, y_test build_dataset(test_scaled.flatten(), look_back12) X_train X_train.reshape((X_train.shape[0], X_train.shape[1], 1)) X_test X_test.reshape((X_test.shape[0], X_test.shape[1], 1)) import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense model Sequential([ LSTM(32, activationtanh, return_sequencesTrue, input_shape(12, 1)), LSTM(16, activationtanh), Dense(1) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossmae, metrics[mae] ) model.summary()归一化这里有个容易出错的细节fit_transform只用在训练集上测试集必须用同一个scaler.transform绝不能让测试数据参与 scaler 的拟合否则就是数据泄漏。你把测试集的信息提前告诉了模型验证结果就没有意义了。这段代码先整体归一化再切窗顺序是反的也没事不顺序很重要——先归一化整段原始数据再按索引切分和先切分再分别归一化效果有细微差别。这里写的是先归一化再切窗前提是 scaler 只 fit 训练部分。稳妥做法是切分后再对训练段 fit再 transform 测试段上面代码里train_raw和test_raw已经切好所以fit_transform作用在训练段逻辑是对的。网络结构用两层 LSTM 加一层 Dense第一层return_sequencesTrue是为了把完整序列传给第二层第二层不返回序列只输出最后一个隐藏状态最后接一个线性层输出预测值。units 从 32 到 16 递减是常见做法序列简单时 16 个单元就够复杂序列可以加大到 64。lossmae不是默认选项我偏好它是因为对异常值比 MSE 稳健预测结果做反归一化后量纲也更直观。4.3 训练回调CSVLogger、TensorBoard、EarlyStopping 与损失解读from tensorflow.keras.callbacks import EarlyStopping, CSVLogger, TensorBoard callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), CSVLogger(train_loss_log.csv, appendFalse), TensorBoard(log_dirlogs/fit, histogram_freq1) ] history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs100, batch_size32, callbackscallbacks, verbose1 )这三个回调是项目里*_loss_log.csv和events.out.tfevents.*的直接来源。CSVLogger把每轮训练和验证的 loss 写进 CSV训练结束后不用重新跑模型就能分析收敛情况。TensorBoard记录更详细的训练过程命令行执行tensorboard --logdir logs/fit就能在浏览器里看曲线。EarlyStopping的patience10表示 val_loss 连续 10 轮不改善就自动停restore_best_weightsTrue会把模型权重回滚到最优的那一轮相当于免费防过拟合。训练完最该看的是 loss_log CSV 里最后几行的训练 loss 和验证 loss 差值。如果训练 loss 一路降、验证 loss 却反弹说明模型开始记训练集了如果两者都高先怀疑归一化或数据切分出了问题。实际项目里我通常把 epochs 设成 100 到 200配合早停真正跑多少轮由数据自己决定。5. 避坑与排查从爬虫到 LSTM 的五个典型翻车点这部分是血泪经验的集合。项目里遇到的坑其实非常集中反爬、乱码、数据泄漏、Loss 不收敛、预测滞后。下面五条是从采集端到预测端最常见的问题按「现象 → 原因 → 解决」拆开讲。先看现象总览再逐条过。5.1 先看现象类型再定位原因现象大概率原因解决方向爬虫跑一会就返回验证码或 403请求频率太高或 UA 缺失随机延时加慢请求补全请求头数据库里中文全是问号建库字符集不是 utf8mb4连接串漏 charset重建库连接串补charsetutf8mb4LSTM 训练 loss 变成 nan数据未归一化或学习率过大检查 inf 值学习率降到 0.001预测曲线比真实值滞后一拍单步滑窗预测的均值回归现象改多步预测评估趋势而非点值测试集指标好得不真实切分数据时打乱了时间顺序按时间截断禁止随机 shuffle5.2 五个典型坑现象、原因、解决坑 1爬虫请求超时或返回验证码页现象是脚本跑几十条后突然拿不到数据返回的 HTML 里全是验证码或者「访问异常」。原因是请求频率太高同一个 IP 和 UA 被服务端识别。解决方法是请求层加上随机延时1 到 3 秒之间的随机 sleep比固定 sleep 更有用UA 做成列表轮换每次请求换一个。把 403 和 429 状态码当作信号而不是报错主动退避重试。我见过不少人一被限流就去换 IP 池其实对小型爬虫来说降频率比换 IP 更靠得住先把频率和伪装做好再谈其他。坑 2MySQL 写入中文乱码现象是数据库表里中文标题全是问号。原因几乎都是字符集问题建库时没有指定utf8mb4或者连接串里漏了charsetutf8mb4。解决方法是删库重建或者新建库时执行 2.3 节那段 DDL确保库、表、连接三层字符集一致。已经在表里的乱码数据靠 ALTER 修复基本没戏直接 drop 掉按正确字符集重建再重跑一遍入库脚本十分钟的事。这个坑踩一次就够了所以我在 2.3 节特意强调 utf8mb4。坑 3LSTM 训练 loss 变成 nan现象是训练到第几轮 loss 突然变成 nan后面所有 epoch 全废。原因最常见的是输入序列里有 inf 值或者数据没归一化其次是学习率太大导致梯度爆炸。解决方法是按顺序排查先检查输入数据有没有 inf 或超大值再确认 MinMaxScaler 已经 fit 过最后把 Adam 的学习率从 0.01 降到 0.001必要时加梯度裁剪。这个坑 90% 的情况出在前两步模型结构本身很少是 nan 的元凶。坑 4预测曲线比真实值「滞后一拍」现象是预测曲线整体向右平移了一段看起来像昨晚的真实值被原样搬到了今天。原因是单步滑窗预测在序列相关性强的数据上会退化成「复制最近值」的模型这是 LSTM 时序预测的经典现象不是模型坏了。解决方法是别拿预测值和真实值逐点比大小改看趋势方向和拐点位置或者改成多步预测一次预测未来 N 个点强迫模型学习外推而不是复制。评估指标也要跟着换用趋势命中率比 MAE 更贴近真实业务。坑 5切分数据时打乱时间顺序指标虚高现象是测试集 MAE 好得不真实R² 高到不敢信但画图发现预测值完美贴合真实值。原因是用train_test_split默认的随机切分把时间顺序打乱了训练集里混进了测试段的数据。解决方法是时序切分必须按时间索引硬截断train_test_split在这里要么传shuffleFalse要么干脆手动切片就像 4.1 节里values[:split_idx]和values[split_idx:]那样。这是我做第一个单变量预测时踩过的坑当时 R² 高达 0.98一查原因就是泄漏从那以后我每次切分都先打印两端的时间戳确认顺序。5.3 跑长任务前留好「后悔药」五分钟检查清单训练跑十几个小时之前先花五分钟过一遍这个清单能省下大量返工时间。第一爬虫字段和数据库表结构是否严格对应列名、类型都要核。第二时间序列是否严格按时间升序排列排序前先确认有没有重复时间戳。第三归一化是否只 fit 了训练集测试集没有参与。第四loss_log.csv的第一行数据是不是正常初始值而不是空文件或乱码。第五MySQL 连接串是否带charsetutf8mb4。6. 机器学习分析与可视化验证用基线对比和指标报告证明 LSTM 效果预测跑完不算完事第 5 章的坑都可能埋在这里。Kaggle 项目里 LSTM 不是竞品是团队方案的差异化亮点。项目说明里写的「机器学习分析」不只是画图而是用 sklearn 的基线模型和指标把 LSTM 的效果量化出来让报告里的每个数字都能被复查。6.1 先跑一个线性回归基线from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score result pd.read_csv(vresult.csv) y_true result[true_value].values y_pred result[pred_value].values X_flat X_test.reshape(len(X_test), -1) lr LinearRegression() lr.fit(X_train.reshape(len(X_train), -1), y_train) lr_pred lr.predict(X_flat) print(LinearRegression MAE:, mean_absolute_error(y_true, lr_pred)) print(LSTM MAE:, mean_absolute_error(y_true, y_pred)) print(R2:, r2_score(y_true, y_pred))机器学习分析的第一步永远是对照组。线性回归在同样的滑窗特征上训练如果它的 MAE 和 LSTM 差不多说明序列规律很简单LSTM 是杀鸡用牛刀只有 LSTM 明显优于线性回归时时序记忆模块的价值才成立。这一步放进课设和毕设报告里是有分量的一笔。6.2 可视化报告损失曲线加预测对比图import matplotlib.pyplot as plt plt.style.use(ggplot) fig, axes plt.subplots(1, 2, figsize(12, 4)) loss_df pd.read_csv(train_loss_log.csv) axes[0].plot(loss_df[epoch], loss_df[loss], labeltrain) axes[0].plot(loss_df[epoch], loss_df[val_loss], labelval) axes[0].set_title(Loss Curve) axes[0].legend() axes[1].plot(y_true, labeltrue) axes[1].plot(y_pred, labelpred) axes[1].set_title(Prediction vs Truth) axes[1].legend() fig.tight_layout() plt.savefig(report.png, dpi150)两张图构成可视化报告的核心左边是训练损失曲线右边是预测对比。如果右边两条线整体贴近但不重合后退一步看是不是 5.2 节坑 4 说的滞后问题。dpi150出图够清晰直接插进毕设报告不会糊。项目包里的资料是按这个思路组织的看到报告基本能反推每个图表对应的 CSV 来源。6.3 核对产物再收工收尾时把 vresult.csv 和 loss_log.csv 打开核对一遍预测结果表的行数等于测试样本数时间列严格递增训练日志最后一轮的损失低于初始状态。确认这两点后报告里的每一个数字都能查到出处不会出现图和数据对不上的尴尬。我从第一次跑类似项目到现在养成一个强迫症训练完不看模型打印的 loss而是先把 vresult.csv 加载进来逐列核对行数、时间和数值区间确认没有 NaN 再画图。包里那几个 CSV 就是我每次都要手动留底的产物虽然多花五分钟但能让报告里的每个结论都查得到来源。希望这个习惯和这套流程对你有帮助。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进