
douyin-downloader 博主主页按时间提前停页设计post 模式分页优化实战解析【免费下载链接】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抖音批量下载工具中博主主页按发布时间下载的提前停页time-range early stop优化展开讲解在保证结果集合与增量语义完全不变的前提下如何将 3000 作品主页的翻页请求从约 150 页压缩到 23 页。读者将掌握日期范围左闭右开契约的修正方式、纯状态机分页边界确认算法的设计思路、post模式与现有筛选/回补逻辑的组合规则以及配套测试与验收标准的完整落地路径。本文以 2026-08-25-post-time-range-early-stop-design.md 设计文档为核心骨架并结合仓库内已落地的源码实现进行深化佐证。一、问题背景与优化动机在引入该优化之前博主主页作品下载采用先完整拉取分页再统一应用start_time/end_time过滤的两段式流程无论用户设置的时间范围多窄程序都会把博主主页的所有分页每页 20 条全部请求完再在内存中做时间过滤。设计文档给出了一个典型的量化场景见 设计文档一个拥有约 3000 个作品的主页完整分页需要约 150 页即便用户只想下载最近几天、最终可能只有一个作品命中仍然要发出约 150 次翻页请求翻页耗时可能达到数分钟且大量请求徒增触发风控的风险。本次优化的范围被严格限定在博主主页的作品模式post。喜欢like、合集mix、音乐music、收藏collect及收藏夹合集collectmix没有可靠的全局发布时间顺序无法安全提前停止因此继续保持完整收集后过滤的既有行为。这一点在 设计文档 中明确声明并在 实施计划 的 Global Constraints 中作为硬性约束Implement only author-homepagepost; do not changelike,mix,music,collect, orcollectmix。二、目标与非目标优化必须满足的四条边界设计文档为本次改动划定了清晰的目标设计文档设置起始日期时在确认分页已经越过时间下界后停止继续请求旧作品——这是核心收益点除修正结束日期的既有契约偏差外提前停页只减少请求不改变完整遍历本应得到的作品集合——即结果必须与完整遍历后过滤严格一致保持现有增量下载语义时间范围决定检查范围范围内是否下载仍由磁盘文件状态决定不增加用户开关不引入新的 YAML、HTTP API 或持久化字段。非目标同样值得强调设计文档不优化其他四种模式不保存或复用博主分页 cursor、不建立数据库增量检查点不使用 SQLite 下载历史或最新作品时间来决定是否继续翻页不改变浏览器回补的滚动与作品详情补全策略不改变单条作品、合集链接、音乐链接和直播链接的下载行为。这些约束在仓库实现中得到完整贯彻提前停页的全部逻辑只存在于core/user_modes/post_time_boundary.py纯状态机与core/user_modes/post_strategy.pypost 策略整合两个文件中BaseDownloader只新增了统一的时间边界解析方法未触碰任何配置结构。三、日期范围语义左闭右开与结束日期契约修正3.1 语义定义日期继续按运行环境的本地时区解释范围采用左闭右开区间设计文档[起始日期 00:00:00, 结束日期次日 00:00:00)具体规则起始日期当天的作品包含在内左闭结束日期当天的作品全部包含在内因为右界是次日零点只设置起始日期时没有时间上界只设置结束日期时仍需从最新作品向后查找不能提前停页未设置起始日期时提前停页功能关闭。3.2 结束日期契约偏差的修正设计文档指出一个既有 bug设计文档现有后端把结束日期解析为当天00:00:00即结束日 00:00:00 之后的作品全部被排除这与 UI结束日期含当日 23:59:59的说明不一致。本次在共享时间过滤逻辑中一并改为次日零点前使 UI、CLI 和实际结果一致。仓库中该修正落地在core/downloader_base.py的_time_range_bounds()方法core/downloader_base.pydef _time_range_bounds(self) - Tuple[Optional[int], Optional[int]]: start_time self.config.get(start_time) end_time self.config.get(end_time) start_ts ( int(datetime.strptime(start_time, %Y-%m-%d).timestamp()) if start_time else None ) end_ts None if end_time: end_date datetime.strptime(end_time, %Y-%m-%d) timedelta(days1) end_ts int(end_date.timestamp()) # per-job 增量窗口(订阅自动下载注入;语义:min 排除等于、max 保留等于, # 与 _filter_by_time 的「start 保留等于 / end 排除等于」拼合后正好是 # (watermark, window_top] 区间)。0/缺失 不启用。 min_ct int(self.config.get(min_create_time, 0) or 0) if min_ct 0: start_ts max(start_ts or 0, min_ct 1) max_ct int(self.config.get(max_create_time, 0) or 0) if max_ct 0: end_ts min(end_ts, max_ct 1) if end_ts else max_ct 1 return start_ts, end_ts可以看到end_time通过timedelta(days1)向后推一天从而实现了结束日期次日零点的独占上界。此外该方法还叠加了订阅自动下载场景注入的min_create_time/max_create_time每任务窗口语义为 min 排除等于、max 保留等于与_filter_by_time的start 保留等于 / end 排除等于拼合后正好形成(watermark, window_top]区间——这一细节由 tests/test_time_window_overrides.py 验证min_create_time1700000000映射为1700000001加一实现排除下界max_create_time1700000500映射为1700000501加一实现保留上界。对应的最终过滤保护_filter_by_time()core/downloader_base.pydef _filter_by_time(self, aweme_list: List[Dict[str, Any]]) - List[Dict[str, Any]]: start_ts, end_ts self._time_range_bounds() if start_ts is None and end_ts is None: return aweme_list filtered: List[Dict[str, Any]] [] for aweme in aweme_list: create_time aweme.get(create_time, 0) if start_ts is not None and create_time start_ts: continue if end_ts is not None and create_time end_ts: continue filtered.append(aweme) return filtered设计文档要求分页判断和最终过滤使用相同边界设计文档_time_range_bounds()正是这个统一出口分页逻辑通过它取start_ts做提前停页判断_filter_by_time()通过它做最终正确性裁剪二者天然一致。日期边界行为由 tests/test_time_range_filter.py 覆盖关键断言包括start_time2026-08-19、end_time2026-08-20时边界为(2026-08-19 00:00:00, 2026-08-21 00:00:00)结束日2026-08-20 23:59:59的作品被保留、次日00:00:00的作品被排除只设置结束日期时结束日当天的作品仍完整保留。四、分页算法保守的边界确认策略4.1 基本原则顺序不是契约post正常 API 分页通常按发布时间从新到旧返回但设计文档明确指出该顺序不是仓库中的正式接口契约设计文档。因此遇到第一条旧作品就立即停止是不可接受的——必须先验证已观察到的顺序并在越过边界后再请求一页作为确认confirmation page以排除乱序、缺失时间戳等异常导致漏作品的可能。4.2 每页处理顺序设计文档规定每页处理遵循如下顺序设计文档请求并标准化当前页保留现有空页、超时和 cursor 停滞检测将当前页作品加入已收集列表最终结果仍经过现有置顶、日期、媒体类型和数量过滤用置顶策略、日期与媒体类型过滤后的候选作品数量判断number.post是否已满足从当前页取出非置顶作品的有效create_time检查页内及跨页是否保持非递增顺序记录是否已经观察到早于start_time的普通作品并按下述确认规则决定是否继续。关键点置顶作品永远不参与顺序验证和停页判断——它们仍按现有设置决定是否下载、仍受日期范围过滤但只作为候选内容不作为排序证据。这是因为置顶作品在主页布局中固定在顶部其时间戳与自然分页顺序无关。4.3 边界确认规则设计文档定义了完整的边界确认流程设计文档第一次观察到普通作品早于start_time的页面称为边界页boundary page。边界页中的范围内作品必须正常保留不能先停后筛——即先收集再判断绝不丢弃本应在范围内的作品如果边界页仍有下一页再请求一页作为确认页确认页的所有非置顶作品都有有效时间、全部早于start_time且整体顺序继续保持非递增时正常停止分页确认页没有非置顶作品时缺少足够的排序证据关闭时间早停并继续完整遍历如果边界页已经是最后一页则按 API 自然结束无需额外确认如果确认页重新出现不早于start_time的作品该任务视为跨页乱序time-range re-entry关闭时间早停并继续完整遍历。该策略最多多请求一个确认页。若时间边界位于前两页3000 个作品的主页通常只需检查 2 至 3 页而不是约 150 页——这是整个优化的核心收益模型。4.4 顺序与时间异常一律降级为完整遍历以下任一情况都会关闭当前任务的时间早停后续恢复现有完整分页设计文档用于判断的非置顶作品缺少有效create_time缺失或非法值同一页内出现发布时间回升页内乱序当前页第一条普通作品比上一页最后一条普通作品更新跨页乱序确认页重新出现时间戳不早于start_time的作品时间范围回插。设计原则是无法证明安全时一律完整遍历关闭优化不会丢弃已经收集的作品也不会改变最终过滤规则只是后续分页退回原有行为。这种保守降级策略保证了正确性永远优先于性能。五、核心实现剖析纯状态机 PostTimeBoundary提前停页的核心是仓库中新增的core/user_modes/post_time_boundary.py——一个纯状态机模块无 I/O、无网络、无下载逻辑便于独立测试与双仓同步。完整实现如下core/user_modes/post_time_boundary.pyfrom __future__ import annotations from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional dataclass(frozenTrue) class TimeBoundaryDecision: should_stop: bool False boundary_reached: bool False degraded_reason: Optional[str] None class PostTimeBoundary: def __init__(self, start_ts: Optional[int]): self._start_ts start_ts self._enabled start_ts is not None self._last_timestamp: Optional[int] None self._boundary_seen False def observe_page( self, items: List[Dict[str, Any]], *, is_pinned: Optional[Callable[[Dict[str, Any]], bool]] None, ) - TimeBoundaryDecision: if not self._enabled: return TimeBoundaryDecision() regular [item for item in items if not self._is_pinned(item, is_pinned)] if not regular: reason confirmation_page_without_regular_items if self._boundary_seen else None return self._degrade(reason) if reason else TimeBoundaryDecision() timestamps self._timestamps(regular) if timestamps is None: return self._degrade(missing_or_invalid_create_time) if self._boundary_seen and any(value self._start_ts for value in timestamps): return self._degrade(time_range_reentry) if self._order_increased(timestamps): return self._degrade(time_order_increased) self._last_timestamp timestamps[-1] if self._boundary_seen: return TimeBoundaryDecision(should_stopTrue, boundary_reachedTrue) self._boundary_seen any(value self._start_ts for value in timestamps) return TimeBoundaryDecision(boundary_reachedself._boundary_seen) staticmethod def _is_pinned(item, checker) - bool: if checker is not None: return checker(item) value item.get(is_top) if isinstance(value, str): return value.strip().lower() in {1, true, yes, on} return bool(value) staticmethod def _timestamps(items: List[Dict[str, Any]]) - Optional[List[int]]: timestamps: List[int] [] for item in items: try: value int(item.get(create_time)) except (TypeError, ValueError): return None if value 0: return None timestamps.append(value) return timestamps def _order_increased(self, timestamps: List[int]) - bool: if any(current previous for previous, current in zip(timestamps, timestamps[1:])): return True return self._last_timestamp is not None and timestamps[0] self._last_timestamp def _degrade(self, reason: str) - TimeBoundaryDecision: self._enabled False return TimeBoundaryDecision(degraded_reasonreason)5.1 状态机内部状态_start_ts起始日期零点时间戳为None时整个状态机失活对应未设置起始日期时提前停页功能关闭_enabled是否仍允许提前停页任何一次异常降级都会将其置为False且不可恢复一次性降级避免每次翻页重复刷告警日志_last_timestamp上一页最后一个普通作品的时间戳用于跨页顺序校验_boundary_seen是否已经观察到早于start_time的普通作品即是否已越过边界页。5.2 一次 observe_page 的决策流未启用直接返回空决策should_stopFalse用is_pinned回调过滤掉置顶作品得到regular列表若无普通作品若已在边界之后判定confirmation_page_without_regular_items降级确认页缺少排序证据否则视为正常页提取普通作品的create_time时间戳列表任何缺失/非法/非正数都触发missing_or_invalid_create_time降级已在边界之后_boundary_seenTrue时若当前页出现任何 start_ts的作品触发time_range_reentry降级确认页重新进入时间范围校验页内非递增与跨页非递增异常触发time_order_increased降级更新_last_timestamp若已在边界之后且全部早于start_time返回should_stopTrue, boundary_reachedTrue——这就是确认页的通过条件否则检查本页是否首次越过边界更新_boundary_seen并返回。注意实现中的两个细节与设计文档的对应关系确认页通过条件为所有非置顶作品都早于start_tsall语义且在此之前先做了time_range_reentry检查——任何一条回插都会先于顺序检查被捕获并降级边界页恰好是最后一页时boundary_reachedTrue但should_stopFalse由策略层在_page_stop_decision中按自然结束、非受限处理对应设计文档如果边界页已经是最后一页则按 API 自然结束无需额外确认的规则。5.3 状态机测试验证状态机的行为由 tests/test_post_time_pagination.py 中一组纯单元测试逐条验证test_boundary_requires_one_all_old_confirmation_page新页 → 混合边界页 → 全旧确认页第三步才返回should_stopTrue验证必须有一整页全旧证据的保守策略test_old_pinned_item_does_not_start_boundary旧置顶不触发边界验证置顶不参与排序证据test_missing_time_disables_boundary_once时间缺失触发missing_or_invalid_create_time且随后再次调用不再重复返回 reason一次性降级test_in_page_or_cross_page_time_increase_disables_boundary页内回升300→310与跨页回插上页末 250本页首 260都返回time_order_increasedtest_confirmation_page_without_regular_items_disables_boundary确认页全是置顶时返回confirmation_page_without_regular_itemstest_confirmation_page_reentry_disables_boundary_with_specific_reason确认页出现回插时返回time_range_reentry且降级后不再触发。六、策略层整合post_strategy.py 的调用链提前停页真正生效的地方在core/user_modes/post_strategy.py的PostUserModeStrategy._collect_api_items()core/user_modes/post_strategy.py。该方法是 post 模式专用的分页循环与基类_collect_paged_awemecore/user_modes/base_strategy.py不同它在循环体内嵌入了时间边界观察与候选计数。6.1 分页循环的完整流程async def _collect_api_items(self, sec_uid: str, user_info: Dict[str, Any]) - _PostPageResult: aweme_list: List[Dict[str, Any]] [] max_cursor raw_items_seen page_number candidate_count 0 number_limit int(self.downloader.config.get(number, {}).get(self.mode_name, 0) or 0) time_boundary self._time_boundary_for_config() self.downloader._progress_update_step(拉取作品列表, 分页抓取中) while True: request_cursor max_cursor page_number 1 page_data await self._request_post_page( sec_uid, request_cursor, page_numberpage_number, collected_countlen(aweme_list), ) if page_data is None: return aweme_list, True page self._normalize_page_data(page_data) page_items self.select_items(page) raw_page_count self._append_page_items(page, aweme_list) if raw_page_count 0: return aweme_list, self._empty_page_is_restricted(page, request_cursor) raw_items_seen raw_page_count candidate_count self._count_page_candidates(page_items) has_more bool(page.get(has_more, False)) max_cursor int(page.get(max_cursor, 0) or 0) time_decision self._observe_time_boundary(time_boundary, page_items, page_number) limit_reached number_limit 0 and candidate_count number_limit should_stop, pagination_restricted self._page_stop_decision( has_morehas_more, next_cursormax_cursor, request_cursorrequest_cursor, limit_reachedlimit_reached, time_boundary_confirmedtime_decision.should_stop, time_boundary_reachedtime_decision.boundary_reached, raw_page_countraw_page_count, raw_items_seenraw_items_seen, user_infouser_info, ) if should_stop: if time_decision.should_stop and not pagination_restricted: self._report_time_boundary_stop(page_number, raw_items_seen) return aweme_list, pagination_restricted6.2 关键辅助方法初始化边界_time_boundary_for_config——通过getattr探测_time_range_bounds使不包含该私有方法的轻量测试替身自然关闭提前停页core/user_modes/post_strategy.pydef _time_boundary_for_config(self) - PostTimeBoundary: bounds_getter getattr(self.downloader, _time_range_bounds, None) start_ts bounds_getter()[0] if callable(bounds_getter) else None return PostTimeBoundary(start_ts)观察边界并记录降级_observe_time_boundary——以原始page_items观察而非已过滤的列表置顶判定复用downloader._is_pinned_aweme降级原因只记录一次 warningcore/user_modes/post_strategy.pydef _observe_time_boundary(self, boundary, page_items, page_number) - TimeBoundaryDecision: decision boundary.observe_page( page_items, is_pinnedgetattr(self.downloader, _is_pinned_aweme, None), ) if decision.degraded_reason: logger.warning( Post time early-stop disabled: page%s reason%s, page_number, decision.degraded_reason, ) return decision候选计数_count_page_candidates——这是数量上限正确性的关键必须统计经过置顶、日期、媒体类型三重过滤后的候选数而不是原始条数core/user_modes/post_strategy.pydef _count_page_candidates(self, items: List[Dict[str, Any]]) - int: filtered self._filter_pinned_items(items) filtered self.downloader._filter_by_time(filtered) return len(self._filter_by_media_type(filtered))停页决策_page_stop_decision——统一裁决 cursor 停滞、时间边界确认、数量上限与自然结束四种情形并正确区分正常时间结束与分页受限core/user_modes/post_strategy.pydef _page_stop_decision(self, *, has_more, next_cursor, request_cursor, limit_reached, time_boundary_confirmed, time_boundary_reached, raw_page_count, raw_items_seen, user_info) - Tuple[bool, bool]: if self._cursor_stalled(has_more, next_cursor, request_cursor): return True, True if time_boundary_confirmed: return True, False if has_more: return limit_reached, False if time_boundary_reached: return True, False ended_early raw_page_count _POST_PAGE_SIZE or self._profile_reports_more( user_info, raw_items_seen) if ended_early and not limit_reached: logger.warning( User post pagination may have ended early: fetched%s, profile_count%s, raw_items_seen, user_info.get(aweme_count)) return True, ended_early and not limit_reached注意其返回值(should_stop, pagination_restricted)的第二项——_PostPageResult的布尔值语义被明确保留为仅表示分页受限见实施计划 Task 3 Interfaces。collect_itemscore/user_modes/post_strategy.py正是依据这一布尔值决定是否触发浏览器回补只有pagination_restrictedTrue才会调用_recover_user_post_with_browser而正常的时间早停返回(True, False)从而不会误触发浏览器回补。正常停页上报_report_time_boundary_stop——输出设计文档约定的用户可见进度文案core/user_modes/post_strategy.pydef _report_time_boundary_stop(self, page_number: int, raw_items_seen: int) - None: detail f已到达起始日期提前结束翻页检查 {page_number} 页共 {raw_items_seen} 条 self.downloader._progress_update_step(拉取作品列表, detail) logger.info( User post pagination stopped at time boundary: pages%s raw_items%s, page_number, raw_items_seen)用户侧看到的典型输出即设计文档给出的示例设计文档已到达起始日期提前结束翻页检查 3 页共 60 条七、与现有筛选能力的组合规则7.1 结束日期只能决定过滤不能成为停页条件比end_time更新的作品不下载但仍需继续翻页直到进入目标范围或越过start_time。结束日期本身不能成为停页条件设计文档。原因很直接主页按时间从新到旧返回最新作品晚于结束日期排在前面若按结束日期停页会漏掉中间的范围内作品。7.2 数量上限只统计过滤后的候选number.post必须统计经过置顶、日期和媒体类型筛选后的候选作品不能因为前几页存在大量晚于结束日期的视频或不匹配的媒体类型就按原始条数提前结束并返回不足数量的结果设计文档。这正是上面_count_page_candidates的三重过滤链存在的意义。候选数量达到上限时可以沿用现有数量上限提前停止不必继续寻找时间边界最终过滤apply_filters见 core/user_modes/base_strategy.py仍负责裁剪为准确数量。实施计划还明确要求删除原先分页循环内的aweme_list[:number_limit]预裁剪切片将精确限量的职责完全交给apply_filters()。7.3 增量下载时间分页与磁盘去重相互独立时间早停与增量下载是两条正交的语义设计文档时间分页只决定哪些作品进入当前任务的检查范围incrementtrueconfig/default_config.py中increase.post默认True见 config/default_config.py时范围内存在有效主媒体的作品跳过缺失、被删除或为空的文件重新下载incrementfalse时范围内作品按现有支持模式进入覆盖下载SQLite 只保留历史与元数据用途不参与停页判断。由此推论用户想补齐很早以前删除的文件时仍需把起始日期设置到那个作品所在日期或取消日期下界——这与现有日期决定补齐范围的产品语义一致。7.4 浏览器回补刻意结束 ≠ 分页受限因时间边界主动结束是正常结果必须与分页受限区分不能触发浏览器回补设计文档。API 超时、cursor 停滞、疑似异常空页等现有受限场景仍走浏览器回补。首期不改浏览器滚动的时间早停一旦进入该兜底路径以完整性优先允许继续完整扫描。实现上这一区分正是通过_PostPageResult的布尔值完成时间早停返回pagination_restrictedFalse而超时_request_post_page返回None→(aweme_list, True)、空页_empty_page_is_restricted与 cursor 停滞_cursor_stalled均返回True。集成测试 tests/test_post_time_pagination.py 中用unexpected_browser占位符触发即AssertionError断言正常时间边界不得触发浏览器回补。八、错误处理与可观察性约定设计文档对日志与故障处理有明确约定设计文档时间早停降级记录 warning包含模式、页码和原因不记录 cookie、URL 查询参数或作品详情避免敏感信息泄漏正常早停记录 info并通过现有进度上报器显示页数和检查条数配置中没有有效起始日期时不创建早停状态也不增加额外日志状态机_enabledFalse时observe_page直接返回空决策零开销早停逻辑自身不得抛出下载失败无法证明安全时一律完整遍历。落地实现中降级 warning 形如Post time early-stop disabled: page2 reasontime_order_increasedcore/user_modes/post_strategy.py正常停页的 info 日志形如User post pagination stopped at time boundary: pages3 raw_items60。由于状态机一次性降级_enabledFalse不可恢复日志只记录一次降级原因避免持续刷屏——这正是设计文档日志只记录一次降级原因的要求。九、测试设计五类场景全覆盖设计文档规划了五类测试设计文档仓库中均已落地正常边界倒序分页包含较新页、混合边界页和全旧确认页断言只请求到确认页范围内作品完整且不触发浏览器回补——对应 tests/test_post_time_pagination.py 的test_post_stops_after_old_confirmation_page_without_browser_recovery其中downloader.api_client.calls [0, 1, 2]精确断言请求数assert any(提前结束翻页 in detail ...)断言进度文案置顶与异常顺序旧置顶不触发边界时间缺失、页内回升、跨页回插均关闭早停并请求到 API 自然结束——对应状态机单元测试同文件 L24-L69及集成测试test_missing_confirmation_time_degrades_and_scans_to_natural_endL200-L218断言calls [0, 1, 2]筛选组合比结束日期更新的作品不计入数量上限媒体类型不匹配的作品不计数满足过滤后数量上限时仍能正常提前结束——对应test_number_limit_counts_only_time_and_media_candidatesL221-L251构造 video/gallery 混合页验证只统计 gallery 候选日期边界起始日零点包含、结束日任意时刻包含、结束日次日零点排除只设置结束日期不启用时间早停——对应 tests/test_time_range_filter.py 三个用例与test_only_end_time_keeps_full_paginationL254-L266断言calls [0, 1]即完整翻页回归与共享语义未设置日期、非post模式、浏览器受限回补以及磁盘缺失文件补下载保持原行为两仓相关测试、全量 pytest 与 ruff 均通过。此外还有两个边界页为最后一页的场景test_boundary_on_last_page_ends_naturally_without_browser_recoveryL168-L182与test_full_sized_last_boundary_page_ends_without_browser_recoveryL185-L197——后者验证满页20 条其中 19 条在范围内的末页也能自然结束且不漏作品、不触发回补。集成测试的 Harness 模式值得借鉴通过object.__new__(UserDownloader)构造最小替身仅注入config、api_client_PagedAPI记录每次调用的 cursor、rate_limiter_NoopRateLimiter与progress_reporter_Reporter并用_page()工厂构造带has_more/max_cursor的模拟分页响应使测试完全可控且无需网络。运行相关测试的命令python -m pytest -q tests/test_post_time_pagination.py tests/test_time_range_filter.py tests/test_time_window_overrides.py ruff check core/user_modes/post_time_boundary.py core/user_modes/post_strategy.py core/downloader_base.py十、双仓同步与共享文件边界本项目与 Desktop 桌面版共享同一套 Python 后端逻辑见根目录 AGENTS.md本次改动涉及的所有文件都必须保持两仓字节级一致core/downloader_base.py新增_time_range_boundscore/user_modes/post_strategy.py整合停页逻辑core/user_modes/post_time_boundary.py新增纯状态机tests/test_time_range_filter.py、tests/test_post_time_pagination.py共享测试实施计划 Task 4 规定将新增的三个共享文件登记进 Desktop 的scripts/sync-to-cli.sh同步清单并通过cmp逐文件比较而非直接对 dirty 的主 CLI checkout 跑--check验证字节一致性最后在双仓分别跑全量python -m pytest tests/、ruff check .与 format 检查作为交付门禁。设计文档的代码边界小节设计文档同时强调不修改 Desktop 日期控件及 HTTP 请求字段因为现有 UI/API 已能传递所需日期——这也是不引入新 YAML、HTTP API 字段目标的延伸。十一、验收标准设计文档给出的验收标准设计文档可作为任何实现是否达标的判据对相同的有序模拟主页优化结果与完整遍历后过滤的作品 ID 集合一致时间边界位于前两页的 3000 作品场景请求数不超过边界页加一个确认页正常时间早停不会启动浏览器回补也不会显示分页异常任一排序或时间异常场景均不漏作品而是恢复完整遍历结束日期实际包含完整当天与界面说明一致Desktop 与 CLI 的共享实现保持一致相关测试、全量测试和 lint 通过。这些标准既约束了性能收益请求数上限也约束了正确性作品集合一致、异常场景不漏作品是只减请求、不改结果目标的量化表达。十二、后续扩展条件有序证据优先设计文档对扩展范围保持克制设计文档只有在获得真实接口证据并建立乱序回归样本后才考虑把相同优化扩展到like。二级展开的mix、music、collect和collectmix不复用本设计的全局时间早停——因为这些模式的作品没有可靠的全局发布时间顺序无法建立已越过时间下界的安全证据。这一结论也呼应了本设计最核心的方法论任何提前停止都必须建立在可验证的排序证据之上。PostTimeBoundary状态机正是这一方法论的工程化载体——用最少的额外请求至多一页确认换取可证明的结果一致性一旦证据链断裂立即退回完整遍历把正确性风险降到零。结语通过本文可以完整掌握 douyin-downloader 主页按时间提前停页的设计全貌从先全量拉取再过滤的性能痛点出发确立左闭右开日期契约 边界页确认 保守降级的算法骨架落地为PostTimeBoundary纯状态机与PostUserModeStrategy分页循环的清晰分层并用五类测试与严格验收标准保证请求数锐减但结果集合不变。相关实现均可在仓库对应源码中直接查阅验证状态机、策略整合、时间边界解析 以及两套共享测试 test_post_time_pagination.py 与 test_time_range_filter.py。【免费下载链接】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),仅供参考