教育数据工程——课程批量下载与跨桶迁移

  • ~3.83K 字
  1. 1. 一、项目背景
  2. 2. 二、逆向与探测
    1. 2.1. 2.1 接口分析
    2. 2.2. 2.2 清单构建
  3. 3. 三、批量下载器设计
    1. 3.1. 3.1 核心架构:流式下载
    2. 3.2. 3.2 关键参数
  4. 4. 四、三大工程问题
    1. 4.1. 4.1 大视频反复失败
    2. 4.2. 4.2 脚本崩溃(FileNotFoundError)
    3. 4.3. 4.3 用户中途变更需求
  5. 5. 五、补跑机制
    1. 5.1. 5.1 失败课程重跑
    2. 5.2. 5.2 最终结果
  6. 6. 六、OSS 跨桶迁移
    1. 6.1. 6.1 需求
    2. 6.2. 6.2 两轮方案
    3. 6.3. 6.3 服务端复制实现
    4. 6.4. 6.4 对账验证
  7. 7. 七、配套 skill:oss-data-management
  8. 8. 八、工程经验

把某在线教育平台的 345 门培优课程(视频 + 学案)批量下载并归档到 OSS,中途经历用户需求变更(停三科)、大视频断连、脚本崩溃、跨桶迁移四轮考验。配套 skill:oss-data-management(devops)与 chinese-edtech-spa-scraping(research)。

一、项目背景

  • 目标:345 门培优课程(视频 mp4 + 学案 docx),按学科归档到 OSS
  • 学科分布:数学 117 / 化学 61 / 语文 56 / 英语 46 / 物理 34 / 生物 31
  • 环境约束:40G 磁盘、1.8G 内存,下载要流式(下→传→删)

二、逆向与探测

2.1 接口分析

配套 skill chinese-edtech-spa-scraping 固化了一套国内教育平台 SPA 逆向方法:

  • 抓包分析 API:课程列表 / 播放地址 / 学案下载
  • 多轮探测脚本(course_recon / course_api_list / course_fetch_all)
  • 鉴权方式确认:无登录态下大部分接口可访问(学案 docx 直接可下)

2.2 清单构建

全量课程清单 course_final_manifest.json:345 门,按学科分组,每门含 videos(url+name)+ docs(url+name)。

三、批量下载器设计

3.1 核心架构:流式下载

1
逐门课程: 下载全部文件 → 上传 OSS → 删除本地
  • 磁盘保护:本地占用超阈值先上传清理(DISK_SAFE_GB=3.0)
  • 断点续传:done_files 标记 + 远端存在性校验,重跑只补失败
  • 温柔对待服务器:每门课之间 sleep 1.5s

3.2 关键参数

  • 单文件超时 1 小时(DOWNLOAD_TIMEOUT),curl 重试 3 次
  • 进度 JSON 实时落盘(course_download_progress.json,可中断恢复)
  • 已标记的跳过逻辑:本地在则跳过;本地不在则确认远端存在;远端也没有才重新下载

四、三大工程问题

4.1 大视频反复失败

2.1GB 的基因定位视频反复断连。curl 无断点续传导致每次从头下。

解法:专项脚本 course_resume.py 用 curl -C - 断点续传 + 5 次重试:

1
2
cmd = ['curl', '-skL', '-C', '-', '--retry', '3', '--retry-delay', '3',
'--max-time', str(DOWNLOAD_TIMEOUT), '-o', local_path, url]

实测:从 517MB 断点续传到 2.1GB 成功(curl=18 服务器断连后自动续传)。

4.2 脚本崩溃(FileNotFoundError)

download_file 中 curl 失败删残文件后未 return,继续执行 os.path.getsize 崩溃:

1
FileNotFoundError: [Errno 2] ... 反证法.mp4

解法:补存在性检查,失败立即返回 False(这是基础 bug,但直到真实跑批才暴露)。

4.3 用户中途变更需求

「语文、英语、化学的课程不要继续下载了,下了的打包成压缩包」——脚本加 SKIP_SUBJECTS 集合:

1
2
SKIP_SUBJECTS = {"语文", "英语", "化学"}
todo = [c for c in manifest if c['subject'] not in SKIP_SUBJECTS]

