news 2026/10/20 8:13:43

SMOG公式:1969年的文本可读性算法,用Python实现与踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SMOG公式:1969年的文本可读性算法,用Python实现与踩坑指南

简介:smog-formula 是一个面向 JavaScript 开发者的可读性检测工具,基于 1969 年的 SMOG(Gobbledygook 的简单度量)公式,通过音节数、多音节词数与句子数计算文本阅读难度等级,适合内容平台、教育测评或文本分析场景使用。该 npm 包仅支持 ESM 导入,需在 Node 12+ 环境中运行,核心 API 为 smogFormula 函数,例如传入 sentence:1、polysillabicWord:4 即可得到约 14.55 的等级值。压缩包共 12 个文件,以 js 源码、json 配置、yml 工作流、md 文档及许可证文件为主,整体仅 6KB,结构清晰。已有 368 人浏览学习。通过阅读源码与工程配置,可以掌握 SMOG 公式的具体实现、可读性算法的参数定义,以及 npm 包的标准发布与测试流程,便于二次开发或直接集成到自有文本分析流程中。

1. 为什么一个1969年的可读性公式,现在还被内容团队当基线

1969年出现的SMOG公式,算年龄比不少做内容质检的从业者还大,但到今天,医疗健康传播、技术文档评审、教育软件里但凡要给英文文本标一个可读性分数,SMOG依然是默认基线之一。它的全称是 Simple Measure of Gobbledygook——Gobbledygook 指的就是那些堆满术语、绕来绕去的废话行话。它只数两样东西:句子个数,以及句子里三个音节以上的长词个数,然后套一个 3 + √P 的公式,映射出读者需要读到哪个年级才能顺畅理解。反直觉的点在于:这个 1969 年的老式子,不做句法分析、不做词向量,却比很多机器学习可读性模型更容易解释、更稳定地反映「长词密度」这一核心难度。

它解决的是文本难易度量化问题:这篇文章到底是给谁读的、要不要把术语换掉、评审时用什么口径跟作者沟通。适合三类人来做:内容平台做质检的开发者、给普通读者写材料的作者、以及搭 NLP 工具链但缺一个可解释基线的人。接下来的写法偏实操:先把手算流程走通,再给出一个能直接复用的 Python 实现,最后把最常见的那几个坑逐个拆掉。

2. 先把SMOG手算跑通:30句抽样、多音节词计数与 3+√P 的由来

2.1 Gobbledygook与阅读难易度:长词率为什么和年级正相关

可读性研究里有一个朴素的共识:读者理解文本的负担,主要来自「认词」和「理句」两步。SMOG 选择盯住认词这一步,并且只盯最难的那一类词——三个音节以上的长词。一个词要三个音节,意味着它大概率带前缀后缀、大概率是拉丁或希腊词根、大概率在口语里不常见,比如 utilization 而不是 use,implementation 而不是 doing。这类词在心理词典里激活慢,拼读负担高,读者扫一眼就得停下来。文本里这种词越多,读得越慢,理解越容易断。

1969年提出这个公式的研究者,当时用一批教育材料做回归,发现「30句样本里多音节词数量的平方根」和「读者所需教育年级」之间近似线性。为什么取平方根而不是直接看比例?因为多音节词数量从10涨到100时,阅读难度不是线性翻十倍,而是从大约6年级涨到13年级,涨幅被明显压缩。平方根这个非线性变换正好把长尾压平,让分数在常见文本区间里和年级大致对齐。这个操作在当年没有机器学习,就是一个经验拟合,但它拟合的是「难度感知」而不是「文本长度」,所以到现在依然能打。

这里要特别说清 SMOG 的性格:它对词汇难度极度敏感,对句法复杂度基本不敏感。两句话各20个词,一句全是短词,一句塞满三音节术语,SMOG 分数会差出好几个年级;但一篇全是简单词却绕了五层从句的文本,SMOG 反而会给出偏低的分数。选它做基线,就等于明确告诉团队:我们先把词汇门槛管住,句法问题交给别的手段。

2.2 手算SMOG的七个步骤:从取段到开根号

