ARTICLE DETAIL

资讯详情

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

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

基于Scrapy的B站数据采集与可视化系统实战解析 这两年接了不少数据采集类的需求但真正让我觉得“这项目值得复盘”的还是这套基于 Scrapy 的 B 站数据分析可视化系统。从选题、抓取、清洗、入库到出图、出报告整个链路一个人打通技术选型和踩坑路径都比较典型拿来当大数据爬虫项目的完整范本挺合适。这篇就按我实际开发的顺序把这个系统的设计思路、核心实现、常见坑和可复用的套路一次说清楚。1. 项目整体设计与思路拆解1.1 为什么选 B 站、为什么做数据可视化很多人问我爬虫项目那么多淘宝京东拼多多不香吗为什么拿 B 站开刀。我的理由很直接数据获取的成本和维度平衡。电商平台的反爬体系成熟且数据集中在价格、销量和评价维度相对单一。而 B 站是一个内容社区视频本身携带的信息量非常大——标题、标签、分区、时长、弹幕、评论、投币、收藏、UP 主信息、发布时间。这些字段组合起来能做的分析维度远超“卖了多少件”。加上 B 站开放了一部分公开接口前端 Web 端有搜索页、排行榜、视频详情页这些页面的数据接口是可以通过合法公开路径获取的。如果再叠加大数据爬虫技术里最常见的“采集—清洗—存储—分析—可视化”链路这个项目几乎能覆盖一个数据工程师日常工作的所有环节。做完之后不仅是一个毕设或练手项目还能直接沉淀出一套可复用的数据采集和分析模板。1.2 系统总体架构五层结构解析我搭这套系统的时候脑子里先画了一条主线数据从哪来、经过什么处理、最后怎么被人看懂。按这个逻辑划分了五层。第一层是数据源层。目标锁定 B 站 Web 端的几个核心 JSON 接口包括搜索接口、排行榜接口、视频详情接口、评论接口和用户空间接口。这层的核心任务是摸清每个接口的请求方式、参数含义和返回结构。第二层是采集层。用了 Scrapy 框架作为主力。为什么选 Scrapy后面专门展开。这里只提一点Scrapy 的并发模型、中间件机制、去重机制、Pipeline 机制几乎是为这种“多页面、多实体、多约束”的采集场景量身定做的。第三层是存储层。我采用了 Redis MongoDB 的组合。Redis 用来做请求指纹去重、布隆过滤器、临时队列MongoDB 存明细数据。为什么不直接扔进 MySQL因为视频和评论的数据结构是半结构化的接口返回的有些字段时有时无MongoDB 的文档模型更贴合这种不确定性。第四层是分析层。数据入库不等于有意义还要做聚合统计。我主要用 Pandas 做清洗、聚合、分组统计再外加一部分简单的文本分析。比如分区热度对比、视频时长的数据分布、UP 主粉丝量级与互动率的关系等。第五层是可视化层。数据最终要变成人能一眼看懂的东西。这里用了 Pyecharts 和 ECharts 的组合最后拼出可视化大屏。大屏的意义不在于炫而在于把几十万条数据浓缩成几个关键指标让决策者不用翻数据表。这五层每一层都有自己独立的坑。下面我从采集层开始逐个拆。2. 核心细节解析与实操要点2.1 Scrapy 的引擎机制与调度逻辑为什么选它很多朋友一上来就喜欢用 requestswith threading 撸一套“简易爬虫框架”短平快。但一旦你的目标站点有分页、有反爬、有多种实体需要关联存储你就会发现线程管理的复杂度直线上升——请求超时重试、分布式去重、任务排队、请求优先级、日志统一管理这些都需要你自己从零写。Scrapy 把这些事全部内建了。它的引擎是一个异步的 Twisted 核心Scheduler 管理请求队列Downloader 负责实际发送 HTTP 请求Spiders 负责解析响应Item Pipelines 负责数据清洗和存储。整个流程是Spider 产生 Request → 交给 Scheduler 排队 → Downloader 下载出 Response → 回到 Spider 解析 → 产出 Item 和新的 Request。你只需要写三个东西Spider 的解析逻辑、Pipeline 的处理逻辑、Middlewares 的拦截逻辑。这套机制最香的地方是你不需要自己管并发。只要在配置文件里设置 CONCURRENT_REQUESTSScrapy 自己就限流和复用连接。对于 B 站这种有风控规则的站点约束并发本身就是一种自保。2.2 B 站接口选型与抓取的合规边界爬虫项目的首要问题不是“能不能爬”而是“爬什么、怎么爬”。我在这套系统里只用了 B 站的公开 Web 接口而且是前端页面本身就会请求的那些 JSON 数据接口。比如搜索综合接口https://api.bilibili.com/x/web-interface/search/all/v2排行榜接口https://api.bilibili.com/x/web-interface/ranking/v2视频详情接口https://api.bilibili.com/x/web-interface/view视频评论接口https://api.bilibili.com/x/v2/reply这些接口不需要额外生成签名参数只需要带好 Cookies 和 User-Agent 就能访问。请求频率我严格控制单 IP 请求速率保持在每秒 1-2 个请求以内而且每天只在低峰时段跑一轮增量。这么做既对目标站点的服务压力最小也符合数据采集中“合法合规获取公开信息”的边界。要注意的是不要碰用户未公开的信息、不要绕过登录验证去获取需要权限才能看的内容、不要恶意高频请求造成对方服务异常。这是我做爬虫项目时给自己画的底线。2.3 Requests 指纹去重Redis 布隆过滤器实践爬虫最怕什么问题重爬。尤其 B 站这种内容更新频繁的平台动态评论和动态榜单会一直回流相同数据。如果不去重数据库里塞满了重复文档分析阶段的计数全部虚高。Scrapy 自带的 RFPDupeFilter 只对 Request 指纹生效处理的力度太粗。我想要的是基于内容实体的幂等比如同一个视频 ID、同一条评论 ID只有第一次出现才允许入库。所以我用 Redis 额外搭了一套去重层。实现的思路很典型用 mmh3MurmurHash3把 av_id 或 cid 哈希成两个 64 位的整数映射到一个位数组上。这个位数组我用 Redis 的 setbit/getbit 命令来维护相当于布隆过滤器的 Redis 版本。好处是内存占用极小一亿条 ID 也只需要一百多 MB 的位图而且 setbit 的操作是 O(1) 的。下面是核心代码的思路import mmh3 from redis import StrictRedis class BloomFilterRedis: def __init__(self, redis_client, key, capacity100000000, error_rate0.001): self.client redis_client self.key key self.bit_size self._compute_bit_size(capacity, error_rate) self.hash_count self._compute_hash_count(capacity, self.bit_size) def _compute_bit_size(self, n, p): return int(- (n * math.log(p)) / (math.log(2) ** 2)) def _compute_hash_count(self, n, m): return int(round((m / n) * math.log(2))) def add(self, item): for seed in range(self.hash_count): index mmh3.hash(item, seed) % self.bit_size self.client.setbit(self.key, index, 1) def contains(self, item): for seed in range(self.hash_count): index mmh3.hash(item, seed) % self.bit_size if not self.client.getbit(self.key, index): return False return TrueBloomFilterRedis 在 Pipeline 中会在每次 Item 进入持久化环节前执行一次判断。这里有概率误判的代价但控制在千分之一以内换来的是 O(1) 的查询速度和极低的内存成本。对“偏向统计”的数据分析项目来说这个权衡非常划算。2.4 User-Agent 池与代理池的配置细节B 站虽然不像某些电商平台那样变态但风控依然存在。最明显的表现就是当请求频率过高或 UA 太单一会遇到 412 错误码这个码的潜台词是“我知道你是爬虫了请出示更仿真的人类行为证明”。我的处理策略是双管齐下。第一维护一个 User-Agent 池里面放不少于 30 个不同平台的 UA包括 Chrome、Edge、Safari、Firefox 的 PC 端和移动端版本。每次请求前从池子里随机抽取一个不重复使用。第二维护一个 HTTP 代理池用稳定的代理服务商而不是免费代理因为免费代理的存活率太低频繁换代理反而更容易触发风控。在 Scrapy 中实现这两件事只需要写两个 Downloader Middleware。核心逻辑大致是process_request 阶段修改 request.headers[User-Agent]同时随机绑定一个代理地址给 request.meta[proxy]。我实际把这些代理作为环境的出口频率控制仍然以目标接口的单 QPS 为基准。3. 实操过程与核心环节实现3.1 数据模型设计Video、Comment、Author 三实体一套数据系统最怕建模的时候不思考入库之后回头改。我设计数据模型时先用脑图把想要的分析维度全部列了一遍然后反推字段需求。最终定下三个核心实体。Video 实体存视频本身的属性bvid、aid、标题、分区、标签、简介、发布时间、时长、播放量、点赞、投币、收藏、分享、弹幕数、评论数。这些字段基本能支撑大部分热度分析。Comment 实体存评论内容评论 IDrpid、根评论 ID、父评论 ID、视频 bvid、用户 mid、点赞数、回复数、评论内容、发布时间。二级评论的结构通过 parent_id 字段表达方便做嵌套展开。Author 实体存 UP 主基本画像mid、昵称、签名、性别、等级、粉丝数、关注数、视频投稿数、认证信息、是否大会员。这部分数据主要从视频详情页返回的 owner 信息基础上再补充抓取用户空间信息。三个实体之间通过 bvid 和 mid 关联。在 Mongo 中我采用了“用户视频的冗余设计”即在 Video 文档里直接内嵌 Author 的基本信息避免分析时频繁跨表 join。MongoDB 是文档型天然支持这种内嵌虽然在严格关系模型下这叫冗余但在分析场景下这叫空间换速度。3.2 配置文件和 Middleware 关键参数解析Scrapy 项目的配置集中在 settings.py我贴几个关键的参数作为参考BOT_NAME bilibili_analysis CONCURRENT_REQUESTS 16 DOWNLOAD_DELAY 0.8 CONCURRENT_REQUESTS_PER_DOMAIN 8 DOWNLOADER_MIDDLEWARES { bilibili_analysis.middlewares.UserAgentMiddleware: 401, bilibili_analysis.middlewares.ProxyMiddleware: 402, } ITEM_PIPELINES { bilibili_analysis.pipelines.BloomFilterPipeline: 200, bilibili_analysis.pipelines.MongoPipeline: 300, } RETRY_ENABLED True RETRY_TIMES 3 RETRY_HTTP_CODES [412, 429, 500, 502, 503] DUPEFILTER_CLASS scrapy.dupefilters.RFPDupeFilter SCHEDULER scrapy.core.scheduler.Scheduler关于 CONCURRENT_REQUESTS 怎么定我的经验是不要无脑调大。抓 B 站这种有风控的平台16 并发加上 0.8 秒延迟已经算比较激进了。如果你用代理池可以将并发升到 32但每个代理 IP 分到的 QPS 依然要控制在 1-2。性能的核心瓶颈不是代码而是目标站点的容忍度。3.3 评论接口的递归翻页实现B 站评论接口有两个翻页参数pn 是页码ps 是每页条数ps 最大是 20。除了普通评论每条根评论下还有若干二级评论。如果二级评论超过一页还需要带着 root 参数递归往上翻。这个逻辑不算复杂但确实很容易写错——最容易犯的错是忘记在二级评论翻页时携带 root 评论 ID导致翻页永远返回空。我实现的思路是用 Scrapy 的 Request callback 链。先从视频详情拿到 aid请求评论第一页解析完根评论后对每条根评论判断是否还有二级评论分页如果有继续 yield Request 请求下一层分页。每次请求把上下文通过 meta 传下去保证解析时知道这个评论属于哪个视频、哪条根评论。def parse_replies(self, response): data json.loads(response.text) replies data.get(data, {}).get(replies) or [] for reply in replies: rpid reply.get(rpid) yield VideoComment(**self._extract_comment(reply)) # 二级回复分页 if reply.get(rcount, 0) 0: yield Request( urlself.reply_page_url, callbackself.parse_replies, meta{ oid: response.meta[oid], root: rpid, pn: 1, }, dont_filterTrue, )这里你必须特别小心参数传递。Scrapy 的 meta 机制默认是浅拷贝如果你在 meta 里放可变对象跨请求后会被共享修改这会导致所有并发请求串数据。我踩过这个坑等到排查时发现一堆评论跑到了别人的视频下面就是因为 meta 里的字典在多个请求间共享了。3.4 数据清洗时间格式、空字段、单位千/万转换B 站接口返回的播放量、点赞数这些数据在 Web 页面显示时是经过格式化的比如“1.2万”“3456”。但 JSON 接口里大多返回的是整数。这里要提醒的是不同接口的数据口径不完全一致。有的排行榜接口返回的是字符串类型的“播放量”字段有的返回纯数字这个需要你在开发过程中用探针脚本全部跑一遍逐个确认。我写了一套清洗工具函数集中处理几个常见场景时间戳转 datetime、字符串万/亿转 int、空字符串补 None、去掉 HTML 标签。这些清洗逻辑统一放在utils/cleaners.py里Pipeline 里调用。永远不要在高斯循环中直接写正则去转因为一个格式异常就可能中断整个采集流程。规范做法是清洗函数内部用 try-except 包裹异常时返回默认值并记录一条 warning 日志。3.5 MongoDB 存储索引设计与查询优化MongoDB 在分析系统中的角色是原始数据仓库。存储本身很简单insert_one就行。但如果后续分析要跑得快索引设计必须跟上。我给 collection 建了三个索引。第一个是bvid的唯一索引这能防止同一视频被重复入库。第二个是pub_ts普通索引后续按时间范围分析的趋势图全靠它。第三个是mid普通索引支撑 UP 主维度的聚合。如果再细一点还会给 comments 集合中的ctime建索引评论时间线分析要用。这里有一个重要的实操经验唯一索引要去重时不要用 upsert 方式写库。MongoDB 的 upsert 在高并发下会有竞态问题而且文档被重复更新的频率很高导致采集和存储速度下降。我采用的方法是先用 BloomFilter 过滤掉大概率的重复再用 insert_one 配合唯一索引兜底重复键错误直接忽略并记录计数。这个“双重去重”方案让我在几百万条数据量下写入效率没有明显下降。4. 数据分析与可视化大屏的搭建4.1 分析维度选择视频热度、UP 主画像、评论画像数据量攒到十万以上之后分析就有意义了。我把分析任务分成四个维度的模块每个维度对应大屏上的一个区域。视频热度分析按分区统计视频数量占比和平均播放量按发布时间绘制播放量和弹幕数的分布曲线识别出播放量异常高的爆款视频并提取标题关键词。UP 主分析基于 mid 聚合粉丝量区间计算不同粉丝量级 UP 主的互动率投币收藏分享除以播放量得出“哪个量级的 UP 主内容质量最稳”之类的结论。评论画像分析统计评论的时间分布分析视频发布后的评论高峰期按评论点赞数做长尾分析找出哪些评论获得了远超均值的点赞。内容趋势分析抓周维度数据做环比分析看哪几个分区的内容在上涨哪几个在下滑。这些分析我用 Pandas 来跑。Pandas 的 groupby、merge、rolling 几乎覆盖所有需求跑百万级数据也就是几秒钟的事完全不需要上 Spark 这类重组件。4.2 ECharts 与 Pyecharts 可视化方案选择可视化方案是我权衡很久的点。可选方案有原生 ECharts Ajax 从后端拿数据、Pyecharts 直接生成 HTML、以及 DataV 等在线大屏工具。我最终选择 Pyecharts 作为主力理由很实际。Pyecharts 的好处是用 Python 的配置思维来生成 ECharts 图表不需要写一行 JavaScript。图表配置完后page.render(dashboard.html)直接产出一个独立 HTML 文件里面自带 JS 引用部署非常简单。用 Pyecharts 的 Page 和 Tab 组件可以把多个图表拼成一个完整的大屏。不过 Pyecharts 有版本坑。你用 pyecharts1.x 和 2.x 的 API 差异很大很多老博客的代码拿到新版直接报错。我的建议是锁定一个版本写不要混用。另外 Pyecharts 生成的 HTML 文件体积会比较大如果部署在服务器上建议用 Nginx 托管静态文件而不是每次请求都动态生成。4.3 大屏数据时延与刷新策略大屏不是实时监控屏不需要秒级刷新。我的做法是每天凌晨采集任务跑完后触发一次分析任务生成当日快照 HTML。大屏页面固定在当天 06:00 之后更新。页面顶部会放几个核心指标总视频数、总播放量、总评论数、活跃 UP 主数。下半部分放区域热度图、分区占比饼图、视频时长分布散点图、评论高峰折线图。这套大屏做好之后我发给了几个做内容运营的朋友看反馈是比较直观“终于不用靠刷视频首页猜热度了”。5. 常见问题与排查技巧实录5.1 频繁 412 风控码的定位与解除这是我被问得最多的问题跑着跑着突然全是 412。我的排查顺序遵循由内到外先看是单 URL 还是全站触发——如果是单个 URL 触发说明某个接口的参数有问题或者这个接口本身对 Cookie 有额外要求如果是全站触发说明 IP 被识别了。处理办法按照优先级排序首先停止任务先冷却一小时不要一边换代理一边继续打其次检查代理池出口 IP 的质量有些代理 IP 的标记特征太明显比如机房 IP 段字段会被直接判定为 Bot最后是给请求补全浏览器特征包括 Accept-Language、Accept-Encoding、Connection 等 header尽量做到与真实浏览器一致。5.2 Redis 连接断开导致任务卡死的修复用 Redis 布隆过滤器之后任务出现过一个诡异现象运行几个小时后Spider 不再产生新的 Request日志也不报错就像死锁一样。后来排查发现是 Redis 连接被服务端断开了爬虫的 Redis 客户端对象没有自动重连机制对 setbit 的调用无限期阻塞。解决方案是在自定义的 BloomFilterRedis 类里增加连接健康检查每次操作前用ping()探测一次失败时重新初始化连接池。这里的成本几乎可以忽略因为 Redis 的 ping 是微秒级操作但换来了任务长时间运行的稳定性。5.3 评论数据采集不全的分析与解决采集评论时常遇到的问题视频评论总数很高但爬虫采下来的只有几百条。经过抓包和分析接口文档发现B 站的评论接口默认只返回热度最高的部分评论而且还有风控过滤不是所有评论都能通过接口拿到。这个不是代码 bug而是数据源本身的限制。我在系统里做了补充策略对同一视频分多次、间隔时间去拉取评论同时把排序方式切换为按时间排序再拉一遍这样能比默认方案多采 30% 左右的评论。采集数据全面性这个指标不是靠并发堆出来的而是靠合理的调度策略。5.4 数据分析结果异常的排查做 UP 主互动率分析时发现有个 UP 主的互动率高达 300%明显异常。排查一圈后发现问题出在数据源上某些视频的播放量字段是缺失的Pandas 在统计时把缺失值跳过导致分母变小互动率虚高。这种问题在数据质量不佳时非常常见。所以我加了数据质量校验逻辑在清洗阶段如果发现播放量为空或明显异常比如小于点赞数整个记录标记为脏数据在分析阶段排除。数据分析项目里清洗环节永远不是能偷工减料的地方。一堆高质量的可视化图表只要底层数据是脏的结论就不可能靠谱。6. 部署、监控与项目扩展空间6.1 Docker 化部署与定时调度整个系统我是用 Docker Compose 排的编排包含三个容器爬虫服务容器、Redis 容器、MongoDB 容器。定时调度用的 Crontab 写在宿主机里每天凌晨 02:00 跑爬虫任务04:00 跑分析任务06:00 刷新大屏静态页。这个编排方式对单机部署非常合适扩展时只需要把这些服务推到集群里改动幅度很小。这里我踩过一个坑Scrapy 在 Docker 容器里跑日志如果直接输出到 stdout容器重启后日志就丢了。所以我在 Dockerfile 里把日志目录挂载到了宿主机并且设置了 log 文件的轮转策略。数据分析项目跑久了日志比代码更值钱很多时候查历史问题全靠日志。6.2 从单机到分布式大数据架构演进思路这套系统目前是单机架构但我设计的代码结构已经为分布式留了后路。比如调度层用 Redis 做 URL 队列只要把 Scrapy 的 SCHEDULER 换成 scrapy-redis 的调度器就能把请求队列从本地内存搬进 Redis多个爬虫节点变成生产者和消费者模式实现分布式采集。存储层因为用的 MongoDB 和 Redis天然支持集群扩展。分析层从 Pandas 切换成 Spark SQL 也只是工程量的问题不需要推翻重来。架构演进的核心原则是每一层之间的耦合尽量通过消息而非函数调用这一开始就要想清楚。6.3 数据合规的边界与长线运营文章最后必须提一句数据合规。这个项目所有的数据都来自公开接口但这不等于可以无限度地采集。合理的采集频率、必要的防爬机制、最终数据的脱敏展示都是我对数据合规的最低要求。这套系统我限制了单日采集上限和单 IP 频率上限而且数据仅用于个人分析和学习用途不对外提供原始数据下载。根据我个人的经验想要把这种类型的项目真正跑得长久、用得放心与其拼命突破反爬限制获取更多数据不如想清楚一个问题你采集的数据到底能解决什么分析问题。就算每天只采集几千条高质量数据只要维度清晰、口径一致、质量可靠分析出的结论价值远超那些“为了爬而爬”攒出来的海量垃圾数据。做数据分析的都知道一个道理脏数据等于没数据。这个项目能顺利跑通并产出可视化结论靠的不是爬得多狠而是每一层都稳。我做这套系统最大的体会是爬虫只是手段让数据变成可辅助决策的信息才是最终目标。如果你也想练手这类项目建议先从分析目标反推数据需求不要一上来就闷头写爬虫。想清楚要回答什么问题再决定采什么数据这个顺序对了整个项目就成功了一半。
返回列表