我们都在学生时代写过作文。在提交之前,总是需要满足最低字数要求——250字、500字等等。在我家里还没有电脑的时候,我常常手动计算单词数。后来父母给我买了一台电脑,发现微软Word就像魔法一样。当时我还不懂编程,所以对MS Word的功能感到惊叹不已。
我最喜欢的功能之一叫做“字数统计”。您可以选择任何一段文本,然后点击**工具 > 字数统计**,查看一些有趣的统计数据,例如所选文本的字符数、单词数和段落数。我当时很喜欢这个功能,现在在 Google Sheets 中起草这篇文章时,我还在使用它。我想自己尝试编写一个类似的功能。
我们今天要制作的应用将计算
- 一段文本中的字符数、单词数和句子数
- 使用频率最高的关键词
- 可读性评分——理解这段文字的难度。
在我们开始之前,您可以先看看我们要制作的内容
查看 Vikas Lalwani (@lalwanivikas) 在 CodePen 上的示例 Word Counter。
现在,让我们开始吧!
应用中的所有计数都严重依赖于正则表达式(在本文的其余部分中称为“regex”)。如果您不熟悉正则表达式,可以查看这篇关于JavaScript 中正则表达式的入门文章。
页面设置
首先,我们需要一些东西来接收用户的输入。还有什么比textarea
HTML 元素更适合处理这种情况的呢?
<textarea placeholder="Enter your text here..."></textarea>
我们可以使用这段 JavaScript 代码选择上面的文本区域
var input = document.querySelectorAll('textarea')[0];
我们可以通过input.value
访问输入字符串。由于我们希望在用户键入时显示统计数据,因此我们需要在keyup
事件上执行我们的逻辑。这是核心逻辑的框架:
input.addEventListener('keyup', function() {
// word counter logic
// sentence count logic
// reading time calculation
// keyword finding logic
});
输出将存储在简单的 HTML div
元素中,所以这里没有什么花哨的东西
<div class="output row">
<div>Characters: <span id="characterCount">0</span></div>
<div>Words: <span id="wordCount">0</span></div>
</div>
<!-- more similar divs for other stats -->
第一部分:计算字符和单词
在完成基本设置后,让我们探讨如何计算单词和句子。最好的方法之一是使用正则表达式。
我将逐步引导您了解单词和句子计数的正则表达式模式。一旦您能够理解这一点,就可以通过查看源代码自己找出其余的部分。
我们需要查找两样东西才能在输入字符串中找到单词
- 单词边界
- 有效的单词字符
如果我们能够找到这些,那么我们就会得到单词列表。为了提高准确性,我们还可以查找连字符(-)。这样,带有连字符的单词(例如,CSS-Tricks)将被计为一个单词,而不是 2 个或更多个单词。
var words = input.value.match(/\b[-?(\w+)?]+\b/gi);
在上面的模式中
\b
匹配单词边界,即单词的开头或结尾\w+
将匹配单词字符。+
使其匹配一个或多个字符-?
将匹配连字符,?
在末尾表示它是可选的。这是一种特殊情况,用于将带有连字符的单词计为一个单词。例如,“long-term”和“front-end”将被计为一个单词,而不是两个单词- 模式末尾的
+
匹配整个模式的一个或多个出现 - 最后,
i
使其不区分大小写,g
使其进行全局搜索,而不是在第一个匹配项处停止
接下来,让我们探讨如何计算句子。
句子相对容易处理,因为我们只需要检测句子分隔符并在这些分隔符处进行分割。这就是执行此操作的代码:
var sentences = input.value.split(/[.|!|?]/g);
在上面的模式中,我们在输入文本中查找三个字符——.
、!
和?
——因为这三个字符用作句子分隔符。
经过上述操作后,sentences
将包含所有句子的数组。但是,在我们计数之前,我们需要处理一个有趣的情况:如果有人输入“come back soon…”会怎样?
根据上述逻辑,sentences 将包含四个条目——一个正确的句子和三个空字符串。但这不是我们想要的。解决这个问题的一种方法是将上述正则表达式模式更改为以下模式
var sentences = input.value.split(/[.|!|?]+/g);
请注意模式末尾的+
。它用于处理用户连续输入句子分隔符的任何情况。通过此修改,“come back soon…”将被计为一个句子,而不是四个句子。
如果您遵循上面关于单词和句子计数的解释,您可以通过查看源代码自己理解其余的逻辑。需要牢记的重要一点是不要忘记边缘情况,例如带有连字符的单词和空句子。
第二部分:查找最常出现的关键词
当您开始键入时,您会注意到页面底部会出现一个新的容器,其中显示文本中最常出现的关键词。这告诉您最常使用哪些关键词,可以帮助您避免在写作中过度使用某些词语。这很酷,但我们如何计算它呢?
为了便于理解,我将计算最常出现的关键词的过程分为了以下 4 个步骤
- 移除所有停用词
- 创建一个包含关键词及其计数的对象
- 通过将其转换为二维数组来对对象进行排序
- 显示前 4 个关键词及其计数
步骤 1) 移除所有停用词
停用词是指语言中最常见的词语,在对文本进行任何分析之前,我们需要将其过滤掉。不幸的是,没有通用的停用词列表,但简单的搜索会提供许多选项。只需选择一个并继续即可。
这是过滤停用词的代码(解释如下)
// Step 1) removing all the stop words
var nonStopWords = [];
var stopWords = ["a", "able", "about", "above", ...];
for (var i = 0; i < words.length; i++) {
// filtering out stop words and numbers
if (stopWords.indexOf(words[i].toLowerCase()) === -1 && isNaN(words[i])) {
nonStopWords.push(words[i].toLowerCase());
}
}
stopWords
数组包含我们需要检查的所有词语。我们遍历 words
数组并检查每个项目是否存在于 stopWords
数组中。如果存在,我们忽略它。如果不存在,我们将其添加到 nonStopWords
数组中。我们还忽略所有数字(因此存在isNaN
条件)。
步骤 2) 创建一个包含关键词及其计数的对象
在此步骤中,我们将创建一个对象,其中键为词语,值为数组中词语的计数。此逻辑非常简单。我们创建一个空对象keywords
并检查词语是否已存在于其中。如果存在,我们将值加 1,如果不存在,则创建一个新的键值对。如下所示
// Step 2) forming an object with keywords and their count
var keywords = {};
for (var i = 0; i < nonStopWords.length; i++) {
// checking if the word(property) already exists
// if it does increment the count otherwise set it to one
if (nonStopWords[i] in keywords) {
keywords[nonStopWords[i]] += 1;
} else {
keywords[nonStopWords[i]] = 1;
}
}
步骤 3) 通过将其转换为二维数组来对对象进行排序
在此步骤中,我们首先将上述对象转换为二维数组,以便我们可以使用 JavaScript 的原生sort
方法对其进行排序
// Step 3) sorting the object by first converting it to a 2D array
var sortedKeywords = [];
for (var keyword in keywords) {
sortedKeywords.push([keyword, keywords[keyword]])
}
sortedKeywords.sort(function(a, b) {
return b[1] - a[1]
});
步骤 4) 显示前 4 个关键词及其计数
上述步骤的输出是一个名为sortedKeywords
的二维数组。在此步骤中,我们将显示该数组的前四个(如果总词语少于 4 个,则显示几个)元素。对于每个项目,词语位于位置0
,其计数位于位置1
。
我们为每个条目创建一个新的列表项,并将其附加到由topKeywords
表示的ul
中
// Step 4) displaying top 4 keywords and their count
for (var i = 0; i < sortedKeywords.length && i < 4; i++) {
var li = document.createElement('li');
li.innerHTML = "" + sortedKeywords[i][0] + ": " + sortedKeywords[i][1];
topKeywords.appendChild(li);
}
现在让我们转到查找可读性评分。
第三部分:获取可读性评分
为了让你的写作能够有效传达信息,你需要使用你的受众能够理解的词汇。否则,写作就没有意义。但是,你如何衡量一段文字的理解难度呢?
这就引出了可读性评分。
一些比我们更聪明的人已经开发了许多量表来衡量阅读文章的难度级别。对于我们的应用程序,我们将使用Flesch 阅读易度测试,这是最常用的测试之一。微软 Word 也依赖于此测试!
在 Flesch 阅读易度测试中,分数范围从零到一百。分数越高表示材料越容易阅读;分数越低表示文章越难阅读。它基于一个稍微复杂的数学公式,我们不需要担心,因为网络上几乎所有东西都有 API。
虽然有很多付费选项可以计算可读性评分,但我们将使用 Mashape 的Readability Metrics API,它是完全免费使用的。如果你有兴趣,它还可以提供其他一些可读性评分。
要使用该 API,我们需要向指定的 URL 发送一个 POST 请求,并附上我们想要评估的文本。Mashape 的好处在于,它允许你直接从浏览器中使用其 API。
我将使用纯 JavaScript 来发出这个 Ajax 调用。如果你打算使用 jQuery,你会发现这个文档页面很有用;或者,如果你想使用服务器端语言,请参考 API 首页上的代码示例。
以下是发出请求的代码示例:
// readability level using readability-metrics API from Mashape
readability.addEventListener('click', function() {
// placeholder until the API returns the score
readability.innerHTML = "Fetching score...";
var requestUrl = "https://ipeirotis-readability-metrics.p.mashape.com/getReadabilityMetrics?text=";
var data = input.value;
var request = new XMLHttpRequest();
request.open('POST', encodeURI(requestUrl + data), true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.setRequestHeader("X-Mashape-Authorization", "Your API key | Don't use mine!");
request.send();
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
// Success!
readability.innerHTML = readingEase(JSON.parse(this.response).FLESCH_READING);
} else {
// We reached our target server, but it returned an error
readability.innerHTML = "Not available.";
}
};
request.onerror = function() {
// There was a connection error of some sort
readability.innerHTML = "Not available.";
};
});
上面大部分代码都是标准的 Ajax 调用,但需要注意三点:
- 使用此 API 需要一个免费的 Mashape 帐户。获得帐户后,你将获得一个密钥,可以将其插入授权标头中。
- API 返回许多不同的评分,我们需要的是响应中
FLESCH_READING
属性的值。 - 响应将包含一个 0 到 100 之间的数字,其本身对用户没有任何意义。因此,我们需要将其转换为有意义的格式,这就是
readingEase
函数的作用。它将该数字作为输入,并根据维基百科的这个表格输出一个字符串。
function readingEase(num) {
switch (true) {
case (num <= 30):
return "Readability: College graduate.";
break;
case (num > 30 && num <= 50):
return "Readability: College level.";
break;
// more cases
default:
return "Not available.";
break;
}
}
就是这样!这就是制作单词计数应用程序所涉及的所有逻辑。我没有解释每一个细节,因为你可以通过查看代码自己弄清楚。如果你在任何地方遇到困难,请随时查看GitHub 上的完整源代码。
进一步开发
虽然该应用程序看起来不错并且运行良好,但你可以通过几种方法对其进行改进,例如:
- 处理句号:目前,如果你输入一个电子邮件地址,例如
[email protected]
,我们的应用程序会将其计为两个不同的句子。如果你输入域名或“vs.”、“eg.”等词,也会出现这种情况。一种处理方法是使用手动列表,并使用正则表达式过滤掉这些内容。但由于没有现成的此类列表,我们将不得不手动创建它。如果你知道更好的方法,请在评论中告诉我。 - 处理更多语言:目前,我们的应用程序仅能很好地处理所有英语情况。但如果你想将其用于德语怎么办?好吧,你需要包含德语的停用词。就是这样。其余部分将保持不变。最好是提供一个从下拉列表中选择语言的选项,应用程序应该相应地工作。
我希望你在学习本教程的过程中玩得开心。如果你有任何问题,请随时在下面发表评论。或者只是打个招呼!我总是乐于助人。
出于好奇,你将如何防止“Mr. Brooks is out of the office.”被计为两个句子?
抱歉。我读了剩下的内容。你在最后解释了!顺便说一句,这是一篇很棒的文章!
单词计数失败。如果字符串是“foo ä bar”,则有 3 个单词,但由于使用了单词边界,应用程序仅显示 2 个单词。可以通过在正则表达式中使用
u
标志或切换到不同的单词计数逻辑来解决此问题。哦。有一段关于这个的文字……抱歉。
好文章!
一个极端情况
这里没有处理使用鼠标右键单击命令粘贴大量内容的情况。不过,添加起来应该不难。
性能增强
在 keyup 事件处理程序中执行如此大量的操作可能不是一个好主意。也许我们可以在这里使用去抖动。https://css-tricks.org.cn/debouncing-throttling-explained-examples/
我会使用
input
事件和去抖动。用一句话达到大学毕业生的阅读水平。
“我发现,拥有双侧使用能力极大地促进了与议会的谈判。”
我认为可以通过使用数组方法而不是循环来改进示例,例如
和
和
这种代码更清晰,更具自文档性——它更明显地表示对文本进行了一系列转换/操作。它还避免了
for
循环带来的视觉噪声。同意这一点。更符合 JS 的习惯用法。
就像停用词字典一样,你应该使用缩写词字典来区分它们与句子末尾使用的句号。域名、用户名等中的句号很容易被捕获,因为它们后面没有空格。
查找单词的正则表达式
/\b[-?(\w+)?]+\b/gi
有一个小错误。它应该是(
而不是[
。当前的正则表达式匹配包含任何字符(
[
]
)的单词,这些字符在-?(\w+)?
中出现一次或多次+
(\w
表示任何单词字符,就像你所说的)。这意味着hello+(-world?)
将被识别为一个单词。本文中使用的正则表达式有点“奇怪”。
例如,使用
/[.|!|?]/g
进行分割意味着也计算|
字符。当你使用分组方括号时,你不需要使用
|
,因为其中的任何字符都将被计为字符,包括括号、句点或|
。使用g
分割字符串也是错误的,因为分割已经匹配字符串中该正则表达式的所有出现。现在,让我们谈谈
value.match(/\b[-?(\w+)?]+\b/gi);
。作者似乎再次没有理解分组。‘non-?sense’ 将被考虑在内,但这不是作者的意图。
/\b[-\w]+\b/g
基本上是作者获得预期结果所需的一切。i
(忽略大小写标志)也没有用,因为\w
包含所有字符,而不仅仅是大写或小写字符。还有其他一些问题不是很好,尤其是在处理非拉丁字母时,但对于英语计数器来说,这是一个不错的起点,所以……我感谢作者的努力,但我也认为你应该在发布前征求技术审查。
这将增加内容质量很高的可能性,就像这里大多数内容一样。
此致
我记得在 1990 年代初有一个程序可以统计所有单词以及每个单词在文本中出现的次数。但现在我对这个程序或应用程序一无所知。它在当时被称为 WORD Count 或 WORD。如果你知道这个程序/应用程序,请告诉我。
我的问题是如何统计文档/文章中不同单词的存在。