15列Dreamstime CSV与上传追踪器:解决被拒和重复上传

用两个纯标准库脚本机械执行各平台 CSV 表头并维护上传台账,杜绝重复上传与手工出错。
作者因手工维护 Dreamstime 15 列 CSV 表头出错而被整批拒收,于是写下 `make_metadata.py`,让脚本按各平台的确切 schema(Adobe Stock 5 列、Vecteezy 4 列、Dreamstime 15 列)生成表头,不写多余列。真正的隐患不是被拒,而是重跑导致的重复上传——可能让账号被标记,因此又写了 `upload_tracker.py`,用纯文本台账在每次成功上传后立即标记 done,使崩溃后的重跑能从断点继续。两个脚本只用 Python 标准库,能在 1 vCPU / 2 GB 无 GPU 服务器上运行,配合固定文件夹结构与内存控制习惯,最终支撑了该项目 39 个产品、856 MB 输出的交付。
一个周日下午,我因为一封只有一句「Invalid CSV header」的拒绝邮件彻底报废了。当时手头有40个 Dreamstime 上传文件积压在队列里,而我手工搭建的表格列顺序错了。Dreamstime 的导入器不做任何猜测:它读取表头行,逐项比对预期的15列布局,只要有一列缺失或改名,整个文件直接失败。那一刻我停止手工编辑 CSV,动手写了 make_metadata.py。

