☰
网盘链接解析与直链下载:从会话校验到分片断点续传
2026/11/4 3:44:58 网站建设 项目流程

上个月帮一个做素材归档的朋友收拾烂摊子:合作方陆陆续续发来四十多条网盘分享链接,每条后面跟一串四位提取码,全散落在聊天记录里。他想把这些文件按项目分门别类存到本地,结果光是复制链接、填提取码、点下载、改文件名,一个下午就耗进去了,中间还有几个大文件下到一半断掉,只能从头再来一遍。他问我,有没有办法把这些分享链接直接变成能丢给下载器的直链地址。

这就是网盘链接解析这件事最朴素的动机——不是要绕开什么,纯粹是把“人工点几十次”换成“机器跑一遍”。所谓直链,指的是跳过网页那一层壳、直接指向文件实体的临时 URL,它带签名、带有效期,拿到手就能交给任何支持 HTTP 的工具去拉。而“在线解析”是把这套流程包成网页,贴链接进去、吐地址出来。

我这次把这个自用工具从零重做了一遍,顺手把踩过的坑都记了下来。下面写的都是解析链路的原理、抓包时该看什么、脚本怎么写、哪些边界不能碰,适合会一点 Python、想把自己手头的文件归档流程自动化的朋友看。完全没有编程基础也能看懂前两节,后面几节建议对着代码一起读。

1. 分享链接到直链之间到底隔着几道门

很多人对这件事的第一印象是“网页上有个下载按钮,我找到那个按钮背后的地址就行了”。真动手才发现,你在浏览器开发者工具里复制出来的那个链接,粘到别的工具里十有八九是 403。原因不复杂:分享页给出来的从来不是一个“永久文件地址”,而是一串带条件、带时效、带身份绑定的临时凭证。理解这一点,后面所有奇怪现象都能解释。

1.1 先认清链接的三种形态,它们各自需要不同的“钥匙”

同样是分享链接,形态差别很大,处理方式完全不一样。我习惯把它们分成三类:

链接形态典型特征解析时需要的东西难度
公开短链短域名 + 一串短 ID,打开直接看到文件列表无,直接请求低
带提取码短链 + 单独一行的 4 位码提取码 + 校验后的会话凭证中
需登录可见打开提示登录,或者只对好友可见账号会话(cookie/token)高,且要看权限

第一类最省事,一个 GET 请求就能拿到页面,页面里往往直接嵌着文件清单。第二类的关键在于“提取码校验”这一步:它通常是一个独立的 POST 接口,校验成功之后服务端会下发一个会话标识,后续所有请求都得带上它,否则你拿到的文件清单永远是空的或者直接跳回输入密码页。第三类最麻烦,它不只是技术问题,还涉及权限——你用自己的账号登录,能看到的只有你有权看到的东西,这点后面第五节还会展开说。

我见过不少人卡在第二类上:他们把提取码塞进 URL 参数里试,或者试图在前端 JS 里找“万能码”,折腾半天。实际上正确姿势就是老老实实走一遍校验接口,拿到会话,这是服务端设计好的正常路径,没必要绕。

1.2 分享页里到底藏了什么:从服务端渲染到 JSON 接口

早期网盘的分享页是服务端渲染的,文件列表直接写在 HTML 里,正则一把梭就能薅出来。现在基本见不到这么厚道的做法了,主流是两种:

一种是首屏注水——HTML 里嵌一段window.__INITIAL_STATE__ = {...}或类似名字的全局变量,把文件列表、目录结构、分享者信息一股脑塞进去,前端拿到之后直接渲染。这种页面你打开源码搜一下文件名字符串,一般能定位到那段 JSON,抠出来解析就行。它的好处是省一次请求,坏处是结构随版本变,今天叫file_list,明天可能改成list.items。

另一种是纯接口驱动——HTML 只是个空壳,真正的数据靠 XHR 拉。这种就得打开开发者工具的 Network 面板,切到 XHR/Fetch 过滤,刷新页面,看哪几个请求返回了 JSON,里面有你想要的字段。这一步是整个解析工作的核心技能,比写代码重要得多。

我个人偏向第二种,因为接口一旦摸清楚,脚本里就只需要发请求、不用解析 HTML,稳定性高得多。页面结构改版的时候,注水 JSON 的变量名和层级经常一起变,而接口的路径和参数往往更稳,毕竟前端也得靠它工作。

