统计博客文章字数

2022-11-12 ⏳3.3分钟(1.3千字)

信息时代注意力极度稀缺,人们很难容忍长篇大论。但为了把事情讲清楚,文章内容又不可能太短。这就产生了矛盾。解决矛盾的方法是给读者一个明确的预期,也就可以让读者快速判断文章的内容有没有用,大约要花多长时间才能读完。前一个问题我用两个办法解决,一个是把第一段当成摘要来写,力求言简意赅;另一个是在文章开头生成子标题列表。后一个问题则需要统计文章字数,根据字数估算阅读时长。

统计字数有两种做法。一种是用 js 在浏览器实现。该方案的好处是服务端不需要做调整,但坏处也很明显,文章的内容很少有变化,但读者每次打开页面都要扫描文字数量,太不低碳了😄

另一种就是在生成 HTML 的时候统计。Pandoc 默认不会统计字数,但可以通过 lua 插件来实现。我在博客技术栈一文中已经介绍过 lua 插件的基本用法,还展示了如何提取文章的第一段内容生成 description meta 标签。字数统计就是基于 description 脚本扩展而来的。

在贴代码之前我们还需要解决中英文字符计数的问题。中文应该按汉字计数,英文最好按单词计数。常见的方案是通过不同的正则规则来匹配中英文字符。但我觉得有点麻烦,想找个简单的方案。但再简单也不能按字节计数,不然一个汉字对三个字节,统计结果要爆炸💥

所以我先尝试直接按 Unicode 码位来计数,也就是一个汉字、字母、数数字都算一,先不管英文单词。lua 有一个 utf8 库,可以直接使用:

local s = "涛叔"
local n = utf8.len(s) -- n == 2

集成到 pandoc 以后发现文章字数偏多。英文占比越多的文章字数偏差越大,因为英文是按字母而非单词计数。我有一篇讲 IPv6 的长篇译文,因为保留了中英双语,统计出来的字数达到了惊人的四万多😱

显然,英文最好还是按单词来。如何按单词计数呢?纯英文比较简单,可以直接根据空格分割然后计数。但中英混杂的内容怎么处理呢?我的想了一个小妙招💡把连续的英文替换成单个字母:

local s = "hello, 涛叔"
s,_ = s:gsub("[a-zA-Z-]+","a")
local n = utf8.len(s) -- n == 5

这样英文内容的偏差就不至于那么夸张了。那篇文章由四万字降到两万字😂考虑到英文单词后面肯定有空格,所以最好也把空格去掉,不然英文内容的字数会翻倍。

local s = "hello, 涛叔"
s,_ = s:gsub("[a-zA-Z-]+","a")
s,_ = s:gsub("%s+","a")
local n = utf8.len(s) -- n == 4

去掉空白字符后,两万字变成了一万六千字,基本跟内容对上了。本来想把标点符号也去掉。但 lua 里%p只能匹配英文标点,中文的还得单独处理。考虑到标点肯定比空格少,就先不折腾了。后面有空再优化。

拿到字数后还需要估计阅读时间。这个时间跟太多因素相关,同样的内容不同读者的阅读时间也不一样。我从网上查到的非权威资料显示,一般人的阅读速度是300-500字/分钟。我们取个中间值400字/分钟。

接下来要考虑如何展示这两个数据。最简单的办法是展示有多少字,需要多少分钟阅读。但是数字中的零头该怎么处理?一万两千字要写成12000字还是汉字「一万两千」呢?如果是12345呢?显然不能直接用汉字表达。我看到有博客参照英文习惯,直接显示12.3k字。这样比较简单。但前面是12.3k后而再加一个字,怎么看怎么别扭,直接写📊12.3k吧,前面加一个 emoji 表情。同样阅读时间就展示成⏰40.2m。两个数字都保留一位小数。

跟朋友交流了一下,使用英文单位还是会造成误解。他们可能会以为📊12.3k表示访问次数。所以最终还是加上中文单位⏳3.2分钟(1.2千字)

因为 pandoc 模版不支持数学运算,所以我们需要在 lua 插件中把这两个数字算好并且设置好数字格式。完整 lua 代码如下:

local description = ""
local runes = 0

function get_description(blocks)
  --- set description

  for _, block in ipairs(blocks) do
    --- 只统计段落内容
    if block.t == 'Para' then
      --- 提取级文本内容
      local s = pandoc.utils.stringify(block)

      s,_ = s:gsub("[a-zA-Z-]+","a")
      s,_ = s:gsub("%s+","")

      runes = runes + utf8.len(s)
    end
  end

  return nil
end

return {{
  --- pandoc 处理 markdown 会反复调用该函数
  Blocks = get_description,
  --- 生成 meta 变量信息
  Meta = function (meta)
    meta.description = description
    --- 计算并设置数字格式
    meta.runes = string.format("%0.1f", runes/1000)
    meta.read_time = string.format("%0.1f", runes/400)
    return meta
  end,
}}

使用 pandoc 生成 HTML 时需要通过--lua-filter=description.lua指定扩展脚本。然后还需要在模版中引用这两个变量:

<h1>$title$</h1>
<a rel="author" href="$author_url$">$author_name$</a>
<date>$date$</date>
<!-- 内容信息 -->
<span>⏳$read_time$分钟($runes$千字)</span>

以上就是本文的全部内容。欢迎留言讨论。