
抖音批量下载器 douyin-downloader 存储层深度解析SQLite 去重历史、异步文件管理与元数据落盘【免费下载链接】douyin-downloaderA practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖音批量下载工具去水印支持视频、图集、合集、音乐(原声)。项目地址: https://gitcode.com/GitHub_Trending/do/douyin-downloader本指南围绕 douyin-downloader 项目中的storage模块展开系统讲解该模块如何通过 SQLite 数据库aiosqlite持久化下载历史、驱动增量下载与去重如何借助aiofiles构建文件目录与原子写入流程以及如何将作品元数据落盘为 JSON 与download_manifest.jsonl清单。读完本文你将掌握这三个组件的职责边界、核心 API、配置开关及其在批量下载流水线中的真实调用链并能直接对照仓库源码继续深入。一、存储层在项目中的定位与整体架构douyin-downloader 是一个基于 Python 3.8、全异步 I/O 的抖音批量下载工具支持视频、图集、合集、音乐/原声其代码按职责划分为auth、cli、config、control、core、storage、utils等目录见 AGENTS.md。其中storage目录承载数据持久化与文件管理这一横切能力是去重、增量下载和历史追溯的地基。storage包对外只暴露三个类见 storage/init.py类文件职责Databasestorage/database.py异步 SQLite 封装下载历史、增量模式所需的作者最新作品时间戳FileManagerstorage/file_manager.py路径构造、目录创建、重复检测、文件写入MetadataHandlerstorage/metadata_handler.py从作品数据中提取并持久化元数据作者、描述、时间戳等storage/AGENTS.md明确了三条关键设计约束是整个模块的灵魂Database是可选组件由配置中的database: true开关控制是否启用Database.get_latest_aweme_time()是增量下载功能的底层支撑所有文件 I/O 一律使用aiofiles保持异步、非阻塞。依赖关系内部依赖 utils/helpers.py时间戳解析、文件大小格式化与 utils/validators.py文件名清洗外部依赖aiosqlite异步 SQLite 访问与aiofiles异步文件读写全项目 I/O 统一异步aiohttp、aiofiles、aiosqlite核心路径严禁阻塞式 I/O见 AGENTS.md。二、Database异步 SQLite 封装与去重增量核心Database类通过aiosqlite提供非阻塞的 SQLite 访问。默认数据库文件为dy_downloader.db可通过配置项database_path覆盖。2.1 连接管理与延迟初始化Database.__init__只记录路径真正的连接创建被推迟到首次_get_conn()调用时并在当时的 event loop 上才创建asyncio.Lockdatabase.py#L54-L71。这一设计避免了在__init__阶段抢到错误 loop 的问题——这是 asyncio 项目中常见的坑。async def _get_conn(self) - aiosqlite.Connection: if self._conn is not None: return self._conn if self._conn_lock is None: self._conn_lock asyncio.Lock() async with self._conn_lock: if self._conn is None: self._conn await aiosqlite.connect(self.db_path) return self._conninitialize()是幂等的重复调用是无害 no-op它完成三件事设置 PRAGMAjournal_modeWAL允许并发读写synchronousNORMAL避免每次提交都 fsync最多在断电时丢失最近几次事务对下载历史而言可接受见database.py#L79-L82建表CREATE TABLE IF NOT EXISTS执行增量迁移ALTER TABLE补列与一次性数据回填。2.2 核心表结构initialize()中创建四张表database.py#L84-L154aweme单条作品记录。字段包括aweme_id唯一、aweme_type、title、author_id、author_name、author_sec_uid、create_time、download_time、file_path、metadata、cover_urls、job_id。metadata是完整的 aweme 详情 JSON单条可达 50~300KBcover_urls是排序后的封面镜像数组 JSON。download_history每次一次下载任务的汇总记录url、url_type、download_time、total_count、success_count、config整份配置 JSON。transcript_jobWhisper 转写任务状态表UNIQUE(aweme_id, video_path, model)记录转写文本路径、JSON 路径、状态、跳过原因与错误信息。job任务中心 Job 的持久化记录只写入终态success/failed/cancelledoverrides、last_retry_summary、retry_history以 JSON 文本存储服务重启后任务状态不丢失。每张表都配了必要的索引idx_aweme_id、idx_author_id、idx_download_time、idx_transcript_*、idx_job_created_at、idx_job_status。2.3 增量迁移重复运行 initialize 是安全 no-op为了兼容旧版本遗留的数据库文件initialize()通过PRAGMA table_info探测列是否存在缺失则ALTER TABLE补齐database.py#L168-L225包括aweme.author_sec_uid2026 增量迁移job.retry_history为旧库补列旧行映射为NULL - []aweme.cover_urls新增列时先ALTER并立即 commit再用键集分页回填——每次取 500 行解析metadata中的video.cover.url_list避免一次性全表加载导致大库内存飙升aweme.job_id。这种探测-补列-回填的迁移策略保证了升级后旧库文件继续可用。2.4 去重判定is_downloaded 的严格语义is_downloaded(aweme_id)并非简单行存在即已下载而是要求file_path非空SELECT id FROM aweme WHERE aweme_id ? AND file_path IS NOT NULL AND file_path ! 原因在源码注释中写得很清楚桌面端 sibling 的 my-content 同步可能插入file_path为空的投影行若把这些行当作已下载like 模式的增量批次会在第一条空行处错误停止database.py#L230-L242。2.5 保留式 Upsert元数据更新、下载产物不丢add_aweme/add_aweme_batch使用INSERT ... ON CONFLICT(aweme_id) DO UPDATE且是保留式preserving语义database.py#L249-L338元数据类字段aweme_type、title、author_id、author_name、author_sec_uid、create_time在传入值非空时才覆盖下载产物file_path、metadata、download_time、cover_urls、job_id在 upsert 未携带时才保留旧值——这保证桌面端同步来的空投影永远不会清掉已下载行的落盘信息。2.6 增量下载基石get_latest_aweme_timeSELECT MAX(create_time) FROM aweme WHERE author_id ? AND file_path IS NOT NULL AND file_path ! 与is_downloaded相同的仅统计已下载行规则未落盘的行不会污染作者增量基线database.py#L340-L350。批量下载作者主页时用该时间戳与最新作品对比即可实现只下新增的增量模式。2.7 历史查询与统计能力get_aweme_history(...)database.py#L371-L467分页查询下载历史默认按download_time DESC可选按create_time排序支持作者名/标题大小写不敏感的子串模糊匹配通过_escape_like转义%、_、\避免用户搜索100%时被当成通配符、author_sec_uid/job_id精确匹配、create_time区间过滤、aweme_type过滤。返回结果统一把cover_urlsJSON 解析回字符串数组。get_aweme_count_by_author统计某作者已下载条数。get_top_authors(days, limit)统计近 N 天下载最多的作者排行按COUNT(*) DESC, author_sec_uid ASC稳定排序保证属性测试确定性作者名取该 sec_uid 最近一次非空昵称空则回退中文占位符未知作者database.py#L475-L521。delete_aweme_by_ids/truncate_history删除指定 aweme 行 / 清空aweme与download_history不动磁盘文件与transcript_job。删除采用每 500 个 id 分块、去重后再分批DELETE ... IN规避 SQLite 历史上 999 个宿主参数上限并用cursor.rowcount精确计数。2.8 转写任务与任务中心持久化upsert_transcript_job/get_transcript_job维护 Whisper 转写状态database.py#L523-L595默认模型为gpt-4o-mini-transcribeupsert_job/load_terminal_jobs/delete_jobs负责任务中心 Job 的持久化只落终态、读取时按created_at DESC返回并对 JSON 字段做防御性解析坏数据回退为None/[]。三、FileManager异步文件路径构造与下载FileManager默认根目录为./Downloaded构造时可传入base_path负责目录规划与媒体写入。项目默认下载目录配置为path: ./Downloaded/见 config/default_config.py。3.1 作者目录风格author_dir_AUTHOR_DIR_STYLES定义了四种作者级目录命名方式file_manager.py#L134-L136与配置author_dir一一对应行为矩阵在_compose_author_dir中实现file_manager.py#L228-L296style目录名说明nicknamesanitize_filename(作者昵称)默认直观但重名会合并、改名会分裂sec_uidsanitize_filename(sec_uid)稳定唯一但不直观nickname_uid昵称_sec_uid直观 唯一user_sec_uiduser_原始 sec_uid复刻 legacy DouYin-Downloader 布局使用保留下划线的_sanitize_sec_uid_token不折叠连续下划线、不截断以匹配用户已有的user_MS4...__...目录任何未知 style 或缺少 sec_uid 的情况都回退到 nickname 并打 WARNING 日志绝不抛异常——配置错误必须降级为仍然能下载而不是硬失败。3.2 get_save_path目录层级规划get_save_pathfile_manager.py#L159-L226计算并创建目标目录完整层级为base_path / author_dir / [mode] / [collection_dir] / [leaf]group_by_modeTrue默认时在作者目录下插入模式层post/like/mix…False则去掉模式层文件直接落在作者目录复刻 legacy 布局无POST文件夹collection_dir非空时再插入一层合集目录base/author/mix/collection/leaf目录名会先经sanitize_filenamefolderstyleTrue时使用folder_name由utils.naming.render_template预渲染的叶子目录名未提供时回退到历史组合{date}_{title}_{id}。3.3 命名模板系统叶子目录/文件名由模板渲染生成模板白名单见 utils/naming.pyid、title、author、author_id、date、year、month、day、time、 hour、minute、second、timestamp、type、mode默认模板{date}_{title}_{id}naming.py#L36-L37缺失变量渲染为空字符串模板长度上限 200渲染结果最长 80 字符最终统一经sanitize_filename清洗。配置文件里的对应项为filename_template与folder_templatedefault_config.py。3.4 download_file媒体流式下载的健壮性设计download_filefile_manager.py#L310-L402负责真正把媒体拉回磁盘是一套经过线上问题打磨的下载引擎流式分块写入块大小_DOWNLOAD_CHUNK_BYTES 256KB注释说明 8KB 会让单流吞吐受限于 chunk 往返开销视频动辄几十 MB256KB 是内存与吞吐的平衡三重超时total300s、connect15s、sock_read60s——connect 单独设限是因为/aweme/v1/play/302 后可能落到打不通的 PCDN 节点握手黑洞没有 connect 上限会耗尽 total慢节点地板_ThroughputGuard以滚动窗口30s 窗口内平均吞吐 20KB/s 即抛SlowDownloadError判定而不是累计均值——累计均值会被开头很快、后面滴水的 PCDN 节点骗过原子写入先写*.tmp临时文件os.replace原子改名任何中断慢节点放弃、单条超时、用户取消都清理.tmpCancelledError属于BaseException因此清理逻辑用except BaseException而非Exceptionfile_manager.py#L443-L457大小校验Content-Length与实写字节数不符则删除临时文件返回失败206 分段响应仅当Content-Range是完整区间start0且end1total时才接受aiohttp 403 回退 httpx抖音图片 CDN 会对 aiohttp 的 TLS 指纹返回 403如biz_tagpcweb_cover封面此时自动改用 httpx 重试httpx 的 TLS 指纹可被 CDN 接受httpx 路径只在Content-Encoding缺失时才信任Content-Length自动解压会改变实际大小进度回调节流on_progress每 2s 最多触发一次避免批量任务事件流刷屏小文件秒下不回报进度只有中途报过进度的下载才在收尾补发 100%避免白白占用事件流回放窗口。3.5 其他工具方法file_exists要求文件存在且大小 0get_file_size对缺失文件返回 0两者对 OSError 都容错。四、MetadataHandler元数据提取与清单落盘MetadataHandler职责单一负责把结构化元数据写成 JSON 文件并把每次下载追加进统一的 JSONL 清单storage/metadata_handler.py。4.1 save_metadataasync def save_metadata(self, data, save_path) - bool: async with aiofiles.open(save_path, w, encodingutf-8) as f: await f.write(json.dumps(data, ensure_asciiFalse, indent2))以 UTF-8 写入、ensure_asciiFalse保留中文异常被捕获并记 ERROR 日志后返回False调用方无需 try/except。在下载流水线中当配置json: true时每部作品会额外写出{file_stem}_data.json的完整 aweme 详情见 core/downloader_base.py评论采集也会复用该方法写出*_comments.json见 core/comments_collector.py。4.2 append_download_manifestJSONL 下载清单async def append_download_manifest(self, base_path, record) - bool: manifest_path base_path / download_manifest.jsonl normalized_record {recorded_at: datetime.now().isoformat(timespecseconds), **record} async with self._manifest_lock: async with aiofiles.open(manifest_path, a, encodingutf-8) as f: await f.write(json.dumps(normalized_record, ensure_asciiFalse)) await f.write(\n)每次成功下载都会向base_path/download_manifest.jsonl追加一行 JSONasyncio.Lock保证并发下载时不会互相交错写坏行。清单记录采用固定 schema见 core/downloader_base.py{ recorded_at: 2026-09-14T06:37:15, date: 2026-09-01, aweme_id: 7350000000000000000, author_name: 作者昵称, author_sec_uid: MS4wLjABAAAA..., author_url: https://www.douyin.com/user/MS4wLjABAAAA..., desc: 作品描述, media_type: video, tags: [], file_names: [7350000000000000000_video.mp4], file_paths: [./Downloaded/作者昵称/POST/2026-09-01_标题_7350000000000000000/7350000000000000000_video.mp4], publish_timestamp: 1725148800 }author_sec_uid字段固定存在未知时为author_url由 sec_uid 重建主页地址这样即便作者改了昵称或出现重名清单消费者也能靠 sec_uid 精确定位作者身份。该行为有专门的测试保障见 tests/test_manifest_author_fields.py。4.3 load_metadata读取 JSON 文件并解析异常时记 ERROR 日志并返回空 dict供后续读取已下载作品的元数据。五、在下载流水线中的真实调用链以单条视频下载为例存储层的三个组件在 core/downloader_base.py 的BaseDownloader中协同工作构造时注入FileManager、可选Database内部自建MetadataHandler见downloader_base.py#L65-L93路径规划self.file_manager.get_save_path(...)计算并创建目标目录downloader_base.py#L472增量判定_should_download先查磁盘本地索引再在启用redownload_missing_files时为False时查询self.database.is_downloaded(aweme_id)历史库查询失败时宁缺毋滥地补下绝不把作品永久误判为已下载downloader_base.py#L239-L267媒体下载self.file_manager.download_file(...)流式写入downloader_base.py#L848元数据落盘json: true时self.metadata_handler.save_metadata(aweme_data, json_path)downloader_base.py#L733入库self.database.add_aweme(record)或批量add_aweme_batch写入 aweme 表downloader_base.py#L750-L784记录含cover_urls封面镜像经order_cover_mirrors排序p3-*域名 403 概率高置后保序截断 3 个与job_id清单追加self.metadata_handler.append_download_manifest(base_path, record)写 JSONLdownloader_base.py#L807-L809。CLI 侧在 cli/main.py 中按配置启停数据库database None if config.get(database): db_path config.get(database_path, dy_downloader.db) or dy_downloader.db database Database(db_pathstr(db_path)) await database.initialize() # ... 下载循环 ... if database is not None: await database.close()下载结束后若启用了数据库还会调用database.add_history写入本次任务的汇总历史cli/main.py#L166-L174。DownloaderFactory.create()会把database、file_manager注入各个下载器见 core/downloader_factory.py因此视频、图集、合集、音乐、直播回放等所有下载器共享同一套存储语义如 core/music_downloader.py、core/live_replay_downloader.py 的调用方式与 BaseDownloader 一致。六、配置开关与实战建议存储层相关的核心配置项见 config/default_config.py配置默认值作用path./Downloaded/下载根目录即FileManager.base_pathdatabaseTrue是否启用 SQLite 历史库False时Database不创建去重退化为纯磁盘检查database_pathdy_downloader.dbSQLite 文件路径folderstyleTrue是否为每条作品创建独立子文件夹filename_template/folder_template{date}_{title}_{id}文件/目录命名模板变量白名单见utils/naming.pyauthor_dirnickname作者目录风格nickname/sec_uid/nickname_uid/user_sec_uid切换只影响后续下载不迁移已存在目录group_by_modeTrue是否在作者目录下按模式post/like/mix…再分一层redownload_missing_filesTrue磁盘文件缺失时是否重新下载False时若数据库存在有效下载记录file_path 非空则继续跳过increase.*各模式True各模式是否启用增量下载False强制重下并原子覆盖当前筛选范围jsonFalse是否额外保存每条作品的完整 JSON 元数据video/music/cover/avatarTrue/False×3各类内容的保存开关实战要点启用数据库做增量保持database: trueDatabase.get_latest_aweme_time()才能为 post/like 等模式提供增量基线避免重复下载目录风格按需选择追求直观选nickname担心重名合并选nickname_uid需要兼容旧版 DouYin-Downloader 目录选user_sec_uid保留下划线依赖清单做归档download_manifest.jsonl每行一条固定 schema 记录适合二次开发归档、统计、去重数据库文件可安全升级initialize()幂等且带增量迁移与回填旧版.db可直接复用。七、测试保障存储层行为有完善的测试覆盖可运行python -m pytest tests/验证tests/test_database.pyaweme 生命周期add_aweme→is_downloaded/get_latest_aweme_time断言、转写任务 upsert、历史查询过滤与分页等tests/test_database_history.py下载历史聚合行为tests/test_database_top_authors.py热门作者排行的确定性tests/test_database_migration.py旧库增量迁移与幂等性tests/test_file_manager.pyfile_exists/get_file_size/get_save_path目录创建、四种 author_dir 风格及缺失 sec_uid 的回退行为tests/test_manifest_author_fields.pydownload_manifest.jsonl固定携带作者 sec_uid 与主页 URLtests/test_download_progress_and_deadline.py 等下载超时、慢节点地板、进度节流相关。八、小结storage模块以三个类切分了持久化三件事Database用aiosqlite WAL 提供去重、增量与历史统计FileManager用aiofiles提供从目录规划作者风格/模式层/合集层/命名模板到流式下载三重超时/慢节点地板/原子写入/httpx 回退的完整落盘链路MetadataHandler提供 JSON 元数据与 JSONL 下载清单。三者共同支撑起 douyin-downloader 的跳过已下载、只下新增、可追溯、可归档的批量下载体验也是理解本项目数据流的关键入口。【免费下载链接】douyin-downloaderA practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖音批量下载工具去水印支持视频、图集、合集、音乐(原声)。项目地址: https://gitcode.com/GitHub_Trending/do/douyin-downloader创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考