Typecho 文章字数统计的规则思考与解决方案
最近在制作 Typecho default-ultra 主题,在这个主题中我加入了对文章字数统计的功能。此前我也参考了其他优秀的开发者的文章字数统计方式,我自己也优化了好几次。
文章字数统计,看起来是一个很简单的小功能,为什么值得被优化好几次?其实原因很简单,现存的方案统计出来的结果,与预估结果误差太大。
先看看现有的文章字数统计方案。
方案 1:源自百度搜索结果。
function count($cid) {
// 方法传入文章cid,然后根据cid从数据库中获取文章的内容
$db = Typecho_Db::get();
$row = $db->fetchRow ($db->select ('table.contents.text')->from ('table.contents')->where ('table.contents.cid = ?', $cid)->order ('table.contents.cid', Typecho_Db::SORT_ASC)->limit (1));
$text = $row['text'];
// 自动检测原文本的编码并转换为UTF-8编码
$text = mb_convert_encoding($text, 'UTF-8', 'auto');
// 过滤空白字符
$text = str_replace('/( |\s|\r\n|\n|\r)+/u', '', $text);
// 使用正则替换删除所有非中文字符
$text = preg_replace('/[^\x{4e00}-\x{9fa5}]/u', '', $text);
// 计算中文字符长度
$count = mb_strlen($text, 'UTF-8');
// 返回统计结果
return $count;
}这个方案的问题在于:它只统计中文字符的数量,文章中的英文不统计?数字也不统计?按理来说,中文字符、英文、数字都要统计到字数中去的。
方案 2:源自百度搜索结果。
<?php
// 方法直接传入文章的内容,输出计算结果
echo mb_strlen($this->content);
?>这个方案的问题在于:mb_strlen 函数能够统计中文、日文、英文、数字等等,UTF-8 编码的中文字符计为 1 个长度,但是一个英文字母也会被计为 1 个长度。理论上来说,一个完整的单词才能计为 1 个长度。本质上,mb_strlen 函数计算的是 Unicode 字符数。
基本上所有的文章字数统计方案,都没办法很好的计算出预估结果。
对于一般情况下的字数统计,我的预估效果是这样的:
- 1 个中文算 1 个字,如“你好”算 2 个字。
- 1 个中文符号算 1 个字,如“你好。” 算 3 个字。中文标点符号占用的是全角字符的位置,每一个标点符号都占有等宽的空间,相当于一个汉字,因此中文符号也可以做为独立的元素被计入字数。
- 1 个英文单词算 1 个字,如“hello world”算 2 个字,“user-name”算 1 个字,“don't”算 1 个字。对于英文来说,字数的统计焦点在于单词的数量。
- 英文符号不计算在内。英文符号多为半角字符,通常被用来分隔单词或短语。英文符号在字数统计中不被计算是因为它们的主要功能是语法上的,而非构成独立语义单位。
- 1 串完整的数字算 1 个字,如“123”算 1 个字,“138 888 8899”算 3 个字。
- 1 串完整的数字 + 英文算 1 个字,如“user123”算 1 个字,“https://www.duozai.cn/page/7.html”算 1 个字。
- 空字符串不计算在内。
- 其他 Unicode 如日文、韩文,正常计算字符。
对于 Typecho Markdown 格式的文章字数统计,我的预估效果是这样的:
- Markdown 的所有标记都不能算入字数,如标题井号、分隔符横杠、列表星号横杠等。
- Markdown 图片链接不能算入字数,图片描述算入字数。
- Markdown 链接地址不能算入字数,链接描述算入字数。
- Markdown 代码块中的内容算入字数,代码块标记、代码块语言标记不能算入字数。
- Typecho Markdown 格式的文章文本开头有一个固定的 markdown 注释,这也不能算入字数。
在多次优化和调整之下,整出了个文章字数统计的方法:
/**
* 计算文章字数
*/
function postWordCount($archive) {
$db = Typecho_Db::get ();
$cid = $archive->cid;
// 获取文章内容
$rs = $db->fetchRow($db->select ('table.contents.text')->from ('table.contents')->where ('table.contents.cid = ?', $cid)->order ('table.contents.cid', Typecho_Db::SORT_ASC)->limit (1));
$content = $rs['text'];
// 匹配 Markdown 标记的正则规则
$rules = [
'/<!--markdown-->/' => '',
'/^#{1,6}\s+/m' => '',
'/(\*{1,2}|_{1,2})/' => '',
'/\[([^\]]+)\]\([^)]+\)/' => '$1',
'/!\[([^\]]*)\]\([^)]+\)/' => '$1',
'/```.*?\R/' => '',
'/\R```/' => '',
'/`/' => '',
'/^[ \t]*[-*+]\s+/m' => '',
'/^[ \t]*\d+\.\s+/m' => '',
'/^>\s*/m' => '',
'/^[-*_]{3,}\s*$/m' => '',
'/\|/' => '',
'/^[-: ]+$/' => '',
];
foreach ($rules as $pattern => $replacement) {
// 移除 Markdown 标记
$content = preg_replace($pattern, $replacement, $content);
}
if (empty($content)) {
return 0;
}
// 处理空格(合并连续空格为1个,保留分隔作用)
$content = preg_replace('/\s+/', ' ', $content);
$content = trim($content);
if ($content === '') {
return 0;
}
// 字数计数器
$count = 0;
// 按空格拆分成独立块
$blocks = preg_split('/\s+/', $content);
foreach ($blocks as $block) {
if (empty($block)) {
// 空格不计数
continue;
}
// 核心正则:按规则优先级拆分内容(先拆1-2类,再拆3类,最后剩4类)
// 匹配单个中文字符、单个中文符号、连续的英文/数字/英文符号序列、其他 Unicode
preg_match_all('/([\x{4e00}-\x{9fa5}]|[\x{3000}-\x{303f}\x{ff00}-\x{ffef}]|[a-zA-Z0-9\!\@\#\$\%\^\&\*\(\)\-\_\+\=\[\]\{\}\|\\\;\:\'\"\,\.\<\>\/\?\`]+|.)/ux', $block, $matches);
// 按规则计数
foreach ($matches[0] as $part) {
// 所有规则均为“单个/连续序列算1”,直接累加
$count++;
}
}
// 向上取整统计大约字数
return ceil($count / 10) * 10;
}这个方案,基本满足了我的预估效果。