原生PHP实现蓝奏云大文件分片上传实战

蓝奏云分片上传大文件上传
于 2026-10-01 02:47:16 修改
·本内容遵循CC 4.0 BY-SA版权协议

做网站和资源分发这些年,我经手过的文件大小早就从几 MB 涨到了几个 GB。安装包、离线文档、视频教程、数据库备份……每次给用户甩下载链接的时候,蓝奏云都是我排第一梯队的选手。原因很简单:国内节点直连,下载不限速,分享页干净,登录拿货都顺。但蓝奏云也有个老生常谈的软肋——单文件体积限制。手里捏着一个 1.2GB 的安装包想传上去,网页端直接拒绝。最初我也没辙,后来干脆用 PHP 写了一套分片上传方案:本地把大文件切开,逐片模拟网页上传请求,全部传完再把分享链接整理出来。这套方案在我自己维护的资源站上跑了小半年,稳定性比想象中好。这篇文章就把完整思路、原生 PHP 源码和实测过程都扒开讲一遍,适合正在做网盘工具、资源分发或 PHP 上传功能开发的你参考。

1. 为什么蓝奏云需要分片上传方案

1.1 蓝奏云的定位与单文件限制

蓝奏云这个产品的定位一直很清晰:轻量分享。它没打算跟那些主打大容量存储的网盘硬碰硬,核心卖点是下载速度快、不强制登录、分享链接能长期有效。可这个“轻量”也直接体现在上传限制上。网页端和客户端默认对单文件大小有硬门槛,普通账户实测下来,超过 100MB 的文件会被拦在入口之外,有些文件类型甚至限制得更死。

这个限制放在日常传文档、传小压缩包、传安装包碎片还好。但凡是遇到“必须整体交付”的场景就很尴尬:

  • 1GB 以上的软件发行包
  • 几十 GB 的虚拟主机备份
  • 培训课程的视频合集
  • 数据库导出后的 SQL 大文件

这些内容不是不能传到别的平台,而是蓝奏云在下载体验上有不可替代的优势。当时我在做一款内部工具的分发,要求用户拿到的下载地址必须是国内直连、免登录、不减速的。比了一圈,还是蓝奏云最合适。那问题就剩一个:如何突破单文件大小限制,把大文件顺利送上去?

1.2 分片上传方案的整体架构

这个问题的答案,其实早在“断点续传”“秒传”这些概念里就写好了:把一个文件拆成多个小块,逐个传输,最后在目标端还原。最常见的是“服务端合并”模式——客户端把文件切分后上传到服务器,服务器接收全部切片后组装回原文件。这种模式适合自己掌控服务端的情况。

但蓝奏云不是自己的服务器,它不接受“切片合并”这种玩法,上传接口收到的就是一个个独立的文件。所以这套方案实际采用的是“逻辑分片”思路:

  • 用 PHP 脚本在本地把大文件切成指定大小的小块
  • 每一块作为一个独立文件,通过网页版上传接口提交到蓝奏云
  • 上传成功后,每个分片对应一个独立的分享链接
  • 下载侧拿到链接集合后,按序号逐个下载或直接合并还原

这种方式看起来很“土”,但胜在完全不用动服务端,也不需要破解任何平台逻辑。蓝奏云看到的是 N 个合法的普通文件上传请求,完全符合平台预期。唯一需要下功夫的,是脚本怎么切分、怎么上传、怎么保证全部传完不遗漏。

我选择用原生 PHP 来实现还有一个实际原因:资源站所在的服务器本来就有 PHP 环境,不需要额外装 Python 或 Go 的运行库。PHP 的 curl 扩展成熟稳定,CLI 模式下跑长时间任务也不会有内存限制问题。脚本可以扔到任何一台带 PHP 的机器上直接跑,维护成本极低。

2. 原生 PHP分片上传方案的核心实现

2.1 分片大小的选择逻辑

分片大小是整个方案里最需要拿捏的参数,不是拍脑袋定的。定太小,比如 5MB,一个 1.2GB 的文件会产生 240 多个分片,上传请求数量激增,不仅让总耗时变长,还更容易触发平台对高频请求的临时限制。定太大,比如 90MB,单片非常接近蓝奏云的上限,一旦上传接口对大小有额外浮动判断,整片直接失败,重试成本极高。

我按自己的网络环境和平台表现反复试了几轮,得出的经验区间是 20MB 到 50MB。默认推荐 50MB,理由如下:

  • 远低于网页端限制,给接口校验留出余量
  • 单片传输时间适中,失败重试的代价可控
  • 1GB 文件只需要 20 次左右请求,日志清晰,后续核对也方便

