十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python爬虫实战:一键解析短视频分享链接,封装结构化提取接口

Python爬虫实战:一键解析短视频分享链接,封装结构化提取接口 做爬虫的朋友应该都有这个体会你刷到一条视频觉得里面那个文案戳中了痛点或者想用里面的素材做进一步分析但手机端复制下来的链接往往是一段又短又乱的分享口令直接打开浏览器请求又拿不到关键信息。这段时间我给自己定了一个小目标每天写一个Python爬虫练手案例专门研究这类视频链接的提取逻辑。今天这篇就是其中一个成果一个用Python做的“视频提取接口”输入小红书、抖音、视频号的分享链接输出标题、封面、作者、清晰度可用的播放地址、以及评论区里经常被提到的资源关键词。这篇文章适合正在学爬虫的Python学习者也适合想把自己零散脚本封装成接口的人。如果你遇到过复制链接、清洗跳转、解析页面内嵌JSON数据这一整套流程那你读完可以直接照搬。先说结论这类接口的核心不是“视频下载”而是“把一条杂乱分享链接变成结构化数据”。这个过程中最大的难点不是正则写不出来而是你根本不了解目标页面到底把数据放在哪里以及请求到的是HTML还是JSON。抓住这两点接口就完成了一大半。1. 视频提取接口到底解决什么问题1.1 从一条分享链接说起抖音里点“复制链接”得到的是类似https://v.douyin.com/xxxxx/的短链小红书点“复制链接”经常是一段带http://xhslink.com/的短链加几个emoji。视频号分享到微信里拿到的基本是https://support.weixin.qq.com/cgi-bin/mmsupport-bin/showpages?page...这种被包过一层的地址。这些链接的共同特点是短、带跳转、参数藏在302响应头里。普通用户看着无所谓但作为爬虫工程师你需要的是这条链接最终指向的真实页面地址以及页面里那一段类似window._ROUTER_DATA或__INITIAL_STATE__的内嵌数据。我最早的时候是手工打开浏览器复制页面里的播放地址一次两次还行量一多就废了。后来把需求拆成三层第一层接受任意外部分享链接第二层跟踪重定向拿到页面源码第三层提取页面里的结构化字段。这三层组合在一起本质上就是一个接口。你把链接发给它它返回JSON前端拿到JSON想怎么展示就怎么展示这比每次手动复制粘贴再拼接下载工具要高效得多。1.2 为什么核心方案是“接口”而不是“定时脚本”很多人写爬虫习惯写一个.py文件直接python xxx.py能跑通就完事。但这种模式不适合“每天持续维护”的案例。原因很简单页面结构会变平台的反爬策略会变如果你把解析逻辑写在脚本的main()里每次改动都要重新跑整个流程而且别人想用你的能力也没法接入。用接口的方式好处是把“采集”和“调用”解耦。采集端只负责取数调用端不需要关心你是正则还是XPath。我在设计时还要求这个接口支持批量一次请求可以传入多个链接后台自动去重、串行或并发抓取最后统一返回列表。这样不管是接入自动化测试、内容运营后台还是个人学习分析都很自然。你想想如果是一个脚本你每次都要打开编辑器改链接列表如果是一个接口你只需要requests.post(url, json{links: [...]})返回值就是干净利落的JSON这才是工程化的姿势。1.3 方案选型上的两个关键取舍第一用requests而不是用scrapy。scrapy很强但对于这种轻量提取接口它的下载中间件、Item Pipeline、Twisted 事件循环会把你拖入复杂度。requests配合Session自动管理 Cookie手动处理重定向和控制超时代码量少、可读性高。我的原则是能用标准库和轻量库解决的绝不上重量级框架。第二用Flask而不是用Django。原因更简单接口只有一个/extract路由不需要ORM、后台管理、模板渲染Flask几十行就能起服务。等以后接口多了再迁移到FastAPI也不迟因为核心的逻辑都在独立的工具模块里web框架只是壳。2. 接口设计拆解拿到链接后该怎么解析2.1 链接清洗与规范化外部传入的链接往往带一堆噪音尤其是小红书分享文案我在小红书发布了一篇笔记快来看吧 http://xhslink.com/xxxx 小红书。如果你不做清洗直接用requests.get(original_text)大概率请求失败。所以第一步永远是“从任意文本中抽出URL”。我写了一个extract_url(text)函数用正则匹配https?://[^\s。、]匹配完还要去掉尾部不需要的标点。这里有个细节链接末尾如果是/就不要动如果是?开头的参数要保留但如果是中文标点直接截断。实测下来这个规则能覆盖九成场景。import re def extract_url(text: str) - str | None: if not text: return None # 匹配常见URL排除尾部中文标点 match re.search(rhttps?://[^\s。、【】], text.strip()) if not match: return None url match.group(0) # 保留路径、查询参数字符 url re.sub(r[),.。!?;!]$, , url) return url清洗之后还需要处理“短链”。抖音和小红书的短链本质上都是302跳转直接请求短链通过response.url就能拿到最终地址。所以我在这里用requests.Session让它自动跟随重定向同时关闭verifyFalse避免部分链接的证书问题。之后一定要去除URL里的无用追踪参数比如加进页面URL里的视频通道ID和来源标识这些参数不会影响数据解析但会影响缓存命中率。我的缓存key只取去掉查询参数后的URL主体再拼接上页面结构版本号。2.2 页面数据的三种常见形态不同平台的页面源码结构差异很大但归纳起来只有三种HTML里嵌JSON、JSONP回调和直出JSON接口。小红书网页版将数据放在window.__INITIAL_STATE__里抖音新版本的分享页会在script标签里内嵌window._ROUTER_DATA视频号页面结构相对固定多个meta标签里已经有og:title和og:video信息无需动用重型解析。处理这三种形态我统一使用一个策略先拿全文然后按特征提取JSON块。比如遇到window._ROUTER_DATA 这种我用split(, 1)[1].strip().rstrip(;)切出后半段然后json.loads。如果loading失败再退化到正则找playAddr附近的key。这里我建议不要一上来就写针对某个平台的精确定位XPath因为XPath一旦页面改版就碎JSON里的key还有一定稳定性。当然key结构偶尔也会调整所以后面要加一层容错。def parse_json_from_script(html: str, marker: str) - dict: if marker not in html: return None start html.index(marker) len(marker) end html.find(/script, start) block html[start:end].strip() # 去掉结尾分号处理可能的尾逗号 block block.rstrip(;).strip() try: return json.loads(block) except json.JSONDecodeError: # 容错截到最后一个 } last_brace block.rfind(}) if last_brace 0: block block[:last_brace 1] try: return json.loads(block) except json.JSONDecodeError: return None2.3 字段设计返回什么比怎么抓更重要接口返回值我固定成统一结构。无论你请求哪个平台最终返回的JSON都包含code、message、data三个顶层字段。data里统一包含platform、title、author、cover、video_url、description、resource_ments。电商或运营同学最关注的resource_ments是一个列表我会把页面中出现的带有“提取码”“全集”“无水印”等关键词的文案一并返回。为什么这样设计因为如果每个平台都返回不同的字段名调用方就会很痛苦。字段统一之后前端的核心渲染逻辑只需要写一次。我还留了一个raw字段里面放解析后未清洗的原始页面数据这样排查问题时有据可查但不会直接暴露给前端。另外video_url并不是每个视频都能拿到永久地址很多地址带签名且过期时间短所以我返回时特意保留了expires_in字段告诉调用方这个链接大概多久会失效。3. 从零实现一个能跑起来的提取接口3.1 目录结构与依赖项目我放在daily_spider/下结构很简单daily_spider/ ├── app.py # Flask接口入口 ├── extractor.py # 链接清洗、解析核心逻辑 ├── platforms.py # 各平台的具体解析器 ├── cache.py # 简单的内存缓存 ├── requirements.txt依赖只写四个flask、requests、beautifulsoup4、lxml。注意BeautifulSoup不算必需品但用来处理meta标签很方便lxml是加速解析器遇到复杂HTML时比html.parser要稳。如果你不想装lxml也可以直接用标准库但速度会慢一些。3.2 编写解析器一个可扩展的注册机制我用一个字典保存平台名到解析函数的映射解析函数接收session和final_url返回统一格式的ExtractResult。为什么用字典而不是if-else因为每加一个平台只需要新增一个解析函数并注册进去不需要动主流程。这跟插件化设计一个道理后续维护成本低很多。class ExtractResult: def __init__(self, platform, title, author, cover, video_url, description, resource_mentsNone): self.platform platform self.title title self.author author self.cover cover self.video_url video_url self.description description self.resource_ments resource_ments or [] def to_dict(self): return { platform: self.platform, title: self.title, author: self.author, cover: self.cover, video_url: self.video_url, description: self.description, resource_ments: self.resource_ments, }解析器内部我倾向于先尝试从meta标签拿数据。快手和视频号这种平台在分享页的og:video里就会给出一个可直接播放的mp4地址这是成本最低的提取路径。万一og:video没有我再尝试找JSON块。顺序不要搞反因为meta标签提取一定比JSON解析稳定页面任何一层的class改版都不影响meta标签。def parse_video_meta(soup): video None og_video soup.find(meta, attrs{property: og:video}) if og_video and og_video.get(content): video og_video[content] return video3.3 主解析入口先清洗再请求再解析extractor.py里的核心函数是extract(text: str) - dict。它会经历四步提取文本中的URL用session.get(url, timeout10, allow_redirectsTrue)拿到最终HTML根据最终URL域名选择对应平台解析器解析失败时尝试通用meta解析再失败则返回错误码。这里有一个我踩过很多次的坑不要用response.text去取HTML要指定编码。抖音和小红书的页面都带charsetutf-8但偶尔部分页面会没有meta声明直接默认ISO-8859-1导致中文全乱码。我的做法是先读response.encoding如果为空或ISO-8859-1就根据response.apparent_encoding重新设置再去操作response.text。实测这个办法比盲改encodingutf-8更可靠。def get_html(session, url): resp session.get(url, timeout10, allow_redirectsTrue, verifyFalse) if resp.encoding is None or resp.encoding.lower() iso-8859-1: resp.encoding resp.apparent_encoding return resp.text, str(resp.url)PLATFORM_MAP { v.douyin.com: douyin, xhslink.com: xiaohongshu, support.weixin.qq.com: weixin, } def extract(session, text): url extract_url(text) if not url: return {code: 400, message: no valid url, data: None} html, final_url get_html(session, url) platform_name None for key, name in PLATFORM_MAP.items(): if key in final_url: platform_name name break if platform_name is None: platform_name generic parser get_parser(platform_name) try: result parser(session, final_url, html) return {code: 0, message: success, data: result.to_dict()} except Exception as exc: return {code: 500, message: f{platform_name} parse error: {exc}, data: None}3.4 接入Flask做成真正的接口接口层我放在app.py。只暴露一个POST /extract请求体是{text: 分享文案}或{links: [链接1, 链接2]}。单链接直接返回对象多链接返回数组。为了控制风险我加了一个limit最多一次解析20条防止有人拿接口疯狂刷。from flask import Flask, request, jsonify from extractor import extract import requests app Flask(__name__) session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36, Referer: https://www.douyin.com/, }) app.post(/extract) def handle_extract(): payload request.get_json(forceTrue, silentTrue) or {} text payload.get(text, ) links payload.get(links, []) if text: return jsonify(extract(session, text)) if links: results [] for link in links[:20]: results.append(extract(session, link)) return jsonify({code: 0, message: success, data: results}) return jsonify({code: 400, message: text or links required, data: None}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)别小看这个User-Agent。很多平台虽然不按UA严格封禁但如果请求头里带着Python默认的python-requests/2.x很容易被识别为爬虫轻则返回空页面重则短时间内封IP。我建议直接维护一个UA池随机轮换代码写起来也不复杂import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.4 Safari/605.1.15, ] def get_headers(): return {User-Agent: random.choice(USER_AGENTS), Accept-Language: zh-CN,zh;q0.9}3.5 缓存设计别让每一次请求都打到目标网站接口可以跑但如果不加缓存用一会儿就会被限流。我实现了一个极其简单的字典缓存key是final_url去掉查询参数后的值value是(timestamp, result_dict)。缓存有效期默认6小时存储量超过500条就清理一次最老的。这个逻辑简单够用不需要引入Redis。import time cache {} CACHE_TTL 6 * 60 * 60 CACHE_MAX_SIZE 500 def get_cache(key): item cache.get(key) if not item: return None timestamp, value item if time.time() - timestamp CACHE_TTL: cache.pop(key, None) return None return value def set_cache(key, value): if len(cache) CACHE_MAX_SIZE: oldest_key min(cache, keylambda k: cache[k][0]) cache.pop(oldest_key, None) cache[key] (time.time(), value)为什么缓存key要“去掉查询参数后的URL”因为你在抖音里看到的/video/123/?previous_pageapp_code_link和/video/123/很像实际内容完全一致。如果不做这个规整同一个视频会缓存出好几个key白白浪费空间也增加了触发目标站请求的次数。4. 常见问题与排查技巧实录4.1 明明浏览器能打开代码请求却拿不到真实页面这个现象我遇到不下十次。技术原因基本是两种一种是没有走重定向短链只返回了一层window.location.replace()的JS跳转requests不会执行JS所以最终停留在中间页另一种是页面在服务端做了browser_check发现UA不是浏览器直接返回验证码页。解决手段第一步把短链请求头里的User-Agent换成真实浏览器UA第二步如果短链响应是一段JS跳转则用正则提取location.replace(...)里的地址再请求一次。这里有个小技巧抖音的短链一般会先跳到www.iesdouyin.com/share/video/...再302到最终页面。所以循环跳转时我最多跟踪5层超过就放弃避免死循环。4.2 页面有了但JSON解析总是报错最容易翻车的不是找不到JSON而是JSON字符串里混入了/script或HTML实体。比如有些标题里含、在script标签内会被转义成\u003c或lt;直接json.loads有可能报未转义字符。我的处理办法先对script块做一次HTML unescape再用json.loads。如果还是失败输出块的最后200个字符辅助定位问题通常可以看到是不是被截断了或者多了个逗号。解决后这种场景其实也是我加容错的原因页面改版时JSON块被截断最后会少一个}靠rfind(})能捞回大部分数据。只要playAddr和title还在其他字段丢了也不影响核心功能。4.3 请求多了会被封IP吗怎么降风险会这个不用怀疑。我的经验是把“单次高频”改成“低频批量”。接口层面加sleep比如一次多链接解析时循环里每解析一条就停止random.uniform(1, 3)秒。这样十分钟才能跑完20条但至少不会触发风控。更安全的做法是把抓取任务丢到队列里比如用Celery但那是后面优化的事情了。如果已经被封最直接的表现是返回的页面里没有视频数据只有登录二维码和验证信息。这时候不要反复换IP硬刚建议冷却30分钟再试同时检查一下是否是代理IP池的出口统一被标记了。如果你只是个人学习用我强烈建议别上IP池那一套成本高、收益低还会把平台搞得很防备。4.4 常见问题速查表这里把我实际处理的几个高频问题整理成一张表方便你对照排查问题现象常见原因快速定位方法解决方案返回HTML没有title和video短链JS跳转搜索location.replace正则提取跳转地址再请求返回乱码中文变??页面编码识别错误r.apparent_encoding手动设置resp.encodingutf-8JSON解析报Expecting valuescript块被截断或转义打印末尾200字符rfind(})截断HTML unescapevideo_url为空但页面有视频字段名变化搜索mp4字符改为搜playAddr/og:video请求报403缺UA或Cookie浏览器请求对比Header补完整UA、Referer、Accept短链访问超时网络或平台拦截用curl -I测试换cookies、减少并发这张表我平时排查问题也直接用算是我自己调试爬虫时的一套“第一反应清单”。你会发现大部分问题的根因就两个请求头不完整、页面结构变了。把这两点养成条件反射处理速度会快很多。4.5 关于验证码和登录墙的底线有些视频页面必须登录才能播放尤其是视频号的部分内容。我的策略是不做自动过验证码也不做模拟登录接口对于这一类请求直接返回“登录才能查看”的错误提示。原因很简单爬虫学习不等于突破风控我从来不鼓励去绕过验证码这一点你在自己的项目里也一定要守住边界。接口能解决95%的公开数据需求就够了剩下的该放弃就放弃。5. 从“能跑”到“好用”我沉淀的几个扩展玩法5.1 引入AI一键提取让接口多一层内容理解最近有一个热词叫“AI一键提取小红书、抖音和视频号链接中文案和资源的接口”本质上就是在原有提取结果之上再对title和description做一次语义理解。你可以把解析出的文案喂给大模型接口让它自动抽出口播文案、产品卖点、资源关键词甚至生成摘要。这一层不需要自己训练模型直接用大模型API就行。我自己在实验时是这么玩的解析出description之后组装一个prompt“从下面文案中提取核心观点、可复用的金句、商品或资源关键词输出JSON。”然后把大模型返回结果合并进接口的data里。这么做的价值在于单纯video_url是给播放器用的而后面的结构化工整内容才是给人或业务系统用的。当然接入大模型会增加响应延迟所以我的策略是大模型分析结果缓存24小时第一次请求慢就慢后面都走缓存。5.2 用定时任务做到“每日spider”既然说“py每日spider案例”建议你也把这个接口跑成一个每日任务。比如每天早上9点用APScheduler定时抓取一批视频链接解析后写入本地SQLite或输出成Markdown日报。别小看这个日报连续跑一个月你就能看出哪些平台的结构变化最频繁哪些链接最容易过期。这类经验光看文档是积累不出来的。5.3 合规提醒做接口前先想清楚边界无论你是做个人学习还是公司内部工具都要遵守目标平台的用户协议和Robots协议。合理控制频率、不采集非公开隐私数据、不绕过登录和付费墙这条底线必须清晰。我把“Robots协议”四个字写进注释里每次跑脚本都提醒自己。接口本身是个中性的东西它能做成高效的素材管理工具也可能变成骚扰工具。我们写代码的人至少应该对用户协议和平台服务条款有一个最基本的尊重。最后再分享一个实用小技巧接口上线后你会发现日志特别重要。我给extractor加了一个极简日志函数每次解析成功或失败都记录平台名、最终URL、耗时、错误摘要。日志存到文件里一天一个。以后页面改版导致解析失败你翻日志就能定位到是哪个平台、哪个链接格式变了。这个习惯帮我省了无数排查时间强烈推荐你也试试。如果你想继续扩展下一步可以把这个接口从Flask迁移到FastAPI顺便加上异步、限流和Redis缓存体验一下工程爬虫和脚本爬虫之间的区别。
返回列表