异步抓取与大模型语义解析:智能视频链接提取工具实践

发布时间:2026/10/21 13:49:48
异步抓取与大模型语义解析:智能视频链接提取工具实践 不卖关子直接说结论基于 Python 的异步抓取框架配合通用大模型做语义解析确实能从大量视频聚合页、导航站、内容索引页里稳定地把有效视频链接抽出来。我花了一周多时间做了这个“智能视频链接抓取工具”核心目标就一句话用尽量少的机器资源在尽量短的时间内从一堆结构乱七八糟的页面里准确捞出用户真正想要的视频播放地址。项目背景是一个内部需求需要周期性监控一批视频资源索引站点把新增的视频链接汇总成结构化清单。页面数量大概有几万个页面结构从极度规范的列表页到深埋多层 iframe 的嵌套页都有如果还用老办法“同步请求 正则硬匹配”去跑既慢又脆。我做完这轮改造后单机抓取效率提升了一个数量级解析成功率也稳定了不少。这篇文章就把整个项目的拆解思路、技术选型、核心代码、常见坑全部写出来给正在做类似爬虫项目的朋友一个参考。1. 项目拆解先想清楚要抓什么、怎么抓1.1 需求本质与边界这个工具的本质不是“爬到视频文件”而是“从 HTML 页面里精确识别并提取视频播放链接”。这两者有本质区别视频文件通常是 .mp4、.m3u8 这类资源地址而播放链接可能是页面内嵌播放器的地址、跳转中间页、经过 JS 动态拼接的地址甚至是一段需要二次请求才能解出来的真实地址。所以在动手之前我先把边界划清楚了只做公开页面和内容规范允许访问的站点不涉及登录态、付费内容、加密协议。目标是提取“视频播放地址”和“视频标题”这两个核心字段至于页面里其他杂七杂八的广告位、推荐文章一律不关心。对抓取频率做严格限制。工具即便跑得再快也不能给对方服务器造成压力更不能触发访问限制影响后续使用。这个边界决定了后续的技术选型。很多人一上来就追求“高并发”“异步”“多线程”这些听起来很酷的能力但实际生产环境里能不能稳定跑完一轮采集比单轮能有多快重要得多。我在项目里反复提醒自己抓取工具的第一原则是持续可用第二原则才是速度。1.2 为什么不能靠简单正则打天下最开始接手这个需求时我的第一反应确实是用正则去匹配。示例页面的结构看起来挺规律像这样的链接反复出现a hrefhttps://example.com/play/12345 classvideo-link第03集/a写了几个正则之后前一百个页面确实没问题。但页面一旦超过一千个问题就开始暴露了有的站点把链接放在 iframe 里有的链接是相对路径有的是用 JavaScript 在点击时才拼接出完整地址有的标题和链接根本不在同一个 DOM 层级里。正则只能处理“看起来规律”的情况一旦页面结构变异就得反复打补丁。还有一个更隐蔽的问题视频链接的“语义”并不总是由 URL 结构决定的。同一个站点可能同时存在“视频详情页”“视频分类页”“用户主页”它们的 URL 格式长得非常像这时候正则无法判断哪个才是真正需要的播放地址。这也是我后来决定引入 AI 解析的根本原因——我需要的是“理解页面内容”而不是“匹配页面字符串”。1.3 技术路线整体选择最终敲定的方案是异步网络请求 轻量级页面预处理 大模型语义解析 结构化输出。具体来说异步抓取用 asyncio aiohttp解决并发效率问题。页面预处理用 BeautifulSoup 做 HTML 转文本、节点裁剪把大模型需要看的内容压缩到最小范围。AI 解析调用一个本地部署的通用大模型接口让它从预处理后的文本中提取视频标题、播放链接、所属分类等结构化字段。数据处理用 JSONL 逐行落盘配合去重和增量更新机制避免重复抓取。这个组合看起来“重”但实际跑下来单页面平均解析成本很低因为发给大模型的内容已经经过裁剪不再是整段 HTML 原文。异步并发让网络等待时间被充分重叠整体吞吐量非常可观。2. 技术选型异步与 AI 解析的组合逻辑2.1 异步抓取从 requests 迁移到 aiohttp 的收益[我是如何用][/我/是/如何/用:] 这段文字被截断了。为完整起见我们来写清这个子部分。2.1 异步抓取从 requests 迁移到 aiohttp 的收益如果你写过同步爬虫一定熟悉这种感受requests.get()发出去之后程序就老老实实地等服务器返回这个等待过程短则几百毫秒长则几十秒。如果是逐个页面按顺序请求一万个页面可能要跑一下午。虽然可以用线程池来做并发但线程本身有开销而且频繁切换上下文并不适合 IO 密集型任务。异步编程的思路完全不同。它把“发出请求”和“等待响应”拆开在等待响应的空隙去处理其他请求让网络 IO 的等待时间被大量重叠。用 asyncio aiohttp 重写后我可以在一个线程里维护几百个并发请求CPU 占用和内存占用都很低跑起来比线程池更轻。简单对比一下两种方式方案并发能力资源消耗代码复杂度适合场景requests 串行极低极低最低几十个页面一次性采集requests 线程池中较高中几百到上千页面结构稳定asyncio aiohttp高低稍高大规模页面IO 密集型任务Playwright 异步并发中极高高需要渲染 JS 的动态页面实际项目中我并不是完全抛弃 requests而是在“批量抓取阶段”全部使用 aiohttp遇到需要浏览器渲染的页面再用 Playwright 做补充。后面第 5 节会细说动态页面的处理。2.2 AI 解析让大模型处理“非结构化 HTML”写规则解析最大的痛点是什么是“规则永远追不上变化”。你针对一个站点的结构写好了解析逻辑过两天对方前端工程师把 class 名一改你的解析器就废了。而视频索引类站点的页面结构非常不统一有的连标题和链接的位置关系都毫无规律写规则的成本高到难以维护。AI 解析改变了这个游戏规则。我不再关心链接具体出现在哪个 class 里而是把页面转成一段精炼文本让模型“阅读”这段文本然后回答几个固定问题这个页面里有哪些视频链接每个链接对应的视频标题是什么链接属于播放页还是列表页模型只要理解了“播放地址”这个概念就能从各种变体写法中把它识别出来。我在实际测试中发现一个参数量不大的开源模型经过简单提示词约束就能达到接近九成的解析准确率。这个准确率对于数据清洗前置环节来说完全够用因为后面还有去重和人工抽检兜底。更重要的是面对新站点时我只需要在提示词里加一句“这类站点通常把播放地址放在 iframe 的 src 属性中”模型就能举一反三不需要我再写一整套解析逻辑。2.3 合规与速率控制的必要设计这部分容易被忽视但我把它放在技术选型同等重要的位置。爬虫不是“能不能爬到”的问题而是“允许不允许爬”的问题。我在项目里做了三层保障第一层是遵守 robots 协议。启动时先读取目标站点的 robots.txt把不允许抓取的路径加入黑名单。这是我写所有爬虫的默认动作不讨论、不妥协。第二层是限速和退避。使用信号量限制并发数同时设置请求间隔的最小值。一旦遇到 429 或 503 响应立即降低全局并发根据响应头里的 Retry-After 字段等待后再继续。第三层是边界控制。不处理需要登录才能看到的页面不解密任何经过混淆的播放地址不绕过任何访问限制机制。工具的运行边界始终停留在“公开页面的信息提取”这个范围内。这些设计看似牺牲了一部分速度但它保证工具能长期稳定运行也符合我一直坚持的从业底线。3. 整体架构与数据处理流程3.1 模块划分与数据流转整个项目由四个模块组成调度器、抓取器、解析器、清洗器。它们之间通过 asyncio 队列传递数据模块之间互不感知实现细节替换成本很低。数据流向是这样的调度器维护一个种子 URL 队列抓取器从队列中取出 URL并发下载页面 HTML拿到 HTML 后预处理模块先把页面转成结构化文本文本进入解析器由大模型提取视频链接和标题最后清洗器做去重、格式校验、字段补全输出 JSONL 文件。我特意把“抓取”和“解析”拆成两个独立阶段而不是边抓边解析。原因很简单抓取阶段是 IO 密集型解析阶段是 CPU 密集型两者混在一起会导致事件循环被阻塞。如果在大模型解析时卡住后面的请求全部排队等待整体吞吐量断崖式下降。3.2 种子管理与去重种子 URL 管理看起来简单实际踩坑不少。最开始我直接用 Python 的 set 做去重几万条 URL 下去内存没问题但持久性很差。程序一旦中途退出已完成部分就全丢了重启之后从头再来。后来改成基于 SQLite 的持久化去重表。URL 入库时先查重新的才进入待抓取队列。这样即使程序崩溃重启也能从断点继续跑。对每个 URL 还记录了抓取状态和抓取时间方便后续做增量更新。还有一个容易被忽略的设计URL 归一化。同一个页面可能有多个 URL 变体比如带 utm 参数和不带参数、http 和 https、末尾带斜杠和不带斜杠。我在入库前统一用小写域名 去除空参数 补全协议头的方式做归一化显著降低重复抓取率。3.3 数据落盘格式数据落盘我选用 JSONL 而不是 CSV。因为每条记录的字段数可能是动态的今天可能只提取标题和链接明天可能增加分类、封面图、发布时间等字段JSONL 对 schema 变化的容忍度比 CSV 高很多。每条记录包含url原始页面地址title视频标题video_url视频播放地址source来源站点crawled_at抓取时间model解析所用模型标识落盘操作使用异步文件写入避免 IO 阻塞事件循环。每批次写入后即时刷盘确保数据不丢失。后续如果要导入数据库或做进一步分析JSONL 也能很方便地逐行读取。4. 核心代码实现与关键细节4.1 异步抓取模块实现异步抓取是整个工具的发动机。下面这段代码是抓取器的核心骨架包含了信号量限速、超时控制、重试机制和响应校验。import asyncio import aiohttp from aiohttp import ClientTimeout async def fetch_one(session, semaphore, url, retry3): async with semaphore: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } for attempt in range(retry): try: async with session.get(url, headersheaders, timeoutClientTimeout(total15)) as resp: if resp.status 200: return await resp.text() if resp.status in (429, 503): retry_after resp.headers.get(Retry-After, 5) await asyncio.sleep(float(retry_after)) continue resp.raise_for_status() except (aiohttp.ClientError, asyncio.TimeoutError) as exc: if attempt retry - 1: raise await asyncio.sleep(2 ** attempt) return None async def crawl_many(urls, max_concurrency16): semaphore asyncio.Semaphore(max_concurrency) async with aiohttp.ClientSession() as session: tasks [fetch_one(session, semaphore, url) for url in urls] results await asyncio.gather(*tasks, return_exceptionsTrue) return results核心细节有三个。第一信号量是并发控制的关键max_concurrency16表示当前最多同时发出 16 个请求实测大部分站点都能接受这个值。第二重试指数退避用的是2 ** attempt第一次失败等 1 秒第二次等 2 秒第三次等 4 秒避免连续快速重试导致被限制。第三429 和 503 不是普通错误要优先读取Retry-After响应头对方明确告诉你等多久就精确等待这是最保险的做法。4.2 AI 解析模块与提示词设计AI 解析是核心中的核心。我选择调用本地部署的开源模型走的是 OpenAI 兼容接口。这样后续如果要换模型只需要改一下基础 URL不需要动业务逻辑。解析器的核心思路分三步用 BeautifulSoup 去掉 script、style、nav、footer 等无用节点。提取页面里所有链接和它们附近的可读文本拼成一段紧凑文本。把这段文本发给大模型要求它输出结构化 JSON。第三步的提示词我反复调了很多轮最终固定为“角色定义 任务目标 输出格式 负例约束”四段式结构。下面是一段实际可用的提示词你是一个页面信息提取助手。请从给定的页面文本中提取所有视频播放链接。 页面文本 {page_text} 要求 1. 找出所有属于视频播放页或视频文件地址的链接。 2. 排除分类页、首页、用户主页、广告链接、评论区链接。 输出 JSON 格式 {items: [{title: 视频标题, video_url: 播放地址}]} 注意 - 如果没有找到任何视频链接返回 {items: []} - 不要输出 JSON 以外的任何内容。这里有一个非常重要的经验让模型输出纯 JSON不要让它输出解释性文字。一开始我的提示词允许“如果存在不确定的情况请说明原因”结果模型真的会输出一大段理由导致 JSON 解析失败。后来改成“不要输出 JSON 以外的任何内容”配合 JSON Mode 开关解析成功率大幅上升。发送给模型之前我还会对页面文本做长度截断。多数视觉类模型有上下文限制超长文本会导致生成结果不稳定。实测把页面压缩到 3000 字以内模型输出质量最高。4.3 并发与重试参数调优并发数是整个工具最敏感的参数我花了好几个晚上做测试。目标是找到“单位时间内抓取最多页面但触发限制概率最低”的平衡点。并发数单轮 1000 页耗时触发限制次数数据质量4约 12 分钟0无异常16约 3 分钟1少量页面丢失靠重试补齐32约 1.5 分钟多频繁 429需要大量重试最终我选择了 16 作为默认并发值。它比 4 快四倍比 32 稳定得多。如果你在自己的项目里用我建议从 8 开始测观察一段时间再逐步往上加。并发不是越大越好大到你被对方限制之后实际速度反而更慢。重试参数也做了配置化放在项目配置文件里request: max_concurrency: 16 timeout_seconds: 15 max_retries: 3 retry_backoff_base: 2做配置化而不是写死在代码里的原因是不同站点的容忍度不一样。有的站点限速很紧并发到不了 16有的站点响应很慢15 秒超时不够用。参数配置化以后为不同站点维护不同的采集配置整体稳定性提升非常明显。5. 实际运行中的常见问题与排查5.1 高频问题与排查对照表项目跑了一周我把遇到的高频问题整理成一张速查表排查的时候效率提高了很多。现象可能原因排查方法解决方案响应速度突然变慢并发过高触发限速检查响应状态码是否大量 429降低并发增加退避时间大批量超时目标服务器响应慢查看耗时分布提高超时上限分流到多个调度批次页面内容为空被跳转到验证页保存原始 HTML 抽查降低频率更换 User-Agent解析结果字段缺失提示词约束不足查看模型原始输出调整提示词加入负例JSON 解析失败模型输出多余字符检查模型输出前 50 字符开启 JSON Mode严格约束输出格式这套排查方法本身的思路比我列的每一条具体问题更重要先保存原始响应再做二次复现。我项目里所有抓取结果都会同时保留一份原始 HTML虽然占磁盘空间但一旦出问题可以直接回放不用重新抓一遍。5.2 动态页面拿不到真实播放地址这类问题在水最深的地方。有些页面乍一看有链接但用 aiohttp 直接抓回来以后HTML 里只有一个播放器容器真正的地址是浏览器执行 JavaScript 之后才拼出来的。这种页面就需要引入无头浏览器。我采用的是分层策略先用 aiohttp 快速抓一遍如果页面里检测不到任何视频相关链接再用 Playwright 做第二遍渲染抓取。Playwright 的渲染过程会执行所有页面脚本等networkidle事件后再获取最终的 HTML。import asyncio from playwright.async_api import async_playwright async def fetch_with_rendering(url): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page() await page.goto(url, wait_untilnetworkidle, timeout30000) html await page.content() await browser.close() return html需要注意的是Playwright 的并发开销远大于 aiohttp。一个浏览器实例就占几百 MB 内存所以我把需要渲染的 URL 单独放进一个队列在渲染阶段将并发控制得很低一般控制在 4 个左右。同时加上页面超时销毁机制避免浏览器实例因为长时间运行导致内存泄漏。5.3 大模型解析不稳的兜底方案即便提示词写得再好大模型也不是 100% 稳定。我在实际运行中遇到最典型的一种情况是模型的输出格式突然变化从标准 JSON 变成了一段自然语言描述。这种情况下我给解析模块加了一道“格式校验层”。校验逻辑很简单先解析模型输出如果不合法尝试提取字符串中{}包裹的最大子串再解析一次。如果还是失败把这条记录存入待人工复核队列同时把原始页面文本一起保存。对解析成功的结果再判断 video_url 是否为合法 HTTP 链接是否包含播放地址特征。这道兜底逻辑虽然不复杂但在整个项目里非常重要。它让单条解析失败不会影响全局流程同时保证了最终交付数据的最低质量线。实际跑下来最终输出的数据里无效链接比例控制在 1% 以内。6. 最后再说两个值得分享的细节6.1 我踩过最值得说的一次坑项目上线第一天我设置了一个自认为很保守的并发值8。以为这样肯定没问题。结果跑了不到十分钟整个网段全部被封。排查下来发现原因不是并发太高而是我的爬虫没有做请求频率控制。并发数是 8但它可能同一个时间片内对同一个域名发出了大量请求等于瞬时频率是 8 倍。这个问题的解决方案是在信号量之外再加一层“每域名最小间隔时间控制”。我用一个字典记录每个域名最近一次请求时间下一次请求之前先检查是否满足间隔要求如果没到间隔时间就挂起等待。这个教训让我明白了一个道理限速的粒度不能只看总并发还要看单位时间内的请求密度。特别是跑多个目标站点时要按域名分别管理限速而不是一个全局并发值搞定一切。6.2 还能怎么扩展当前这个版本的定位是“提取视频链接”但它整个架构和模型完全可以扩展到更广的场景。比如把提示词换成“提取页面的主要观点”它就能变成一个通用的内容抽取器把抓取对象从页面 URL 换成 API 接口再配合异步抓取就变成了一个轻量级数据采集平台。如果后续需要面对上百万量级的页面我会把 SQLite 换成真正的消息队列把单机任务改成多机分布式任务。现阶段这个工具在单机上已经能覆盖绝大多数场景关键是把异步并发、限速控制、AI 解析这三个核心环节配合到位。我个人在实际操作中体会最深的一点是AI 解析看起来是“黑科技”但它解决不了抓取环节的基础问题。如果页面下载不下来、下载下来内容不完整再强的模型也白搭。先把抓取链路做稳再叠加智能解析才能得到真正可用的工具。这套组合拳打下来以后不论遇到什么“结构千变万化”的页面类型我心里都有底了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询