feat(发布计划): 标题行两种写法都认 + 行首 # 不再无条件当注释

一、标题行解析(core/video_plan.parse_title_line)
- 原来只认「标题内容_手机号_日期_编号」(右锚定)。现在**两种都认**:
   ① 标题内容_手机号_日期_编号        (推荐,右锚定 —— 标题里带下划线也不会错配)
   ② 手机号_日期_编号_标题            (左锚定,跟视频文件名同序 = "文件名去掉扩展名 + 标题")
  两种里都**先试带编号的**,保证同一行永远只有一种解释;编号都可省略。
- 新拒收一条:`手机号_日期_1`(只有编号、没标题)**直接拒** ——
  不能把它当标题"1"(静默生成一条标题是"1"的文案,比拒收危险得多);
  标题真是纯数字的用写法①。

二、`#` 开头的行(parse_titles_text)
- 原来是"以 `#` 开头就整行忽略" → **`#中秋快乐_...` 这种正常标题会被静默丢掉**。
- 改成**先按标题行解析,解析得出就当标题;解析不出且以 `#` 开头才算注释**。
  `# 这是注释` 照样忽略,`#话题` 开头的标题照收(`#` 保留在标题里)。

三、其它
- 文案同步:上传标题面板/帮助文案、doc/API.md 的 upload_titles 语义(两种写法、`#` 规则、拒收条件)
- 测试:新增 8 条(两种写法 × 带/不带编号 × 标题含下划线与 #话题、只有编号要拒收、整段注释与报错)
This commit is contained in:
2026-09-29 09:00:43 +08:00
parent 5bedab370e
commit a800d051ae
6 changed files with 47 additions and 13 deletions
+39 -8
View File
@@ -162,32 +162,56 @@ def parse_video_name(filename):
def parse_title_line(line):
"""标题行 `标题内容_手机号_日期_编号` → (meta, error)。标题里可以带下划线。
"""标题行 → (meta, error)。**两种写法都认**,标题里都可以带下划线。
右锚定 + 逐级回退(**必须先试带编号的**,否则带编号的行会被当成
"标题里含日期"而错配):
A) …_手机号_日期_编号 B) …_手机号_日期
① 标题在前(推荐,右锚定):
A) 标题内容_手机号_日期_编号 B) 标题内容_手机号_日期
② 手机号在前(左锚定,跟视频文件名同序 —— "文件名去掉扩展名 + 标题"):
C) 手机号_日期_编号_标题 D) 手机号_日期_标题
为什么**先右后左**:右锚定是主格式(标题里带下划线也不会错配)。
左锚定的四种里先试带编号的 C(`手机号_日期_1_标题` —— 那个 `1` 当编号,
不当标题的开头),保证同一行永远只有一种解释。
什么时候两种都不会误判:右锚定要求"倒数第 3/2 段是手机号",左锚定要求
"第 1 段是手机号、第 2 段是日期" —— 一行同时满足两边时(例如 `手机号_日期_1`)
右锚定先命中,且标题为空会被下面的校验拒掉,不会静默发出一条错文案。
"""
t = (line or "").strip()
if not t:
return None, "空行"
parts = t.split("_")
phone = date = seq = None
title_parts = None
# ① 右锚定:先试带编号的,再试不带编号的
if len(parts) >= 4 and _SEQ_RE.match(parts[-1]) \
and _DATE8_RE.match(parts[-2]) and _PHONE_RE.match(parts[-3]):
seq = int(parts[-1])
phone, date8, title_parts = parts[-3], parts[-2], parts[:-3]
elif len(parts) >= 3 and _DATE8_RE.match(parts[-1]) and _PHONE_RE.match(parts[-2]):
phone, date8, title_parts = parts[-2], parts[-1], parts[:-2]
# ② 左锚定(手机号在前,跟视频文件名同序):同样先试带编号的
elif len(parts) >= 4 and _PHONE_RE.match(parts[0]) and _DATE8_RE.match(parts[1]) \
and _SEQ_RE.match(parts[2]):
phone, date8, seq = parts[0], parts[1], int(parts[2])
title_parts = parts[3:]
elif len(parts) >= 3 and _PHONE_RE.match(parts[0]) and _DATE8_RE.match(parts[1]):
# `手机号_日期_1` 这种"只有编号、没有标题"要**拒掉**,不能被当成标题"1"
# (静默生成一条标题是"1"的文案,比拒收危险得多;标题真是纯数字就用"标题在前"的写法)
if len(parts) == 3 and _SEQ_RE.match(parts[2]):
return None, ("只有手机号/日期/编号、没有标题文字 ——"
"标题是纯数字的话请用「标题_手机号_日期」写法")
phone, date8, title_parts = parts[0], parts[1], parts[2:]
else:
return None, "格式不对,应为 标题内容_手机号_日期_编号(编号可省略)"
return None, ("格式不对,应为「标题内容_手机号_日期_编号」"
"或「手机号_日期_编号_标题」(编号都可省略)")
d = parse_date8(date8)
if not d:
return None, f"日期 {date8} 不是合法日期(应为 20260912 这种 8 位)"
date = d
title = "_".join(title_parts).strip()
if not title:
return None, "缺标题内容(行首就是手机号)"
return None, "缺标题内容(只有手机号/日期/编号,没有标题文字)"
if len(title) > MAX_TITLE_LEN:
return None, f"标题太长({len(title)} 字 > {MAX_TITLE_LEN} 字上限),请改短或拆成两条"
return {"title": title, "phone": phone, "date": date,
@@ -195,15 +219,22 @@ def parse_title_line(line):
def parse_titles_text(text):
"""标题 txt 全文 → (rows, errors)。`#` 开头的行与空行自动跳过(不算错)。"""
"""标题 txt 全文 → (rows, errors)。空行跳过;`#` 开头**只有认不出标题行时**才算注释。
⚠ 顺序是"先当标题解析、再当注释":`#中秋快乐_19286456013_20260929` 是一条**正常标题**
(`#话题` 本来就长这样),过去的写法会把整行当注释**静默丢掉**。现在只有像
`# 这是注释` 这种解析不出手机号/日期的行才按注释忽略。
"""
rows, errors = [], []
lines = (text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
for i, raw in enumerate(lines, start=1):
s = raw.strip()
if not s or s.startswith("#"):
if not s:
continue
meta, err = parse_title_line(s)
if err:
if s.startswith("#"): # 认不出 + 以 # 开头 → 当注释,静默跳过
continue
errors.append({"line": i, "reason": err, "raw": s[:60]})
continue
meta["_line"] = i