如果你正在从同一条流水线向多个平台推送素材或模板,元数据格式和重跑问题一定比什么都更早找上你。以下是我修复它们时学到的东西。
每个平台列数固定,而且互不兼容
我提交的三个平台有三套不同的 schema,没有一个容忍多余的列:
- Adobe Stock 要求 5 列 CSV:标题、关键词、分类、授权文件、文件名。表头文字稍有偏差,整个上传就报错。
- Vecteezy 要求 4 列。字段更少,但对表头的严格程度毫不放松。
- Dreamstime 就是那个花掉我一个周日的 15 列格式。15 列,列名精确,顺序精确。
我写的第一个脚本试图耍小聪明,生成一个带超集列的「通用」CSV。结果在所有平台都失败。真正有效的做法反而很无聊:脚本知道每个平台要求的确切表头,只写那个表头,不写别的。make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv --ext .jpg 生成的文件首行与导入器检查的内容逐字节一致。没有多余列,没有重排字段,没有「善意」的额外添加。
一句实话:如果没有标题文件,脚本会把文件名转成标题格式并使用它。作为第一遍处理没问题,但在提交前你应该用真正的标题和关键词替换它。用通用文件名当标题,是通向质量类软拒绝的快车道,那种问题比硬性的表头失败更难排查。
重复上传比被拒更麻烦
CSV 错了会被拒,你修好,继续走。重复上传的批次则完全不同。因为重跑脚本,或者在你测试时定时任务被触发,上传任务跑了两遍,同一个素材就会上线两次。在素材平台上,这种行为足以让账号被标记,而被标记的账号远比一个被拒文件损失更大。
我的流水线按批次运行,批次被重跑是常态。这是事实。所以我写了第二个脚本 upload_tracker.py,维护一份简单的台账,记录已经发布过的内容。工作流只有三条命令:
python upload_tracker.py pending items.txt # 哪些还没完成
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
关键习惯是每次成功上传后立即标记 done,在下一个开始之前,而不是在批次末尾统一标记。如果任务在第23项崩溃,台账知道前22项已完成,重跑从第23项开始。这是一个纯文本台账,只用标准库,没有数据库可以损坏或迁移。这种简单性是特性,不是缺陷。台账必须比它追踪的东西更可靠。
两个脚本只靠 Python 标准库运行
这一点比听起来更重要。整条流水线跑在一台 1 vCPU、2 GB 内存、没有 GPU 的服务器上。你每加一个依赖,就是占用一份你没有的内存。make_metadata.py 和 upload_tracker.py 只用标准库,所以没有东西要安装,也没有东西在两次运行之间占着内存。整个工具包中只有两个第三方包:用于封面渲染的 Pillow 和用于 PDF 输出的 reportlab,它们住在你只在打包时才运行的独立脚本里。
同样的约束塑造了流水线的其他习惯:单核上一次只跑一个进程,每个素材处理后执行 gc.collect(),用 resource.getrusage(RUSAGE_SELF).ru_maxrss 测量峰值 RSS 而不是平均占用,只在检查而非交付时以 60 dpi 渲染预览。
resource.getrusage(RUSAGE_SELF).ru_maxrss 是 Python resource 模块暴露的进程资源查询接口,返回当前进程及已回收子进程的峰值常驻内存(RSS,单位在 Linux 上为 KB),而非某一瞬间的占用。它的价值在于:内存问题往往出现在短时突发(比如 Pillow 载入大图、reportlab 拼装 PDF),平均值会把这些尖峰摊平,而 ru_maxrss 记录的是历史最高水位,正好能抓到最危险的那一刻。在 1 vCPU / 2 GB 的服务器上,知道「峰值有没有逼近 OOM 线」比知道「平均用了多少」更有用。配合每次处理完素材后主动 gc.collect(),可以避免循环引用或大对象迟迟不释放导致的峰值堆积。用法很简单:import resource; print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),无需第三方依赖,符合整条流水线只用标准库的约束。
文件夹结构也是修复的一部分
这两个脚本对着一个杂物抽屉式的目录都没法好好工作。流水线假设的布局是固定的:assets/ 放原始输出,output/ 放可直接提交的文件和 CSV,packs/ 放完成的压缩包,research/ 放笔记和日志,scripts/ 放工具。把 make_metadata.py 指向 output/,它就能精确拾取真正准备好的文件。
我犯过一次、不会再犯的错误:在一次清理中删掉了 research/。那些抓取日志和笔记是后来三个产品的原材料。永远保留它。磁盘比重新做研究便宜。
「杂物抽屉式目录」在这里指所有东西——原始输出、可提交文件、压缩包、日志笔记、脚本——混在同一个目录里。它的问题不是不美观,而是不可判定:脚本无法仅凭文件名和扩展名可靠地区分「还没处理的草稿」和「已经可以提交的成品」。当 make_metadata.py 指向这样一个目录时,它可能把半成品也写进 CSV,于是你带着错误条目一起上传,在平台侧被拒后再回头排查,成本成倍上升。把目录按生命周期切分(assets 是输入,output 是可直接交付,packs 是最终打包,research 是知识与日志,scripts 是工具)之后,每个脚本只需要面对一种状态明确的文件,判断逻辑从「猜」变成「读取」。这也是为什么目录约定属于修复的一部分:它把一部分正确性从人的记忆转移到了文件系统本身。
我会告诉过去的自己
失败不是因为粗心。手工维护横跨40个素材的15列 CSV,这个流程在真实批次面前根本无法存活。修复方法是两个小脚本,用机械方式强制执行平台规则,而不是靠我去记住它们。元数据表头来自脚本,从不来自记忆。上传状态来自台账,从不来自「我记得好像传过了」。
这两个脚本,加上把 Markdown 文档转成样式化 PDF 的 md2pdf.py,以及把压缩包与 1280×720 封面和 600×600 缩略图一起打包的 pack_product.py,就是在这台小服务器上打包了该项目交付的39个产品、856 MB 输出的全部工具。
如果你本周的经历有哪部分听起来很熟悉,那这套工具就是从这个工作中长出来的。
相关推荐

零成本搭建AI超级员工系统:智能体自动工作流全解析
深度拆解B站演示的AI超级员工系统:Agent智能体编排、数字人口播、地图与企业获客、24小时智能客服及法务模块全覆盖。分析零成本开户背后的商业逻辑与合规风险,帮助创业者判断这套AI数字员工系统的真实投产比。

Chatbot与AI Agent的本质区别:从对话到执行的跨越
Chatbot和AI Agent有什么区别?本文基于Learn Tech频道的MCP系列讲解,用订机票、笔记摘要等实例说明:Chatbot只给答案,Agent通过思考-行动-观察循环真正完成任务,并拆解模型、工具、记忆、目标四大部件及MCP的桥梁作用,同时讨论Agent的适用边界与权限风险。

免费GitHub仓库:342小时AI课程,从数学到MCP服务器
一个免费GitHub仓库提供342小时AI课程,包含523节课、20个阶段,覆盖数学基础到Agent构建和MCP服务器。支持目标驱动学习,每节课产出可留存的prompt、skill或agent,已有近10万人开始学习。