简介smog-formula 是一份基于 1969 年 SMOGGobbledygook 的简单度量公式实现文本阅读难易程度检测的 JavaScript 工具包面向需要评估英文文本可读性等级的前端开发者、教育技术研究者与内容编辑。它通过统计句子数与多音节词数量快速换算出对应的阅读年级水平适用于教材分级、文章难度评估等场景。资源包共 12 个文件以 js 源码与 json 配置为主辅以 yml 工作流、md 说明文档及 license、editorconfig 等工程配置文件压缩后仅约 6KB体量轻巧。该包仅支持 ESM需 Node 12 环境并以 import 方式引入核心导出 smogFormula 一个标识符调用时传入句子数与多音节词数即可返回难度数值例如一句含四个多音节词的句子可算出约 14.55 的等级结果。目前已有 368 人学习下载适合希望快速集成可读性检测能力、了解 SMOG 公式工程实现的读者参考使用。1. 可读性公式为什么一段文字“难不难”可以被算出来很多人第一次听到“用公式算文本难度”都会觉得玄学——阅读难度不是读者主观感受吗怎么能用一个数字量化但 SMOG 公式偏偏就是这么干的而且它从 1969 年活到今天被大量健康教育材料、知情同意书、保险条款的编写规范直接引用。它的核心结论反直觉判断一段文字难不难不需要理解它的意思只需要数两样东西——句子数和三个音节以上的复杂词数量。换句话说可读性在 SMOG 眼里是一个统计特征而不是语义特征。这背后是一个很硬的假设长词密度越高读者需要解码的认知负担越大句子越长工作记忆被占用的时间越久。SMOG 把这两个变量压缩成一个“达到完全理解所需的受教育年限”让“难”变成一个可比较、可回归、可自动化的数字。搞懂它你就能明白为什么很多可读性检测工具的输出看起来“不太准”却依然被广泛采用——因为它衡量的从来不是“你懂不懂”而是“文本本身要求读者付出多少解码成本”。2. SMOG 公式的数学骨架从音节计数到年级分数2.1 公式长什么样每个符号代表什么SMOG 的全称是 Simple Measure of Gobbledygook直译就是“对官样文章的简单度量”。它的原始形式并不复杂SMOG 1.0430 * sqrt( polysyllables * (30 / sentences) ) 3.1291这里polysyllables是“多音节词”的数量通常定义为三个或三个以上音节的词sentences是句子总数。注意那个30 / sentences它把多音节词数量归一化到“每 30 个句子”的尺度上。为什么要固定 30 句因为原始研究是在 30 句样本上做的校准样本太短会让统计噪声放大样本太长又增加人工成本。固定 30 句是一个工程折中既保证统计稳定性又让标注者能在合理时间内完成计数。公式里的1.0430和3.1291不是拍脑袋来的它们来自对大量已标注文本的回归拟合。sqrt的存在说明多音节词密度对难度的影响是非线性的——密度低的时候难度上升慢密度高的时候难度上升快。这符合直觉一篇文章偶尔出现几个长词读者能靠上下文猜但如果长词密集到每句都有解码负担就会指数级累积。2.2 音节计数为什么是最大的不确定性来源整个 SMOG 里最脆弱的一环就是“怎么数音节”。英文里fire是一个音节还是两个every是两个还是三个不同词典、不同地区发音给出的答案不一样。常见的实现思路是写一套启发式规则连续元音字母算一个音节词尾不发音的e不计le结尾特殊处理再维护一个例外表。但这套规则永远不可能 100% 准确因为音节本质上是语音单位而输入是拼写。这就引出一个关键设计取舍SMOG 要的是“可自动化、可复现”而不是“语言学精确”。所以绝大多数实现选择用规则近似接受一定误差。误差在单篇文本上可能让分数浮动 0.5 到 1 个年级但在大批量筛查场景里这种浮动不影响“把明显过难的文本挑出来”这个核心目标。理解这一点你就不会纠结于“为什么我的工具算出来和另一个工具差了一点”——它们大概率用了不同的音节规则和不同的句子切分策略。2.3 句子切分被低估的工程难点句子数sentences在分母位置它的误差会被放大。Mr. Smith里的句点不是句末e.g.也不是省略号、分号、换行符算不算句末常见的实现思路是用正则先做粗切再维护一个缩写词表做排除。但缩写表永远补不全尤其是当文本里出现代码、URL、小数、版本号的时候。一个稳健的做法是先按[.!?]切再合并那些“句点后紧跟小写字母且前一个词在缩写表里”的片段。这个策略在普通说明文上表现不错但在技术文档上会翻车因为技术文档里v1.2.3这种版本号会被误切。所以如果你要把 SMOG 用在非通用文本上句子切分规则必须针对领域做调整不能直接套默认实现。3. 从公式到代码一个可运行的 SMOG 实现拆解3.1 音节计数器的实现逻辑下面这段 JavaScript 展示了一个典型的音节计数实现它不追求完美但覆盖了大部分常见情况// 启发式音节计数连续元音算一个音节处理词尾 e 和 le function countSyllables(word) { word word.toLowerCase().replace(/[^a-z]/g, ); if (word.length 3) return 1; // 短词直接算一个音节 // 去掉词尾不发音的 e但保留 le 结尾如 table 是两个音节 word word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ); word word.replace(/^y/, ); // 词首 y 不算元音 // 匹配连续元音组每组算一个音节 const groups word.match(/[aeiouy]{1,2}/g); return groups ? groups.length : 1; }这段代码的关键决策有三个。第一长度小于等于 3 的词直接返回 1因为短词几乎都是单音节走规则反而容易出错。第二词尾e的处理用了一个负向字符类避免把the这种词误伤。第三y的处理很微妙词首的y通常是辅音如yes词中的y可能是元音如rhythm这里只去掉了词首的y词中的y保留在元音组里。参数上[aeiouy]{1,2}这个量词上限是 2意味着连续三个元音字母会被拆成两组。这在英语里很少见但遇到beautiful这种词时eau会被拆成ea和u结果偏多。更精细的实现会维护一个元音组合表eau、iou、ai等但代码复杂度会上升。这就是典型的工程取舍准确率和可维护性之间的平衡。3.2 多音节词统计与公式组装有了音节计数器多音节词统计就是一次遍历// 统计多音节词3 音节和句子数计算 SMOG 分数 function smogScore(text) { const sentences text.split(/[.!?]/).filter(s s.trim().length 0); const words text.toLowerCase().match(/[a-z]/g) || []; let polysyllables 0; for (const w of words) { if (countSyllables(w) 3) polysyllables; } const sentenceCount Math.max(sentences.length, 1); // 防止除零 const raw 1.0430 * Math.sqrt(polysyllables * (30 / sentenceCount)) 3.1291; return Math.round(raw * 10) / 10; // 保留一位小数 }这里有几个容易被忽略的细节。Math.max(sentences.length, 1)是防御性写法空文本或纯符号文本会导致句子数为 0直接除零得到Infinity。Math.round(raw * 10) / 10保留一位小数是因为 SMOG 本身的精度也就到这个量级给太多小数位是虚假精确。还有一个隐含假设这段代码只处理英文。如果文本里混入中文、日文或其他非拉丁字符match(/[a-z]/g)会直接忽略它们导致词数偏少、分数偏低。所以 SMOG 的适用范围是明确的——它只对以英文单词为基本单位的文本有意义。这不是实现的缺陷而是公式本身的设计边界。3.3 分数怎么读年级映射与解释边界SMOG 的输出是一个“年级分数”比如 8.2 表示需要大约 8 年级的阅读能力才能完全理解。常见的解释区间是低于 6 属于通俗6 到 8 属于中等9 到 12 属于偏难13 以上属于学术或专业级别。但这个映射有一个重要前提它假设读者是英语母语者且文本是标准书面英语。如果你把 SMOG 用在翻译文本、儿童读物、或者大量专有名词的技术文档上分数会失真。专有名词往往多音节但它们对目标读者的认知负担远低于同等音节的普通词汇。这就是为什么 SMOG 更适合做“相对比较”而不是“绝对判定”——比较同一领域内不同文档的分数比拿一个绝对阈值去卡所有文本要可靠得多。4. 避坑与排查SMOG 实现中最容易翻车的几个点4.1 现象分数异常高动辄 18 以上原因通常是音节计数器把大量普通词误判为多音节词。比如every被算成 3 个音节实际通常算 2 个business被算成 3 个实际 2 个。当这类词在文本里高频出现时多音节词数量被系统性高估。解决方式是维护一个高频例外表把every、business、interest、different这类词强制指定音节数。例外表不需要覆盖所有词只要覆盖出现频率最高的那几百个误差就能显著下降。4.2 现象空文本或极短文本返回NaN或Infinity原因是句子数为 0 导致除零或者Math.sqrt收到负数如果多音节词统计逻辑有 bug 返回了负值。防御性写法是句子数取Math.max(n, 1)多音节词数取Math.max(n, 0)并在函数入口判断文本长度过短时直接返回null而不是硬算。4.3 现象同一段文本两次运行结果不一致原因通常是句子切分依赖了正则的全局状态或者音节计数器修改了传入的字符串。JavaScript 里正则的lastIndex在带g标志时会跨调用保留如果复用了同一个正则对象第二次匹配会从上次结束的位置开始。解决方式是每次调用都新建正则或者在函数内部用String.prototype.match而不是RegExp.prototype.exec。另外音节计数器里对word做了replace赋值如果传入的是引用类型会有副作用确保传入的是原始字符串。4.4 现象技术文档分数虚高但人工读起来并不难原因是技术文档里大量出现代码标识符、版本号、缩写这些被当成普通英文词参与音节计数。比如config、async、params会被算成多音节词但它们对目标读者几乎不构成解码负担。解决方式是在预处理阶段过滤掉代码块、行内代码、URL 和版本号模式。常见的做法是先用正则把...、...、http...替换成空字符串再送入 SMOG 计算。这一步不做分数就没有参考价值。4.5 现象中文文本返回的分数完全不可用原因前面提过SMOG 的统计单位是英文单词和英文句子中文没有空格分词音节定义也不同。如果强行把中文按字符切分送入得到的数字没有任何解释力。正确的做法是SMOG 只用于英文文本。如果要做中文可读性评估需要换用针对中文的公式比如基于常用字比例或句子长度的模型不能直接套 SMOG。这一点在实现文档里必须写清楚否则使用者会误用。5. 设计取舍为什么一个 1969 年的公式还值得放进你的工具箱SMOG 最值得玩味的地方是它在“准确性”和“可操作性”之间做了一个非常明确的选择。它放弃了语义理解放弃了句法分析甚至放弃了对音节精确计数的追求换来的是任何人拿一支笔、一张纸数两个数字就能算出一个可比较的分数。这种极简主义让它在计算机时代反而更容易被自动化——规则清晰、依赖少、计算快。但这也意味着它的边界非常硬。它衡量的是“文本表面的解码成本”不是“内容的理解难度”。一篇用词简单但逻辑跳跃的文章SMOG 分数可能很低但读者依然读不懂一篇满是专业术语但结构清晰的论文SMOG 分数很高但目标读者读起来并不费力。所以正确的用法是把它当成一个筛查工具而不是判定工具——先用它把明显过难的文本挑出来再人工判断是否真的需要改写。如果你要自己实现或集成 SMOG我的习惯是先固定音节计数规则和句子切分规则写一组已知分数的测试文本做回归确保改动不会让分数漂移。然后针对你的目标文本类型单独调例外表和预处理过滤规则。最后永远不要只看分数要结合人工抽读——公式给的是线索不是结论。希望这套拆解能帮你在自己的项目里把可读性检测做得更靠谱。本文还有配套的精品资源点击获取