完整的手算流程不复杂,但每一步都有细节,先按原版规则走一遍:

  1. 从文本开头取前10句,中间取10句,结尾取10句,凑成30句样本。文本总句子数不足30句时,全部纳入。
  2. 逐句识别包含3个及以上音节的单词,缩写、数字、专有名词都算数。
  3. 统计这30句里多音节词的总数,记为 P。
  4. 计算 SMOG = 3 + √P。
  5. 结果直接对应年级:6代表读者大约要读到六年级,12代表十二年级,数字越高越难。
  6. 如果分数超过12,说明文本基本只适合高等教育以上读者,普通大众材料通常要求压到8以下。
  7. 记录时注明采用了哪个口径,比如连字符是否拆分、未知词音节怎么判,否则分数不可比。

举个例子,假设某一段文本里有这样一句:

Nevertheless, the implementation of additional automation within our organization will substantially facilitate comprehensive documentation procedures.

逐词数音节:nevertheless 4音节,implementation 5,additional 4,automation 4,organization 5,substantially 4,facilitate 3,comprehensive 4,documentation 5,procedures 3。这10个都是多音节词。30句抽下来如果总数是49个,SMOG 就是 3 + √49 = 10,对应十年级。手算时最容易吵起来的两个点:连字符词算一个词还是拆开算,以及 e.g.、Dr. 这类缩写会不会干扰数句。这两个问题后面第4章会专门给结论,手算阶段先记录自己的处理方式就行。

注意:SMOG 原版是为英语母语读者设计的,分数含义依赖英语的拼读规则和年级体系,中文文本不能直接用,这点在第4章展开。

2.3 SMOG、Flesch-Kincaid、Gunning Fog:三个公式怎么选

做文本可读性的人一定绕不开另外两个老牌公式,Flesch-Kincaid 和 Gunning Fog。三者的核心差异在「拿什么衡量难」:SMOG 只看多音节词密度,Flesch-Kincaid 把平均句长和平均音节数揉在一起,Gunning Fog 看句子长度加复杂词占比。选型不是越新越好,而是看你要拦什么。

公式核心输入输出强项弱项
SMOG30句样本中的多音节词数年级(约3–13+)对术语密度敏感,适合健康传播、公共文档短文本不稳定,句法复杂度盲区
Flesch-Kincaid Grade平均句长 + 平均音节数年级数值平滑,适合教材分级长词占比被平均句长稀释
Gunning Fog平均句长 + 复杂词占比年级计算简单,结果直观复杂词定义粗糙,波动大
Flesch Reading Ease平均句长 + 平均音节数0–100分大众传播常用分数与年级非线性,不直观

我一般这样选:给大众读者做健康科普、说明书、政策解读,用 SMOG,因为它直接暴露术语密度;给教材或分级阅读做连续标定,用 Flesch-Kincaid,因为它对难度的变化更平滑;做内部快速筛查时用 Gunning Fog,手算快,但别拿它做对外结论。还需要记住一个经验:同一篇文本,SMOG 给出的年级通常比 Flesch-Kincaid 高 1 到 2 个年级,这是两个公式的基准不同,不是谁算错了,做横向对比时千万不能混用两套公式的数字。

3. 用Python把smog-formula落地成函数:分词、句式切分与音节判定

3.1 第一版:用元音群近似音节,先跑通主流程

先给一个不依赖外部词典的版本,逻辑最简单,也最容易调试。核心思路是把连续的元音字母当成一个音节组,数元音组的数量来近似音节数。

import math import re def count_syllables_vowel_group(word: str) -> int: # 用连续元音字母组的数量近似音节数,silent e 场景会偏高 word = word.lower() groups = re.findall(r"[aeiouy]+", word) return max(1, len(groups)) def smog_v1(text: str) -> float: # 第一版只数全文词,不做30句抽样,先把计算主流程跑通 words = re.findall(r"[a-zA-Z]+(?:['-][a-zA-Z]+)*", text) polys = sum(1 for w in words if count_syllables_vowel_group(w) >= 3) return 3 + math.sqrt(max(0, polys))