2. 把解析链路拆到每一步:请求、校验、取清单、换直链

一条完整的解析链路,我实测下来基本是四步走:建立会话、通过校验、拉文件清单、换取直链。每一步失败的表现都不一样,所以拆开讲,方便你对着现象定位。

2.1 建立会话:为什么必须用 Session 而不是裸 requests

这一步看着最没技术含量,其实最容易埋雷。服务端在分享页那次响应里,通常会用Set-Cookie下发几个 cookie,其中有的是设备标识,有的是防爬用的令牌,还有的跟提取码校验结果绑定。如果你后面每个请求都用requests.get()裸发,等于每次都是个“新访客”,校验状态自然带不过去。

正确做法是全程用一个requests.Session(),它会自动维护 cookie jar。我还会额外做两件事:

  • 固定一个真实的 User-Agent。有些服务端会按 UA 判断是不是浏览器,UA 为空或者明显是脚本特征时,返回的页面结构都可能不一样。
  • 显式设Referer为分享页地址。很多直链在最后一步会校验 Referer,缺了直接 403,这个坑下面第三节细说。

超时一定要设,而且连接超时和读取超时要分开设。我给的是(5, 30)——连不上 5 秒就该放弃,但读数据给 30 秒,因为有些接口在冷启动时响应会慢。不设超时的脚本在批量跑的时候极容易挂死在某个请求上,卡一晚上。

2.2 拉文件清单:分页是最大的暗坑

拿到会话之后,请求文件列表接口。这里有个特别隐蔽的问题:大部分接口默认分页,而且 total 字段不总是准的。

我遇到过两种情况。第一种,接口默认page=1&size=50,一个大目录有三百多个文件,我第一版脚本只取了第一页,结果归档完发现少了一半,还是过了一周才发现的。第二种更阴险:total返回的是顶层目录数,而不是文件总数,你按total / size算页数会直接少算。

稳妥的做法是“翻到空页为止”:循环请求,每次page += 1,直到返回的数组长度为 0 就停。多请求一次的成本可以忽略,比漏文件强太多。同时给个硬上限(比如 200 页),防止接口异常时死循环。

另外注意目录型分享。有些分享是一个文件夹,点进去还有子目录,接口会要求你带上父目录 ID 再请求一次。这种情况就得做递归遍历,用一个栈或者队列把待展开的目录 ID 存起来,逐个处理。写递归的时候记得加个深度上限,我在测试时碰到过一个异常的循环引用结构,不加限制直接把栈打爆了。

2.3 换取直链:看懂 URL 里的三件套

清单拿到之后,最后一个接口是“把文件 ID 换成下载地址”。返回的通常是一个长 URL,长得像这样(参数名各家不同,结构高度相似):

https://dlXX.example-cdn.net/file/abc123?fid=998877&expires=1730000000&sign=8f3a...c2&ns=1

拆开看就是三件套:

  • 文件标识:fid这类参数,指向具体哪个文件,通常还对应该文件在存储层的物理分片信息。
  • 过期时间:expires是 Unix 时间戳,一般有效期在几十分钟到几小时之间。这是“临时直链”里“临时”二字的来源。
  • 签名:sign(或token、auth)是服务端用私钥对前面若干参数算出来的摘要。你改任何一个参数,签名就对不上,直接拒绝。

理解这三件套很重要,因为它决定了一个关键事实:直链不能提前批量生成然后放着慢慢下。你一次换两百条直链,等你下到第一百五十条的时候,前面生成的早就过期了。正确做法是“用完再换”——下载队列里每条任务在真正开始前才去换直链,或者按批次小规模换(比如一次换 10 条,下完再换下一批)。

我第一版就犯了这个错,一次性把所有直链换出来存进 JSON,结果跑了两小时之后后半段全是 403,排查了半天才反应过来是过期。这个教训值一篇博文。

3. 直链拿到了却下不动:五类卡点与定位方法

这一段是我踩坑最密集的地方。直链明明在浏览器里能点开下载,脚本里就是不行,现象五花八门,但根因就那么几类。我整理成一张表,方便你按状态码快速定位。

