
简介面向新闻数据获取与全链路分析需求这份资源提供了基于Flask与Vue3构建的前后端分离式新闻数据分析平台完整实现适合具备Python与前端基础、希望通过真实项目掌握爬虫采集、数据清洗、模型构建与可视化展示的开发者学习参考。包内共458个文件约171.84MB包含70个Python后端脚本、154个TypeScript与64个Vue前端组件、SQL数据库脚本及说明文档还配有Excel数据样例、图表配置与Chromedriver等运行辅助文件目录结构清晰可直接导入MySQL和Redis后启动使用。项目以今日头条为目标源完整覆盖Selenium数据爬取、Pandas清洗分析、NumPy建模及PyEcharts可视化输出等环节针对常见运行环境Python3.9、Node16、MySQL8、Redis6给出了明确配置指引。目前已有197人学习或下载是一份能帮助快速搭建新闻数据分析全流程的实战型资源。1. 新闻数据分析平台从爬虫到看板的完整闭环说实话接到这套资源的时候我第一反应是“又来一个爬虫包”。但真正把源码和数据库跑起来之后我改变了判断——它解决的不是“怎么爬今日头条”这一个点而是把「数据采集 → 清洗入库 → 后端建模 → 前端可视化」整条链路串通了。对于一个想学 Flask Vue3 ECharts 全栈开发或者想把新闻数据变成可分析报表的从业者来说这比单独看爬虫教程有用得多。项目核心是用 Python 爬虫采集今日头条新闻流把标题、来源、发布时间、评论数、热度字段落库再通过 Flask 提供查询接口最后在 Vue3 前端用 ECharts 呈现趋势曲线、来源占比和热词排行。适合正在做数据分析课设、公司内部舆情看板、或者想找一份完整全栈案例照着改的人。2. 后端与数据模型Flask 接口设计、新闻表结构和采集入库2.1 Flask 还是 FastAPI为什么这套资源用 Flask我去年用一个 FastAPI 项目做了异步爬虫服务的改造性能确实好但代价是协程上下文、依赖注入、异步 ORM 这些概念把团队里两个刚入门的同事卡了好几天。这套新闻数据分析平台的定位是“能看懂、能改、能跑”Flask 的同步路由写法更符合多数人已有的 Django/Flask 经验而且网上能搜到的 Flask 教程、部署案例、报错解决方案数量是 FastAPI 的几倍。真要对比我认为关键差异不在性能而在生态成熟度和团队认知成本爬虫采集本身是 IO 密集但低频的任务Flask 默认的开发服务器加threadedTrue足够应付前端看板一次只拉几个聚合接口也没有高并发压力。所以选 Flask 是合理的工程取舍不是技术落后。如果你之后要把它改造成多用户高并发平台再迁移 FastAPI 也不迟路由和模型部分改动量可控。2.2 表结构设计article 主表字段与索引我拆源码时第一个看的就是建表语句因为新闻数据平台的核心资产是数据模型。这套资源用的是 MySQL主表article的设计很有代表性。重点字段我整理了一张表字段名类型含义备注idBIGINT自增主键无业务含义titleVARCHAR(255)新闻标题全文索引候选字段summaryTEXT摘要爬虫截取的前 N 个字符sourceVARCHAR(64)来源媒体例如“人民日报”“科技日报”categoryVARCHAR(32)新闻分类爬虫带入库publish_timeDATETIME发布时间需做时间归一化comment_countINT评论数可能缺失默认 0like_countINT点赞数可能缺失默认 0view_countINT阅读数部分新闻拿不到urlVARCHAR(500)原文链接加唯一索引做去重created_atDATETIME入库时间用于增量判断这个设计的聪明之处在于把“展示型数据”和“分析型数据”都放了进去热度分可以直接在 SQL 里算也可以抽到 pandas 里算。我一般会在publish_time和source上各加一个普通索引资源里可能只给了主键索引你运行的时候按需补上就行。注意url的唯一索引必须要有否则爬虫重复采集时表会越塞越脏。2.3 爬虫采集今日头条搜索接口的请求参数与入库代码爬虫部分我单独抽出来看核心逻辑是请求今日头条搜索接口、解析 JSON、逐条入库。代码不长但有几个参数值得细讲。下面是核心采集函数的结构import requests import time import hashlib from datetime import datetime HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.toutiao.com/, Accept: application/json, text/plain, */*, } def fetch_news(keyword, offset0, count20): 请求今日头条搜索接口 参数说明 keyword: 搜索关键词 offset: 分页偏移量翻页时每次 count count: 单页数量建议 20太大容易触发校验 params { aid: 24, app_name: web_search, keyword: keyword, offset: str(offset), count: str(count), format: json, search_id: , # 空字符串会让后端按当前请求重新生成 timestamp: str(int(time.time() * 1000)), } resp requests.get( https://www.toutiao.com/api/search/content/, paramsparams, headersHEADERS, timeout10, ) resp_json resp.json() return resp_json.get(data, [])这段代码的关键点有两个。第一timestamp参数用毫秒级时间戳这是很多请求签名逻辑的组成部分如果你在代码里写死一个时间戳服务端很可能返回无效响应保持“每次请求都生成新的时间戳”这个习惯能避免大多数偶发失败。第二count不要贪大头条搜索接口单次拉到 50 条以上容易被风控20 条一页翻页是最稳的节奏。还有返回的data里有些条目是广告或聚合卡片入库前要过滤掉log_pb和article_type不匹配的数据这一步我放到清洗章节处理。拿到数据后的入库也很直接先按url的 MD5 值判断是否已存在避免重复采集。这个去重逻辑建议放在 Python 层而不是数据库层因为爬虫是批量写入的如果直接用数据库唯一索引去重会引发大量异常记录def save_articles(articles): for item in articles: url item.get(url) or item.get(display_url, ) if not url: continue url_hash hashlib.md5(url.encode()).hexdigest() exists db.session.execute( SELECT id FROM article WHERE url_hash :uh, {uh: url_hash} ).first() if exists: continue article Article( titleitem.get(title, ).strip(), summaryitem.get(abstract, ).strip(), sourceitem.get(source, 未知), categoryitem.get(tag, 综合), publish_timeparse_time(item.get(publish_time)), comment_countitem.get(comment_count, 0) or 0, like_countitem.get(like_count, 0) or 0, view_countitem.get(view_count, 0) or 0, urlurl, url_hashurl_hash, ) db.session.add(article) db.session.commit()这里parse_time()是时间解析函数头条返回的时间格式有两种一种是标准时间戳字符串另一种是“今天 12:30”“30分钟前”这种相对时间。相对时间解析逻辑我在第 4 章专门讲入库前必须处理完否则DATETIME字段会因为类型不匹配直接报错。2.4 接口层设计/api/* 路由与前端对接约定Flask 后端主要负责把统计结果以 JSON 形式给前端。我比较认可这套资源里把接口按业务拆分的做法而不是让前端直接查原始表。核心接口有三个/api/stats/overview返回新闻总数、来源数、时间范围/api/stats/trend按日返回新闻数量/api/stats/sources返回来源前 10 名。路由代码大致如下app.route(/api/stats/trend) def stats_trend(): days request.args.get(days, 7, typeint) rows db.session.execute( SELECT DATE(publish_time) AS d, COUNT(*) AS cnt FROM article WHERE publish_time DATE_SUB(CURDATE(), INTERVAL :days DAY) GROUP BY DATE(publish_time) ORDER BY d , {days: days}, ).fetchall() return {data: [{date: str(r[0]), count: r[1]} for r in rows]}接口约定很关键前端只需要关心data数组不需要关心 SQL。这样前后端可以并行开发后端改了查询逻辑前端也不用动。如果你拿到源码后发现接口路径不一致先看app.py里的路由前缀这套资源的前端 axios 封装里会有对应的 baseURL改成一致就行。3. 前端看板Vue3 ECharts 的数据可视化做法3.1 Vite 初始化与组合式 API 的目录组织Vue3 项目用 Vite 初始化是当前主流做法比 Vue CLI 冷启动快很多。资源里的前端目录是标准结构src/views放页面组件src/api放 axios 请求封装src/components放图表组件。如果你手里拿到的是旧版本 Vue2 代码我建议直接重建成 Vue3你会明显感受到组合式 API 带来的组织便利。一个看板页面组件里setup函数把数据请求和图表渲染放在一起相关逻辑内聚性比data methods created强太多。我刚上手 Vue3 时最不习惯的是ref和reactive的选择。经验是普通标量和数组用ref嵌套对象用reactive。图表配置对象我一般用ref({})包一层因为 ECharts 的setOption需要整对象替换或 merge用ref改起来符合直觉。3.2 Axios 请求封装与 ECharts 事件绑定前端看板的数据管道是axios - response interceptor - 页面 ref - ECharts option。资源里把 axios 封装放进了src/api/request.js超时时间设为 15 秒baseURL指向后端的/api。这个封装值得学习的地方是错误处理统一拦截后端如果抛 500前端弹提示而不是白屏。import axios from axios; const service axios.create({ baseURL: /api, timeout: 15000, }); service.interceptors.response.use( (response) { return response.data; // 直接返回业务数据省去一层 .data }, (error) { console.error([API ERROR], error.response?.status, error.message); return Promise.reject(error); } ); export default service;为什么baseURL写成/api而不是http://localhost:5000/api因为这样部署时 Nginx 做反向代理最省事前端构建产物和后端接口同域不会触发跨域问题。本地开发时你在vite.config.js里配一个 proxy 把/api转发到http://127.0.0.1:5000就行。这套资源的约定是对的我见过太多人直接写死 IP换台电脑就抓狂。3.3 三个图表折线、柱状、饼图的 option 配置看板页面核心是三个图表。趋势折线图最常规按时段展示新闻量来源占比用饼图媒体活跃度用横向柱状图。折线图的配置核心是xAxis.data和series.data对齐常见翻车点是后端返回的日期是字符串数组而前端图表的xAxis.data也必须是数组两者格式不一致直接空白。import * as echarts from echarts; import { ref, onMounted } from vue; import { getTrend } from /api/stats; const trendOption ref({}); async function loadTrend() { const res await getTrend(7); const dates res.data.map((item) item.date); const counts res.data.map((item) item.count); trendOption.value { tooltip: { trigger: axis }, grid: { left: 40, right: 20, bottom: 30, top: 40 }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: 新闻量 }, series: [ { name: 新闻数量, type: line, smooth: true, data: counts, areaStyle: { opacity: 0.1 }, }, ], }; } onMounted(loadTrend);注意areaStyle我只给了 0.1 的透明度这样图表不会太“重”看板整体视觉干净很多。另外 ECharts 的resize一定要监听否则浏览器窗口变了图表会挤成一团。我在mounted里加window.addEventListener(resize, () chartRef.value?.resize())组件卸载时记得移除监听。4. 数据清洗与分析pandas 预处理、热度建模和统计口径4.1 清洗规则去重、剥 HTML、时间归一化爬虫采下来的数据直接做分析会得出错误结论因为头条搜索结果里混着广告、软文、重复推荐。这套资源里的清洗模块用 pandas 做了四步处理。第一步是丢弃title为空或长度小于 5 的记录第二步按url_hash去重且保留首次入库的那条第三步用正则剥离标题和摘要里的 HTML 标签、空格、不可见字符第四步把发布时间统一成datetime类型。import pandas as pd import re def clean_articles(df): # 1. 去空标题和明显广告标题 df df[df[title].str.len() 5] df df[~df[title].str.contains(广告|推广|荐, naFalse)] # 2. 去重保留首次入库 df df.sort_values(created_at).drop_duplicates(subseturl_hash, keepfirst) # 3. 去除HTML标签和多余空白 def strip_html(text): text re.sub(r[^], , str(text)) return re.sub(r\s{2,}, , text).strip() df[summary] df[summary].map(strip_html) # 4. 时间归一化 df[publish_time] df[publish_time].map(normalize_time) return df时间归一化是个容易被低估的坑。头条接口给的时间如果是“10分钟前”pandas 的to_datetime直接处理会报错。我惯用的normalize_time思路是先判断是否为纯数字时间戳再判断是否符合%Y-%m-%d %H:%M:%S格式最后处理相对时间——“30分钟前”用now - timedelta(minutes30)“昨天 08:00”用now.date() - timedelta(days1)拼接。这部分代码在资源里应该有如果没有按我这个思路补全即可。4.2 热度与词频建模计算口径与阈值数据洗干净之后才轮到“建立数据模型”这一步。这套资源里的热度模型不是简单排序而是构造了一个综合热度分公式我拆出来后发现是加权归一化H 0.4 * N(comment) 0.3 * N(like) 0.2 * N(view) 0.1 * N(time_decay)其中N()是 Min-Max 归一化time_decay用的是半衰期函数发布时间越近权重越高。def hot_score(row, now): # 各指标做 0-1 归一化 c row[comment_count] / (row[comment_count] 50) l row[like_count] / (row[like_count] 200) v row[view_count] / (row[view_count] 10000) age_hours max((now - row[publish_time]).total_seconds() / 3600, 1) t 2 ** (-age_hours / 24) # 半衰期 24 小时 return 0.4 * c 0.3 * l 0.2 * v 0.1 * t这个公式的工程意义在于避免单个指标失控——某条新闻可能阅读量巨大但评论极少单看view_count会把它误判为爆款加权后更接近真实热度。你拿这份源码去做数据分析时如果想换成“评论主导”的口径把系数改成0.5, 0.3, 0.1, 0.1就行。词频分析则是用 jieba 分词后统计 Top N注意要做停用词过滤否则“我们”“什么”“一个”这类词会霸榜。资源里应该带了一份中文停用词表如果没有就从网上找一个常见版本放进去。4.3 聚合查询和统计维度口径统计口径直接决定看板有没有参考价值。这套资源的三个聚合维度是按天统计新闻发布量、按来源统计条数占比、按分类统计新闻结构。其中“来源占比”我建议用条数而不是阅读量因为阅读量缺失率太高条形图比例会失真。后端聚合可以用 SQL 一次算完也可以把表抽出来用 pandas 算数据量在一万条以内差别不大。以下做法是资源里的常见写法——在 Flask 里直接执行 GROUP BY 查询返回列表app.route(/api/stats/sources) def stats_sources(): rows db.session.execute( SELECT source, COUNT(*) AS cnt FROM article GROUP BY source ORDER BY cnt DESC LIMIT 10 ).fetchall() return {data: [{name: r[0], value: r[1]} for r in rows]}这里需要提醒一个统计陷阱头条搜索接口返回的source字段偶尔是空字符串直接分组统计会把一堆数据塞进“未知来源”。清洗阶段要么丢弃source为空的记录要么统一替换为“未知”否则饼图会出现一个巨大的灰色扇区。我拿到这套资源后第一件事就是把这条规则补进清洗脚本。5. 实战避坑签名校验失败、跨域拦截与图表空白 5 个排查记录5.1 爬虫偶发返回 400签名和时间戳参数现象爬虫跑了几分钟后突然连续报错requests.exceptions.HTTPError: 400 Client Error重试几次后又恢复正常。原因头条搜索接口对高频请求有风控逻辑固定 User-Agent 且每个请求都带相同参数时很容易被判定为脚本请求另外search_id如果复用上一次的值超过有效期会被拒绝。解决不要用固定search_id每次请求置空让服务端重新生成抓取间隔加随机延迟time.sleep(random.uniform(1, 3))如果连续 3 次 400暂停 60 秒再继续。我在实际跑数据时还发现一个细节——把timestamp放到请求参数里而不是 headers命中风控的概率会低一些。5.2 前端请求跨域OPTIONS 预检被拦现象本地启动前端npm run dev打开页面后看板空白浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy。原因Flask 后端默认不允许跨域而vite.config.js里没配 proxy前端直接请求http://127.0.0.1:5000。浏览器的预检请求OPTIONS被 Flask 默认的app.route拦截因为路由没注册 OPTIONS 方法。解决开发阶段在vite.config.js加 proxy 转发不要把请求地址写死如果非要走跨域可以在 Flask 里加flask-cors并配置CORS(app, resources{r/api/*: {origins: *}})。我更推荐 proxy 方案因为部署时同域天然没有跨域问题。5.3 图表空白容器高度或异步数据时机现象接口返回正常trendOption.value也有数据但图表区域一片空白没有任何报错。原因ECharts 初始化发生在容器还没有高度的时候或者setOption在数据异步返回之前执行了。很多新手在mounted里同步初始化图表但 axios 请求是异步的数据回来时图表已经用空数据渲染完。解决给图表容器设置固定高度height: 400px不要依赖父容器的高度撑开在setOption前判断data.length 0如果用了多个图表每个图表实例必须有唯一的ref绑定。最保险的写法是先echarts.init再等请求结束调用setOption和resize。5.4 中文乱码连接串与建表排序规则现象前端页面显示的新闻标题全是???SQL 查询结果也是乱码但 MySQL 命令行里数据正常。原因数据库连接串没有指定charsetutf8mb4建库时排序规则选成了latin1_swedish_ci。SQLAlchemy 的create_engine默认字符集不是 utf8mb4中文在传输过程中被转成了问号。解决连接串里加?charsetutf8mb4数据库和表统一COLLATE utf8mb4_unicode_ci。如果你拿到这套资源时数据库已经建好可以执行ALTER TABLE article CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci补救但经验是老数据可能已经损坏不如清空重导。5.5 Flask 开发服务器并发崩溃复制进程与连接保持现象看板页面同时打开两三个标签页后端突然报MySQL server has gone away或者页面卡住十几秒。原因Flask 自带的开发服务器是单进程多线程模式线程没开threadedTrue时会排队处理请求加上 PyMySQL 长连接在 MySQL 的wait_timeout超时后会被服务端断开旧连接还在用就直接报错。解决开发调试时应用实例用app.run(debugFalse, threadedTrue)生产环境不要用 Flask 自带服务器替换成 gunicorn数据库会话请求结束时统一db.session.remove()避免连接复用过期。这套资源的第 4 章提了 gunicorn 部署我把启动配置放到第 6 章展开。6. 部署与验证gunicorn Nginx 迁移清单和冒烟自检6.1 gunicorn 启动与 Nginx 反代本地跑通只是第一步真的放到服务器上才会暴露问题。我惯用的部署结构是gunicorn 起 Flask 后端Nginx 同时反代后端/api和前端静态文件。后端启动命令如下gunicorn -w 2 -b 127.0.0.1:5000 app:app \ --timeout 60 \ --worker-class sync \ --access-logfile /var/log/news/app.log-w 2表示两个 worker 进程小内存服务器别开太多每个 worker 都会复制一份模型和数据上下文--timeout 60是必须的因为爬虫接口如果没做异步处理单个请求超过 30 秒时 gunicorn 默认会杀 worker。注意app:app前面的app是文件名后面是实例名要对齐你的入口文件。Nginx 配置核心片段server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:5000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/news-dist; try_files $uri $uri/ /index.html; } }proxy_pass后面的 URI 带不带/会影响转发路径我踩过几次坑这里写的是最稳妥的带/形式。前端构建产物直接放到/var/www/news-distNginx 会优先处理location /下的静态文件。6.2 一键构建脚本与 curl 冒烟自检部署完最忌讳的是点开页面看一眼就宣布完成。我每次都会先在前端目录执行构建再对后端接口做冒烟自检# 前端构建 cd frontend npm install npm run build # 后端接口冒烟 curl -s http://127.0.0.1:5000/api/stats/overview | head -c 200 curl -s http://127.0.0.1:5000/api/stats/trend?days7 | head -c 200 curl -s http://127.0.0.1:5000/api/stats/sources | head -c 200三个接口都返回 JSON 后再访问 Nginx 的静态页面看图表是否正常渲染。如果接口 200 但图表空白问题一定在前端构建产物路径或 API 请求地址上。把日志实时盯一下tail -f /var/log/news/app.log这套资源我拆完后的整体感受是技术选型务实、代码结构清晰对于想在 Flask 和 Vue3 之间打通全栈的人来说是很好的参考模板。最后说一个我的习惯——部署完第一件事永远是看错误日志设置好重启机制后再补一版勘察数据。从那以后我每次交付项目都强制走一遍“构建 → 冒烟 → 日志确认”的流程省掉了大量远程排查的尴尬。希望帮到你。本文还有配套的精品资源点击获取