使用方法
- 把中文正文粘进「正文」框(示例已服务端直出,202 个汉字)。关键词密度检测的读数完全取决于这一步怎么切。上限 200000 汉字,超出会被拒绝并提示实际字数。
- 在「目标关键词」填要盯的词(默认 密度,1~20 字符)。留空就只出词频榜,不算目标词密度。
- 调「词长下限 / 上限」(默认 2~4 字)。下限设 1 会把「的、了、是」这类单字虚词算进来,密度立刻虚高——本页把「的」单独列在正文里说明这件事。
- 点「检测密度」。上方六张卡是三种口径的分母与分子,下方是词频榜。按 Ctrl/Cmd + Enter 也能触发,Enter 留给换行。
- 榜里带蓝底的三行是当前目标词所在的行;点「导出词频表 CSV」可拿到带 BOM 的表格,Excel 打开不乱码。
这个关键词密度检测能解决什么问题
一是解释「为什么两个工具报的数字不一样」。同一段中文,A 工具说 4.41%、B 工具说 1.49%,两边都没算错——差别全在「总词数」怎么定义。本页把子串 ÷ 标点分段、子串 ÷ 汉字数、分词 ÷ 全部词、分词 ÷ 多字词、二元 ÷ bigram 五种分母一次列全,你在别处看到任何一个数,都能在这里找到它对应的口径。
二是揪出「看起来正常、其实超标」的词。示例正文里「关键」「口径」「密度」各出现 3 次,在分词口径下都是 4.76%,已经越过 3.5% 的经验上限;而同样三个词按字符密度算只有 1.49%,看起来毫无问题。两种读法指向完全不同的改稿动作。
三是看清「短文本的密度天生不稳」这件事。本文分母只有 63 个词,3.5% 对应的是「出现 2 次就超标」;而同样阈值放到 202 个汉字的分母上,允许出现 7 次。同一个 3.5%,在 200 字短文和 3000 字长文里完全不是一个尺度。
本页的关键词密度检测不做这些事:不判断你的密度「好不好」——它没有 Google's 排名数据,只有口径对照;不替代逐字统计,正文字数请用字数统计工具;也不算字符数(含标点空格),那是字符数统计工具的口径。本页只回答一件事:这个密度数字是怎么来的。
原理:中文没有空格,所以「总词数」有五种答案
口径①:子串计数 ÷ 标点分段。把文本按标点切成若干「段」,数目标词在原文里出现了几次(用 String.prototype.indexOf 做不重叠匹配,每次前进 needle 长度,避免「好好」在「好好好」里被数成 2 次)。分母是段数。这是最粗糙但最常见的做法:示例里 3 次 ÷ 30 段 = 10.00%,一篇文章里但凡出现两三个词就「超标」。
口径②:子串计数 ÷ 汉字数。分母换成 [\u4e00-\u9fff] 的匹配数。示例 3 ÷ 202 = 1.49%。这个口径把「词」等同于「字」,只有在中英文混排且英文占比极低时才勉强成立。
口径③与④:Intl.Segmenter 词典分词。这是 ECMA-402 定义的国际化分词接口,本页调用 new Intl.Segmenter('zh-Hans', { granularity: 'word' }),然后遍历 segment() 返回的片段,只保留 isWordLike === true 的片段——这一步会滤掉标点和空格,但保留数字与字母串(所以示例里的「3.5」也进了词表)。示例实测:174 个片段中 143 个 isWordLike,其中单字 75 个、多字 68 个,压缩比 143 ÷ 202 = 0.708。分母取全部词得 2.10%,只取多字词得 4.41%。
口径⑤:二元切分(bigram)。把纯汉字串的相邻两字配成词:密度 合成「密度」,「关键」合成「关键」。词数恒等于汉字数减一,没有分词歧义,也不需要词典——这是它在 Intl.Segmenter 不可用时作为回退方案的唯一理由。示例 3 ÷ 201 = 1.49%。
五种口径的极差是 8.51 个百分点,最高(10.00%)是最低(1.49%)的 6.7 倍。分子完全相同(都是 3 次),全部差异来自分母。这就是本页存在的理由:报密度不报口径,等于没报。
为什么分词口径会漏计,这是本页最实测的结论。逐个喂给 Intl.Segmenter:分词、自然、承接、堆砌、密度 都整词命中;而 关键词 被切成 ["关键","词"]、优化 被切成 ["优","化"]、搜索引擎 被切成 ["搜索","引擎"]、密度检测 被切成 ["密度","检测"]。V8 内置的 ICU 中文断词词典收录的是统计模型能独立成词的部分,对「关键 + 词」这种高频组合反而拆得更细。后果是:如果你把目标词设成「关键词」,分词口径会报 0%,而子串口径报 3 次——这不是 bug,是两套词典的差异,所以本页永远把子串口径排在第一位。
停用词表为什么必须内置而不是照搬。本页内置 83 条(中文 59 + 英文 24)。Google 从未公布中文停用词表,各家工具的列表都是自拟的,这就是「同文不同数」的第二个来源。示例里过滤掉的 4 个词是「不是 / 没有 / 所以 / 怎么」——它们在多字词分母里出现两次(「不是」两次、「没有」「所以」「怎么」各一次,按实测是剔除 5 个:分母 68 → 63)。过滤停用词不改变分子、只缩小分母,所以密度会升高:示例的口径④从 4.41% 升到 4.76%。很多工具不告诉用户自己做了这一步,于是同样的文本得出两个都「有道理」的结果。
单字词为什么不计。示例里单字「的」出现 2 次,占全部词 1.40%、占汉字 0.99%。单字虚词在中文里承担语法功能而非主题,SEO 密度统计惯例一律排除——所以本页默认词长下限是 2。把下限调到 1,「密度」的分母会从 68 涨到 143,密度随之腰斩,这就是「下限设 1 就不超标」的机制。
遵循的标准。分词接口由 ECMA-402(Internationalization API)定义,granularity: 'word' 的切分遵循 ICU 的 UAX #29 词边界规则加上中文词典;语言标签用 BCP 47 形式 zh-Hans。isWordLike 是 ECMA-402 里 SegmentData 的标准字段,判据是「该片段是否由字母、数字或汉字构成」。经验阈值 0.8% / 1% / 3.5% 不是任何标准的一部分,是内容营销实践里的经验值,本页把它标为「经验区间」而非「合格线」。
完整示例(数值仅作演示)
示例条件与页面默认值完全一致:正文 202 汉字、目标词「密度」、词长 2~4 字。逐项核对:
- 目标词子串命中 3 次(不重叠匹配;允许重叠也是 3 次,因为「密度」没有自重叠结构);
- 标点分段 30 段、汉字 202 个 → 口径① 10.00%、口径② 1.49%;
Intl.Segmenter切出 174 个片段、isWordLike143 个(单字 75 / 多字 68)→ 口径③ 2.10%、口径④ 4.41%;- 二元切分 202 汉字 → 201 个 bigram → 口径⑤ 1.49%;
- 停用词过滤后多字词分母 68 → 63,「密度」3 次不变 → 4.76%,与词频榜第 3 行完全一致;
- 极差 8.51 个百分点,最高 / 最低 = 6.7 倍。
这六个数字都能在正文里互相印证:口径④的 4.41% 对应分母 68(未过滤),词频榜的 4.76% 对应分母 63(已过滤),两者之差纯粹来自停用词表。下面「分母 → 同一词密度」对照表把 63 这个分母换算成 1~5 次出现时各自的密度,能直接看出 3.5% 在这份正文里只有 2 次余量。
输入范围与对照表
- 正文:最多 200000 汉字。上限不是随手定的:
Intl.Segmenter对 20 万字的分词在桌面浏览器上是秒级,而再往上会明显卡顿;超过上限页面直接拒绝并报实际字数,不会截断后静默计算。 - 目标关键词:1~20 字符,超出会被拒绝。它同时喂给子串匹配和分词比对两条路径,所以「含标点的短语」(如「密度。」)只有子串口径能命中——这是子串口径唯一不可替代的场景。
- 词长下限 1~4、上限 1~6,且下限必须小于上限。留空或相等都会被拒绝并说明原因。
下面这张表回答「同一个词,换个分母会变成多少」——这是本页最容易被忽略、也最容易造成误判的一层:
| 分母口径 | 分母 | 目标词 1 次 | 2 次 | 3 次 | 4 次 |
|---|---|---|---|---|---|
| ① 子串 ÷ 标点分段 | 30 段 | 3.33% | 6.67% | 10.00% | 13.33% |
| ② 子串 ÷ 汉字数 | 202 字 | 0.50% | 0.99% | 1.49% | 1.98% |
| ③ 分词 ÷ 全部词 | 143 词 | 0.70% | 1.40% | 2.10% | 2.80% |
| ④ 分词 ÷ 多字词 | 68 词 | 1.47% | 2.94% | 4.41% | 5.88% |
| ⑤ 二元 ÷ bigram | 201 个 | 0.50% | 1.00% | 1.49% | 1.99% |
| 本页词频榜(④去停用词) | 63 词 | 1.59% | 3.17% | 4.76% | 6.35% |
最后一列读法:按本页默认口径,3.5% 的上限对应 2 次——出现 2 次就到 3.17%、3 次就到 4.76%。而按口径①,同样 3 次是 10.71%,早就越界。这就是「同一篇文章,有人说刚好、有人说爆表」的完整解释。
边界与口径精度说明
- 分母为 0 时必须报错,不能输出 Na%。三种情况实测都会归零:正文为空、只有标点(如「,。!?」)、或全是被过滤掉的字符。页面会明确提示「没有可统计的词」并告诉你原因,而不是给一个 NaN% 或 0.00%。
- 「未找到」和「密度 0%」是两件事。目标词长度超过正文汉字数、或正文里确实没有这个词时,子串命中 0 次——这在数学上就是密度 0%,但用户真正想知道的是「我没写这个词」。页面在这种情况下额外给出一句「目标词未出现」,两种信息分开表达。
- 英文大小写要自己归一。
Intl.Segmenter的英文分词不合并大小写,Keyword与keyword是两个不同的词。中文没有这个问题,但一段中英混排的文本里,本页的子串路径是大小写敏感的、分词路径也是——所以目标词含英文时请自行统一大小写后再比。实测英文对照:keyword在 17 词文本里出现 3 次,全词口径 17.65%、去停用词口径 25.00%。 - 中英文的压缩比天差地别,这是中文密度不可跨工具比较的根源。英文按空格分词,压缩比恒为 1.000;同一段中文按 V8 的 zh 断词,压缩比是 0.708。分母直接依赖分词器实现,而
Intl.Segmenter的行为随浏览器内置的 ICU 版本变化——同一段文字在 Chrome 与旧版 Firefox 上可能得到不同的词数。 - 超长文本不会撑爆表格。200000 汉字的 bigram 数是 199999 个,但它们进的是
Map而不是 DOM:全部重复的文本(如 20 万个「密」)只会产生 1 个唯一 bigram。表格永远只渲染前 12 行加「加载更多」,不会一次性插入十万个节点。 - 词频计数不涉及大整数精度。词频最大等于分母,本页最大分母是 200000 量级,离 JavaScript 安全整数上限 253 − 1 = 9007199254740991 极远,不存在计数溢出。
- 不重叠匹配是刻意选择。目标词「好好」在「好好好」里,不重叠计数得 1 次,重叠计数得 2 次。本页取不重叠,因为重叠计数会把「词中词」重复计入、虚高分子;如果你需要另一种口径,在目标词里手动改写即可。
常见问题
我把目标词设成「关键词」,分词口径报 0%,是 bug 吗?
不是。本页实测 Intl.Segmenter('zh-Hans') 会把「关键词」切成 ["关键","词"]、「优化」切成 ["优","化"]——V8 内置 ICU 的中文断词词典对这类高频双字组合反而拆得更细。所以分词口径按「完整词」比对时必然是 0 次。子串口径不经过分词,仍然能数出真实次数,这也是本页把它排在第一位的原因。
密度 5% 算高吗?
先看你用的是哪个分母。同样 5 次出现,在本页 63 词的分母下是 7.94%,在 202 汉字的分母下只有 2.48%。经验区间 1%~3.5% 是相对「总词数」说的,而中文的总词数本身有五种算法。更实用的判断标准是词频榜:如果目标词排在第一位且明显甩开第二名,多半是写重复了;如果它排在第三名以下、密度也只有 1% 左右,那不是问题。
为什么停用词过滤会让密度升高?
因为分子不变、分母变小。示例里「密度」出现 3 次,多字词分母从 68 降到 63,密度就从 4.41% 升到 4.76%。停用词(「不是」「所以」「怎么」这类)承担语法功能而非主题,把它们算进分母只会稀释真实密度。关键是要知道工具做没做这一步——本页在结果里明确写出过滤前后的两个分母。
单字词(「的」「是」「了」)要不要算?
默认不算,本页词长下限是 2 字。示例里单字「的」出现 2 次,占全部词 1.40%——一个虚词就能贡献 1.4 个百分点,如果计入的话所有词的密度都会被稀释得很低,看起来「都很健康」,这正是把虚词算进去的弊病。把下限调到 1 可以看到变化,但那个数字没有参考价值。
正文里全是标点或空格会怎样?
会被明确拒绝并提示「没有可统计的词:正文里 0 个汉字、0 个可切分词」。页面不会输出 Na%、也不会静默显示 0.00%。这类输入通常是粘贴失败或选中了错误的文本范围,看到提示就回去重粘一次。
目标词要不要加引号或书名号?
不要。目标词必须是纯文本,页面对任何非空输入都按字面子串匹配。如果原文里写的是「“密度”」带引号形式,引号不参与匹配,所以能正常命中;反过来,输入目标词时若带上了引号,就永远匹配不到。
同一段文字在 Chrome 和 Firefox 上结果不同,正常吗?
正常,而且这正是本页强调「口径」的原因。分词口径的分母来自 Intl.Segmenter,而它背后是浏览器内置的 ICU 词典,不同版本收录的词条不同,压缩比会变(示例是 0.708)。子串口径和二元口径不依赖词典,跨浏览器结果一致。想要可复现的结论,就只用口径① ② ⑤,或者自己固定分母。
检测过的正文会被上传吗?
不会。全部计算在当前页面完成,只调用 String.indexOf、正则和 Intl.Segmenter,没有任何网络请求,也不写 Cookie 或 localStorage。你可以在开发者工具的 Network 面板里反复点「检测密度」,面板始终是空的——需要粘贴敏感文稿时这一点很关键。