现象最可能的成因快速验证方式
403 Forbidden缺 Referer / UA 被识别 / 签名过期补 Referer 头重试;检查 expires 是否已过
401 Unauthorized会话 cookie 丢失或校验状态失效打印 session.cookies 看关键 cookie 在不在
302 跳转到登录页该文件需要登录权限用浏览器无痕窗口打开同一链接对照
200 但内容是一小段 HTML被风控页拦截,返回了提示页检查 Content-Type 和响应体前 200 字节
下载到一半断流单连接限速或被判定异常换小分片 + 断点续传重试

前两类好理解,重点说说后面三类。

3.1 那个“200 但下下来是网页”的坑

这个坑最坑人,因为状态码是 200,脚本不报错,你以为下载成功了,结果打开一看是个几 KB 的 HTML,里面写着“访问过于频繁,请稍后再试”之类的话。我第一次遇到时,是在批量归档一百多个小文件,跑到第六十个开始全部“成功”,实际全是这种页面,白白浪费了一晚上。

防御方法很简单,下载前先做一次预检:发一个stream=True的请求,只看响应头,检查Content-Type是不是application/octet-stream或者video/*这类二进制类型,检查Content-Length是不是跟你预期的文件大小对得上。对不上就直接判失败,不要写盘。

更进一步,可以在写盘前读前 512 字节看看有没有<!DOCTYPE html>或<html这种特征。真有就立刻中止并把这条任务标记为待重试。这个检查我后来加进了自己的工具里,代码不长但救了我很多次。

3.2 限速、并发与 IP 绑定:为什么加线程反而更慢

新手最常见的想法是“一个连接慢,那我开 32 个线程”。实测下来这个思路在网盘场景基本是反效果。

原因在于服务端一般做的是双层限速:单连接限速(比如每个连接给 500KB/s)加账号或 IP 维度的总带宽上限(比如总共 2MB/s)。你把并发从 4 提到 32,单连接速度不会变,但总请求数暴涨,很容易触发风控,轻则降速重则直接拒绝。我实测的最优区间通常在4 到 8 个并发之间,具体取决于你那边网络和服务端的策略,建议从 4 开始二分往上试,每次只改一个变量,记录总吞吐和失败率。

还有个容易忽略的点:直链可能跟换取它的那个 IP 绑定。如果你的环境出口 IP 会变(比如多个网络出口、或者中途切换了网络),换了 IP 之后再用旧直链就会 403。这种情况不是签名过期,而是签名里隐含了 IP。判断方法是:把直链原封不动在浏览器里打开,如果浏览器能下而脚本不能,八成是 IP 或 UA 的问题。

4. 把这套流程做成能长期跑的小工具

一次性的脚本和能长期用的工具,中间差的是工程化。我这次重做的重点就放在这上面,因为定期归档这种事,跑一次是玩,跑一年才是需求。

4.1 请求层:会话复用、退避重试、超时三件套

所有对外请求我都收在一个薄薄的封装里,统一处理三件事:

import time, random, requests from requests.adapters import HTTPAdapter def build_session(pool_size=8): s = requests.Session() adapter = HTTPAdapter(pool_connections=pool_size, pool_maxsize=pool_size, max_retries=0) s.mount("https://", adapter) s.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", "Accept": "*/*", }) return s def request_with_backoff(session, method, url, retries=5, **kwargs): kwargs.setdefault("timeout", (5, 30)) for attempt in range(retries): try: resp = session.request(method, url, **kwargs) if resp.status_code in (429, 500, 502, 503, 504): wait = min(2 ** attempt, 30) + random.uniform(0, 1) time.sleep(wait) continue return resp except (requests.ConnectionError, requests.Timeout): if attempt == retries - 1: raise time.sleep(min(2 ** attempt, 30) + random.uniform(0, 1)) raise RuntimeError(f"retries exhausted: {url}")

几个细节值得说明。max_retries=0是因为我自己在外层做退避,不想让 urllib3 的重试和我自己的重试叠加,那样一次失败会被放大成好几次请求,反而更容易触发风控。退避用指数增长加一点随机抖动,抖动的作用是避免多个线程同时醒来、同时重试造成新的尖峰。遇到 429 时如果响应头里有Retry-After,应该优先用它,这个我留了个口子没写进上面的代码,实际用的时候记得补。

4.2 分片下载与断点续传:Range 请求的正确用法

大文件必须分片,一是能断点续传,二是能利用多连接把单连接限速绕开(在总带宽允许的前提下)。核心就是Range: bytes=start-end这个请求头。

