209 lines
6.8 KiB
Python
209 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
pre-commit-check.py · 冰朔小说创作系统 · 提交前自动检查
|
|||
|
|
|
|||
|
|
放在 scripts/ 目录,git commit 前手动或通过 pre-commit hook 自动调用。
|
|||
|
|
检查范围:chapters/ 下所有 .txt 文件(正文)。
|
|||
|
|
|
|||
|
|
检查项:
|
|||
|
|
1. 字数:非空白字符 2000-2800 区间(仅单章文件)
|
|||
|
|
2. 违禁词:AI 化套话
|
|||
|
|
3. 半角标点:引号、省略号、破折号
|
|||
|
|
4. 代词一致性:许怀钰=他,何采霏=她(按书检测)
|
|||
|
|
5. Markdown 泄露:** ## 等
|
|||
|
|
6. 元叙述:正文中不出现"第X章"(标题行除外,多章文件跳过)
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python3 scripts/pre-commit-check.py
|
|||
|
|
|
|||
|
|
退出码:
|
|||
|
|
0 = 全部通过
|
|||
|
|
1 = 有不达标项
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import glob
|
|||
|
|
|
|||
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
CHAPTERS_DIR = os.path.join(REPO_ROOT, "chapters")
|
|||
|
|
|
|||
|
|
# --- 检查规则 ---
|
|||
|
|
|
|||
|
|
# 违禁词:AI 化套话
|
|||
|
|
# 注意:"最后"单独检查,只匹配段首/句首的"最后,"模式(AI 三段式标志)
|
|||
|
|
FORBIDDEN_WORDS = [
|
|||
|
|
"值得注意的是", "不得不说", "总的来说",
|
|||
|
|
"作为一个", "说道",
|
|||
|
|
"心中充满", "不禁", "不由得",
|
|||
|
|
"与此同时",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# "最后"只检查句首用法(AI 三段式 "最后,")
|
|||
|
|
FORBIDDEN_PATTERNS = [
|
|||
|
|
(r'\n\s*最后[,。]', '"最后"句首(AI 三段式)'),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
HALF_WIDTH_CHECKS = [
|
|||
|
|
('"', "半角引号(应使用中文双引号)"),
|
|||
|
|
("...", "半角省略号(应使用……6点)"),
|
|||
|
|
("--", "半角破折号(应使用——中文破折号)"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
MARKDOWN_LEAKS = ["**", "##"]
|
|||
|
|
|
|||
|
|
# 每本书的主角代词规则:主角名 -> 正确代词
|
|||
|
|
PROTAGONIST_RULES = {
|
|||
|
|
"许怀钰": "他",
|
|||
|
|
"何采霏": "她",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def count_chars(text):
|
|||
|
|
"""非空白字符统计(D169 铁律)"""
|
|||
|
|
return len(re.findall(r'[^\s]', text))
|
|||
|
|
|
|||
|
|
def is_multi_chapter_file(filename):
|
|||
|
|
"""判断是否为多章合并文件(之之侧旧格式)"""
|
|||
|
|
# 匹配 "肖轩447-450章" / "沈婉凝278-282章" / "女频续写263章" 等
|
|||
|
|
if re.search(r'\d+-\d+章', filename):
|
|||
|
|
return True
|
|||
|
|
# 匹配 "肖轩447-450章【8684字】" 这种
|
|||
|
|
if '【' in filename and '章' in filename:
|
|||
|
|
return True
|
|||
|
|
# 匹配 "女频续写263章(沈婉凝)" 这种单文件多章
|
|||
|
|
if re.search(r'\d+章.*(', filename) and '第' not in filename:
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def check_chapter(filepath):
|
|||
|
|
"""检查单个章节文件,返回 (passed, issues, skipped_reason)"""
|
|||
|
|
issues = []
|
|||
|
|
skipped_reason = None
|
|||
|
|
|
|||
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|||
|
|
text = f.read()
|
|||
|
|
|
|||
|
|
filename = os.path.basename(filepath)
|
|||
|
|
|
|||
|
|
# 0. 空文件检查
|
|||
|
|
if not text.strip():
|
|||
|
|
issues.append(f" ❌ 空文件")
|
|||
|
|
return False, issues, None
|
|||
|
|
|
|||
|
|
# 判断是否为多章合并文件
|
|||
|
|
multi_chapter = is_multi_chapter_file(filename)
|
|||
|
|
if multi_chapter:
|
|||
|
|
skipped_reason = "多章合并文件(旧格式,跳过字数和元叙述检查)"
|
|||
|
|
|
|||
|
|
# 1. 字数检查(仅单章文件)
|
|||
|
|
char_count = count_chars(text)
|
|||
|
|
if not multi_chapter:
|
|||
|
|
if char_count < 2000:
|
|||
|
|
issues.append(f" ❌ 字数不足: {char_count} < 2000(差 {2000 - char_count} 字)")
|
|||
|
|
elif char_count > 2800:
|
|||
|
|
issues.append(f" ⚠️ 字数超标: {char_count} > 2800")
|
|||
|
|
else:
|
|||
|
|
issues.append(f" ✓ 字数: {char_count}")
|
|||
|
|
else:
|
|||
|
|
issues.append(f" ℹ️ 字数: {char_count}(多章合并,不判定)")
|
|||
|
|
|
|||
|
|
# 2. 违禁词检查(所有文件都查)
|
|||
|
|
for word in FORBIDDEN_WORDS:
|
|||
|
|
if word in text:
|
|||
|
|
count = text.count(word)
|
|||
|
|
issues.append(f" ❌ 违禁词 \"{word}\": {count} 次")
|
|||
|
|
|
|||
|
|
# 2b. 违禁模式检查(句首"最后"等)
|
|||
|
|
for pattern, desc in FORBIDDEN_PATTERNS:
|
|||
|
|
matches = re.findall(pattern, text)
|
|||
|
|
if matches:
|
|||
|
|
issues.append(f" ❌ {desc}: {len(matches)} 处")
|
|||
|
|
|
|||
|
|
# 3. 半角标点检查(所有文件都查)
|
|||
|
|
for pattern_str, desc in HALF_WIDTH_CHECKS:
|
|||
|
|
if pattern_str in text:
|
|||
|
|
count = text.count(pattern_str)
|
|||
|
|
issues.append(f" ⚠️ {desc}: {count} 处")
|
|||
|
|
|
|||
|
|
# 4. Markdown 泄露(所有文件都查)
|
|||
|
|
for md in MARKDOWN_LEAKS:
|
|||
|
|
if md in text:
|
|||
|
|
issues.append(f" ❌ Markdown 泄露: \"{md}\"")
|
|||
|
|
|
|||
|
|
# 5. 元叙述检查(仅单章文件)
|
|||
|
|
if not multi_chapter:
|
|||
|
|
lines = text.split('\n')
|
|||
|
|
for i, line in enumerate(lines):
|
|||
|
|
if i == 0: # 标题行,跳过
|
|||
|
|
continue
|
|||
|
|
if re.search(r'第\s*\d+\s*章', line):
|
|||
|
|
issues.append(f" ❌ 元叙述: 第{i+1}行出现\"第X章\"")
|
|||
|
|
|
|||
|
|
# 6. 代词一致性
|
|||
|
|
dirname = os.path.basename(os.path.dirname(filepath))
|
|||
|
|
for name, correct_pronoun in PROTAGONIST_RULES.items():
|
|||
|
|
if name in dirname or name in filename:
|
|||
|
|
wrong_pronoun = "她" if correct_pronoun == "他" else "他"
|
|||
|
|
wrong_pattern = f"{name}{wrong_pronoun}"
|
|||
|
|
if wrong_pattern in text:
|
|||
|
|
issues.append(f" ❌ 代词错误: \"{wrong_pattern}\"({name}应为{correct_pronoun})")
|
|||
|
|
|
|||
|
|
# 判断是否通过
|
|||
|
|
# ⚠️ 标记为警告(不阻断提交),❌ 标记为错误(阻断提交)
|
|||
|
|
has_errors = any("❌" in issue for issue in issues)
|
|||
|
|
passed = not has_errors
|
|||
|
|
return passed, issues, skipped_reason
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("冰朔小说创作系统 · 提交前自动检查")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# 找到所有 .txt 文件
|
|||
|
|
txt_files = glob.glob(os.path.join(CHAPTERS_DIR, "**", "*.txt"), recursive=True)
|
|||
|
|
|
|||
|
|
if not txt_files:
|
|||
|
|
print("未找到章节文件,跳过检查。")
|
|||
|
|
sys.exit(0)
|
|||
|
|
|
|||
|
|
all_passed = True
|
|||
|
|
total_files = 0
|
|||
|
|
failed_files = 0
|
|||
|
|
skipped_files = 0
|
|||
|
|
|
|||
|
|
for filepath in sorted(txt_files):
|
|||
|
|
rel_path = os.path.relpath(filepath, REPO_ROOT)
|
|||
|
|
total_files += 1
|
|||
|
|
|
|||
|
|
passed, issues, skipped_reason = check_chapter(filepath)
|
|||
|
|
|
|||
|
|
if skipped_reason:
|
|||
|
|
skipped_files += 1
|
|||
|
|
|
|||
|
|
if not passed:
|
|||
|
|
all_passed = False
|
|||
|
|
failed_files += 1
|
|||
|
|
print(f"📋 {rel_path}")
|
|||
|
|
if skipped_reason:
|
|||
|
|
print(f" ℹ️ {skipped_reason}")
|
|||
|
|
for issue in issues:
|
|||
|
|
print(issue)
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# 汇总
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"检查完成: {total_files} 个文件, {total_files - failed_files} 通过, {failed_files} 不达标, {skipped_files} 跳过")
|
|||
|
|
|
|||
|
|
if all_passed:
|
|||
|
|
print("✅ 全部通过,可以提交。")
|
|||
|
|
sys.exit(0)
|
|||
|
|
else:
|
|||
|
|
print("❌ 有不达标项(❌ 为阻断项,⚠️ 为提醒项)。")
|
|||
|
|
print(" 阻断项请修复后再提交。")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|