
1. 动手之前先想清楚热门榜单数据到底能解决什么问题先聊个挺常见的工作场景你负责一个内容账号的日常运营每天早上打开后台第一件事就是看竞品又更新了什么今天平台上什么话题在涨哪个方向值得跟。这些信息如果全靠人工刷网页一天至少耗掉一两个小时而且刷完还容易漏。我以前就是这种状态直到某天发现自己把大量时间浪费在“看榜单”而不是“用榜单”上才下决心做一个能自动抓取视频平台热门榜单的小工具。这个工具的本质就是把平台公开的热门数据定时抓下来存进本地数据库再按自己的需求做过滤、排序、提醒。它能解决的问题大致有这几类追热点每天自动记录全站热门视频不用半夜盯着榜单变动第二天起床看前一天的快照即可。竞品监控把同行账号的视频数据也纳入抓取范围定期对比播放量、点赞数、弹幕数的增长趋势。选题决策连续抓一周数据后统计哪些题材反复出现在高位哪些是“一日游”型热点选题时心里更有底。汇报自动化每周把热门数据的统计结果自动整理成表格省掉手动截图和抄数字的时间。适合参考这篇文章的人我大致分成三类一是做内容运营和自媒体想用数据辅助选题的二是刚开始学爬虫想找一个真实、可控、又不复杂的练手项目的三是团队内部想做竞品情报或行业观察需要低成本方案的技术人员。无论你是哪一类这篇文章都会从一个完整的工程视角来拆解从目标设定、技术选型到代码实现、避坑经验都覆盖到。需要先说明的是我这里以主流的视频平台公开榜单接口为例子抓取的都是无需登录就能看到的公开数据并且抓取频率会控制在一个对平台没有任何压力的范围内。项目本身不涉及任何违规手段核心目的是把“数据采集—存储—分析”这条链路跑通。2. 技术选型为什么用Python轻量组合而不是重型框架先说说技术栈。这个项目我用的是 Python 3.10 httpx SQLite解析方面因为返回的是标准 JSON所以直接用 Python 内置的 json 模块连 BeautifulSoup 都没用上。很多人一听到“抓取”两个字就想到 Scrapy、Selenium、Playwright 这些重量级工具但对于抓公开榜单接口这个场景属于典型的大炮打蚊子。2.1 请求库httpx 比 requests 好在哪requests 是老牌的 HTTP 库但我在新项目里更倾向于 httpx。核心原因有几个一是 httpx 支持 HTTP/2对现代接口的兼容性和性能更好二是它的接口设计跟 requests 高度相似迁移成本几乎为零三是它天然支持异步万一后面要同时抓多个榜单写异步并发很顺手。import httpx headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_json(url: str, params: dict | None None) - dict: with httpx.Client( headersheaders, timeout15.0, follow_redirectsTrue, ) as client: resp client.get(url, paramsparams) resp.raise_for_status() return resp.json()这段代码是一个最基础的请求封装。注意我设置了 15 秒超时和自动跟随重定向这些细节在真实环境里很重要否则一个慢接口就能把整个定时任务卡死。2.2 存储选型SQLite 完全够用有人会习惯性地上 MySQL 或 PostgreSQL但说实话个人脚本、小团队内部工具数据量在百万级以下SQLite 是体验最好的方案。它不需要单独部署服务一个文件就是整个数据库备份直接复制文件搞定。配合 Python 内置的 sqlite3 模块零依赖。我在设计表结构时参考了“排行榜快照”的模式也就是每次抓取都记录当时榜单的完整状态而不是只记录增量。这样做的目的是热门榜单本身是动态变化的只有记录每一时刻的完整排名才能分析出“哪些视频一直在榜”“哪些视频是突然后劲爆发”这类趋势信息。CREATE TABLE if NOT EXISTS hot_rank ( id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT NOT NULL, rank INTEGER NOT NULL, title TEXT, author TEXT, play_count INTEGER, like_count INTEGER, danmaku_count INTEGER, snapshot_time TEXT NOT NULL DEFAULT (datetime(now, localtime)), UNIQUE(video_id, snapshot_time) );这张表的关键在最后一行的联合唯一约束同一时刻、同一个视频只能有一条快照记录。这样即使脚本意外重复执行也不会插入脏数据。2.3 为什么不用 ScrapyScrapy 是一个很优秀的爬虫框架但它解决的是“大规模、多站点、需要管道化处理”的复杂问题。对于单接口、单表的小工具Scrapy 的配置成本反而很高——你要搭项目结构、配置 Item Pipeline、写 Spider还得理解框架的异步调度机制。这就好比你想煮一碗泡面结果把整套厨具都搬出来。能用十行代码解决的问题不要用一百行去解决这是我一直坚持的原则。等你真的需要抓几百个站点、做分布式调度的时候再迁移到 Scrapy 也不迟。3. 抓取链路核心实现从定位接口到数据落库这章是动手环节我按正常的开发顺序来讲先找到数据从哪来再分析请求参数最后写代码把数据解析、清洗、入库。3.1 通过开发者工具定位真实数据接口很多教程会直接教你去解析 HTML 页面用 BeautifulSoup 去抠节点。但稍微正规一点的平台首屏数据基本都是通过 XHR 异步加载的。这意味着你只要打开浏览器的开发者工具F12切到 Network 面板刷新页面就能看到浏览器到底向哪个接口发了请求、返回了什么数据。以 B 站的热门榜单为例打开页面后 Network 面板里会有一个 popular 相关的请求响应是标准的 JSON 结构。这种方式找到的接口请求参数清晰、响应结构稳定比解析 HTML 高出不止一个维度。操作步骤很简单打开目标榜单页面F12 打开开发者工具切到 Network刷新页面筛选 XHR 或 Fetch 类型的请求逐个查看响应内容找到返回视频列表的接口右键这个请求复制为 cURL方便分析请求头第 5 步特别实用。复制为 cURL 之后你可以把它粘贴到 Postman 或 https://curlconverter.com 这类工具里自动转换成 Python 代码包括所有请求头都能带过来。3.2 请求参数与频率控制找到接口后别急着写代码先分析一下它的 Query String。通常这些参数里有两个关键信息一个是分页参数pagination一个是时间窗口参数time window。我在实际项目里建议控制抓取频率在每 10 到 15 分钟一次。热门榜单的更新频率本身就不是秒级的太频繁的请求不仅浪费资源还容易被平台的反爬机制盯上。如果你只是做每日回顾甚至每天抓三次就够早上、中午、晚上各一次捕捉不同时段的热点变化。3.3 数据解析与字段清洗从接口返回的 JSON 里视频列表通常嵌套在 data 字段下面。解析的时候建议写一个独立的解析函数方便测试和维护。def parse_hot_items(raw: dict) - list[dict]: items [] video_list raw.get(data, {}).get(list, []) for item in video_list: # 跳过播放量为空或明显异常的数据 if not item.get(stat, {}).get(view): continue items.append({ video_id: item[bvid], title: item[title].strip(), author: item[owner][name], play_count: item[stat][view], like_count: item[stat][like], danmaku_count: item[stat][danmaku], rank: int(item.get(rank, 0)), }) return items清洗这一步很多人会省略但我觉得非常值得做。比如标题里的首尾空格比如播放量字段偶尔会出现负数或超大的异常值这些都是脏数据。如果不处理后续做趋势分析的时候会出现很离谱的误差。3.4 主流程串联把前面的模块串起来主程序大概长这样import sqlite3 from datetime import datetime def main(): url 你的目标接口地址 params {ps: 50, pn: 1} # 每页50条取第一页 raw fetch_json(url, paramsparams) items parse_hot_items(raw) conn sqlite3.connect(hot_data.db) for item in items: item[snapshot_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) insert_snapshot(conn, item) conn.commit() conn.close() print(f成功写入 {len(items)} 条快照数据)这个主流程看起来简单但它把“请求—解析—入库”三个环节彻底解耦了。后面的章节里我会在这个基础上叠加增量更新、异常重试和定时调度让它从一个“手动跑一次”的脚本变成“每天自动干活”的工具。4. 增量更新与去重让工具从“跑一次”变成“天天用”如果只是在命令行里手动执行一下脚本那这工具还停留在一半的状态。真正让它产生价值的是长期持续运行而这背后依赖两件事去重机制和增量更新。4.1 基于联合唯一约束的天然去重我在建表时已经用UNIQUE(video_id, snapshot_time)做了约束。这意味着同一个视频在同一分钟内只会有一条记录。即使定时任务因为某种原因重复执行二次插入也会触发冲突异常而不会产生重复数据。不过这里有个细节要注意SQLite 的唯一约束冲突会抛出IntegrityError如果你让它裸奔程序会直接崩溃。正确的做法是在插入时捕获这个异常或者使用INSERT OR IGNORE语句def insert_snapshot(conn: sqlite3.Connection, item: dict) - None: sql INSERT OR IGNORE INTO hot_rank (video_id, rank, title, author, play_count, like_count, danmaku_count, snapshot_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?) conn.execute(sql, ( item[video_id], item[rank], item[title], item[author], item[play_count], item[like_count], item[danmaku_count], item[snapshot_time], ))用INSERT OR IGNORE之后重复执行就变成静默跳过脚本天然具备幂等性。这个经验在几乎所有数据采集任务里都适用。4.2 快照机制与趋势分析为什么我要强调“快照机制”因为热门榜单是一个动态排名并且视频本身也是涨数据的。如果你只在第一次看到某个视频时记录一个静态条目那你看不到它的增长曲线。快照机制记录了每一个时间点的排名和互动数据这样你可以很方便地跑出类似这样的分析-- 查询最近7天出现在榜单超过3次的视频 SELECT video_id, title, COUNT(*) AS appear_cnt FROM hot_rank WHERE snapshot_time datetime(now, -7 days) GROUP BY video_id HAVING COUNT(*) 3 ORDER BY appear_cnt DESC;这个查询能帮你找出真正的“常青树”视频——连续多天都在榜单上这种内容比那些昙花一现的视频更有参考价值。如果你想看一个具体视频的排名变化只要按 video_id 查快照记录就能画出它的排名波动曲线。4.3 定时调度的三种方式脚本写好后定时调度是一个绕不开的问题。我试过不少方案给你列个对比方案优点缺点适用场景Windows 任务计划程序系统自带无需额外安装配置稍繁琐跨机器迁移不方便Windows 本机运行cronLinux/macOS稳定可靠配置简单需要熟悉 cron 语法服务器长期运行云函数/定时触发器零运维不占本机资源有免费额度限制需要平台部署云端托管我个人推荐如果你有云服务器直接用 cron 是最省心的方案如果不想管服务器云函数定时触发也不错。本机跑 cron 的问题是电脑一关就罢工不适合长期任务。下面是一个 cron 例子每天早上 8 点到晚上 11 点每隔 2 小时抓一次0 */2 8-23 * * * cd /path/to/project /usr/bin/python3 main.py logs/hot.log 21记得把日志输出到文件里不然脚本出错时你根本不知道什么情况。5. 反爬机制与边界意识我踩过的几个真实坑公开接口虽然门槛低但不代表没有任何防护。这个项目我前前后后跑了一两个月遇到过几个典型问题整理出来给你做个参考。5.1 频率过高从正常请求到访问异常我第一次跑的时候为了拿到更细的时间粒度设置了每 3 分钟请求一次结果跑了不到半天接口开始随机返回空数据。当时第一反应是代码出了 bug排查了半天才发现是请求过于频繁被服务端限流了。这个问题的背后是服务端通常有 QPS 限制和单位时间请求数限制你短暂超过阈值不会立刻被封但会进入一种“软限制”状态——请求能发出去但返回的数据要么是空的要么是固定的缓存数据。解决方式很简单降低频率。我把抓取间隔从 3 分钟改成 15 分钟之后连续跑了一周再也没有出现过空数据。这个经验给我的教训是不要贪多抓取频率够用就好追求秒级实时更新对热门榜单没有任何意义。5.2 User-Agent 和请求头伪装有些接口会对没有浏览器特征的请求做拦截。我在初版代码里随便写了个 User-Agent结果部分接口返回 403。后来把请求头补全成浏览器标配UA、Accept、Accept-Language问题就解决了。这里分享一个日常的组合头headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.bilibili.com/, }Referer 这个字段有时候比 UA 还关键因为服务端会校验请求是不是从本站页面发起的。你直接请求 API 可能 403带上主页面的 Referer 就放行了。5.3 登录态与 Cookie 的作用热门榜单这类公开数据通常不需要登录但如果你要抓取一些需要登录才能看到的个性化推荐或者抓取量很大的场景可能就需要登录后的 Cookie。获取 Cookie 的方式很正统手动登录一次从开发者工具的 Application 面板或 Network 请求里复制 Cookie 值放到代码配置里。这里有一个坑Cookie 是会过期的少则几天多则几个月。用带 Cookie 的请求时要做好失效预案——通常是设置一个合理的过期时间到期后重新登录换 Cookie并在代码里把 Cookie 写到单独的配置文件中不要硬编码。5.4 异常重试与指数退避网络请求不可能 100% 成功超时、连接重置、返回 5xx 都会发生。一个健壮的工具需要异常重试机制。我的做法是在请求封装里加重试逻辑每次重试间隔翻倍import time def fetch_with_retry(url: str, params: dict | None None, max_retries: int 3): for attempt in range(max_retries): try: return fetch_json(url, paramsparams) except Exception as e: if attempt max_retries - 1: raise wait_time 2 ** attempt # 指数退避 print(f请求失败{wait_time}秒后重试错误{e}) time.sleep(wait_time)指数退避的核心逻辑是第一次失败等 2 秒第二次失败等 4 秒第三次失败等 8 秒给服务端足够的恢复时间。这样既不会把服务端打爆也不至于因为一次临时抖动就丢掉整轮数据。5.5 合规与边界意识这个必须单独说。抓取公开数据本身是常见的工程技术但有两个边界要守住抓取频率不要对目标平台造成压力合理频次内使用抓下来的数据不要用于商业转售或大规模分发尤其是涉及用户隐私的内容我见过有人把爬虫写成高频并发去抓接口给平台带来不小的压力这种做法既不负责任也容易给自己惹麻烦。做工具的人应该对自己写出去的每一行代码可能产生的影响负责。6. 从“跑通”到“有用”数据可视化与下游应用到这一步工具已经能稳定地定时采集数据了。但裸数据躺在 SQLite 里如果不消费价值等于零。我自己的经验是把数据变成三类实际可用的产物日报、提醒、周报。6.1 生成每日热点快报每天早上抓取完成后跑一个统计脚本把昨天的榜单变化汇总成一份 Markdown 或 HTML 报告。比如哪些视频首次进入前 10哪些视频排名上升最快这些信息用几条 SQL 就能算出来-- 对比昨天和前天同一时间点找排名上升最快的视频 SELECT cur.title, cur.rank, pre.rank AS prev_rank FROM hot_rank cur JOIN hot_rank pre ON cur.video_id pre.video_id WHERE cur.snapshot_time datetime(now, -2 hours) AND pre.snapshot_time datetime(now, -1 day) AND pre.snapshot_time datetime(now, -22 hours) ORDER BY (pre.rank - cur.rank) DESC LIMIT 10;这是快照机制真正值钱的地方。没有历史快照你只能说“今天有这个视频”有了快照你能说“这个视频 24 小时内排名从 50 上升到了第 7”这对内容运营来说是完全不同级别的信息量。6.2 关键词命中提醒我在使用中发现单纯看榜单排名提升还不够更实用的是按关键词过滤。比如你负责美食类账号就可以设置一些关键词“探店”“食谱”“深夜食堂”每天抓完数据后自动扫描标题命中关键词的视频自动汇总并推送到你的消息里。这里的实现也很轻量keywords [探店, 食谱, 食堂] def filter_by_keywords(items: list[dict]) - list[dict]: result [] for item in items: if any(kw in item[title] for kw in keywords): result.append(item) return result推送渠道优先推荐飞书群机器人或钉钉群机器人Webhook 配置几十行代码就能搞定。如果不想折腾这些也可以直接发邮件。6.3 周维度趋势分析数据积累超过两周后可以做的事情就更多了。比如分析一周内热度最高的视频类型分布、分析不同视频创作者的霸榜时长、预测下一个可能成为热点的方向。这个阶段已经不算“爬虫”了更接近数据分析。我在实际使用中发现一个特别有意思的分析对比“当天霸榜视频”和“三天后仍在榜的视频”两类内容有着非常明显的特征差异。前者往往是话题性强的争议内容后者则是真正有干货或情绪价值的作品。这种洞察靠人工看榜单很难形成体感但数据会告诉你答案。6.4 可扩展的方向如果你觉得 SQLite 不够用了可以无缝迁移到 PostgreSQL如果想做更复杂的分析可以把数据定期导出到 ClickHouse 或 Elasticsearch如果想把工具产品化可以加一个简单的 Web 面板用 FastAPI ECharts 画排名趋势曲线。但这些都属于锦上添花。对于大多数场景现有这套基于 SQLite 的轻量方案已经能够以极低的成本支撑起一个内容团队对热门趋势的数据需求。我在实际跑这个工具的过程中最大的感受是技术难点并不在爬虫本身而是在于你有没有想清楚数据要用来干嘛。抓着数据存着不分析跟没抓没什么区别。建议你动手之前先给自己的数据规划一个明确的消费场景——哪怕是每周导出一次表格发给同事也算是有明确用途。