青空文库是什么?
青空文库 是一个在线图书馆,收集了17751部(截止到2025年10月26日)无版权的文学作品,明治、昭和初期的作品居多。作品集合可以方便地从 Github 仓库:aozorabunko 上下载。
17751部文学作品中有多少日语单词呢?
我比较好奇,同时也是为了熟悉 Elixir 语言,因此花了点时间下载作品,用 Elixir 写脚本统计单词数量。
先把答案贴出来:114010 个
统计过程
首先需要从Github 仓库:aozorabunko 下载所有作品
但是注意,原始仓库有 5000 多个 commit,如果将完整仓库(包含所有历史记录)下载到本地的话需要大概 10G 存储空间,而且下载时间较长。我们只需要最新的记录即可,即使用 git clone --depth=1 [email protected]:aozorabunko/aozorabunko.git 命令。
通过查看仓库文件可以知道,基本上所有文学作品都在 html 后缀的文件中,仓库中还有很多其他图片、JS、CSS 文件我们就不考虑了。
下面我们就可以开始通过编写代码实现单词统计了,在开始之前先简单整理下思路:
- 找到所有 html 文件
- 从 html 中提取所有文字数据(注意:其实我没有将文学作品正文和页面上的其他文字区分开,因为没有多大必要,正文外的文字其实占比很少,就笼统地将他们视为一体吧)
- 将每个页面的文字数据进行分词处理,获取到单词信息
- 对单词进行过滤(比如会有一些乱码单词)、去重
下面开始写 Elixir 代码。
Elixir 的 Path 模块提供了对检索文件的方法,其中有一个是:Path.wildcard/2,可以通过正则表达式查找文件,很适合这种大范围、根据某种规则的文件查找场景。
html_files = Path.wildcard("/Users/xxxxx/aozorabunko/**/*.html")
从 html 文件中提取和查找信息,一般是通过某个 HTML 解析器来做到的。Elixir 生态中最流行的 HTML 解析器是 Floki,API 很简洁,使用起来很方便。
但我在提取文字信息时,还遇到了字符编码问题:Elixir 只能处理 UTF-8 编码的字符。但处理青空文库中的 HTML 网页时发现很多网页是Shift_JIS编码,因此需要需要检测出这些网页,然后使用编码转换工具,转换成 UTF-8。
编码检测库我用的是 charset_detect、编码转换库是:iconv
html_files
|> Enum.map(fn file ->
data = file |> File.read!()
content =
case CharsetDetect.guess!(data) do
"UTF-8" ->
IO.puts("valid data")
data
"Shift_JIS" ->
IO.puts("invalid data, file: #{file}")
:iconv.convert("SHIFT_JIS", "UTF-8", data)
unknown_encoding ->
IO.puts("unknown encoding, file: #{file}")
:iconv.convert(unknown_encoding, "UTF-8", data)
end
content
|> Floki.parse_document!()
|> Floki.find("body")
|> Floki.text()
end)
对字符串进行分词的工作由开源分词引擎:Mecab 完成,我实际使用的是它的 Elixir 包装库:mecab-elixir。
这里多介绍两句 Mecab。它不仅只是简单的分词,还会同时提供很多有用的信息,比如:词性、读音、原型/基本型…
提供一个真实例子:
iex> Mecab.parse("今日は晴れです")
[%{"conjugation" => "",
"conjugation_form" => "",
"lexical_form" => "今日",
"part_of_speech" => "名詞",
"part_of_speech_subcategory1" => "副詞可能",
"part_of_speech_subcategory2" => "",
"part_of_speech_subcategory3" => "",
"pronunciation" => "キョー",
"surface_form" => "今日",
"yomi" => "キョウ"},
%{"conjugation" => "",
"conjugation_form" => "",
"lexical_form" => "は",
"part_of_speech" => "助詞",
"part_of_speech_subcategory1" => "係助詞",
"part_of_speech_subcategory2" => "",
"part_of_speech_subcategory3" => "",
"pronunciation" => "ワ",
"surface_form" => "は",
"yomi" => "ハ"},
%{"conjugation" => "",
"conjugation_form" => "",
"lexical_form" => "晴れ",
"part_of_speech" => "名詞",
"part_of_speech_subcategory1" => "一般",
"part_of_speech_subcategory2" => "",
"part_of_speech_subcategory3" => "",
"pronunciation" => "ハレ",
"surface_form" => "晴れ",
"yomi" => "ハレ"},
%{"conjugation" => "基本形",
"conjugation_form" => "特殊・デス",
"lexical_form" => "です",
"part_of_speech" => "助動詞",
"part_of_speech_subcategory1" => "",
"part_of_speech_subcategory2" => "",
"part_of_speech_subcategory3" => "",
"pronunciation" => "デス",
"surface_form" => "です",
"yomi" => "デス"},
%{"conjugation" => "",
"conjugation_form" => "",
"lexical_form" => "",
"part_of_speech" => "",
"part_of_speech_subcategory1" => "",
"part_of_speech_subcategory2" => "",
"part_of_speech_subcategory3" => "",
"pronunciation" => ""
"surface_form" => "EOS",
"yomi" => ""}]
返回字段的完整含义对照表如下:
| 字段 | 日语 | 中文 |
|---|---|---|
surface_form |
表層形 | 表层形 / 原文形(句中实际出现的词形) |
part_of_speech |
品詞 | 词性 |
part_of_speech_subcategory1 |
品詞細分類1 | 词性细分类1 |
part_of_speech_subcategory2 |
品詞細分類2 | 词性细分类2 |
part_of_speech_subcategory3 |
品詞細分類3 | 词性细分类3 |
conjugation_form |
活用形 | 活用形态(词在句中出现时的变形) |
conjugation |
活用型 | 活用类型(词本身所属的变形类别) |
lexical_form |
原形 | 原形 / 基本形(词典形) |
yomi |
読み | 读音(假名读法) |
pronunciation |
発音 | 发音(实际发音,可能与读音不同) |
这一步的代码如下:
parse_article = fn article ->
Mecab.parse(article)
|> Enum.reject(fn %{
"lexical_form" => lexical_form,
"part_of_speech" => part_of_speech,
"surface_form" => surface_form
} ->
# 作用是过滤掉很多特殊的词,比如:助词、记号词、结尾标记符等
case {lexical_form, part_of_speech, surface_form} do
{"*", _, _} -> true
{_, _, "EOS"} -> true
{_, "助詞", _} -> true
{_, "記号", _} -> true
_ -> false
end
end)
|> Enum.map(fn %{"lexical_form" => lexical_form} -> lexical_form end)
|> Enum.uniq()
end
将上一步获取到的单词数据汇总、去重、过滤即可得到最终的单词列表。
File.read!("./words.txt")
|> String.split(",")
|> Enum.reject(fn str ->
cond do
# 检测乱码的拉丁字符
String.match?(str, ~r/[\x{00C0}-\x{024F}]/u) ->
true
# 检测全角数字,并且只有一个字符
String.match?(str, ~r/^[\x{FF10}-\x{FF19}]$/u) ->
true
# 检测全角英文字母,并且只有一个字符
String.match?(str, ~r/^[\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}]$/u) ->
true
# 检测半角英文字母、数字,并且只有一个字符
String.match?(str, ~r/^[0-9A-Za-z]$/) ->
true
# 检测(全角、半角)平片假名,并且只有一个字符
String.match?(
str,
~r/^[\x{3040}-\x{309F}\x{30A0}-\x{30FF}\x{31F0}-\x{31FF}\x{FF66}-\x{FF9D}]$/u
) ->
true
true ->
false
end
end)
|> tap(fn words ->
File.write!("./clean_words.txt", Enum.join(words, ","))
end)
最后贴 100 个处理后的单词作为展示:
立ち寄る,いただく,ありがとう,ござる,ます,はじめて,おいで,なる,方,ため,おさめる,ある,本,ファイル,形式,読み方,紹介,する,基本,フォーマット,各,登録,作品,原則,種,用意,いる,それぞれ,特徴,読む,必要,道具,以下,とおり,です,テキスト,データ,できる,最も,シンプル,軽い,ルビ,ふりがな,入力,れる,もの,ない,圧縮,リンク,除く,解凍,復元,ソフト,入手,先,フリー,ウェア,シェア,以上,付属,その,最新,版,及び,こちら,ダウンロード,窓,杜,インターネット,標準,一部,社,リリース,リーダー,表示,いま,使い,縦,組み,製品,ページ,単位,構成,電子,ほとんど,つくる,上,専用,(株),注意,マック,ユーザー,皆さん,改行,コード,多く,エディター,ワープロ,開く,行頭
本文的完整代码在 Gist。