import os from concurrent.futures import ThreadPoolExecutor, as_completed def download_part(session, url, headers, start, end, part_path): h = dict(headers) h["Range"] = f"bytes={start}-{end}" with session.get(url, headers=h, stream=True, timeout=(5, 60)) as r: if r.status_code not in (200, 206): raise RuntimeError(f"part failed: {r.status_code}") with open(part_path, "wb") as f: for chunk in r.iter_content(chunk_size=256 * 1024): if chunk: f.write(chunk) expected = end - start + 1 actual = os.path.getsize(part_path) if actual != expected: raise RuntimeError(f"size mismatch: {actual} != {expected}") def download_file(session, url, headers, size, out_path, part_dir, workers=6): part = 8 * 1024 * 1024 # 8MB 一片 ranges = [(i, min(i + part - 1, size - 1)) for i in range(0, size, part)] os.makedirs(part_dir, exist_ok=True) parts = [] with ThreadPoolExecutor(max_workers=workers) as pool: futures = {} for idx, (s, e) in enumerate(ranges): p = os.path.join(part_dir, f"{idx:05d}.part") parts.append(p) if os.path.exists(p) and os.path.getsize(p) == e - s + 1: continue futures[pool.submit(download_part, session, url, headers, s, e, p)] = idx for fut in as_completed(futures): fut.result() with open(out_path, "wb") as out: for p in parts: with open(p, "rb") as f: out.write(f.read()) for p in parts: os.remove(p)

为什么要先写成分片的独立文件再合并,而不是用seek往同一个文件里随机写?两个原因。第一,随机写在多线程下需要加锁,写法麻烦还容易出错。第二,也是更实际的:分片文件的存在本身就是断点续传的状态记录,重跑的时候判断一下文件大小对不对,对就跳过,简单可靠。我试过用seek的版本,一次异常退出之后文件长度是对的但中间有空洞,查了很久。

分片大小我选 8MB,是权衡的结果。太小了请求数多、握手开销占比高;太大了单片的失败重传成本高,而且末尾那片如果只有几百 KB 还按 8MB 请求会浪费。你也可以按文件总大小动态调整,比如小于 100MB 的文件直接单线程整下。

关键的一点:同一个文件的所有分片必须用同一个直链。如果你中途重换了直链,签名里包含的分片信息可能不一致,导致拼起来的文件损坏。所以我的做法是,一个文件开始下载前换一次直链,整个文件下完之前不换;如果直链中途过期导致分片失败,就重换直链并把所有分片重下,不要混着用。

4.3 落盘校验:别信“下完了”这三个字

合并完成后一定要校验。最理想的是服务端提供文件的 MD5 或 SHA1,很多网盘的清单接口里带这个字段。有就比,没有就至少比大小。

校验不通过怎么办?我的策略是重试一次;还不对就换直链重下,最多两轮;再不行就标记为失败,把文件 ID 和几次的哈希值记进日志,人工去看看。千万别默默覆盖,那样你永远不知道有问题。

日志我是按“任务 ID + 时间戳 + 阶段 + 结果”写成 JSONL,一天一个文件。这个格式好处是能直接grep也能直接喂给脚本统计。跑了几个月之后,这份日志帮我发现了一个规律:某些文件在特定时段失败率明显高,后来确认是那段时间服务端在做维护。有日志才能看到这类模式,没有日志你只会觉得“这工具不太稳”。

5. 这些做法我从一开始就不碰

技术上讲得通,不代表应该做。这条线上有几件事我给自己划了明确的界线,写出来也是给想动手的朋友一个参考。

5.1 公开的在线解析站,我为什么不建议做

“在线解析”这四个字听着轻巧,真做起来是三个大坑叠在一起。

第一个是流量成本转嫁。你提供一个网页,别人贴链接、你返回直链,那么所有下载流量最后都从你的服务器出去,或者说从你的出口 IP 出去。用户越多,成本和风险全在你这边。我见过有人搭了个小站,一个月之后流量账单直接把人吓退。

第二个是被当成绕过限制的通道。工具一旦公开,使用者用它做什么你完全控制不了,有人拿它去批量抓取、去规避正常的访问限制,这时候你作为提供方是说不清的。这也是我坚持“只给自己用”的核心原因——自用工具的边界是清楚的,公开服务的边界是失控的。

