scripts/check_params.py
21.5 KB| 1 | #!/usr/bin/env python3 |
| 2 | """公文/党建 参数自检器 |
| 3 | |
| 4 | 设计原则:只判"硬冲突"(文种识别错误),其余仅作对照提示。 |
| 5 | 理由:真实公文的个体差异极大(如调研报告"一是二是"从 0 到 20 次都有), |
| 6 | 用均值当合格线会把好作品判成不合格。本脚本的参考值统一为 |
| 7 | 中位数 + 四分位(p25-p75),与 参数卡.md 完全一致。 |
| 8 | |
| 9 | 用法: |
| 10 | python3 check_params.py draft.md --genre 调研报告 |
| 11 | python3 check_params.py draft.md --genre 短经验材料 |
| 12 | python3 check_params.py draft.md --match 猜它最像哪个文种 |
| 13 | python3 check_params.py --list |
| 14 | """ |
| 15 | import argparse, json, re, sys |
| 16 | from pathlib import Path |
| 17 | |
| 18 | # ── 语料中位数与常见区间(p25-p75),仅供对照,不作判定 ── |
| 19 | REF = { |
| 20 | '调研报告': dict(n=75, chars=(3108,3820,3492), sent=(45,59,53), h1=(3,3,3), h2=(0,11,9), |
| 21 | h3=(0,0,0), yishi=(0,12,9), dun=(19,26,22), qz=(0,2,1), yq=(18,31,26), |
| 22 | jy=(2,5,3), pct=(0,1,0)), |
| 23 | '领导讲话': dict(n=44, chars=(3100,4200,3700), sent=(48,64,56), h1=(3,3,3), h2=(3,9,5), |
| 24 | h3=(0,0,0), yishi=(2,8,5), dun=(18,27,21), qz=(1,3,2), yq=(35,55,45), |
| 25 | jy=(0,4,2), pct=(0,0,0)), |
| 26 | '工作意见': dict(n=25, chars=(2900,3800,3300), sent=(52,66,58), h1=(4,6,5), h2=(9,14,13), |
| 27 | h3=(0,0,0), yishi=(0,3,1), dun=(25,31,28), qz=(1,3,2), yq=(30,55,40), |
| 28 | jy=(0,2,1), pct=(0,1,0)), |
| 29 | '经验材料': dict(n=22, chars=(2900,4600,3600), sent=(50,63,56), h1=(2,3,2), h2=(0,4,2), |
| 30 | h3=(0,3,1), yishi=(2,9,6), dun=(14,23,19), qz=(0,3,1), yq=(12,30,20), |
| 31 | jy=(1,5,3), pct=(0,2,1)), |
| 32 | '工作方案': dict(n=10, chars=(2700,5000,3600), sent=(45,68,55), h1=(4,6,5), h2=(4,9,8), |
| 33 | h3=(5,17,11), yishi=(0,2,1), dun=(14,22,18), qz=(1,4,2), yq=(14,32,22), |
| 34 | jy=(3,10,6), pct=(0,1,0)), |
| 35 | '经验总结': dict(n=8, chars=(2100,2700,2500), sent=(58,71,64), h1=(3,4,3), h2=(2,6,4), |
| 36 | h3=(0,0,0), yishi=(3,8,5), dun=(23,31,27), qz=(0,1,0), yq=(10,20,14), |
| 37 | jy=(0,1,0), pct=(0,0,0)), |
| 38 | '短经验材料': dict(n=77, chars=(950,1300,1119), sent=(45,57,51), h1=(0,0,0), h2=(0,1,0), |
| 39 | h3=(0,0,0), yishi=(0,1,1), dun=(20,27,24), qz=(0,1,0), yq=(1,4,3), |
| 40 | jy=(0,1,0), pct=(0,1,0), paras=(5,7,6), para_len=(130,185,156), quote=(10,18,14), |
| 41 | quote_long=(1.8,4.0,2.9)), |
| 42 | } |
| 43 | |
| 44 | # ── 硬冲突规则:违反即为文种识别错误,不是风格差异 ── |
| 45 | # (字段, 判据函数, 报错文案, 依据) |
| 46 | HARD = { |
| 47 | '短经验材料': [ |
| 48 | ('h1', lambda v: v <= 1, '千字级经验材料不应用多个一级标题', '77篇语料共 3 个一级标题'), |
| 49 | ('chars', lambda v: 700 <= v <= 2000, '短经验材料篇幅应在 700-2000 字', '语料区间 558-3022,中位 1119'), |
| 50 | ], |
| 51 | '经验总结': [ |
| 52 | ('pct', lambda v: v == 0, '经验总结不用百分比', '8篇语料 100% 零百分比'), |
| 53 | ], |
| 54 | '领导讲话': [ |
| 55 | ('h3', lambda v: v == 0, '领导讲话不用三级标题', '44篇语料三级标题总数为 0'), |
| 56 | ], |
| 57 | '工作意见': [ |
| 58 | ('h3', lambda v: v <= 2, '工作意见几乎不用三级标题', '25篇语料平均 0.1 个,最多 2 个'), |
| 59 | ], |
| 60 | } |
| 61 | |
| 62 | # 跨文体族硬约束(所有公文族文种共用) |
| 63 | GONGWEN = ['调研报告','领导讲话','工作意见','经验材料','工作方案','经验总结'] |
| 64 | for g in GONGWEN: |
| 65 | HARD.setdefault(g, []).append( |
| 66 | ('chars', lambda v: v >= 1200, '公文族篇幅不应低于 1200 字(低于此值应按党建族写)', |
| 67 | '公文语料最短 1423 字')) |
| 68 | |
| 69 | # 软提示:偏离即提醒,但不判错(因真实作品存在合法变体) |
| 70 | SOFT = { |
| 71 | '工作方案': [ |
| 72 | ('h3', lambda v: v >= 3, '工作方案通常有三级标题("1.")', |
| 73 | '语料平均 11.5 个,但 10 篇中 3 篇为 0(存在无三级标题的变体)'), |
| 74 | ], |
| 75 | '工作意见': [ |
| 76 | ('h2', lambda v: v >= 5, '工作意见通常有较多二级标题("(一)")', |
| 77 | '语料平均 13.2 个,但存在把重点任务拆成多个平行一级标题的扁平变体'), |
| 78 | ], |
| 79 | '调研报告': [ |
| 80 | ('yishi', lambda v: v >= 3, '调研报告通常用"一是二是"做段内分层', |
| 81 | '语料中位 9 次,但四分之一的作品完全不用'), |
| 82 | ], |
| 83 | } |
| 84 | |
| 85 | # ── 重心分布:最重一段占全篇的比例,p25-p75 与中位 ── |
| 86 | # |
| 87 | # 数据来源:对 188 篇可切分语料的**全量统计**(含至少两个一级标题者)。 |
| 88 | # |
| 89 | # ⚠ 不要用 `文章结构模板.md` 里的区间做验收。那份数据来自每文种 3 篇精读 |
| 90 | # 抽样,与全量差异很大:经验材料抽样得 47-59%,全量实为 27-56%(中位 33); |
| 91 | # 工作意见抽样得 44-53%,全量实为 24-50%(中位 42)。用抽样值当合格线, |
| 92 | # 真实公文的命中率只有 8%-16%。 |
| 93 | # |
| 94 | # 因此本项**只作对照提示,不参与硬冲突判定**。个体离散度极大 |
| 95 | # (调研报告最重一段从 17% 到 72% 都有),区间外不等于写错。 |
| 96 | # 它的用途是提醒一种特定失误:该单段独大的文种写成了均衡分布, |
| 97 | # 或反之——此时通常伴随其他参数同向偏离,两者合看才有意义。 |
| 98 | FOCUS = { |
| 99 | '工作方案': (37, 58, 46, '任务段较重'), |
| 100 | '工作意见': (24, 50, 42, '任务段较重'), |
| 101 | '经验材料': (27, 56, 33, '做法段较重'), |
| 102 | '调研报告': (35, 49, 41, '各功能段较均衡'), |
| 103 | '经验总结': (29, 42, 36, '各段近乎等重'), |
| 104 | '领导讲话': (33, 41, 36, '各段近乎等重'), |
| 105 | } |
| 106 | |
| 107 | ID_PARAM = { |
| 108 | '调研报告': '一是二是分层最密 + 建议词最多(向上级建言)', |
| 109 | '领导讲话': '要求词密度最高 + 三级标题为 0', |
| 110 | '工作意见': '二级标题最多(约13个)+ 几乎不用"一是"', |
| 111 | '经验材料': '一级标题最少(约2个),靠段落推进', |
| 112 | '工作方案': '三级标题最多(约11个),唯一大量用"1."的文种', |
| 113 | '经验总结': '引号概念密度冠军 + 零百分比', |
| 114 | '短经验材料': '零一级标题 + 长引号引语多(引群众原话)', |
| 115 | } |
| 116 | |
| 117 | # 法定公文:结构由制度锁定,是固定填空而非写作技法,故不设参数行。 |
| 118 | # 用 --genre 指定这些文种时给出解释,而不是只报"未知文体"。 |
| 119 | NO_PARAM = ('报告', '通知', '请示', '批复', '函', '决定', '条例', '意见', '通报', '纪要') |
| 120 | |
| 121 | BAD_WORDS = { |
| 122 | '我觉得': '公文不用第一人称主观判断 → 研究认为/实践表明', |
| 123 | '我认为': '公文不用第一人称主观判断 → 研究认为/实践表明', |
| 124 | '非常': '空洞程度副词 → 用数据或事实替代', |
| 125 | '取得了显著成效': '万能废话 → 说清具体成效', |
| 126 | '希望领导重视': '太直白 → 用论证逻辑自然导向结论', |
| 127 | '问题很多': '缺乏具体性 → 列举具体问题', |
| 128 | '尽快': '模糊时限 → 明确到月/日', |
| 129 | '相关部门': '模糊主体 → 明确牵头和配合单位', |
| 130 | } |
| 131 | |
| 132 | |
| 133 | def analyze(text): |
| 134 | lines = [l.strip() for l in text.split('\n') if l.strip()] |
| 135 | body_lines = [] |
| 136 | for l in lines: |
| 137 | if re.match(r'^#{1,6}\s', l): |
| 138 | body_lines.append(re.sub(r'^#{1,6}\s*', '', l)) |
| 139 | elif re.match(r'^(\||-{3,}|>)', l): |
| 140 | continue |
| 141 | else: |
| 142 | body_lines.append(l) |
| 143 | body = '\n'.join(body_lines) |
| 144 | plain = re.sub(r'\*\*|__|`', '', body) |
| 145 | chars = len(plain.replace('\n', '')) |
| 146 | if chars == 0: |
| 147 | return None |
| 148 | |
| 149 | sents = [s for s in re.split(r'[。!?;]', plain) if len(s.strip()) >= 4] |
| 150 | paras = [l for l in body_lines if len(l) > 40 |
| 151 | and not re.match(r'^[一二三四五六七八九十]+、|^([一二三四五六七八九十]+)|^\d+[..]', l)] |
| 152 | |
| 153 | h1 = [] |
| 154 | for m in re.finditer(r'^\**([一二三四五六七八九十]+、)([^\n]{2,80})', body, re.M): |
| 155 | t = re.split(r'[。!?;]', m.group(2))[0] |
| 156 | t = re.split(r' {2,}|\s{3,}', t)[0] |
| 157 | h1.append(t.strip()[:40]) |
| 158 | |
| 159 | # 力度词统计前先剔除引号内内容。 |
| 160 | # 理由:自造概念常带力度字样(如需求分类"必须改""可以缓"), |
| 161 | # 计入后会顶破配额。经验总结强制词配额仅 0-1,任何误判都致命。 |
| 162 | depunct = re.sub(r'[“"][^”"]{0,20}[”"]', '', plain) |
| 163 | # "不得不"是"只能"义,不是禁止义,需从"不得"中排除 |
| 164 | qz_text = depunct.replace('不得不', '###') |
| 165 | |
| 166 | return dict( |
| 167 | chars=chars, |
| 168 | sent=round(sum(len(s) for s in sents)/len(sents), 1) if sents else 0, |
| 169 | h1=len(h1), h1_titles=h1, |
| 170 | h2=len(re.findall(r'^\**([一二三四五六七八九十]+)', body, re.M)), |
| 171 | h3=len(re.findall(r'^\**\d+[..]\s*\D', body, re.M)), |
| 172 | yishi=len(re.findall(r'[一二三四五六七八九]是[,、]?', plain)), |
| 173 | dun=round(1000*plain.count('、')/chars, 1), |
| 174 | qz=sum(qz_text.count(w) for w in ['应当','必须','不得','严禁']), |
| 175 | yq=sum(depunct.count(w) for w in ['要','切实','确保','务必']), |
| 176 | jy=sum(depunct.count(w) for w in ['建议','可以','鼓励','支持']), |
| 177 | # 占位符内的 % 不是真实数据(如【待补:xx%】),先剔除 |
| 178 | pct=len(re.findall(r'\d+(?:\.\d+)?%', |
| 179 | re.sub(r'【[^】]*】', '', plain))), |
| 180 | quote=len(re.findall(r'[“"][^”"]{1,8}[”"]', plain)), |
| 181 | # 长引号引语:党建族引群众原话,中位 2.9‰,是它的正向指纹。 |
| 182 | # 网评族移除后这项不再承担两个千字文体的分界任务,但公文族语料 |
| 183 | # 无此字段,实测值高仍能把党建族拉近、把公文族推远,保留有效。 |
| 184 | # 仅统计中文弯引号——ASCII 直角引号无法配对,会把相邻两个引号 |
| 185 | # 之间的正文误判为引语。 |
| 186 | quote_long=round(1000*len(re.findall(r'[“][^”]{9,}[”]', plain))/chars, 1), |
| 187 | paras=len(paras), |
| 188 | para_len=round(sum(len(p) for p in paras)/len(paras)) if paras else 0, |
| 189 | ascii_quote=plain.count('"'), |
| 190 | dash=plain.count('——'), |
| 191 | sections=section_weights(body_lines), |
| 192 | plain=plain, |
| 193 | ) |
| 194 | |
| 195 | |
| 196 | def section_weights(body_lines): |
| 197 | """按一级标题切分,返回各段字数占比。 |
| 198 | |
| 199 | 自检器原先只查参数不查重心分布,而重心分布恰是区分 |
| 200 | 「单段独大型」与「均衡型」的关键——参数可以全部达标而文种写错。 |
| 201 | """ |
| 202 | secs, cur = [], ['导语', 0] |
| 203 | for l in body_lines: |
| 204 | if re.match(r'^\**[一二三四五六七八九十]+、', l): |
| 205 | secs.append(cur) |
| 206 | cur = [re.sub(r'^\**', '', l)[:16], 0] |
| 207 | cur[1] += len(l) |
| 208 | secs.append(cur) |
| 209 | tot = sum(n for _, n in secs) or 1 |
| 210 | return [(name, n, round(100 * n / tot, 1)) for name, n in secs] |
| 211 | |
| 212 | |
| 213 | def match_genre(a): |
| 214 | """算参数向量与各文种中位数的归一化距离,返回排序结果 |
| 215 | |
| 216 | quote_long 保留在 KEYS 里:它只定义在党建族一行,公文族各文种没有 |
| 217 | 这个字段而被跳过,因此实测长引语高会拉近党建族、拉远公文族。 |
| 218 | """ |
| 219 | KEYS = ['chars','sent','h1','h2','h3','yishi','dun','qz','yq','jy','pct', |
| 220 | 'quote_long'] |
| 221 | out = [] |
| 222 | for g, r in REF.items(): |
| 223 | d, cnt = 0.0, 0 |
| 224 | for k in KEYS: |
| 225 | if k not in r: continue |
| 226 | lo, hi, mid = r[k] |
| 227 | scale = max(hi - lo, abs(mid) * 0.3, 1) |
| 228 | d += abs(a[k] - mid) / scale |
| 229 | cnt += 1 |
| 230 | out.append((g, d / cnt)) |
| 231 | return sorted(out, key=lambda x: x[1]) |
| 232 | |
| 233 | |
| 234 | def bar(val, lo, hi): |
| 235 | """在 p25-p75 区间中的位置示意""" |
| 236 | if hi <= lo: |
| 237 | return '│' if val == lo else ('←' if val < lo else '→') |
| 238 | span = hi - lo |
| 239 | pos = (val - lo) / span |
| 240 | if pos < -0.15: return '←低' |
| 241 | if pos > 1.15: return '高→' |
| 242 | slot = max(0, min(9, int(pos * 9))) |
| 243 | return '·' * slot + '●' + '·' * (9 - slot) |
| 244 | |
| 245 | |
| 246 | def report(a, genre): |
| 247 | r, hard = REF[genre], HARD.get(genre, []) |
| 248 | print(f'\n{"="*76}') |
| 249 | print(f' 文体:{genre} 语料样本 n={r["n"]}') |
| 250 | print(f' 身份证参数:{ID_PARAM[genre]}') |
| 251 | print(f'{"="*76}\n') |
| 252 | |
| 253 | # ① 硬冲突 |
| 254 | fails = [] |
| 255 | for key, ok_fn, msg, basis in hard: |
| 256 | if not ok_fn(a[key]): |
| 257 | fails.append((key, a[key], msg, basis)) |
| 258 | print('【硬冲突检查】判文种识别错误,非风格差异') |
| 259 | print('-'*76) |
| 260 | if not fails: |
| 261 | print(f'✓ 通过({len(hard)} 项)') |
| 262 | else: |
| 263 | for key, val, msg, basis in fails: |
| 264 | print(f'✗ {msg}') |
| 265 | print(f' 实测 {key}={val} 依据:{basis}') |
| 266 | |
| 267 | # 软提示 |
| 268 | softs = [(k, a[k], m, b) for k, fn, m, b in SOFT.get(genre, []) if not fn(a[k])] |
| 269 | if softs: |
| 270 | print() |
| 271 | for key, val, msg, basis in softs: |
| 272 | print(f'⚠ {msg}') |
| 273 | print(f' 实测 {key}={val} {basis}') |
| 274 | print() |
| 275 | |
| 276 | # ② 对照表 |
| 277 | print('【参数对照】仅供参考,偏离不等于错误') |
| 278 | print('-'*76) |
| 279 | LABEL = {'chars':'字数','sent':'平均句长','h1':'一级标题','h2':'二级标题','h3':'三级标题', |
| 280 | 'yishi':'"一是二是"','dun':'顿号密度‰','qz':'强制词','yq':'要求词','jy':'建议词', |
| 281 | 'pct':'百分比','paras':'段落数','para_len':'段落长度','quote':'引号概念', |
| 282 | 'quote_long':'长引语‰'} |
| 283 | print(f'{"指标":<12}{"你的":>8}{"中位":>8}{"常见区间":>12} 位置') |
| 284 | for k, label in LABEL.items(): |
| 285 | if k not in r: continue |
| 286 | lo, hi, mid = r[k] |
| 287 | v = a[k] |
| 288 | flag = '' if lo <= v <= hi else ' ⚠偏离' |
| 289 | print(f'{label:<12}{v:>8}{mid:>8}{f"{lo}-{hi}":>12} {bar(v,lo,hi)}{flag}') |
| 290 | |
| 291 | # ②b 标点修辞检查 |
| 292 | dash_issues = [] |
| 293 | if a['dash'] > 1: |
| 294 | dash_issues.append(f'正文破折号 {a["dash"]} 个(语料平均 0.2-0.9 个/篇,建议不超过 1 个)') |
| 295 | # 非引语冒号 |
| 296 | non_quote_colons = [] |
| 297 | for cm in re.finditer(r'([^\n]{0,12}):', a['plain']): |
| 298 | before = cm.group(1).strip() |
| 299 | if re.search(r'[一二三四五六七八九十]+、|([一二三四五六七八九十])|\d+[..]', before): |
| 300 | continue |
| 301 | if re.search(r'说|问|讲|提|指出|反映|回忆|告诉|表示|答', before): |
| 302 | continue |
| 303 | if re.search(r'人民政府|部门|单位|书记|镇长|同志|负责人', before): |
| 304 | continue |
| 305 | if '待补' in before or '待核' in before: |
| 306 | continue |
| 307 | non_quote_colons.append(cm.group(0)[:25]) |
| 308 | if non_quote_colons: |
| 309 | dash_issues.append(f'非引语冒号 {len(non_quote_colons)} 处(冒号应只用于引语引入、主送机关、层次标题)') |
| 310 | # 元评论词(排除引号内——引语里的口语保留) |
| 311 | depunct_meta = re.sub(r'[“][^”]*[”]', '', a['plain']) |
| 312 | meta_words = re.findall(r'其实[是就]|说到底|归根到底|本质上', depunct_meta) |
| 313 | if meta_words: |
| 314 | dash_issues.append(f'元评论词 {len(meta_words)} 处({", ".join(set(meta_words))})') |
| 315 | if dash_issues: |
| 316 | print('\n【标点修辞检查】') |
| 317 | print('-'*76) |
| 318 | for di in dash_issues: |
| 319 | print(f'⚠ {di}') |
| 320 | elif a['dash'] <= 1: |
| 321 | pass # 不额外打印通过信息,保持报告简洁 |
| 322 | |
| 323 | # ③ 一级标题字数 |
| 324 | if a['h1_titles']: |
| 325 | avg = sum(len(t) for t in a['h1_titles'])/len(a['h1_titles']) |
| 326 | biz = genre in ('工作意见','工作方案') |
| 327 | tgt = '4-12(业务类)' if biz else '14-24(分析类)' |
| 328 | print(f'\n一级标题平均 {avg:.1f} 字,目标 {tgt}') |
| 329 | for t in a['h1_titles'][:6]: |
| 330 | print(f' · {t}') |
| 331 | |
| 332 | # ③b 重心分布:参数表测不到这一项,但它单独不足以判错(见 FOCUS 注释) |
| 333 | if genre in FOCUS and len(a['sections']) > 1: |
| 334 | lo, hi, mid, note = FOCUS[genre] |
| 335 | body = [s for s in a['sections'] if s[0] != '导语'] |
| 336 | top = max(body, key=lambda s: s[2]) if body else None |
| 337 | print(f'\n【重心分布】仅供对照,偏离不等于错误') |
| 338 | print('-'*76) |
| 339 | for name, n, pct in a['sections']: |
| 340 | mark = ' ←最重' if top and name == top[0] else '' |
| 341 | print(f'{pct:>6.1f}% {n:>5}字 {name}{mark}') |
| 342 | if top: |
| 343 | print(f'最重一段 {top[2]}% 语料中位 {mid}%,常见 {lo}-{hi}%({note})') |
| 344 | if top[2] < lo or top[2] > hi: |
| 345 | print(f' 个体离散度大,此项区间外不作为错误;' |
| 346 | f'若同时有多项参数同向偏离,再考虑整体节奏问题。') |
| 347 | |
| 348 | # ③c 引号书写提示:ASCII 直角引号无法配对,会令引号三项统计失真 |
| 349 | if a['ascii_quote']: |
| 350 | print(f'\n⚠ 检出 {a["ascii_quote"]} 个 ASCII 直角引号(")。' |
| 351 | f'语料使用中文弯引号,混用会使引号概念与长引语统计失真') |
| 352 | |
| 353 | # ④ 文种匹配度(仅在跨文体族误配、或差距显著时提示) |
| 354 | m = match_genre(a) |
| 355 | rank = [g for g, _ in m] |
| 356 | FAMILY = {'短经验材料': '党建族'} |
| 357 | fam_self = FAMILY.get(genre, '公文族') |
| 358 | fam_top = FAMILY.get(rank[0], '公文族') |
| 359 | |
| 360 | if fam_self != fam_top: |
| 361 | # 跨文体族误配:两族在篇幅和一级标题上不重叠,这个信号强。 |
| 362 | # 注:旧版记录的"网评识别准确率 94%"是含网评族时的三族数字, |
| 363 | # 网评族移除后未重测(语料原文已因版权移除),不再引用该数值。 |
| 364 | print(f'\n【文体族检查】') |
| 365 | print('-'*76) |
| 366 | print(f'⚠ 你声明「{genre}」({fam_self}),但参数上更像「{rank[0]}」({fam_top})') |
| 367 | print(f' 两族参数互斥,判错族后所有参数都会偏。距离:' |
| 368 | + ' '.join(f'{g} {d:.2f}' for g, d in m[:3])) |
| 369 | elif genre in rank[3:]: |
| 370 | # 同族内排名靠后才提示——同族文种参数重叠严重,Top1 准确率仅 33-55% |
| 371 | print(f'\n【结构习惯提示】') |
| 372 | print('-'*76) |
| 373 | print(f'ⓘ 参数上「{genre}」排在第 {rank.index(genre)+1} 位,前三位是 ' |
| 374 | + '、'.join(rank[:3])) |
| 375 | print(' 同族文种参数重叠大,此信号仅供参考,不代表文种判错。') |
| 376 | |
| 377 | # ⑤ 避坑词 |
| 378 | hits = [(w, why) for w, why in BAD_WORDS.items() if w in a['plain']] |
| 379 | if hits: |
| 380 | print(f'\n【避坑词】') |
| 381 | print('-'*76) |
| 382 | for w, why in hits: |
| 383 | print(f'⚠ "{w}" — {why}') |
| 384 | |
| 385 | # ⑥ 力度词配额提示:改稿时最易踩的坑 |
| 386 | if 'qz' in r and a['qz'] > r['qz'][1]: |
| 387 | print(f'\n⚠ 强制词超配额({a["qz"]} > {r["qz"][1]})。' |
| 388 | f'"要"属要求词、"必须/应当"属强制词,两者配额独立且相差一个量级,' |
| 389 | f'\n 改稿时把"要"整批替换成"必须"会直接顶破上限。') |
| 390 | |
| 391 | print() |
| 392 | return len(fails) |
| 393 | |
| 394 | |
| 395 | def main(): |
| 396 | ap = argparse.ArgumentParser(add_help=False) |
| 397 | ap.add_argument('file', nargs='?') |
| 398 | ap.add_argument('--genre', '-g') |
| 399 | ap.add_argument('--match', action='store_true') |
| 400 | ap.add_argument('--list', action='store_true') |
| 401 | ap.add_argument('-h', '--help', action='store_true') |
| 402 | args = ap.parse_args() |
| 403 | |
| 404 | if args.help or (not args.file and not args.list): |
| 405 | print(__doc__); sys.exit(0) |
| 406 | if args.list: |
| 407 | print('支持的文体:') |
| 408 | for k, v in ID_PARAM.items(): |
| 409 | print(f' {k:<10} n={REF[k]["n"]:<4} {v}') |
| 410 | sys.exit(0) |
| 411 | |
| 412 | try: |
| 413 | raw = Path(args.file).read_text(encoding='utf-8') |
| 414 | except FileNotFoundError: |
| 415 | print(f'找不到文件:{args.file}', file=sys.stderr); sys.exit(2) |
| 416 | except IsADirectoryError: |
| 417 | print(f'这是一个目录,不是文件:{args.file}', file=sys.stderr); sys.exit(2) |
| 418 | except UnicodeDecodeError: |
| 419 | print(f'无法按 UTF-8 读取(请确认是文本文件):{args.file}', file=sys.stderr); sys.exit(2) |
| 420 | except OSError as e: |
| 421 | print(f'读取失败:{args.file}({e.strerror})', file=sys.stderr); sys.exit(2) |
| 422 | |
| 423 | a = analyze(raw) |
| 424 | if not a: |
| 425 | print('文件为空或无法解析', file=sys.stderr); sys.exit(2) |
| 426 | |
| 427 | if args.match or not args.genre: |
| 428 | print(f'\n{args.file} 参数向量匹配结果:\n') |
| 429 | for i, (g, d) in enumerate(match_genre(a)): |
| 430 | print(f'{"★" if i==0 else " "} {g:<10} 距离 {d:.2f}') |
| 431 | print(f'\n实测:{a["chars"]}字 句长{a["sent"]} 一级{a["h1"]} 二级{a["h2"]} ' |
| 432 | f'三级{a["h3"]} 一是{a["yishi"]} 顿号{a["dun"]}‰ 引号{a["quote"]} ' |
| 433 | f'长引语{a["quote_long"]}‰') |
| 434 | body = [s for s in a['sections'] if s[0] != '导语'] |
| 435 | if body: |
| 436 | top = max(body, key=lambda s: s[2]) |
| 437 | print(f'重心:最重一段 {top[2]}%({top[0]})' |
| 438 | f' 各文种语料中位 33-46%\n') |
| 439 | else: |
| 440 | print() |
| 441 | sys.exit(0) |
| 442 | |
| 443 | if args.genre not in REF: |
| 444 | print(f'未知文体:{args.genre}', file=sys.stderr) |
| 445 | print(f'可选:{"、".join(REF)}', file=sys.stderr) |
| 446 | if args.genre in NO_PARAM: |
| 447 | print(f'\n说明:{args.genre}属法定公文,本 skill 不为其设参数行——' |
| 448 | f'其正文结构由制度锁定,是固定填空而非写作技法,统计参数没有意义。', |
| 449 | file=sys.stderr) |
| 450 | print(f'`示例/` 里确有 {args.genre} 范文,但它们只做文体族校验与版式核对,不做参数验收。', |
| 451 | file=sys.stderr) |
| 452 | print(f'版式要素(发文字号、主送机关、成文日期、公开标识)请对照 ' |
| 453 | f'`参考/法定公文样例/` 的真件;用 --match 可查它是否落在公文族内。', file=sys.stderr) |
| 454 | sys.exit(2) |
| 455 | |
| 456 | fails = report(a, args.genre) |
| 457 | sys.exit(1 if fails else 0) |
| 458 | |
| 459 | |
| 460 | if __name__ == '__main__': |
| 461 | main() |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