分片数量的计算逻辑很简单,直接对文件大小除以分片大小向上取整:

PHP
$fileSize = filesize($sourceFile);
$chunkSize = 50 * 1024 * 1024; // 50MB
$chunkCount = (int) ceil($fileSize / $chunkSize);

以 1.2GB 的文件为例,50MB 分片会产生 25 个分片。如果网络条件比较差,比如在跨网宽带上跑,可以把 $chunkSize 降到 20MB,用分片数量换单次传输成功率。这个参数改一行就能生效,建议实际跑批量任务之前,先用一个小文件做一次全链路演练。

2.2 上传请求的源码解析

原生 PHP 实现这套方案的核心,不是文件切分本身,而是“怎么把一个文件分片以合法请求的形式提交到蓝奏云的上传接口”。网页端上传文件时,浏览器会发起一个 multipart/form-data 类型的 POST 请求,里面既包含文件二进制内容,也包含若干表单字段、Cookie 和必要的请求头。PHP 脚本要做的,就是用 curl 把这个请求完整复刻出来。

先看封装好的上传类骨架:

PHP
class LanzouChunkUploader
{
private string $cookie;
 
public function __construct(string $cookie)
{
$this->cookie = $cookie;
}
 
/**
* 从上传页面动态获取上传接口所需参数
*/
public function getUploadParams(): array
{
// 请求上传页 HTML,正则提取隐藏字段
// 关键字段:upload_id、sign、token、folder_id
// 注意:这些字段名以你抓包结果为准,不要写死
}
 
/**
* 上传单个分片
*/
public function uploadChunk(string $filePath, array $params): bool
{
$curl = curl_init();
$postData = array_merge($params, [
'file' => new CURLFile($filePath, 'application/octet-stream', basename($filePath)),
]);
 
curl_setopt_array($curl, [
CURLOPT_URL => 'https://pc.woozooo.com/file/upload', // 以抓包获取的实际地址为准
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_COOKIE => $this->cookie,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
]);
 
$response = curl_exec($curl);
curl_close($curl);
 
// 解析响应,判断是否上传成功
return $this->isSuccess($response);
}
}

很多人卡在“模拟上传请求”这一步,是因为不知道这些参数从哪来。实际上不需要把所有参数都手动构造,网页端上传页面里本身就藏着绝大部分字段。用浏览器开发者工具抓一次上传过程,把请求里的表单字段对照页面源码看一下,就能发现字段来源。脚本里专门写一个 getUploadParams() 方法,每次上传前动态去页面提取,就是为了避免平台调整参数之后脚本立刻失效。

这里有个很关键的细节:CURLFile 的第三个参数是文件名,这个值会直接影响蓝奏云里显示的最终文件名。如果这里传的是分片的临时路径名,比如 chunk_0001.part,那蓝奏云上就会出现一堆奇奇怪怪的名字。正确做法是传一个有意义的文件名,比如 setup_1.2g_full.part001,方便后续排序和识别。

2.3 主循环与断点续传设计

上传单个分片并不难,真正考验脚本功底的是主循环的健壮性。整个文件有几十个分片,随便一个分片因为网络波动失败,如果不处理,整个任务就要从头再来。这跟下载工具里的断点续传是一个道理。

我在主循环里加了“已上传分片记录”机制:

PHP
$doneFile = $sourceFile . '.done';
$doneList = file_exists($doneFile) ? file($doneFile, FILE_IGNORE_NEW_LINES) : [];
 
for ($i = 1; $i <= $chunkCount; $i++) {
if (in_array($i, $doneList)) {
echo "分片 {$i} 已存在,跳过\n";
continue;
}
 
$chunkFile = sprintf('%s/chunk_%03d.part', sys_get_temp_dir(), $i);
// 从源文件读取指定偏移量并写入 chunkFile
// 代码省略文件切片逻辑,核心是 fopen + fseek + fread
 
if ($this->uploadChunk($chunkFile, $params)) {
file_put_contents($doneFile, $i . PHP_EOL, FILE_APPEND);
echo "分片 {$i}/{$chunkCount} 上传成功\n";
} else {
echo "分片 {$i} 上传失败,稍后重试\n";
sleep(5);
$i--; // 重试当前分片
}
}

这套逻辑里最值钱的不是循环本身,而是 .done 记录文件。脚本跑到一半中断了,重新启动后会自动跳过已经完成的分片,不用重新花老长时间传前面那些。对动辄几十个分片的大文件来说,这个设计能省下大量重复劳动。