第三个是稳定性无从保证。上游接口一改,你的站立刻全线崩,而你没法像自用那样随时停下来慢慢调。公开服务挂着的时候,用户只会觉得“这站挂了”。

所以我的工具从头到尾就是个命令行程序,跑在我自己机器上,输入是我自己整理好的链接清单,输出是本地文件。没有网页,没有服务端,没有对外接口。

5.2 我给自己定的几条硬规矩

具体到操作层面,这几条是我一直守着的:

  • 只解析自己有权访问的内容。分享链接的提取码是分享者正常提供的,账号会话是我自己的账号,不碰任何“猜码”“撞库”之类的路子。
  • 不做批量爬取。一次几十上百个文件是正常的归档需求,成千上万地扫就是另一回事了,性质完全不同。
  • 严格遵守对方的服务条款。有些网盘的用户协议里明确写了不允许程序化批量下载,那就别做,或者先去看清楚条款怎么写的。这一条比技术重要。
  • 不把直链二次分发。直链是带签名和时效的临时凭证,转发出去等于把你的访问权限借给别人,这事儿不能干。
  • 遇到风控就停下来,不要靠加代理、换 IP、改 UA 去硬顶。被限速了就降并发、拉长间隔,让节奏看起来正常一点。这不是技术对抗,是礼貌。

说白了,这类工具的价值在于“省掉我自己重复点击的时间”,不在于“突破什么”。把定位摆正,很多灰色地带自然就绕开了。

6. 一次“全量 404”的排查实录

最后讲一次印象最深的排查,因为它把前面几节的东西几乎串了一遍。

某天早上定时任务跑完,日志显示两百多个文件全部失败,状态码清一色 404。前一天还好好的,代码一行没改。这种情况最容易被误判成“接口改版了”,然后一头扎进去改代码,改半天发现根本不是。

我的排查顺序是这样的。

第一步,确认是不是所有请求都 404。我在日志里筛了一下,发现分享页请求是 200,文件清单接口是 200,只有最后的“换直链”接口是 404。这就排除了整体改版,问题集中在最后一步。

第二步,看请求参数。把失败请求的完整 URL 和参数打出来,跟历史成功记录对比。发现有个参数share_id的值变了——以前是纯数字,这次是一串带字母的。我怀疑是分享 ID 的编码方式改了。

第三步,回到浏览器验证。用同一个链接在浏览器里正常打开,能列文件、能点下载。抓包看它发的请求,发现浏览器发的share_id也是那串带字母的,但还多带了一个share_token参数——这个参数在页面加载时的另一个接口里下发的,我脚本里压根没去取。

第四步,验证假设。手动把这个 token 补进请求,404 立刻变成 200,直链正常返回。确认无误。

第五步,改代码并加防御。我在清单接口的响应里解析出这个 token 存进会话对象,后续换直链时自动带上。同时加了一条规则:换直链返回 404 时,自动回退到“重新走一遍分享页 → 重新取 token → 再试一次”的流程,避免下次再出现类似的前置依赖变化时直接全线崩。

这次排查花了我两个小时,其中真正改代码只有十分钟,剩下全在定位。但我觉得值,因为它让我意识到一件事:解析链路是有状态依赖的,前面某一步的响应里藏着后面要用的东西。你不能假设每一步都是独立的,写脚本的时候就要有意识地把中间产物(token、会话 ID、目录树)存下来,用不上的时候不碍事,用得上的时候能救命。

后来我还加了个简单的前置自检:任务开始前先跑一个“最小闭环”——拿一条已知可用的测试链接,完整走一遍四步流程,任何一步失败就整个任务中止并报警。这样问题会在最开始暴露,而不是跑到第一百个文件才发现全线崩。这个自检逻辑不到三十行,但它把“第二天早上看到一堆失败日志”的概率压下去了大半。


说个实操上的小体会。这套东西写完之后我最大的收获其实不是脚本本身,而是那份 JSONL 日志。跑了两三个月,翻日志能看出很多平时注意不到的模式:哪些时段下载快、哪类文件容易失败、并发调到多少总吞吐最高。这些数据比任何“最佳实践”都靠谱,因为它是你自己网络环境下的真实测量结果。所以我建议你哪怕只是写个几十行的临时脚本,也顺手把每次请求的关键信息记下来,成本几乎为零,回头查问题时价值极大。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询