每天写一个小爬虫案例是我保持了很长一段时间的练习习惯。今天整理的这个“随机视频接口获取”项目是我在一次日常练习中沉淀下来的典型实战样本。从表面看它做的事情很简单——从公开的视频数据接口里随机取一条视频信息但真正把它做稳定、做通用背后涉及的请求构造、响应解析、超时处理、数据校验、日志记录是一条非常完整的链路。对于想从“会写Python语法”过渡到“能写真正能跑起来的爬虫脚本”的朋友来说这个案例刚好卡在那个关键的进阶节点上。也有很多朋友问我为什么愿意花时间做这种“看起来没什么用”的小项目。我的看法是恰恰是这种小项目才能把那些大项目里被业务复杂度掩盖掉的底层细节暴露出来。你写一个视频接口调用脚本你会被迫去思考User-Agent怎么设置、接口返回的字段为什么有的带null、频率限制到底怎么规避、上游接口突然改版之后你的解析代码能不能在十分钟内适配。这些经验是任何教程都替代不了的。1. 项目目标与核心需求拆解1.1 这个案例到底要解决什么问题先说这个案例的原始需求。当时我给自己定的练习题目是写一个Python脚本从某个公开的视频数据源中获取一条随机视频记录字段至少包括视频标题、封面图地址、播放链接、时长和分类标签并且把数据以标准JSON格式落盘保存。这个需求拆开来看其实包含几个隐含的技术点接口的URL是什么参数如何构造才能拿到“随机”的结果返回值是JSON还是XML字段结构长什么样如果接口频繁调用导致限流脚本该如何重试和退避如果上游返回的数据字段缺失或为空脚本是否健壮地跳过而不是崩溃数据保存之后下一次运行是覆盖还是追加很多人一上来就急着写代码结果拿到一个接口就开始撸。我的建议是反过来先花半小时把上述问题一个一个确认清楚再动手写。尤其是“随机”这个需求不同的接口对随机的实现方式完全不同有的接口本身提供random参数有的需要你从全量列表里自行抽样。这个差异直接决定了你的代码架构。1.2 适用场景与边界意识这个脚本的适用场景其实比想象中广。比如你在做一个内容推荐算法的Demo需要一批随机视频作为测试数据比如你在做前端开发需要一个动态变化的视频数据源来验证页面渲染效果又比如你在做数据分析教学需要演示如何处理不规整的JSON数据。在这些场景里这个脚本都能充当“数据供给端”的角色。但这里必须说清楚边界。爬虫也好、接口调用也好有一条底线不能碰只能访问公开的、允许访问的数据源并且必须遵守数据源所在平台的条款约束和robots协议。我在整个案例中使用的全部是模拟数据源和公开演示接口没有任何绕过鉴权、越权抓取、批量下载的行为。这个意识做爬虫的人第一天就要有而且要刻进骨子里。2. 技术方案选型与设计思路2.1 为什么选Python requests 而不是 Selenium做接口类爬虫很多人会纠结技术选型。我见过有人用Selenium去调接口的说实话这是典型的杀鸡用牛刀。Selenium适合的是需要模拟用户真实操作、页面动态渲染严重、接口数据隐藏在复杂JS逻辑里的场景。而纯接口获取这种需求数据源已经明确暴露了HTTP接口直接用requests库发GET请求就能拿到结构化数据完全没必要启动一个浏览器实例。requests库的优势在于轻量、稳定、生态成熟。它处理HTTP连接、响应编码、会话保持这些东西已经被社区打磨得非常可靠你不需要关心底层的socket连接怎么维护只需要关心业务逻辑。配合Python标准库里的json模块解析返回数据也就是几行代码的事。2.2 数据源选择的一个原则我挑选练习数据源的时候有一个原则选那种返回结构规范、字段清晰、访问稳定的公开接口。这样你的注意力可以集中在“怎么把一个脚本写健壮”上而不是花大量时间去适配一个格式古怪的返回体。举个例子我设计这个练习时目标接口的返回结构是这样设计的返回外层是一个对象包含一个status字段和一个data字段。data字段是一个列表列表里每个元素是一条视频记录。视频记录包含vid、title、cover_url、play_url、duration、tag六个字段。这样一个标准的返回结构恰好能覆盖JSON解析的常见场景嵌套对象、数组遍历、字段缺失兜底、类型强制转换。实际开发中你拿到的接口结构可能比这个复杂得多。但核心逻辑是一样的先搞清楚数据结构再写解析代码最后做异常兜底。2.3 随机性方案的选择“随机获取”这个需求有两种典型实现路径。第一种接口本身就支持随机参数比如在URL里加一个randomtrue由服务端返回一条随机记录。这种方式最简单客户端代码少但依赖服务端实现。第二种接口只提供全量或分页列表由客户端自行实现随机抽样。方式是先获取列表长度或者分页总数然后随机生成一个页码或者索引再获取对应位置的数据。我在案例里采用的是第二种思路的变体先请求一次接口获取视频ID列表然后通过random模块从ID列表里随机选取一定数量再通过详情接口获取这些ID对应的完整视频信息。这样做的好处是即使接口不直接支持随机参数你依然能够实现随机获取的效果而且代码对数据源的适配性更强。随机抽样看起来简单其实有个容易忽略的细节要不要去重如果一轮抽样里取出重复的视频那就失去“随机多样性”的意义了。我在代码里专门加了一个去重逻辑用集合来维护已经抽过的ID确保同一轮次内不重复。3. 核心代码实现与逐步解析3.1 请求模块的封装我先说一个习惯任何爬虫脚本我第一件事不是写业务逻辑而是先把请求模块封装好。这个模块负责统一的请求头、超时设置、重试机制和请求日志。把这一层做好后面所有业务代码都可以复用。import requests import time import logging class VideoAPIClient: def __init__(self, base_url, timeout10, max_retries3): self.base_url base_url self.timeout timeout self.max_retries max_retries self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, Accept-Language: zh-CN,zh;q0.9, }) def _get(self, path, paramsNone): url self.base_url path for attempt in range(1, self.max_retries 1): try: resp self.session.get(url, paramsparams, timeoutself.timeout) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: logging.warning(请求超时第 %s 次重试, attempt) time.sleep(attempt * 2) except requests.exceptions.HTTPError as e: logging.warning(HTTP错误%s第 %s 次重试, e, attempt) time.sleep(attempt * 2) except requests.exceptions.RequestException as e: logging.error(请求异常%s, e) time.sleep(attempt * 2) return None这段封装里有几个值得说的细节。第一使用 Session 对象来维持连接多次请求时不重复握手性能更好。第二超时时间设为10秒而不是用默认的无限等待避免线程卡死。第三重试次数和退避策略用了指数递增第一次失败等2秒第二次等4秒防止对服务端造成压力。第四把日志直接打进标准logging模块后续方便排查问题。3.2 数据清洗与字段校验接口返回的数据永远不会像文档里写的那么干净。这是我做爬虫以来最深刻的体会之一。字段突然变null、空字符串、数字类型变成字符串类型、时长单位分钟写成秒这些问题几乎每天都在发生。所以数据清洗这一步不能省。我在代码里写了一个专门的校验函数对每条视频记录做字段级检查def clean_video_record(raw): required_fields [vid, title, play_url] for field in required_fields: if not raw.get(field): logging.warning(记录缺少字段 %s已跳过, field) return None try: duration int(raw.get(duration, 0)) except (ValueError, TypeError): duration 0 return { vid: str(raw[vid]), title: str(raw[title]).strip(), cover_url: raw.get(cover_url, ), play_url: raw.get(play_url, ), duration: duration, tag: raw.get(tag, unknown), fetched_at: time.strftime(%Y-%m-%d %H:%M:%S) }这个函数做的事情过滤掉核心字段缺失的脏数据把duration强转成int类型给所有字段提供默认值再补充一个抓取时间戳。可能有人觉得这是小题大做但数据清洗直接决定了下游的数据质量。你拿一条脏数据去做分析或展示结果错了要花十倍时间排查。3.3 主流程获取ID列表、随机抽样、获取详情主流程我把它拆成三步先获取候选ID列表再随机抽样最后逐条获取详情。这个设计的核心原因是ID列表接口和详情接口通常是分开的ID列表数据量小、响应快适合一次性拉取详情则重量级只对选中的ID请求避免浪费资源。def fetch_random_videos(sample_size5): client VideoAPIClient(https://api.example.com) # 第一步获取候选视频ID列表 id_list client._get(/videos/ids, params{limit: 100}) if not id_list or id_list.get(status) ! ok: logging.error(获取ID列表失败) return [] all_ids id_list.get(data, []) if len(all_ids) sample_size: logging.warning(候选ID不足返回全部) sample_size len(all_ids) # 第二步随机抽样保证不重复 selected_ids random.sample(all_ids, sample_size) # 第三步逐个获取完整视频信息 results [] for vid in selected_ids: detail client._get(/video/detail, params{vid: vid}) if detail and detail.get(status) ok: record clean_video_record(detail.get(data, {})) if record: results.append(record) time.sleep(0.5) # 控制频率避免对服务端造成压力 return resultsrandom.sample 是一个容易被忽略但非常好用的函数它直接从列表中随机取不重复的指定数量的元素底层已经帮你去重了比自己写random.choice循环要可靠得多。加上sleep控制请求频率后整个流程无论从性能还是礼貌性来说都是及格的。3.4 数据落盘与日志记录数据拿到之后不能只在内存里转一圈就完事。我这里的落盘方案是JSON Lines格式每行一条记录方便后续逐行读取和追加。import json import os def save_to_file(records, filepathvideo_data.jsonl): with open(filepath, a, encodingutf-8) as f: for record in records: f.write(json.dumps(record, ensure_asciiFalse) \n) logging.info(已保存 %s 条记录到 %s, len(records), filepath)选择JSON Lines而不是单一大JSON数组的原因追加场景下JSON Lines不需要读入整个文件再重写一行就是一条独立数据数据如果中途写入失败也不会像大数组那样破坏整个文件结构。这个格式在做日志型数据落盘时非常实用。4. 常见问题与排查技巧实录4.1 接口突然返回403做接口爬虫遇到403可以说是家常便饭。我在这类练习中遇到的403绝大多数原因是请求头不完整尤其是缺少User-Agent或者Referer。很多数据源会在网关层做简单的UA校验直接拒绝那些看起来像程序默认UA的请求。排查思路先用curl带一模一样的信息去请求如果curl成功而Python失败那就说明问题出在请求头。把浏览器开发者工具里的完整请求头copy过来逐项比对重点看UA、Accept、Referer这三个字段。4.2 明明有数据但parse出来全为空这个问题的根源往往是分析的不是真正的接口而是一个需要特定Header才能获取完整内容的接口。数据传输过程中如果返回体被压缩过比如Content-Encoding: gziprequests库通常会自动解压但有些返回结构会用deflate这时就要手动处理。还有一种情况返回的JSON字符串里存在不可见的非法字符。排查方法是先打印resp.text的前200个字符看看是不是形如{status:ok}的干净JSON如果前面有诡异的字符大概率是BOM头或者前导空白需要在json.loads之前做strip处理。4.3 频率限制怎么应对站在对方服务端角度想限制频率是完全合理的。我在脚本里固定加了一个0.5秒的请求间隔。有人会觉得慢但真正做起数据任务稳定性远比单次速度重要。你猛打一次能拿到100条被限流之后可能要等半小时哪个划算一目了然。如果确实需要加快最优雅的方式是加指数退避的重试机制配合缓存策略。能复用数据的地方就不请求第二次这是对上游也是对你自己的尊重。4.4 上游接口改版后的快速适配接口改版是爬虫从业者必须接受的事实。我经历过的改版种类包括字段名从下划线风格改成驼峰、分页参数从page改成offset、返回结构从数组改成对象。应对改版的关键思路把解析逻辑和请求逻辑解耦。我在代码里把请求、解析、落盘分成了三个模块。上游改版时只需要修改解析层的clean_video_record函数或者在请求层调整一个参数名完整流程不用动。这个架构习惯在长期维护的爬虫项目中带来的收益是巨大的。5. 一些围绕边界与效率的额外思考5.1 什么时候该放弃自己写接口获取有些人误以为爬虫是万能的什么站都能爬。但现实是很多数据源的接口受复杂签名保护、登录态限制、甚至风控模型动态识别。在这种场景下硬写爬虫是典型的投入产出倒挂。我的建议是优先查官方API文档看有没有开放的数据服务其次再看有没有公开的数据集最后才是自己写爬虫去获取公开页面数据。合规获取数据的手段永远优先级最高。这次的视频接口案例本质上是我在安全边界内做的一次技术练习。它练的不是“怎么绕过限制”而是“如何把一条正规的数据获取链路做到稳定可靠”。哪怕是纯练习代码里的礼貌性请求间隔、重试退避、字段校验都是我日常写生产级爬虫时也在使用的套路。5.2 这个练习还能怎么延伸如果你跑通了上面的代码我建议你接着做几个方向的延伸第一把脚本改造成支持命令行参数的模式通过argparse接收要获取的随机视频数量第二把落盘格式从JSON Lines改成写入SQLite数据库练习一下结构化存储第三加一个定时调度让脚本每天固定时间自动运行一次你会发现“稳定运行”和“手动运行一次成功”之间还有不小的距离。这些延伸练习的价值不在于代码量有多少而在于你经历了多少“看起来应该成功但实际失败了”的时刻。爬虫这个方向没有什么秘密捷径见过足够多的问题处理过足够多的脏数据经验自然就出来了。