ARTICLE DETAIL

资讯详情

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

基于Scrapy与Python的B站数据采集与分析可视化系统实战

基于Scrapy与Python的B站数据采集与分析可视化系统实战 做这个项目的最初动机其实特别朴素我想知道B站某个分区的内容到底在怎么变化什么样的视频更容易被推荐哪些UP主是真正在稳定涨粉。平台自带的数据后台只能看到自己的账号想看更大范围的内容生态只能自己动手。于是就有了这套基于 scrapy 和 python 构建的 B站数据采集与分析可视化系统。整个项目从爬虫策略设计、数据清洗入库到最终的数据大屏展示前前后后折腾了两个月。后面我会把整个系统的搭建思路、遇到的具体问题、以及我认为最有价值的踩坑经验完整写出来希望能给正在做类似爬虫项目的朋友一些参考。这个项目适合谁呢一种是像我一样想做内容但手里没有数据支撑的创作者另一种是想系统学习 scrapy 工程化、数据分析可视化链路的学生或开发者。它不是一个简单的 Python 爬虫脚本而是一条从数据采集到数据展示的完整流水线能跑、能看、能复用。1. 为什么选B站做数据分析项目想解决的真实问题在做技术选型之前我花了两天时间想清楚一个问题这个系统做出来到底要回答什么如果只是为了爬点数据画个图那项目做到一半基本就会烂尾。只有把业务问题定义清楚后面每一个环节才知道该往哪使劲。1.1 内容创作者的数据饥渴在B站做内容最难受的事情是看不到同赛道对手的完整数据。我知道自己的播放量、点赞数但不知道整个分区的中位数是多少不知道同类视频的互动率处于什么水平更不知道一个视频从发布到爆发通常要经过多长时间。市面上的第三方数据平台要么收费高要么维度不够细。自己动手采集公开数据是性价比最高的解法。我的核心需求有三个。第一掌握分区热度变化比如知识区、生活区近30天的更新量、播放总量、互动总量是涨还是跌。第二搞清楚什么样的视频更容易获得高互动互动率(点赞加投币加评论加收藏再除以播放量)和标题长度、视频时长、发布时间有没有关系。第三跟踪UP主成长轨迹一个UP主从几千粉涨到十万粉的过程中播放量和互动量分别是怎么变化的。1.2 系统要解决的三个核心问题把需求翻译成技术问题就变成了下面三个。数据采集问题B站的数据分散在列表页、详情页、动态加载的内容甚至 iframe 里需要一套稳定可靠的爬虫方案并且不能对目标站点造成压力。这里我选择了 scrapy 作为主框架配合 playwright 处理动态渲染内容。数据建模问题采集下来的原始数据是脏的、乱的、字段不统一的。比如有的视频没有投币数据有的发布时间是时间戳有的带时区有的播放数可能因为反爬返回了空值。清洗和建模的好坏直接决定分析结果的可信度。数据展示问题辛辛苦苦把数据存下来如果只会用 Excel 拉几个透视表效率太低了。我需要一个能自动刷新、支持多维筛选的数据看板让不懂 SQL 的人也能直观看到数据变化。1.3 做之前先想清楚的边界这里必须多说一句做爬虫项目不能一股脑往前冲。我给自己定了几条边界也建议所有做类似项目的人提前想清楚。只采集公开可访问的数据不碰需要登录才能看到的非公开内容不采集用户私信、手机号、邮箱这类个人隐私信息。采集频率控制在合理范围不追求高并发不攻击站点接口尽量避开用户高峰期。数据仅用于个人学习和分析不对外售卖不批量转发原始数据。把这些边界写在项目 README 里既是提醒自己也是让这个项目能长期跑下去的基础。2. 系统架构与技术选型scrapy在整条链路里的位置这个项目表面上是一个爬虫项目实际上是一条完整的数据流水线。很多新手容易犯的错是只写爬虫不写存储数据爬到一半发现不知道往哪放最后只能用 CSV 凑合分析的时候又全部重来。我建议在一开始就把整条链路想明白。2.1 从采集到看板的完整数据链路我的系统链路是这么设计的采集层用 scrapy 编写爬虫管理请求调度、解析、去重和重试遇到动态页面时通过 playwright 渲染补充。对于分布式采集场景scrapy 本身也可以通过 scrapy-redis 扩展支持但个人项目初期单机足够后面再扩展不迟。数据处理层拿到 Item 后先做基础校验再进入数据清洗管道。我用 pandas 做内存级清洗主要处理缺失值、异常值、字段格式统一。清洗后的数据写入存储层。存储层选了 MySQL 作为主存储因为数据结构相对规整维度表和事实表的关系明确。如果后续数据量达到千万级我会考虑把分析查询切到 ClickHouseOLAP 聚合性能会比 MySQL 好很多。分析计算层用 SQL 完成大部分指标计算比如分区热度、互动率、TOP N 榜单复杂逻辑用 Python 再加工。展示层用 FastAPI 提供聚合接口前端用 Vue3 加 ECharts 做数据大屏。这条链路看起来长但每一层都很清晰。最重要的是每一层的输入输出都是明确的出问题的时候能快速定位到底是在采集阶段、清洗阶段还是数据接口阶段。2.2 为什么爬虫层选了scrapy而不是requests说实话如果只爬几十个页面用 requests 加 BeautifulSoup 完全够用。但当我意识到要持续采集、每天增量更新、需要断点续爬和去重的时候requests 就变成了一把需要自己造很多轮子的光杆司令。scrapy 给我的核心价值有几点。异步并发是基于 Twisted 的异步模型同样的硬件条件下可以同时发起多个请求效率远高于 requests 同步循环。中间件机制可以很方便地插入随机 User-Agent、请求频率控制、Cookie 更新、异常重试不需要在业务代码里到处写 try except。Item Pipeline 把解析和处理两个环节解耦spider 只需要负责提取数据清洗、入库、去重逻辑都下沉到 pipeline 里。去重能力自带基于请求指纹的重复过滤还会对 URL 做规范化省掉不少事。此外它还内置了日志统计、信号系统、扩展机制爬虫运行状态一目了然。我也对比过自研异步方案比如用 httpx 加 asyncio 自己写一个采集框架。优点是完全可控、轻量缺点是调度、重试、去重、并发控制这些坑都要自己踩一遍时间成本太高。对一个以数据分析为重心的项目来说scrapy 是最合适的平衡点。2.3 大数据存储层选择单机MySQL还是直接上数仓大数据这个词听起来很唬人但 B 站一个分区的视频数据量哪怕每天全量采集一次连续跑一年也就是几十万条量级。这个量级用 MySQL 完全可以扛住不需要一上来就搭 Hadoop 集群。真正的麻烦不是数据量而是分析查询的灵活性。所以我的建议是主存储用 MySQL表结构按照维度表和事实表的方式设计为后续迁移到 ClickHouse 或 StarRocks 留好接口。事实表只存每天要变的数值指标维度表存基本不变的静态属性这样既省空间也方便按日期做增量更新。如果哪天数据量真的上来了把 MySQL 的表结构导出数据导入 ClickHouse 做列式存储查询性能会有质的提升。我自己在项目里也做了这个迁移预案只是目前还没触发。值得一提的是如果这个系统要开放给团队里多个人使用行列权限设计就绕不开。行权限可以按分区或业务线过滤比如不同运营只看自己负责的分区列权限主要针对敏感字段比如用户 UID、作者真实信息要脱敏。成熟的方案有 Apache Ranger但个人项目上这么重没必要。我的做法是在表里预留 owner 字段和 department 字段访问层做一层视图或 Python 拦截后续真要接入权限框架时不用大改表结构。2.4 可视化层数据大屏和自助分析怎么分工数据展示不只是画个好看的图。我把展示层拆成两个场景。数据大屏放在办公室大屏上7×24 小时轮播主要是核心指标的总览比如总视频数、总播放量、互动率趋势、分区排名。它要的是信息密度高、更新及时、一眼能抓住异常。自助分析则面向我自己按分区、时间、UP主等维度自由下钻用类似 BI 工具的思路做。两者共用一个数据接口层只是查询参数和聚合粒度不同。3. 采集层核心实现从静态列表页到动态iframe的完整策略采集层是整个系统里踩坑最多的部分也是最能体现 scrapy 功力的一部分。B站的页面形态比想象中复杂同一个数据在不同页面有不同呈现方式有的藏在接口返回值里有的在 HTML 的 script 标签里有的要等 JS 渲染完才能看到还有的嵌在 iframe 里。下面我把我的处理策略完整讲清楚。3.1 先理清B站页面的请求规律动手写爬虫之前我先做了一件很重要的事打开浏览器开发者工具把 B 站某个分区页的请求逐个看了一遍。不做这一步后面的爬虫就是盲人摸象。我观察到的情况大概是这样的。列表页最初进来时主要内容往往在浏览器地址栏对应的 URL 里但是如果往下翻页就会出现动态加载URL 不变数据通过 XHR 接口请求回来。详情页源码里有一个window.__INITIAL_STATE__的 JavaScript 变量里面包含了视频标题、UP主信息、发布时间、点赞投币收藏等大量结构化数据用正则或 JSON 解析就能取到这比一个个请求接口要稳定得多。部分页面模块比如某些活动页、数据页是通过 iframe 嵌套进来的iframe 内部是独立的文档直接用 scrapy 的 XPath 抓不到。理清这些规律之后我的策略就确定了优先从详情页的window.__INITIAL_STATE__取结构化数据列表翻页用接口或页面解析动态 iframe 和需要 JS 渲染的模块交给 playwright 处理。3.2 静态接口优先能拿API就不解析HTML比如采集视频详情时我先尝试直接请求详情页 URL从源码里提取window.__INITIAL_STATE__。这样写的好处是不太容易受前端 DOM 结构变化影响只要 JavaScript 变量名不变解析逻辑就不用大改。下面是一个简化版的 scrapy spider 示例演示核心思路。这个 spider 先从起始队列拿到视频详情页 URL然后从页面源码里找window.__INITIAL_STATE__用正则切出来再转 JSON最后把需要的字段封装成 Item。import json import re import scrapy class BiliVideoSpider(scrapy.Spider): name bili_video def start_requests(self): start_urls [ https://www.bilibili.com/video/BV1xxxxxxxxxx, ] for url in start_urls: yield scrapy.Request(urlurl, callbackself.parse_detail) def parse_detail(self, response): text response.text m re.search(rwindow\.__INITIAL_STATE__\s*\s*({.*?});, text, re.S) if not m: self.logger.warning(未找到 __INITIAL_STATE__: %s, response.url) return data json.loads(m.group(1)) video_info data.get(videoData, {}) if not video_info: return yield { bvid: video_info.get(bvid), aid: video_info.get(aid), title: video_info.get(title), pub_ts: video_info.get(pubdate), owner_name: video_info.get(owner, {}).get(name), view: video_info.get(stat, {}).get(view), like: video_info.get(stat, {}).get(like), coin: video_info.get(stat, {}).get(coin), favorite: video_info.get(stat, {}).get(favorite), share: video_info.get(stat, {}).get(share), danmaku: video_info.get(stat, {}).get(danmaku), reply: video_info.get(stat, {}).get(reply), }这段代码看起来简单但里面有几个容易忽略的细节。__INITIAL_STATE__赋值后面可能还有分号所以正则要尽量匹配到第一个};的位置必要时用json.JSONDecoder按raw_decode方式解析。视频 stat 里的某些字段可能缺失比如新视频没有投币数据Item 里就不能直接用.get()之后不处理否则后面入库会读到None。标题里可能包含引号和换行存储前需要清洗。3.3 动态渲染与iframe场景scrapy如何接playwright有些页面数据不在源码里必须等浏览器执行完 JavaScript 才出现。还有一个典型场景是 iframe 嵌套页面iframe 内部是独立的文档普通 scrapy 请求拿到的 HTML 里只有一个iframe src...标签真正的内容在 src 对应的地址里。对于 iframe 简单的情况我直接请求 iframe 的 src就能拿到内容性能损失最小。但对于更复杂的动态页面比如渲染完成之后还要点击按钮、滚动加载或者内容经过多轮异步请求就必须用 playwright 驱动浏览器。scrapy 社区有现成的scrapy-playwright中间件它把 playwright 的异步能力接进了 scrapy 的下载器。关键设置如下。# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor在 spider 里发请求时通过 meta 参数告诉 scrapy 这个请求需要渲染。from scrapy_playwright.page import PageMethod yield scrapy.Request( urlurl, callbackself.parse_iframe_page, meta{ playwright: True, playwright_include_page: True, playwright_page_methods: [ PageMethod(wait_for_selector, .some-loaded-class), PageMethod(evaluate, window.__DATA__), ], }, )这段代码的意思是当前请求用 playwright 打开真实浏览器等某个选择器出现再执行 JavaScript 把数据取出来。遇到 iframe 时我通常会在evaluate里遍历document.querySelectorAll(iframe)把 iframe 的 contentWindow 里的关键数据拿出来因为 playwright 的 Page 对象可以直接访问同源 iframe跨域 iframe 则需要通过 frame 对象来操作。必须提醒的是playwright 的渲染速度远低于普通接口请求一个页面可能要等 1 到 3 秒。所以我只在确实需要动态渲染的 URL 上启用 playwright能用普通请求解决的绝不开浏览器。另外单机跑 playwright 时要注意并发数我一般把并发压到 2 到 4否则资源占用会很吓人页面也容易加载失败。3.4 数据管道与去重设计scrapy 的 Item Pipeline 是我最常用的扩展点。每个 Item 从 spider 出来后会依次经过配置的 pipeline 类。我的管道里做了几件事。先做字段完整性检查必填字段缺失的直接丢弃并记录日志。接着统一数据类型播放、点赞这些数字字段全部转成 int带千分位或中文单位的数据先清洗再转换。然后是网络时间转换pub_ts如果是从接口拿到的 Unix 时间戳统一转成YYYY-MM-DD HH:MM:SS字符串避免后续查询时时区混乱。最后增强数据比如根据 bvid 生成视频详情页 URL根据 aid 生成数据关联键。去重这块 scrapy 自带 RFPDupeFilter它会对请求 URL 做指纹去重。但是我要统计视频每日变化同一个视频今天和明天的播放数肯定不一样如果只按 URL 去重当天第二次爬同一个视频会被直接过滤掉导致数据不更新。所以我的策略是爬虫运行期间临时去重用 scrapy 默认过滤器入库时加上uid (date, aid)的唯一索引同一天内重复采集的数据用ON DUPLICATE KEY UPDATE更新数值字段既不产生重复行又能保留最新值。跨周期的增量采集单独跑当天的任务即可日期字段作为分区条件。4. 数据清洗与建模从原始字段到分析指标的转换采集只是万里长征第一步。我最初把数据入库后直接画图结果发现曲线里全是毛刺有的视频播放量突然归零有的分区数据当天翻了三倍仔细排查发现都是清洗不到位导致的。数据质量决定了分析结果的底线这条线守不住后面所有图表都是自欺欺人。4.1 B站数据的脏数据形态我总结了一下最常见的脏数据有这几种。字段缺失新视频可能没有投币、收藏、分享数据部分视频没有等级或分区信息。异常值某个视频播放量是 0 或负数可能是反爬返回的假数据也可能是视频被锁定。格式不统一时间有的是时间戳有的是2024-01-01字符串数字有的带万字有的带逗号。编码问题标题里的特殊字符、换行、emoji 存进 MySQL 时出现乱码或报错。重复数据同一个视频可能被多个列表页抓到需要按业务键去重。4.2 清洗规则与字段标准化我的清洗代码用 pandas 实现逻辑简单直接。设定必填字段和可空字段。必填字段缺失直接删除该行。数值字段先去掉中文单位、逗号、空格再转成int转换失败按 0 处理并记录警告。文本字段统一去除首尾空格把多个空白字符压缩成一个空格换行符替换成\n的转义存储避免破坏 CSV 或 JSON 结构。日期字段统一成datetime类型时区统一为北京时间的东八区入库前格式化为字符串。重复判断用date aid的组合键保留最新采集的那条。下面是核心清洗函数的一个简化版本。import pandas as pd def clean_video_data(df: pd.DataFrame) - pd.DataFrame: required [bvid, aid, title, pub_ts, view, like] df df.dropna(subsetrequired) numeric_cols [view, like, coin, favorite, share, danmaku, reply] for col in numeric_cols: if col not in df: df[col] 0 df[col] ( df[col] .astype(str) .str.replace(,, , regexFalse) .str.replace(万, 0000, regexFalse) .astype(float) .fillna(0) .astype(int) ) # 时间统一成字符串 df[pub_ts] pd.to_datetime(df[pub_ts], units, utcTrue) df[pub_time] df[pub_ts].dt.tz_convert(Asia/Shanghai).dt.strftime(%Y-%m-%d %H:%M:%S) # 重复数据保留最后一条 df df.sort_values(crawl_time).drop_duplicates(subset[date, aid], keeplast) return df注意数字列里的万处理直接替换成 0000 在遇到1.2万时会出错我会先按字符串切分再计算比如提取数字部分乘以 10000。上面代码只是示意实际项目里要写得更精细。4.3 指标体系设计播放、点赞、投币、评论如何联动清洗完之后就该设计分析指标了。我常用的一套核心指标如下。播放量衡量视频触达规模。点赞、投币、收藏、评论、弹幕衡量用户互动深度。互动率等于点赞、投币、收藏、评论、弹幕之和除以播放量反映内容质量与用户共鸣程度。转发率等于分享数除以播放量。弹幕密度等于弹幕数除以视频时长反映观众在观看过程中的活跃度。完播率是理想指标但 B 站公开数据拿不到所以我用弹幕密度和互动率组合来代替。还有一个非常重要的指标是发布后 X 天增速比如视频发布 24 小时、72 小时的播放量。这个指标能从每日快照表里算出来能看出内容生命周期长短。有的视频当天爆发第二天就沉寂有的视频细水长流按月缓慢上涨这两种内容在运营策略上完全不同。4.4 多维分析表结构设计为了支撑这些指标我把表结构拆成三张核心表。dim_video存视频静态信息包括 aid、bvid、标题、分区、UP主动态、发布时间、时长等主键是 aid。dim_uploader存 UP 主信息包括 mid、昵称、粉丝数、签名等。fact_video_daily存每日快照字段包括 date、aid、view、like、coin、favorite、share、reply、danmaku联合主键是 date 加 aid。这个结构的核心思想是不变的属性放维度表每天变化的值放事实表按日期做增量采集更新。查询时把事实表和维度表 join 起来再做聚合。下表是我实际用的几个典型查询维度。分析主题关联表核心聚合字段典型输出分区热度dim_video fact_video_dailysum(view), sum(like)各分区近7天播放量趋势UP主成长dim_uploader fact_video_dailyavg(interact_rate)UP主互动率排行内容生命周期fact_video_dailymax(view), median(view)发布后第N天播放量分布标题效果dim_videocount, avg(interact_rate)标题长度与互动率关系有了这套结构我才能放心地做后面的可视化。可视化系统只是把表里的数据换一种形式呈现底层的数据模型不扎实看板再漂亮也是空中楼阁。5. 可视化大屏的实现让数据能讲清楚B站内容生态数据大屏是这套系统最有成就感的部分也是把前面所有工作可视化呈现的关键一环。刚开始我直接把所有图表堆在一个页面上又密又乱后来重新梳理了信息层级和阅读路径才算有点样子。5.1 大屏主题与指标确定大屏不是展示越多越好而是要回答现在发生了什么这个即时问题。我的大屏主题定为B站内容生态总览分成四个核心区域。页面顶部是核心 KPI 区显示累计采集视频数、当日新增视频数、累计播放量、当日总互动量让人一进门就能知道系统在监测的盘子有多大。左侧是分区维度的对比分析包括各分区播放量、投稿量、互动率排名和趋势。中间是时间趋势区展示总体播放量和互动率随日期的变化曲线同时叠加 UP 主投稿活跃度。右侧是榜单区包括播放量 TOP10 视频、互动率 TOP10 视频、涨粉最快的 UP 主。底部放一个滚动列表展示最近采集到的异常数据或新增内容比如播放量异常暴涨的视频。这个布局的阅读顺序是总体到分区、趋势到榜单、全局到异常看的人不需要说话就能快速理解当前状态。5.2 图表选型与布局图表选型我遵循一个原则连续变化用折线维度对比用柱状占比结构用饼图或环形图排行用横向条形图。展示内容图表类型为什么这么选全站播放趋势折线图强调时间连续性分区投稿量对比柱状图强调横向大小差异内容类型占比环形图强调部分与整体的关系视频传播生命周期热力图展示发布后不同天数的播放强度TOP榜横向条形图标签好读排名清晰大屏的分辨率适配是个容易忽略的问题。我直接在根容器上用 CSS 缩放方案设计稿按 1920×1080 制作页面加载后根据实际屏幕宽高计算缩放比统一transform: scale()。这样能保证在普通办公电脑和 4K 大屏上都不变形。5.3 后端接口与前端实现细节后端我用 FastAPI 写数据接口每个接口负责一个区域的聚合查询。比如/api/v1/dashboard/core-kpi返回顶部 KPI/api/v1/dashboard/category-rank返回分区排名。SQL 查询统一封装在 DAO 层避免视图层到处写裸 SQL。下面是一个简单的接口示例。from fastapi import FastAPI import databases app FastAPI() DB_URL mysql://user:passwordlocalhost/bili database databases.Database(DB_URL) app.get(/api/v1/dashboard/core-kpi) async def core_kpi(): sql SELECT COUNT(DISTINCT aid) AS total_video, SUM(view) AS total_view, SUM(like coin favorite) AS total_interact FROM fact_video_daily WHERE date (SELECT MAX(date) FROM fact_video_daily) row await database.fetch_one(querysql) return {total_video: row[total_video], total_view: row[total_view]}前端用 Vue3 加 ECharts每个图表的配置项独立成一个组件数据通过 axios 请求接口获得。ECharts 本身不复杂麻烦的是大屏自动刷新。我设置了一个全局定时器每隔 60 秒轮询一次核心接口其他低频图表每 5 分钟刷新一次。这样避免了所有图表同时请求造成瞬时压力。5.4 大屏里的几个实用细节做数据大屏过程中有几个细节让我印象很深。单位格式化要处理万和亿ECharts 的 label 里写一个 formatter 函数把数字转成中文单位比在数据层拼字符串干净得多。颜色不要全用高饱和色我用B站标志色的粉色系做主色蓝色和绿色做辅助背景用深色这样能减少长时间看大屏的视觉疲劳。大屏的滚动列表一定要有时间戳否则会让人误以为是实时监控数据。另外接口层要做限流和鉴权大屏页面本身不直接暴露 MySQL 连接信息所有数据库访问都走后端接口。6. 爬虫工程化与稳定性调优从能跑到长期跑爬虫写出来能跑和能长期稳定地跑是两个概念。我的爬虫项目刚开始每天跑都会出点问题要么反爬返回了异常页要么某个页面结构改了要么服务器重启后爬虫没有自动恢复。经过一段时间的工程化改造现在基本可以无人值守跑上几周。6.1 请求频率控制与反爬基础策略很多人做爬虫喜欢追求并发一到手就把并发调到几十。我的建议是反过来先从极低频率开始验证抓到的数据质量稳定之后再逐步往上加。我最终的参数大致是这样的。参数配置值说明DOWNLOAD_DELAY0.8s每个请求之间最小间隔CONCURRENT_REQUESTS_PER_DOMAIN4同一域名下的并发请求数CONCURRENT_REQUESTS8全局并发请求数RETRY_TIMES3失败重试次数RETRY_HTTP_CODES403, 429, 500, 502, 503遇到这些状态码重试ROBOTSTXT_OBEYFalse实测 B 站 robots 对部分接口限制较严个人学习项目按合理频率手动控制这里的ROBOTSTXT_OBEY我给了 False但不是我无视 robots 协议而是在项目文档里写清楚采集边界自己控制频率和范围。对于公开课、技术分享这类页面B站实际上设置了某些反爬规则我在合规前提下只做低频公开数据采集不做攻击性请求。除了频率控制我还做了随机 User-Agent 中间件维护一个常见浏览器的 UA 列表每次请求随机取一个。这个中间件实现起来非常简单但对降低风控误杀有明显作用。import random class RandomUserAgentMiddleware: def __init__(self, ua_list): self.ua_list ua_list classmethod def from_crawler(cls, crawler): return cls(crawler.settings.getlist(USER_AGENT_LIST)) def process_request(self, request, spider): request.headers[User-Agent] random.choice(self.ua_list) return None6.2 Cookie管理与会话保持B站部分数据需要登录状态才能看到完整信息比如某些分区的高赞内容或UP主后台数据。我在项目里准备了一个仅供采集公开数据使用的账号登录后把 Cookie 配置到爬虫的 settings 里。但 Cookie 是会过期的过期的表现通常是接口返回 -101 或跳转到登录页。针对这个问题我在 pipeline 和中间件里做了状态码检查一旦检测到登录失效就暂停当前队列并发送告警等待人工更新 Cookie 后再继续而不是让爬虫在错误页面上空转。这里要特别说明账号登录只用于访问本人有权访问的公开数据绝不用于抓取非公开信息。如果你的爬虫目标站不提供公开登录后的可读权限那就不要硬闯换一个数据源或者调整分析口径更实际。6.3 scrapy扩展机制监控和报警怎么做很多人在用 scrapy 时没注意到扩展(Extensions)这个能力。简单来说Extensions 是挂在 scrapy 信号系统上的独立组件可以在爬虫打开、关闭、出错、抓取到 Item 时触发一段自定义逻辑。它和中间件最大的区别是中间件主要影响请求和响应的处理流程而扩展更适合做横切关注点比如统计、监控、报警。我写了一个简单的错误报警扩展在请求持续失败时把错误栈和 URL 发到企业微信或钉钉机器人这样爬虫半夜挂了我也能知道。from scrapy import signals class ErrorAlertExtension: def __init__(self, stats): self.stats stats self.error_count 0 classmethod def from_crawler(cls, crawler): ext cls(crawler.stats) crawler.signals.connect(ext.spider_error, signalsignals.spider_error) return ext def spider_error(self, failure, response, spider): self.error_count 1 if self.error_count 10 and self.error_count % 10 0: spider.logger.error(连续错误超过10次请检查: %s, response.url)在 settings.py 里注册这个扩展即可数字是优先级越小越优先。EXTENSIONS { myproject.extensions.ErrorAlertExtension: 500, }6.4 定时调度与部署演进爬虫必须有稳定的调度环境。我最后选择了在云服务器上用 Docker 打包爬虫项目然后用系统的 cron 定时触发。每天凌晨 2 点跑全量增量采集避开晚上 8 点到 11 点的用户高峰。凌晨跑完任务后自动生成当天快照白天大屏展示用的就是这份最新的数据。具体的调度命令大概是这样。0 2 * * * cd /opt/bili-spider docker compose run --rm spider python main.py crawl --daily不过 cron 也有个劣势任务执行时间不可控一旦服务器负载高爬虫启动会晚。如果后面任务变多我会改成内部的调度器比如 APScheduler 或 Airflow但目前单机任务量用 cron 已经足够。7. 项目效果复盘与几个真实结论系统跑起来之后我从数据里发现了一些靠直觉感知不到的东西这也是这个项目最有价值的地方。这里分享几个让我印象比较深的分析结论。7.1 从可视化里发现的几个规律视频发布时段对初期播放量影响明显。晚上 8 点到 10 点发布的视频发布后 24 小时平均播放量比其他时段高 30% 左右。这可能不是因果关系但至少说明发布时间值得做 AB 测试。互动率更受内容选题影响而非时长我原以为视频越短互动率越高结果按分区拆分后不同分区的表现差异很大有些知识区的长视频互动率反而稳定高于短视频。大部分播放量集中在少数头部内容身上分区内播放量的中位数远低于平均数数据的长尾效应非常强做内容不要被头部爆款误导。弹幕密度和评论数的相关性非常高比播放量和点赞的相关性还高说明愿意发弹幕的用户往往也愿意评论这两个指标可以视作同一类互动行为。这些结论本身不复杂但如果没有数据可视化系统我很难持续观察到它们。数据大屏的价值不是给出最终答案而是帮你发现值得进一步验证的线索。7.2 系统的局限与可以扩展的方向这套系统当然也有局限。比如拿不到完播率这对内容质量评估是个很大的缺口。数据样本只覆盖我会主动采集的分区不一定是全站规律。另外采集频率是每天一次对突发爆款视频的时效性跟踪不够及时。后续如果可以扩展我会做三件事。接入更多数据源比如弹幕流、评论楼中楼做更细粒度的用户情绪分析。增加 NLP 能力对标题和标签做主题聚类看哪些关键词组合更容易出爆款。优化查询性能把历史明细表从 MySQL 迁到 ClickHouse让大屏可以支持任意时间范围的秒级聚合。7.3 最后再分享几个踩坑总结走到最后我觉得几个在实践中得到的经验对正在做类似项目的人可能更有用。第一不要在爬虫阶段追求一步到位先用最快方式拿到 1000 条数据走通清洗、入库、展示全链路再回头优化采集细节这样你始终能看到完整成果不容易半途而废。第二处理动态页面和 iframe 时优先尝试直接请求背后的数据接口或读取页面上的结构化变量实在不行再上 playwright这个判断能帮你省下大量时间和服务器资源。第三把日志当成系统的第一公民爬虫的每个 Item 都要有 trace 信息哪天数据对不上了日志就是唯一的破案线索。第四合规意识要刻在项目设计里只做公开数据的低频采集、不采集隐私、不搞攻击性请求这个项目才能长期健康地跑下去。这个系统现在已经稳定运行了一段时间我每天打开大屏看数据的时候仍然会觉得当时选择从零搭建它是值得的。技术栈并不复杂但把 scrapy、python 数据分析和可视化串成一条完整流水线中间遇到的问题和解决方案比一百篇教程都更能让人成长。
返回列表