le-wechat-article v1.3:融合 human-writing 活人感写法(材料关 / 说话位置 / 活人感审读 + check_prose.py 双自检);附 check_article front matter 剥离修复
This commit is contained in:
+644
@@ -0,0 +1,644 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检查中文成稿的硬禁令与常见模型化形状。只报警,不自动改文。
|
||||
|
||||
来源:https://github.com/KKKKhazix/human-writing (MIT,版权归 Human Writing Skill
|
||||
contributors)。本工坊原样引入未改检测规则;禁令清单见 references/human-voice.md
|
||||
第七节与 review-playbook.md「模型腔硬禁清单」(增删禁词须同步改本脚本)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HARD_STOPS = (
|
||||
"说白了",
|
||||
"说穿了",
|
||||
"先说结论",
|
||||
)
|
||||
|
||||
HARD_JARGON = (
|
||||
"赋能",
|
||||
"抓手",
|
||||
"商业闭环",
|
||||
"价值闭环",
|
||||
"能力沉淀",
|
||||
"拉通",
|
||||
"底层逻辑",
|
||||
"顶层设计",
|
||||
"认知跃迁",
|
||||
"价值释放",
|
||||
"能力建设",
|
||||
"降本增效",
|
||||
"内容矩阵",
|
||||
"全链路",
|
||||
"组合拳",
|
||||
"打开想象空间",
|
||||
"结构性机会",
|
||||
"关键命题",
|
||||
"深层逻辑",
|
||||
"技术底座",
|
||||
"公共底座",
|
||||
"技术主权",
|
||||
"单点风险",
|
||||
"主脊柱",
|
||||
"材料锚点",
|
||||
"认知增量",
|
||||
"迭代闭环",
|
||||
)
|
||||
|
||||
CONTEXT_JARGON = (
|
||||
"沉淀",
|
||||
"颗粒度",
|
||||
"对齐",
|
||||
"协同",
|
||||
"链路",
|
||||
"生态位",
|
||||
"心智",
|
||||
"范式",
|
||||
"方法论",
|
||||
"核心变量",
|
||||
"打法",
|
||||
"想象空间",
|
||||
"闭环",
|
||||
"不丢",
|
||||
)
|
||||
|
||||
LYRIC_WORDS = (
|
||||
"安放",
|
||||
"抵达",
|
||||
"微光",
|
||||
"褶皱",
|
||||
"丰盈",
|
||||
"滚烫",
|
||||
"轻盈",
|
||||
"赤裸",
|
||||
"剥开",
|
||||
)
|
||||
|
||||
ROAD_SIGNS = (
|
||||
"更微妙的是",
|
||||
"还有一层",
|
||||
"只说对了一半",
|
||||
"值得注意的是",
|
||||
"需要指出的是",
|
||||
"从某种意义上说",
|
||||
)
|
||||
|
||||
ROAD_STRIP_CHARS = "。!?!? \n"
|
||||
|
||||
FORBIDDEN_PUNCTUATION = {
|
||||
":": "中文冒号",
|
||||
":": "英文冒号",
|
||||
"—": "破折号",
|
||||
"–": "连接号式破折号",
|
||||
}
|
||||
|
||||
PIVOT_PATTERNS = (
|
||||
re.compile(r"(?:并)?不是[^。!?\n]{0,90}而是"),
|
||||
re.compile(r"并非[^。!?\n]{0,90}而是"),
|
||||
re.compile(r"不在于[^。!?\n]{0,90}而在于"),
|
||||
re.compile(r"与其说[^。!?\n]{0,90}(?:不如|毋宁|倒不如)"),
|
||||
re.compile(r"[。!?!?]\s*而是"),
|
||||
re.compile(r"表面(?:上)?[^。!?\n]{0,90}(?:其实|实际|实则)"),
|
||||
re.compile(r"看似[^。!?\n]{0,90}(?:其实|实际|实则)"),
|
||||
)
|
||||
|
||||
SEMANTIC_PIVOT_PATTERNS = (
|
||||
re.compile(r"(?:总|一直|曾|都)?以为[^!?\n]{2,60}?(?:其实|才发现|才明白|才知道|后来才)"),
|
||||
re.compile(r"(?:总|都|一直)以为[^!?\n]{2,60}?[。,](?:可|但|其实)"),
|
||||
re.compile(r"回头(?:看|一看)?才(?:发现|明白|知道)"),
|
||||
re.compile(r"(?:并)?不是[^。!?\n]{1,40},(?:更|才)?是[^,。!?\n]"),
|
||||
re.compile(r"从来(?:都)?(?:不是|与[^。!?,\n]{1,12}无关)"),
|
||||
re.compile(r"答案(?:是否定的|恰恰相反)|恰恰相反"),
|
||||
re.compile(r"表面(?:上)?[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"),
|
||||
re.compile(r"看似[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"),
|
||||
re.compile(r"[^,。!?\n]{1,12}不重要,(?:重要|要紧)的是"),
|
||||
re.compile(r"真正[^,。!?\n]{0,16}的(?:,)?是"),
|
||||
re.compile(r"不只(?:是)?[^。!?\n]{0,90}(?:还|也)"),
|
||||
)
|
||||
|
||||
NOMINALIZATION_PATTERNS = (
|
||||
re.compile(r"进行(?:了|一次|一场|着)?[^。,!?\n]{0,10}(?:调整|优化|升级|分析|讨论|沟通|梳理|复盘|迭代|探索|尝试|思考|规划|布局)"),
|
||||
re.compile(r"实现了?[^。,!?\n]{0,14}的?[^。,!?\n]{0,6}(?:提升|增长|突破|转变|跃升|落地)"),
|
||||
re.compile(r"完成了?对[^。,!?\n]{0,16}的"),
|
||||
re.compile(r"起到了?[^。,!?\n]{0,12}的?作用"),
|
||||
re.compile(r"具有[^。,!?\n]{0,10}(?:意义|价值)"),
|
||||
)
|
||||
|
||||
CONJUNCTIONS = (
|
||||
"因为",
|
||||
"所以",
|
||||
"但是",
|
||||
"然而",
|
||||
"同时",
|
||||
"此外",
|
||||
"而且",
|
||||
"并且",
|
||||
"因此",
|
||||
"不仅",
|
||||
)
|
||||
|
||||
ROAD_SIGN_PATTERNS = (
|
||||
re.compile(
|
||||
rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[0])}[^。!?!?\n]{{0,24}}",
|
||||
re.MULTILINE,
|
||||
),
|
||||
re.compile(
|
||||
rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[1])}(?=(?:更|原因|问题|意思|考虑|变化|逻辑|价值|作用|风险|影响|值得|很少|不容易|常被|往往))[^。!?!?\n]{{0,24}}",
|
||||
re.MULTILINE,
|
||||
),
|
||||
*(
|
||||
re.compile(
|
||||
rf"(?:^|[。!?!?]\s*){re.escape(phrase)}[^。!?!?\n]{{0,24}}",
|
||||
re.MULTILINE,
|
||||
)
|
||||
for phrase in ROAD_SIGNS[2:]
|
||||
),
|
||||
)
|
||||
|
||||
SOFT_MARKERS = (
|
||||
"真正",
|
||||
"本质上",
|
||||
"更深层次",
|
||||
"归根结底",
|
||||
"换句话说",
|
||||
"不可否认",
|
||||
"核心是",
|
||||
"关键在于",
|
||||
"这意味着",
|
||||
)
|
||||
|
||||
REPEATED_OPENERS = (
|
||||
"其实",
|
||||
"不过",
|
||||
"当然",
|
||||
"所以",
|
||||
"但是",
|
||||
"后来",
|
||||
"当时",
|
||||
"很多人",
|
||||
"问题是",
|
||||
"更重要的是",
|
||||
"说到这里",
|
||||
)
|
||||
|
||||
LEFT_BRANCH_PATTERNS = (
|
||||
re.compile(r"(?:^|[。!?]\s*)在[^,。!?\n]{12,70}(?:以后|之后|之前|以前|过程中|情况下|背景下),"),
|
||||
re.compile(r"(?:^|[。!?]\s*)那些[^,。!?\n]{10,60}的[^,。!?\n]{2,30}[,。]"),
|
||||
re.compile(r"(?:^|[。!?]\s*)(?:真正|最终|最后)让[^,。!?\n]{8,70}的,是"),
|
||||
)
|
||||
|
||||
METAPHOR_FIELDS = {
|
||||
"温度": ("降温", "升温", "冷却", "余温", "温度最高"),
|
||||
"生死战争": ("杀死", "死因", "枪响", "开火", "战场", "引爆", "弹药"),
|
||||
"建筑灾害": ("坍塌", "崩塌", "地基", "砖头", "支柱", "废墟"),
|
||||
"仓储租赁": ("仓库", "库房", "租金", "取货", "入库", "库存"),
|
||||
"道路竞赛": ("赛道", "跑道", "岔路", "十字路口", "终点线", "门票"),
|
||||
"机器器官": ("齿轮", "引擎", "发动机", "血管", "骨架", "肌肉"),
|
||||
"海洋航行": ("蓝海", "浪潮", "潮水", "航船", "灯塔", "彼岸"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Paragraph:
|
||||
position: int
|
||||
text: str
|
||||
han: int
|
||||
sentences: int
|
||||
|
||||
|
||||
def han_count(text: str) -> int:
|
||||
return len(re.findall(r"[\u4e00-\u9fff]", text))
|
||||
|
||||
|
||||
def line_number(text: str, position: int) -> int:
|
||||
return text.count("\n", 0, position) + 1
|
||||
|
||||
|
||||
def excerpt(value: str, width: int = 72) -> str:
|
||||
value = re.sub(r"\s+", " ", value).strip()
|
||||
return value if len(value) <= width else value[: width - 1] + "…"
|
||||
|
||||
|
||||
def mask_non_prose(text: str) -> str:
|
||||
"""屏蔽代码、网址和机器元数据,同时保留字符位置与换行。"""
|
||||
|
||||
def mask(match: re.Match[str]) -> str:
|
||||
return "".join("\n" if char == "\n" else " " for char in match.group())
|
||||
|
||||
patterns = (
|
||||
re.compile(r"\A---\s*\n.*?\n---\s*(?:\n|\Z)", re.DOTALL),
|
||||
re.compile(r"```.*?```", re.DOTALL),
|
||||
re.compile(r"`[^`\n]*`"),
|
||||
re.compile(r"\]\([^\n)]*\)"),
|
||||
re.compile(r"https?://[^\s)>]+"),
|
||||
re.compile(r"<[^>\n]+>"),
|
||||
)
|
||||
masked = text
|
||||
for pattern in patterns:
|
||||
masked = pattern.sub(mask, masked)
|
||||
return masked
|
||||
|
||||
|
||||
def non_overlapping_terms(text: str, terms: tuple[str, ...]):
|
||||
matches = []
|
||||
occupied = []
|
||||
for term in sorted(terms, key=len, reverse=True):
|
||||
for match in re.finditer(re.escape(term), text):
|
||||
start, end = match.span()
|
||||
if any(start < old_end and end > old_start for old_start, old_end in occupied):
|
||||
continue
|
||||
matches.append((start, term))
|
||||
occupied.append((start, end))
|
||||
return sorted(matches)
|
||||
|
||||
|
||||
def all_matches(text: str, patterns: tuple[re.Pattern[str], ...]):
|
||||
matches = []
|
||||
for pattern in patterns:
|
||||
matches.extend(pattern.finditer(text))
|
||||
return sorted(matches, key=lambda match: match.start())
|
||||
|
||||
|
||||
def heavy_de_sentences(text: str):
|
||||
"""找出主干可能被多个“的”压到后面的长句。"""
|
||||
|
||||
matches = []
|
||||
pattern = re.compile(r"[^。!?!?\n]+(?:[。!?!?]|$)")
|
||||
for match in pattern.finditer(text):
|
||||
value = match.group()
|
||||
if han_count(value) >= 38 and value.count("的") >= 4:
|
||||
matches.append(match)
|
||||
return matches
|
||||
|
||||
|
||||
def anaphora_runs(text: str, minimum: int = 3):
|
||||
"""找出同一句里三个以上小句用同一个开头的排比。"""
|
||||
|
||||
matches = []
|
||||
for sentence in re.finditer(r"[^。!?!?\n]+(?:[。!?!?]|$)", text):
|
||||
clauses = [
|
||||
clause.strip()
|
||||
for clause in re.split(r"[,、;,;]", sentence.group())
|
||||
if han_count(clause) >= 3
|
||||
]
|
||||
if len(clauses) < minimum:
|
||||
continue
|
||||
run = 1
|
||||
for previous, current in zip(clauses, clauses[1:]):
|
||||
if previous[:2] == current[:2] and re.match(r"[一-鿿]{2}", current):
|
||||
run += 1
|
||||
if run >= minimum:
|
||||
matches.append(sentence)
|
||||
break
|
||||
else:
|
||||
run = 1
|
||||
return matches
|
||||
|
||||
|
||||
def sentence_length_cv(text: str):
|
||||
"""句长变异系数。人写的长短句差距大,模型的句长彼此接近。"""
|
||||
|
||||
lengths = [
|
||||
han_count(match.group())
|
||||
for match in re.finditer(r"[^。!?!?\n]+[。!?!?]", text)
|
||||
if han_count(match.group()) >= 4
|
||||
]
|
||||
if len(lengths) < 12:
|
||||
return None
|
||||
mean = sum(lengths) / len(lengths)
|
||||
if mean == 0:
|
||||
return None
|
||||
variance = sum((value - mean) ** 2 for value in lengths) / len(lengths)
|
||||
return (variance ** 0.5) / mean, len(lengths)
|
||||
|
||||
|
||||
def bracket_highlights(text: str):
|
||||
"""「」括起来的短语。太密说明在批量造金句。"""
|
||||
|
||||
return list(re.finditer(r"[「『][^」』\n]{1,6}[」』]", text))
|
||||
|
||||
|
||||
def prose_paragraphs(text: str) -> list[Paragraph]:
|
||||
paragraphs = []
|
||||
cursor = 0
|
||||
for block in re.split(r"\n\s*\n", text):
|
||||
position = text.find(block, cursor)
|
||||
cursor = max(position + len(block), cursor)
|
||||
clean = re.sub(r"[>*_`]", "", block).strip()
|
||||
if not clean or clean.startswith(("#", "http", "![", "```")):
|
||||
continue
|
||||
if re.match(r"^(?:[-+*]|\d+[.、])\s", clean):
|
||||
continue
|
||||
count = han_count(clean)
|
||||
if count < 4:
|
||||
continue
|
||||
sentences = max(1, len(re.findall(r"[。!?!?]", clean)))
|
||||
paragraphs.append(Paragraph(position, clean, count, sentences))
|
||||
return paragraphs
|
||||
|
||||
|
||||
def metaphor_cluster(text: str, distance: int = 800):
|
||||
hits = []
|
||||
for field, words in METAPHOR_FIELDS.items():
|
||||
for word in words:
|
||||
for match in re.finditer(re.escape(word), text):
|
||||
hits.append((match.start(), field, word))
|
||||
hits.sort()
|
||||
for index, (start, _, _) in enumerate(hits):
|
||||
window = [hit for hit in hits[index:] if hit[0] - start <= distance]
|
||||
fields = {hit[1] for hit in window}
|
||||
if len(fields) >= 3:
|
||||
return window, fields
|
||||
return None
|
||||
|
||||
|
||||
def short_streak(paragraphs: list[Paragraph], limit: int = 4):
|
||||
streak = []
|
||||
for paragraph in paragraphs:
|
||||
if paragraph.han <= 24 and paragraph.sentences <= 1:
|
||||
streak.append(paragraph)
|
||||
if len(streak) >= limit:
|
||||
return streak
|
||||
else:
|
||||
streak = []
|
||||
return None
|
||||
|
||||
|
||||
def opener_counts(paragraphs: list[Paragraph]):
|
||||
counts = collections.Counter()
|
||||
examples = {}
|
||||
for paragraph in paragraphs:
|
||||
value = paragraph.text.lstrip("“‘\"((")
|
||||
for opener in REPEATED_OPENERS:
|
||||
if value.startswith(opener):
|
||||
counts[opener] += 1
|
||||
examples.setdefault(opener, paragraph.position)
|
||||
break
|
||||
return counts, examples
|
||||
|
||||
|
||||
def read_text(path: str) -> str:
|
||||
if path == "-":
|
||||
return sys.stdin.read()
|
||||
return Path(path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检查中文成稿的硬禁令与模型化形状")
|
||||
parser.add_argument("path", help="Markdown 或文本文件路径。使用 - 从标准输入读取")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
text = read_text(args.path)
|
||||
except (OSError, UnicodeError) as error:
|
||||
print(f"无法读取稿件。{error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
prose = mask_non_prose(text)
|
||||
total_han = han_count(prose)
|
||||
if total_han == 0:
|
||||
print("没有检测到汉字。", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
failures = []
|
||||
warnings = []
|
||||
|
||||
quote_colons = []
|
||||
for symbol, label in FORBIDDEN_PUNCTUATION.items():
|
||||
matches = list(re.finditer(re.escape(symbol), prose))
|
||||
if symbol in (":", ":"):
|
||||
hard = []
|
||||
for match in matches:
|
||||
tail = prose[match.end() : match.end() + 2].lstrip()
|
||||
if tail[:1] in ("「", "『", "“", "‘", '"'):
|
||||
quote_colons.append(match)
|
||||
else:
|
||||
hard.append(match)
|
||||
matches = hard
|
||||
if matches:
|
||||
lines = "、".join(str(line_number(text, match.start())) for match in matches[:8])
|
||||
failures.append(f"{label}共 {len(matches)} 处,出现在第 {lines} 行。")
|
||||
if quote_colons:
|
||||
lines = "、".join(
|
||||
str(line_number(text, match.start())) for match in quote_colons[:8]
|
||||
)
|
||||
warnings.append(
|
||||
f"引出原话的冒号 {len(quote_colons)} 处,第 {lines} 行。确认引号里确实是原话,且不是提示性用法。"
|
||||
)
|
||||
|
||||
stop_matches = non_overlapping_terms(prose, HARD_STOPS)
|
||||
for position, phrase in stop_matches:
|
||||
failures.append(f"硬停词,第 {line_number(text, position)} 行,{phrase}")
|
||||
|
||||
jargon_matches = non_overlapping_terms(prose, HARD_JARGON)
|
||||
for position, phrase in jargon_matches:
|
||||
failures.append(f"黑话,第 {line_number(text, position)} 行,{phrase}")
|
||||
|
||||
context_jargon_matches = non_overlapping_terms(prose, CONTEXT_JARGON)
|
||||
hard_spans = [
|
||||
(position, position + len(phrase)) for position, phrase in jargon_matches
|
||||
]
|
||||
context_jargon_matches = [
|
||||
(position, phrase)
|
||||
for position, phrase in context_jargon_matches
|
||||
if not any(
|
||||
position < end and position + len(phrase) > start
|
||||
for start, end in hard_spans
|
||||
)
|
||||
]
|
||||
if context_jargon_matches:
|
||||
samples = "、".join(
|
||||
dict.fromkeys(phrase for _, phrase in context_jargon_matches)
|
||||
)
|
||||
lines = "、".join(
|
||||
dict.fromkeys(
|
||||
str(line_number(text, position))
|
||||
for position, _ in context_jargon_matches[:8]
|
||||
)
|
||||
)
|
||||
warnings.append(
|
||||
f"有 {len(context_jargon_matches)} 处词语需要结合语境判断。"
|
||||
f"第 {lines} 行出现 {samples}。本义准确时保留,用来抬价时改写。"
|
||||
)
|
||||
|
||||
road_signs = all_matches(prose, ROAD_SIGN_PATTERNS)
|
||||
for match in road_signs:
|
||||
failures.append(
|
||||
f"模型路标,第 {line_number(text, match.start())} 行,"
|
||||
f"“{excerpt(match.group().lstrip(ROAD_STRIP_CHARS))}”"
|
||||
)
|
||||
|
||||
pivots = all_matches(prose, PIVOT_PATTERNS)
|
||||
for match in pivots:
|
||||
failures.append(
|
||||
f"禁用翻案句,第 {line_number(text, match.start())} 行,"
|
||||
f"“{excerpt(match.group())}”"
|
||||
)
|
||||
|
||||
occupied_spans = [match.span() for match in pivots]
|
||||
semantic_pivots = []
|
||||
for match in all_matches(prose, SEMANTIC_PIVOT_PATTERNS):
|
||||
if any(
|
||||
match.start() < end and match.end() > start
|
||||
for start, end in occupied_spans
|
||||
):
|
||||
continue
|
||||
semantic_pivots.append(match)
|
||||
occupied_spans.append(match.span())
|
||||
for match in semantic_pivots:
|
||||
warnings.append(
|
||||
f"疑似翻案腔变形,第 {line_number(text, match.start())} 行,"
|
||||
f"“{excerpt(match.group(), 44)}”。先立误解再推翻就改成正面陈述,正常用法保留。"
|
||||
)
|
||||
|
||||
anaphoras = anaphora_runs(prose)
|
||||
for match in anaphoras[:4]:
|
||||
warnings.append(
|
||||
f"三连以上同构排比,第 {line_number(text, match.start())} 行,"
|
||||
f"“{excerpt(match.group(), 44)}”。留两项,第三项换说法或删掉。"
|
||||
)
|
||||
|
||||
lyric_matches = non_overlapping_terms(prose, LYRIC_WORDS)
|
||||
if len(lyric_matches) >= 2:
|
||||
samples = "、".join(dict.fromkeys(term for _, term in lyric_matches))
|
||||
warnings.append(
|
||||
f"模型偏爱的抒情词 {len(lyric_matches)} 处。{samples}。"
|
||||
"写具体事物时保留,给抽象概念穿衣服时删掉。"
|
||||
)
|
||||
|
||||
nominalizations = all_matches(prose, NOMINALIZATION_PATTERNS)
|
||||
for match in nominalizations[:4]:
|
||||
warnings.append(
|
||||
f"名词化句式,第 {line_number(text, match.start())} 行,"
|
||||
f"“{excerpt(match.group(), 36)}”。还原成直接的动词。"
|
||||
)
|
||||
|
||||
conjunction_hits = non_overlapping_terms(prose, CONJUNCTIONS)
|
||||
if total_han >= 600 and len(conjunction_hits) * 1000 / total_han > 7:
|
||||
samples = "、".join(
|
||||
f"{term} {count} 次"
|
||||
for term, count in collections.Counter(
|
||||
term for _, term in conjunction_hits
|
||||
).most_common(4)
|
||||
)
|
||||
warnings.append(
|
||||
f"连词密度偏高,每千字 {len(conjunction_hits) * 1000 // total_han} 个。{samples}。"
|
||||
"中文小句靠语序和事理相接,删掉一半试试。"
|
||||
)
|
||||
|
||||
highlights = bracket_highlights(prose)
|
||||
highlight_limit = max(3, total_han // 700)
|
||||
if len(highlights) > highlight_limit:
|
||||
samples = "、".join(dict.fromkeys(match.group() for match in highlights[:6]))
|
||||
warnings.append(
|
||||
f"「」括起的短语共 {len(highlights)} 处。{samples}。太密说明在批量造金句。"
|
||||
)
|
||||
|
||||
cv_result = sentence_length_cv(prose)
|
||||
if cv_result and cv_result[0] < 0.42:
|
||||
warnings.append(
|
||||
f"全文 {cv_result[1]} 个句子长度过于接近(变异系数 {cv_result[0]:.2f})。"
|
||||
"人写的段落里十个字的句子会挨着四十个字的句子,放开几句,压短几句。"
|
||||
)
|
||||
|
||||
marker_matches = non_overlapping_terms(prose, SOFT_MARKERS)
|
||||
marker_limit = max(2, total_han // 900)
|
||||
if len(marker_matches) > marker_limit:
|
||||
samples = "、".join(dict.fromkeys(term for _, term in marker_matches))
|
||||
warnings.append(
|
||||
f"洞察路标共 {len(marker_matches)} 处,当前提醒线为 {marker_limit} 处。"
|
||||
f"重点检查 {samples}。"
|
||||
)
|
||||
|
||||
left_branches = all_matches(prose, LEFT_BRANCH_PATTERNS)
|
||||
left_limit = max(2, total_han // 1200)
|
||||
if len(left_branches) > left_limit:
|
||||
samples = ";".join(
|
||||
f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”"
|
||||
for match in left_branches[:4]
|
||||
)
|
||||
warnings.append(
|
||||
f"长前置成分共 {len(left_branches)} 处,可能让主干来得太晚。{samples}"
|
||||
)
|
||||
|
||||
dense_de = heavy_de_sentences(prose)
|
||||
dense_de_limit = max(1, total_han // 1500)
|
||||
if len(dense_de) > dense_de_limit:
|
||||
samples = ";".join(
|
||||
f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”"
|
||||
for match in dense_de[:4]
|
||||
)
|
||||
warnings.append(
|
||||
f"有 {len(dense_de)} 个长句包含四个以上的“的”,可能要先交代人和动作。{samples}"
|
||||
)
|
||||
|
||||
paragraphs = prose_paragraphs(prose)
|
||||
if len(paragraphs) >= 10:
|
||||
one_sentence = sum(paragraph.sentences <= 1 for paragraph in paragraphs)
|
||||
ratio = one_sentence / len(paragraphs)
|
||||
if ratio >= 0.75:
|
||||
warnings.append(
|
||||
f"可识别段落中有 {ratio:.0%} 只有一句话,可能形成统一的短段鼓点。"
|
||||
)
|
||||
|
||||
streak = short_streak(paragraphs)
|
||||
if streak:
|
||||
first = streak[0]
|
||||
warnings.append(
|
||||
f"从第 {line_number(text, first.position)} 行起连续出现 {len(streak)} 个短促单句段,"
|
||||
"检查是否在排队喊结论。"
|
||||
)
|
||||
|
||||
counts, opener_examples = opener_counts(paragraphs)
|
||||
repeated = [(opener, count) for opener, count in counts.items() if count >= 4]
|
||||
if repeated:
|
||||
details = "、".join(f"{opener} {count} 次" for opener, count in repeated)
|
||||
first_position = min(opener_examples[opener] for opener, _ in repeated)
|
||||
warnings.append(
|
||||
f"段落开场重复,从第 {line_number(text, first_position)} 行附近开始。{details}。"
|
||||
)
|
||||
|
||||
metaphors = metaphor_cluster(prose)
|
||||
if metaphors:
|
||||
window, fields = metaphors
|
||||
samples = "、".join(dict.fromkeys(hit[2] for hit in window))
|
||||
warnings.append(
|
||||
f"八百字内出现 {len(fields)} 套借喻。{'、'.join(sorted(fields))}。"
|
||||
f"例词有 {samples}。"
|
||||
)
|
||||
|
||||
print(f"汉字数 {total_han}")
|
||||
print(
|
||||
f"翻案句 {len(pivots)},翻案腔变形 {len(semantic_pivots)},"
|
||||
f"同构排比 {len(anaphoras)},名词化 {len(nominalizations)},"
|
||||
f"黑话 {len(jargon_matches)},硬停词 {len(stop_matches)},"
|
||||
f"模型路标 {len(road_signs)},需辨语境词 {len(context_jargon_matches)},"
|
||||
f"抒情词 {len(lyric_matches)},洞察路标 {len(marker_matches)},"
|
||||
f"长前置成分 {len(left_branches)},重定语句 {len(dense_de)}"
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("\n需要修改")
|
||||
for item in failures:
|
||||
print(f"- {item}")
|
||||
|
||||
if warnings:
|
||||
print("\n需要人工判断")
|
||||
for item in warnings:
|
||||
print(f"- {item}")
|
||||
|
||||
if not failures and not warnings:
|
||||
print("\n未发现这份检查器覆盖的问题。")
|
||||
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user