这段代码的逻辑分三层:正则把所有字母串提取成词,数字和符号被直接忽略;每个词通过元音组数量判定是否达到 3 音节;最后套原版公式。max(1, ...)是为了防止 magic 这种词被算成 0 音节。连字符词如 well-known 被正则里的(?:['-][a-zA-Z]+)*保留为整体,不拆开。

这个版本的优点是零依赖,缺点是 silent e 会让音节数普遍虚高,比如 made 会被算成 2 音节。作为主流程验证够用,但如果直接拿去对外输出分数,第4章会告诉你为什么容易翻车。

3.2 第二版:接入cmudict发音词典,让多音节判定更稳

要提升多音节判定的准确度,常见做法是接入发音词典。NLTK 自带的 cmudict 数据把每个词拆成音素并按重音标了数字,数字的个数就是音节数。

from nltk.corpus import cmudict _d = cmudict.dict() def count_syllables_cmu(word: str) -> int: # cmudict 每个发音里的数字个数即为音节数 word = word.lower() try: prons = _d[word] counts = [len([p for p in pron if p[-1].isdigit()]) for pron in prons] # 同一个词有多种读音时取最大音节数,宁可高估难度 return max(counts) except KeyError: # 词典未收录的词,回退到元音群近似 return count_syllables_vowel_group(word)

逻辑说明:p[-1].isdigit()是在每个音素串的末尾找重音标记,CMU 数据里重音标记就是数字字符。max(counts)用的是保守策略:像 either 这种存在两种读音的词,按音节多的那个计入。SMOG 的定位本来就是「宁可高估难度」,用 max 符合这个取向。回退分支很重要,专有名词、新造词、拼写变体在词典里查不到,不兜底的话整个函数会直接抛异常。

注意第一次跑之前需要下载数据,在环境里执行nltk.download('cmudict')。这一步卡住的话,多半是网络没到位,也可以手动把数据文件拷到 NLTK 数据目录下。

3.3 按规则抽样:头10句、中10句、尾10句各取10句

原版 SMOG 要求从开头、中间、结尾各取 10 句,目的是覆盖文本不同位置的难度分布。很多现成实现直接随机抽 30 句,结构强的文本里会漏掉结论段落。

def split_sentences(text: str) -> list[str]: # 先保护常见缩写尾点,避免把一句话切成两半 text = re.sub(r"\b(Dr|Mr|Mrs|Ms|Inc|e\.g|i\.e)\.", r"\1<DOT>", text) raw = re.split(r"[.!?]+", text) sentences = [s.strip().replace("<DOT>", ".") for s in raw if len(s.strip()) > 0] return sentences def sample_30(sentences: list[str]) -> list[str]: # 头10、中10、尾10;不足30句时全量返回 if len(sentences) <= 30: return sentences head, tail = sentences[:10], sentences[-10:] mid_start = len(sentences) // 2 - 5 mid = sentences[mid_start:mid_start + 10] return head + mid + tail

参数说明:mid_start取len(sentences) // 2 - 5,当总句数为奇数时中间段会稍微偏前,这是刻意选的,宁可偏前也不要越界。split_sentences里的缩写保护正则覆盖了最常见的几个词,但 U.S.A.、Ph.D. 这类更复杂的缩写仍会漏,后面第4章会讲怎么补强。抽样数量固定 30 是原公式的经验参数,不建议放大,放大后年级映射就不准了。

3.4 组装成命令行工具:输入文本路径,输出SMOG分数

把前面几节拼成一个完整的命令行工具,输入文件路径,输出 SMOG 年级分数,顺便支持抽样量和置信度标记。

import argparse from pathlib import Path def smog_formula_score(text: str) -> float: # 完整流程:分句 -> 抽样 -> 统计多音节词 -> 原版公式 sents = sample_30(split_sentences(text)) polys = 0 for sent in sents: words = re.findall(r"[a-zA-Z]+(?:['-][a-zA-Z]+)*", sent) for w in words: if count_syllables_cmu(w) >= 3: polys += 1 return 3 + math.sqrt(polys) def main(): parser = argparse.ArgumentParser(description="smog-formula 可读性计算") parser.add_argument("file", type=Path, help="输入文本文件路径") parser.add_argument("--sample", type=int, default=30, help="抽样句子数,默认30,通常不建议修改") args = parser.parse_args() text = args.file.read_text(encoding="utf-8", errors="ignore") score = smog_formula_score(text) sents = sample_30(split_sentences(text)) print(f"SMOG Grade: {score:.2f}") print(f"样本句子数: {len(sents)}") if __name__ == "__main__": main()

逻辑说明:--sample参数保留出来不是为了让你日常改它,而是方便做灵敏度测试——把抽样量从 30 改成 20、15,观察分数波动,就能判断一个文本的分数是否稳定。errors="ignore"处理非 UTF-8 编码文件时避免整个程序崩掉,代价是丢弃少量乱码字符,对统计多音节词影响很小。跑通这个工具后,前面手算的那些规则就有了一个可重复执行的落点。

4. smog-formula落地避坑:预处理、短文本偏差与多语言误用

4.1 缩写词尾点把文本切成碎片:先保护再分句

现象:一段本来 40 句的文本,跑完分句逻辑变成 90 句,抽样位置全乱,SMOG 分数明显偏低。

原因:句切分正则[.!?]+把 Dr.、e.g.、Inc. 这类缩写词后面的英文句点当成了句子结束符。缩写被拦腰截断后,多音节词计数被稀释到更多「假句子」里,30句抽样落在毫无意义的位置。

解决:分句前先做缩写保护。第3章代码里用的<DOT>替换方案是常见做法,但覆盖面有限。我一般会额外维护一份带点缩写的列表,把 Ph.D.、U.S.A.、etc. 这些也加进去,替换逻辑统一:先用正则把所有[A-Za-z]\.后面紧跟小写字母或空格缩写词的情况保护起来,再切分。切完再还原句点。这一步做完,句数量基本能回到和人眼判断一致的水平。

4.2 多音节计数和在线工具对不上:口径不一致

现象:同一份文本,用自己实现的工具算出来是 7.5 年级,丢进某个在线可读性检测网站却是 6.1,两边差出一个半年级。

原因:不同工具对「多音节词」的判定口径差异很大。有的用cmudict发音词典,有的用元音群近似,有的把连字符词拆开算两个词,有的把数字串也当成单词。这些差异在长词密度高的文本里会被放大,最终体现在根号后的年级数上。

解决:选定一套口径并在报告里写明:连字符词按整体计,未知词按元音群回退,多音读法按最大音节数计。对外对比时,只用同一套算法跑所有文本,不要拿 A 工具的 SMOG 和 B 工具的 SMOG 直接对比。做基准测试时,建议用一小段公开的谚语或百科文本作为标准样例,把自己的输出和已知参考值对齐一次。

4.3 短文本直接翻车:小样本下平方根放大误差

现象:一段只有 8 句话的说明文字,数出 6 个多音节词,SMOG 分数 5.45;删掉其中两个长词后,分数直接掉到 4.83。内容只改了一行,分数却跳了半个年级。

原因:公式里的平方根对小样本极度敏感。多音节词总数 P 从 4 变到 8,分数从 5 涨到 5.83,而样本句子数不足时这个波动没有任何统计缓冲。SMOG 本来就是按 30 句样本设计的,短文本完全不在它的适用区间内。

解决:总句子数低于 30 句时,输出分数旁要带置信度标记,比如加一行「警告:样本不足30句,分数仅供参考」。如果这个文本会进入自动决策流程,比如内容平台拿它决定要不要人工复审,那就直接用全量文本计算并调低决策阈值,别套用 30 句样本的年级阈值。

4.4 中文、混合语料直接套英文公式:结果没有意义

现象:有人拿一封中文邮件或者中英混杂的聊天记录跑 SMOG,得到一个看似合理的年级数,比如 7.2,但中文读者看了完全无法对应这个难度。

原因:SMOG 的多音节判定依赖英语音节的拼读规则,中文没有音节这个概念,分词边界也不一样。混合语料里只要英文单词占比变化,分数就会剧烈波动,但这个波动反映的不是难度变化,而是语言比例变化。

解决:SMOG 只适用于英文文本,这是底线。中文文本的可读性需要另走一条路径:先分词,再统计词长分布、低频词占比、句子平均长度,必要时引入中文词汇频率表做加权。如果平台里必须处理多语言内容,就给每种语言单独配一个可读性模块,绝不共用一个 SMOG 实现。

4.5 长词结构不同,SMOG分数失真:术语密度和常见长词被等权处理

现象:一篇学术摘要和一篇新闻报道,SMOG 分数都是 9.5,但前者让普通读者读得满头问号,后者基本能顺下来。分数没变,实际难度感受完全不同。

原因:SMOG 把所有三音节以上单词一律等权计入,information 这种高频长词和 environmental sustainability 这种术语组合在公式里贡献相同。它不区分「长但常见」和「长且生僻」,而真正卡住读者的往往是后者。

解决:落地时不要只输出一个 SMOG 分数,加一个「生僻长词占比」的辅助指标。实现上可以接一个词汇频率表,对多音节词按频率分层,低频词单独加权。不做加权的话,至少按语料类型分别设阈值:故事类内容 8 年级可能没问题,法律条款类内容压到 6 年级以下才算达标。

5. 锁死实现:回归测试与SMOG分数的可信区间

5.1 维护一组黄金样例,跑通回归测试

公式实现最怕的不是写错,而是改完一处代码后分数悄悄漂移。解法是维护一组固定文本和对应期望值,每次改动后用这组样例跑一遍自动化回归。

def load_golden_samples(): # 每项是(文本, 期望分数),期望分数来自人工复核过的基准值 return [ ("A simple sentence has few long words.", 3.0), ("Nevertheless, the implementation of additional automation substantially facilitates comprehensive documentation procedures.", 5.57), ] def test_smog_regression(): for text, expected in load_golden_samples(): score = smog_formula_score(text) assert abs(score - expected) < 0.1, f"期望 {expected},实际 {score}"

这段测试的意义在于把「我感觉这个公式没问题」变成「跑得过才叫没问题」。expected的值建议用你自己代码跑通后人工复核过的数字,不要从网上抄参考值,因为音节判定口径不同,期望值很难完全一致。只要口径不变,回归测试就能在后续重构分词正则、换音节表时第一时间暴露偏差。

5.2 给分数标一个可信区间,而不是一个单点数字

SMOG 的输出不适合拿来做精确定位。我的习惯是:对超过 100 句的长文档,用滑动窗口重采样多次,取分数的均值和波动范围。方法是把文本按句子切成列表,从不同起点各取 30 句算一遍分数,看最大最小差。常见结果在 ±0.5 年级以内,如果某个文本波动超过 1 个年级,说明它的难度分布不均匀,一个单点分数会误导决策。

汇报时写成「SMOG 8.2 ± 0.4」比写「SMOG 8.2」诚实得多。配合 Flesch-Kincaid 做交叉验证也很有用:一个句子长、词汇短的文本,SMOG 会给出低分而 Flesch-Kincaid 会偏高,两者差值本身就是文本结构偏词汇难度还是偏句法难度的信号。我看完两个分数再决定是建议作者换词还是拆句。

我现在的习惯是,接任何可读性需求先问三件事:给谁读、拿来做什么决策、能不能接受跑多个公式。口径没定就跑 SMOG,后面全得返工。这也算这几年踩坑踩出来的血泪经验,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:13:42

npm 查看包版本全攻略:npm view、npm ls 与 semver 规则详解

在没有 UI 的 Linux 服务器上排查问题&#xff0c;或者写 Dockerfile 时不敢把版本号写死&#xff0c;再或者就是纯粹想知道某个包到底更新到哪个版本了——这种时候&#xff0c;你需要的核心技能就是快速准确地查看 npm 包里存着的版本信息。很多开发者天天npm install&#x…

作者头像 李华
网站建设 2026/10/20 8:13:42

Neo4j 5.25 Windows安装配置与Cypher实操指南

简介&#xff1a;Neo4j社区版5.25.1的Windows安装包面向需要在Windows环境部署图数据库的开发者与技术团队&#xff0c;用于解决社交网络分析、推荐系统、欺诈检测等场景下的复杂关联数据查询与关系建模需求。压缩包共包含272个文件&#xff0c;其中244个jar为Neo4j核心库、浏览…

作者头像 李华
网站建设 2026/10/20 8:13:11

AI原生开发实战:用Python重写Modbus仿真器

1. 为什么我要用AI原生思路重写一个Modbus仿真器搞工业自动化和物联网的朋友对Modbus协议肯定不陌生。这东西诞生于上世纪七十年代&#xff0c;按理说早该进博物馆了&#xff0c;但现实是——你去任何一个工厂车间、配电房、水处理站转一圈&#xff0c;Modbus RTU和Modbus TCP还…

作者头像 李华
网站建设 2026/10/20 8:12:47

A2L生成工具实战:从地址提取到模板渲染的完整指南

做电控标定的工程师&#xff0c;多半都有过这种经历&#xff1a;新项目启动&#xff0c;先把ECU的固件刷进去&#xff0c;然后在标定工具里新建工程、加载描述文件&#xff0c;结果满屏都是红色报错&#xff0c;读取的测量值不是0就是乱码。折腾半天&#xff0c;最后定位到问题…

作者头像 李华
网站建设 2026/10/20 8:12:46

C#上位机开发实战:多品牌PLC与仪表通讯的稳定架构设计

如果你在工控行业待过一段时间&#xff0c;大概率见过这种需求描述&#xff1a;“帮我做一个上位机&#xff0c;要和西门子、三菱、汇川的PLC通讯&#xff0c;另外还有几台仪表&#xff0c;要把数据采集上来&#xff0c;界面上能看能存。”很多人一听到“上位机”三个字&#x…

作者头像 李华