为什么推荐用原生 PHP 而不是上框架?因为这种脚本根本没有路由、模板、ORM 的需求,框架自带的依赖注入和中间件机制只会增加内存占用和启动耗时。原生 PHP 配合 curl 扩展,几十行代码就能解决问题,性能更好,调试也更直接。

3. 实测过程:把 1.2GB 安装包拆成 25 片传上去

3.1 运行前的准备

先说环境。这套脚本在 Windows 和 Linux 上都能跑,但生产环境建议放在 Linux 服务器上,原因后面讲。需要确认 PHP 已安装并开启了 curl 和 openssl 扩展:

BASH
php -m | grep curl
php -m | grep openssl

如果输出里没有这两个扩展,需要先安装。Debian/Ubuntu 系统一般执行 apt install php-curl 即可,Windows 下则在 php.ini 里打开对应 extension。

然后是登录凭证。蓝奏云网页端上传需要登录态,最直接的获取方式是用浏览器登录账户,打开开发者工具,在 Network 面板里找到任意一个请求,复制请求头里的 Cookie 字符串。这个字符串有效期通常能维持几天,但建议每次批量上传前都重新拿一次。

测试文件我选了一个 1.2GB 的软件安装包。按 50MB 分片计算,总共 25 个分片。开始之前先算好并记录源文件的 md5 值:

BASH
md5sum setup_1.2g_full.exe

这个值不是给蓝奏云校验的,而是上传完成后下载回来做完整性比对的依据。没有这一步,后面验证时只能凭感觉判断文件是否损坏。

3.2 脚本执行与结果记录

执行命令很简单:

BASH
php lanzou_upload.php /data/dist/setup_1.2g_full.exe

脚本运行后输出如下:

TEXT
[10:02:11] 分片 1/25 上传成功,耗时 8.2s
[10:02:26] 分片 2/25 上传成功,耗时 5.7s
[10:02:33] 分片 3/25 上传成功,耗时 4.9s
...
[10:15:47] 分片 24/25 上传成功,耗时 6.1s
[10:16:02] 分片 25/25 上传成功,耗时 7.8s

整个过程大约 14 分钟,中途没有出现失败分片,一次跑完。这符合我这边普通家庭宽带上行速度的正常水平。如果你所在的网络环境上传速度只有几 Mbps,建议把分片调小,同时把 curl 超时时间拉长,避免单片传输时间过长导致连接被服务端断开。

上传完成后,每个分片在蓝奏云里都是一个独立文件,排列在同一目录下。脚本随后会输出一串分享短链。把短链按分片序号整理成下载清单,这一步我是让脚本直接生成一个 HTML 文件,按文件名排序把所有链接放进去,用户拿到后逐一点击下载即可。

3.3 完整性与可复现性验证

传输结束不等于事情结束。我把所有分片下载回本地,在命令行里做拼接校验。Linux 下执行合并:

BASH
cat setup_1.2g_full.part001 setup_1.2g_full.part002 ... > recovered.exe

然后比对 md5:

BASH
md5sum recovered.exe

比对结果与源文件完全一致,说明分片上传过程没有数据损坏。这个验证必须做,因为分片文件一个接一个传,任何一片丢失或内容错误,最终合并结果都会有偏差。不要以为蓝奏云网页上传就一定可靠,网络层的偶发错误谁都无法完全避免。

整个流程跑下来,本质上就是把一次“不允许的大上传”变成若干次“允许的小上传”,平台侧逻辑完全没碰,数据也没有经过第三方中转,属于非常干净的实现路径。

4. 实测中踩过的坑与排查实录

4.1 Cookie 失效与登录态过期

这是我遇到过最频繁的问题。脚本跑着跑着,突然连续出现上传失败,查看响应内容发现返回的是“未登录”或“请先登录”之类的提示。原因很简单:Cookie 过期了,或者账户在别处登录把旧登录态顶掉了。

解决方案分两层。第一层,在脚本中检测响应内容中的关键词,一旦发现未登录提示,立即停止上传并输出错误信息,不要傻乎乎地继续重试。第二层,封装一个登录方法,在 main 循环开始前自动用账号密码换取新的登录态。如果实在不想处理登录接口,就保持“每次跑任务前手动刷新 Cookie”的习惯,也能用。

4.2 超时中断导致的分片缺失

PHP CLI 模式默认没有 max_execution_time 限制,但 curl 自身有超时设置。我第一次跑 90MB 分片的时候,单片上传耗时超过 120 秒,curl 直接报超时错误,分片实际上已经传到服务器了,但脚本这边收到了超时信号,准备重传同一片。

