ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Scrapy到ECharts:新闻爬虫数据清洗与可视化大屏实战解析

从Scrapy到ECharts:新闻爬虫数据清洗与可视化大屏实战解析 简介一套基于Python网易新闻Scrapy爬虫的数据分析与可视化大屏毕业设计源码案例面向计算机相关专业学生、教师及入门学习者可满足毕设、课设、项目立项演示等场景。资源共557个文件压缩包约31.5MB涵盖52个py源码、82个pyc编译文件、69个vue组件、39个js脚本及大量svg/png/jpg静态资源并包含sql数据库脚本、bat一键安装与运行脚本、mp4操作演示及doc说明文档代码结构清晰便于直接部署和二次开发。目前已有113人学习下载。项目包含完整的技术实现思路与文档说明支持从爬虫采集、数据清洗到可视化大屏展示的全流程复现适合验证大数据分析能力并快速生成可交付成果。可在现有代码基础上扩展栏目或改版样式用于课程汇报或答辩演示均较为合适。1. 从Scrapy爬虫到可视化大屏这条数据链路缺一不可拿到这个源码包的时候我先翻的不是爬虫文件而是前端目录——看到IndexMain.vue、IndexAsideStatic.vue、IndexHeader.vue、BreadCrumbs.vue这些组件名就清楚这不是一个“爬完存CSV就交差”的课设而是一条完整的数据工程链路Scrapy负责采集网易新闻Pandas负责清洗和统计MySQL负责落库Vue配合ECharts把结果渲染成大屏。真正有价值的不是那几万条新闻数据而是这套从前端到后端再到爬虫的分层结构。文章按数据流向展开先拆Scrapy采集层的组件与参数再讲清洗和入库时的口径设计然后落到可视化大屏的接口适配最后聊一键启动脚本和运行期最容易翻车的几个点。适合正在做数据采集、数据分析或大屏展示项目的读者照着这个骨架改业务逻辑比自己从零搭要快得多。2. 网易新闻Spider的组件拆分与Scrapy采集节奏控制2.1 项目结构与Spider的yield流转逻辑Scrapy项目的核心不只在spider里写了多少行解析代码而在Items、Pipelines、Settings这几个文件怎么配合。这个工程采用的是标准Scrapy目录布局spiders/下放站点爬虫items.py定义字段结构pipelines.py做去重和入库settings.py控制采集节奏。很多新手会把所有解析逻辑塞进spider导致后续加字段、换存储都要大改而这个项目把字段定义抽到Item里是一个值得保留的习惯。Spider内部采用了“频道入口 → 列表页 → 详情页”的三级yield流转而不是一次性把所有详情页URL收集完再发起请求。这样做的好处是内存占用稳定且Scrapy的调度器可以按优先级平滑处理请求队列。核心代码结构如下import scrapy from news_scrapy.items import NewsItem class NewsSpider(scrapy.Spider): name news_spider allowed_domains [news.163.com] start_urls [https://news.163.com/] def parse(self, response): # 只进入科技、体育等模块避开时政类栏目保证内容边界可控 columns response.css(div.nav-area a::attr(href)).getall() for col in columns: if any(k in col for k in (tech, sports)): yield scrapy.Request(response.urljoin(col), callbackself.parse_list) def parse_list(self, response): news_links response.css(a::attr(href)).re(r.*?news\.163\.com/2\d{3}/.*) for link in news_links: yield scrapy.Request(link, callbackself.parse_detail) def parse_detail(self, response): item NewsItem() item[title] response.css(h1::text).get() item[source] response.css(div.post_info a::text).get() item[time] response.css(div.post_info::text).re_first(r\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) item[content] .join(response.css(div.post_body p::text).getall()).strip() item[url] response.url yield item这里要说明两点。第一parse返回的是Request对象而非ItemScrapy会把新请求继续交给调度器再由对应callback处理这就是“yield流转”的实质第二parse_list里用正则筛选包含“/2024/”这类路径的链接是因为新闻详情页URL通常带日期层级直接过滤掉首页导航和专题聚合页能减少大量无效请求。选择器本身要按目标站点的实际HTML结构调整但三级流转的骨架是通用的。如果目标页面是iframe或动态渲染的内容Scrapy原生的response拿不到完整数据。常见做法是引入Playwright中间件让页面先渲染再交给Scrapy解析但网易新闻这类门户的文章页基本是服务端直出HTML静态解析就够用没必要把动态渲染引入而增加维护成本。2.2 Item字段与Pipeline去重入库Item在这里起到字段约束的作用防止详情页结构变化时静默产生脏数据。比如title取不到时None会被写进数据库而下游统计时还要再清洗。更稳妥的做法是在Item里限制字段名在Pipeline里做空值丢弃。import hashlib import pymysql from scrapy.exceptions import DropItem class NewsItem(scrapy.Item): title scrapy.Field() source scrapy.Field() time scrapy.Field() content scrapy.Field() url scrapy.Field() class DuplicatesPipeline: def __init__(self): self.seen set() def process_item(self, item, spider): fp hashlib.md5(item[url].encode(utf-8)).hexdigest() if fp in self.seen: raise DropItem(fduplicate url: {item[url]}) self.seen.add(fp) return item class MysqlPipeline: def open_spider(self, spider): self.conn pymysql.connect( host127.0.0.1, userroot, password123456, databasenews_db, charsetutf8mb4 ) def close_spider(self, spider): self.conn.close() def process_item(self, item, spider): sql INSERT INTO news(title, source, time, content, url) VALUES (%s, %s, %s, %s, %s) params (item[title], item[source], item[time], item[content], item[url]) with self.conn.cursor() as cur: cur.execute(sql, params) self.conn.commit() return itemPipeline的执行顺序由settings.py里的ITEM_PIPELINES字典值决定数字小的先执行。这里把去重放在100入库放在300意思是重复数据在进数据库之前就被拦下如果去重逻辑放在入库之后那么重复写入已经发生再删就晚了。提醒一下DuplicatePipeline用内存集合去重在单机小数据量下没问题但如果跑到几十万条级别建议改用Redis的Set做去重否则爬虫重启后seen会被清空前一次的URL会再次入库。2.3 settings.py中的并发与采集节奏参数采集节奏是这个项目里最容易拍脑袋乱调的部分。很多人一上来就把CONCURRENT_REQUESTS调到32认为跑得快就是好。实际新闻站点的列表页和详情页响应体都不小并发过高时本机带宽和数据库写入都会成为瓶颈而且会给对方站点造成压力。这个工程里采用的参数组合偏保守参数推荐值说明ROBOTSTXT_OBEYTrue遵守robots.txt这是合规底线DOWNLOAD_DELAY1.0每次请求间隔1秒单位是秒CONCURRENT_REQUESTS8全局并发请求数含列表页和详情页CONCURRENT_REQUESTS_PER_DOMAIN4单域名并发防止触发站点防护ITEM_PIPELINES见代码控制去重和入库顺序如果只是做课程设计或毕业设计数据量在1万条以内DOWNLOAD_DELAY可以降到0.5运行时间大概5到10分钟如果要抓更大量级我一般会提高CONCURRENT_REQUESTS_PER_DOMAIN而不是全局并发因为单域名的请求过快才是被拒绝的主要原因。另外LOG_LEVEL建议设为INFO不然后期DEBUG日志会把终端刷到看不清运行进度。抓完后务必先看日志里的item_scraped_count这个数值比数据库里数行数更直观。3. Pandas清洗与MySQL入库分析口径在入库前就要定好3.1 空值、重复值与内容长度的三层过滤爬虫落库后的数据不能直接拿来做可视化因为页面里混着不少噪声有的正文只有一句话有的是图片专题页title或time字段可能为空。如果不处理直接统计大屏上的“新闻发布趋势”会被空数据拉低词云里也会出现无意义的单字。用Pandas做三层过滤是常见做法import pandas as pd df pd.read_csv(news_raw.csv, encodingutf-8) df df.dropna(subset[title, time]) df df.drop_duplicates(subset[url]) df df[df[content].str.len() 50] print(df.shape) print(df[source].value_counts().head(10))第一层dropna只删除title和time为空的行content为空就留给长度过滤处理。第二层drop_duplicates按URL去重比按标题去重更可靠因为同一篇新闻可能被多个栏目转载标题一样但URL不同。第三层对正文字数做阈值过滤阈值设为50个字符能去掉多数纯图片页和跳转页。这里需要强调去重字段的选择直接影响数据量。按URL去重会保留同一稿件的不同转载版本按标题去重则可能误删同题报道。如果后续统计“稿源分布”建议保留不同来源的版本如果统计“新闻总量”则要在分析时再按标题聚合。这个口径一旦定错大屏上的数字就解释不通。3.2 时间字段规整与发布时段统计网易新闻页面上常见的时间格式有两种2024-05-18 10:30:00和昨天 10:30。后者在入库后无法直接按小时聚合所以要在Pandas里统一转成datetime类型并拆出hour和date字段。这里的转换要用errorscoerce因为脏数据转换失败会变成NaT再统一过滤。df[time] pd.to_datetime(df[time], errorscoerce) df df.dropna(subset[time]) df[hour] df[time].dt.hour df[date] df[time].dt.date hour_stats df.groupby(hour)[title].count() print(hour_stats.sort_values(ascendingFalse).head(5))dt.hour返回的是0到23的整数这个字段会直接提供给大屏的“发布时段分布”图表。需要注意时区问题如果爬虫抓下来的是UTC时间必须先dt.tz_convert(Asia/Shanghai)再取小时否则大屏上的高峰时段会整体偏移8小时。这个坑在新闻类爬虫里很常见因为服务端渲染页面经常直接输出时间戳而不是本地化字符串。3.3 入库表结构设计与executemany批量写入清洗完的数据通常有几万行逐条INSERT在MySQL里会很慢。常见做法是把Pandas的DataFrame转成元组列表用executemany做批量写入速度能提升一个数量级。表结构在设计时就把统计需要的字段拆好避免在查询时反复做函数运算。字段名类型说明idINT AUTO_INCREMENT主键titleVARCHAR(255)新闻标题sourceVARCHAR(64)来源站点timeDATETIME发布时间contentMEDIUMTEXT正文内容urlVARCHAR(255)原文链接hourTINYINT发布时间小时冗余字段dateDATE发布日期冗余字段把hour和date直接存成字段看似冗余但能避免每次聚合查询都对time做函数转换。这种“用空间换查询速度”的思路在大屏项目里很重要因为前端每5分钟刷新一次接口如果后端每次都对几十万行做DATE_FORMAT响应时间会明显变长。rows [tuple(x) for x in df[[title, source, time, content, url, hour, date]].values] sql INSERT INTO news(title, source, time, content, url, hour, date) VALUES (%s, %s, %s, %s, %s, %s, %s) with conn.cursor() as cur: cur.executemany(sql, rows) conn.commit()executemany的第二个参数是元组列表单个元组的字段顺序必须和SQL语句里的字段顺序一一对应。这里最容易出的问题是Pandas的NaN在转成Python对象后是float(nan)写入MySQL时会被当成字符串“nan”所以我习惯在转rows之前先做df df.fillna()。另外time字段必须是datetime对象或字符串Pandas的Timestamp类型不能直接交给pymysql要先转成str。4. 可视化大屏的数据接口设计Vue组件与ECharts的对接方式4.1 大屏工程的目录结构与数据流这个工程的前端不是简单的HTML页面而是Vue组件化结构。IndexMain.vue是大屏主体IndexHeader.vue负责标题和时间IndexAsideStatic.vue是侧边栏统计卡片BreadCrumbs.vue控制页面导航层级。项目里还留着main.js.bak和update-password.vue.bak这类备份文件说明在迭代过程中改过路由和权限模块。数据流向是MySQL → Flask接口 → Vue的axios请求 → ECharts渲染整个链路是前后端分离的。后端接口不需要做得很重Flask写几个只读接口就够用。关键是返回的JSON结构要和前端的图表配置对齐比如折线图需要一个日期数组和一个数值数组饼图需要一个name/value的对象数组。我习惯在后端就把数据组装成前端想要的格式而不是让前端拿到原始行再自己groupBy。from flask import Flask, jsonify import pymysql from collections import OrderedDict app Flask(__name__) def get_conn(): return pymysql.connect( host127.0.0.1, userroot, password123456, databasenews_db, charsetutf8mb4 ) app.route(/api/trend) def trend(): sql SELECT date, COUNT(*) AS cnt FROM news GROUP BY date ORDER BY date conn get_conn() with conn.cursor() as cur: cur.execute(sql) rows cur.fetchall() conn.close() return jsonify({ dates: [str(r[0]) for r in rows], counts: [r[1] for r in rows] }) app.route(/api/source_pie) def source_pie(): sql SELECT source, COUNT(*) AS cnt FROM news GROUP BY source ORDER BY cnt DESC LIMIT 10 conn get_conn() with conn.cursor() as cur: cur.execute(sql) rows cur.fetchall() conn.close() return jsonify([{name: r[0], value: r[1]} for r in rows])第一个接口/api/trend返回的是两个等长数组供折线图和面积图直接绑定xAxis和series。第二个接口/api/source_pie返回的是对象数组这是ECharts饼图的标准数据格式。统一JSON结构可以减少前端转换代码也方便后续替换图表类型。注意jsonify默认无法序列化datetime.date对象所以要提前str(r[0])转成字符串不然接口会直接报500。4.2 大屏首页图表的ECharts配置与动态刷新大屏的视觉主体通常是中间的新闻趋势图和两侧的统计卡片。ECharts配置里的tooltip、grid、dataZoom这三项是影响观感的关键不设置grid图表会贴着容器边缘不用dataZoom时间跨度超过30天时折线会挤在一起tooltip的trigger要选axis才能在鼠标悬停时显示完整日期。fetch(/api/trend) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis, axisPointer: { type: line } }, grid: { left: 60, right: 30, top: 50, bottom: 40 }, xAxis: { type: category, data: data.dates, boundaryGap: false }, yAxis: { type: value }, dataZoom: [ { type: inside, start: 0, end: 100 } ], series: [{ name: 新闻发布量, type: line, smooth: true, data: data.counts, areaStyle: { opacity: 0.15 } }] }); });smooth: true会让折线变成曲线适合展示趋势boundaryGap: false让折线从第一个数据点开始而不是和y轴留空隙。dataZoom的inside类型支持鼠标滚轮缩放在大屏上比滑块更隐蔽。补充一点如果容器宽度变化必须在window的resize事件里调用chart.resize()否则图表会保留初始宽度在大屏缩放后出现拉伸或留白。4.3 多组件状态同步与.gcharts按需加载Vue大屏里如果同时放了多个图表不能每个组件各自fetch后独立渲染否则接口请求会重复且各个图表的数据时间点不一致。常见做法是在IndexMain.vue的mounted里统一请求所有接口把数据存到组件的data里再通过props分发给子组件。这样爬虫增量更新后只需在父组件触发重新fetch所有图表自动同步。另一个容易忽略点是ECharts的按需加载。全量引入echarts包会让前端打包体积多出400KB以上大屏加载变慢。项目里一般用echarts/core配合echarts/charts里的LineChart、PieChart、BarChart注册而不是整包引入。这个工程在组件里能拆成独立图表模块说明作者已经考虑了首屏性能这部分在大屏项目里比普通后台管理页面更重要因为大屏通常挂在电视或投屏设备上性能不高。5. 一键启动脚本、大屏适配与运行期故障排查5.1 安装与运行脚本的核心逻辑项目根目录下的安装.bat和运行.bat不是简单的pip install而是做了环境隔离和分步启动。Windows场景下先把Python虚拟环境和依赖一次性装好再把爬虫、分析、接口服务依次拉起。拆开写的原因是爬虫可能跑几分钟分析脚本依赖爬虫产物接口服务依赖数据库任何一个环节失败都能从脚本的echo提示里定位。echo off cd /d %~dp0 if not exist venv ( python -m venv venv ) call venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple echo 依赖安装完成 pause运行脚本的要点是先用scrapy crawl news_spider抓数据并输出为CSV再执行analysis.py把清洗结果写入MySQL最后启动api_server.py并自动打开浏览器。前后用连接能保证上一步成功才执行下一步避免数据库里还没有数据时接口就已经启动。5.2 大屏适配transform scale与rem的取舍大屏最常见的适配方案是固定设计稿为1920乘1080再用CSS transform做等比缩放。这个工程如果走的是这套思路在App.vue或IndexMain.vue的mounted里监听resize计算缩放比例即可。代码实现很直接const screen document.getElementById(screen); const designWidth 1920; const designHeight 1080; function resizeScreen() { const scaleX window.innerWidth / designWidth; const scaleY window.innerHeight / designHeight; const scale Math.min(scaleX, scaleY); screen.style.transform scale(${scale}); screen.style.transformOrigin left top; } window.addEventListener(resize, resizeScreen); resizeScreen();用transform: scale的好处是开发时所有尺寸都按1920设计稿写不用考虑响应式缺点是缩放后ECharts的tooltip会出现偏移因为tooltip的定位用的是canvas内部坐标缩放比例不会实时同步。规避方法是tooltip的confine: true让提示框限制在图表区域内。5.3 排查顺序从日志到数据再到接口运行期遇到大屏无数据按这个顺序排查最快。第一步看爬虫日志里的item_scraped_count如果为0检查目标页面的选择器是否失效用scrapy shell直接拉取页面验证。第二步看CSV清洗后的数据量如果清洗后为空检查time字段格式是否正确。第三步看接口返回浏览器打开/api/trend确认JSON里有没有数据这里最常见的问题是Flask和前端跑在不同的端口跨域没配置需要在Flask里加CORS头。第三点特别容易在前后端分离项目里出现因为浏览器会拦截跨域请求而后端日志里看不到任何报错。本文还有配套的精品资源点击获取
返回列表