重启进程自动跳过 163 门。已下内容保留,未下三科不再下。

五、补跑机制

5.1 失败课程重跑

错误按课程记录(errors dict),补跑只移除失败课程的 done_courses 标记,保留 done_files(已完成文件跳过):

1
2
3
4
5
6
retry_ids = {cid for cid in errors if cid_subj.get(cid) not in SKIP_SUBJECTS}
for cid in retry_ids:
if cid in p['done_courses']:
p['done_courses'].remove(cid)
p['errors'].pop(cid, None)
os.execvp('python3', ['python3', 'course_download.py']) # 直接重跑下载器

5.2 最终结果

  • 数学 117/117、物理 34/34、生物 31/31 全量完成
  • 语文 41/56、英语 34/46、化学 27/61(按用户要求停止)
  • 706 文件上传 OSS 备份桶
  • 剩余 18 门错误全部是停科目(按用户要求不再下载)

六、OSS 跨桶迁移

6.1 需求

「把下载好的全部转移到归档桶里面,我现在给了你两个桶的权限(在归档桶账号里面),直接同区域复制即可」

6.2 两轮方案

  1. 下载→上传中转(初始,无跨桶权限时):30GB 走本地网络中转,约 60 对象/分钟
  2. 服务端复制(拿到权限后):copy_object 同区域秒级复制,不占本地带宽——17 分钟复制 86.6GB

6.3 服务端复制实现

1
2
3
4
5
6
7
8
9
10
DST = oss2.Bucket(oss2.Auth(DST_AK, DST_SK), 'http://oss-cn-<region>-internal.aliyuncs.com', 'archive-bucket')
for obj in oss2.ObjectIterator(SRC, prefix='courses/'):
try:
head = DST.head_object(key) # 幂等: 已存在且大小一致跳过
if head.content_length == obj.size:
skipped += 1
continue
except oss2.exceptions.NoSuchKey:
pass
DST.copy_object(SRC.bucket_name, key, key)

关键细节:

  • 同区域内网 endpoint 直连(oss-cn-<region>-internal)
  • 幂等:目标已存在且大小一致自动跳过,重跑安全
  • 源桶用 default 凭据,目标桶用子账号凭据

6.4 对账验证

逐学科核对源/目标对象数与字节数:

1
2
3
语文 103 ✅  数学 290 ✅  英语 77 ✅
物理 82 ✅ 化学 72 ✅ 生物 100 ✅
总计 724 对象 / 86.6GB 全一致

首次发现数学缺 4、生物缺 2(复制期间新文件刚上传的时序差),幂等重跑补齐(复制 5 + 复制 2),失败 0。

七、配套 skill:oss-data-management

  • OSS 桶管理:个人资料桶 / 备份桶 / 量化数据桶 / 归档桶(多桶统一管理)
  • 凭证规范:~/.hermes/.env(systemd 注入)或 ~/.oss_credentials(ini,cli 工具用)
  • 数据路径映射表:量化数据本地 ↔ OSS 的对应关系
  • 上传下载 / 访问追踪 / 冷数据归档

关键教训(skill 内记录):OSS 凭据节名要查脚本确认(误用业务桶名当凭据节名得 0 对象,改 ‘default’ 才通)。

八、工程经验

  1. 断点续传是数据工程的命门:大文件传输必须 -C -,否则永远在下最大的那个
  2. 幂等是重跑的底气:大小校验 + 标记跳过,随便重跑不重复
  3. 服务端复制优先:同区域 copy_object 秒级且零带宽,别傻下载
  4. 对账不能省:对象数 + 字节数双指标,缺一不可
  5. 需求变更要留接口:SKIP_SUBJECTS 集合让「停三科」一行配置生效
  6. 日志要防二进制污染:下载 mp4 时 stdout 被内容污染(\u0000 垃圾),日志重定向 + grep 过滤
  7. 基础 bug 也会爆:getsize 前没查 exists——真实跑批才能暴露
赞助喵
非常感谢您的喜欢!
赞助喵
分享这一刻
让朋友们也来瞅瞅!