这听起来不严重,但实际会带来重复文件问题。蓝奏云接收到同名文件,默认行为不一定是覆盖,有可能生成新文件,最后目录里出现两个相同分片,排序还错乱。避免这个问题的办法有两个:

  • 分片大小控制在 50MB 以内,让单片上传时间显著低于超时阈值
  • 上传前检测目录是否已存在同名文件,如果有就先删除或复用

CURLOPT_TIMEOUT 我最终设置为 300 秒,配合 50MB 分片,几乎不会再出现超时误判。

4.3 上传接口参数变动

平台方偶尔会调整上传请求的参数名或校验逻辑。如果脚本里把所有字段名都写死,一旦平台改动,脚本立刻报废。我踩过一次这个坑,排查半天才发现是请求里少了一个平台新加的校验字段。

规避方案就是前面提到的:每次上传前先 GET 一次上传页面,用正则把页面里的隐藏字段全提取出来,动态构造 POST 请求。即使平台加字段,只要它还在页面上,脚本就能自动适应。用浏览器抓包对比页面源码,就能建立起“页面字段到请求参数”的映射关系,这块理解了,以后改接口都不慌。

4.4 文件名编码与分享页排序

分片文件如果直接叫 1.part、2.part,排序会非常混乱,尤其是超过 10 个分片后,10.part 会排到 2.part 前面。解决方案就是统一用固定宽度编号命名:

PHP
sprintf('%s.part%03d', $baseName, $i);

例如 setup.part001 到 setup.part025。这样在蓝奏云文件管理页和最终下载页里,分片顺序都一目了然。

中文文件名也有坑。PHP 在不同系统下的文件名编码表现不一致,Windows 下容易出现 GBK 与 UTF-8 混用。我的建议是最外层包装成发布用的英文名,源文件中文名留在本地记录,尽量避免把中文名直接塞给上传接口。

4.5 排查速查表

现象 可能原因 处理方式
所有分片全部失败 Cookie 过期 重新登录,更新 Cookie
偶发单片失败 网络波动或服务端限流 增加重试,失败后 sleep 5-10 秒
重复分片文件 超时误判或同名文件 上传前检查文件是否存在
响应提示非法请求 缺少动态参数 重新抓包,确认上传页面字段
合并后 md5 不一致 分片遗漏 重新核对分片列表顺序

5. 可扩展的方向与个人心得

5.1 从命令行脚本到 Web 管理面板

脚本跑通以后,我很快就发现纯命令行不够方便。毕竟不是每次都想 SSH 到服务器敲命令,于是给它套了一层极简的 Web 前端:一个 PHP 页面,列出本地待上传目录下的所有大文件,点击按钮即可触发后台任务,前端用轮询显示进度条。核心上传类完全复用,只多了任务队列和日志模块。

队列的设计要点是上传任务的串行化。蓝奏云对高频并发请求是有风控的,同时传三个大文件,每个文件又有几十个分片,并发滥用很容易触发临时封禁。所以在 Web 面板里,任务队列严格执行 FCFS(先来先服务),同一时刻只允许一个文件在上传进程里。

5.2 扩展到备份与分发自动化

这个分片上传脚本最实用的衍生场景是定时备份。我在服务器上写了个 cron 任务,每天凌晨把网站目录、数据库导出文件打包,如果大小超过 100MB,就自动走分片上传逻辑推到蓝奏云。这样即使服务器本地磁盘出问题,备份文件也有一个异地的安全副本。

定时任务场景下,建议在分片上传后加入通知机制。脚本执行完成后,通过邮件或 Webhook 把本次上传生成的新链接推送出来。否则备份文件每天产生几十条新链接,靠人工去盯完全不现实。

5.3 关于平台规则和实际效果的几点反思

整套方案跑了大半年,有几点体会很深。分片上传不是“绕过平台限制”,而是改变文件交付方式,让大文件以平台允许的形态完成分发。使用时还是要注意分寸:只上传自己拥有版权的合规资源,控制上传频率,避免瞬间大量请求对平台造成压力。合规操作下,这套方案才能长期稳定地运转下去。

如果让我重头再做一次,我会在一开始就把分片序号设计成三位固定宽度,并且在文件名里直接写源文件的 md5 前 8 位。这样后续核对分片完整性时,不用打开文件内容,光看文件名就能判断归属。这个习惯我后来沿用到所有生成文件的脚本里,省了无数次排错时间。上传完成后,也请务必保留一份 .done 记录文件。分片列表就是整个任务的索引,丢了它,等于几十个文件在网盘里成了一堆无主的数据。