🍇

BudouXを使った自作textlintルールで文章の読点を校正してみたかった

2023/10/09に公開

はじめに

「ESLintのような感じで日本語文章を校正してくれるものがあればいいなぁ」と思っていたところ、「textlint」なるものを知りました。TypeScriptで開発されていて、自作ルールの追加も可能です。そこで試しにBudouXで読点位置チェックを行うルールを作ってみました。現段階では私のとったアプローチがよくなかったために、精度的な問題がある出来栄えになってしまっていますが、textlint自体は非常によいものだったためここにメモを残します。

https://github.com/okayurisotto/textlint-rule-budoux-comma

textlintについて

textlintは自然言語用のリンターです。TypeScriptで開発されていて、npmなどを使ってインストールできます。

https://github.com/textlint/textlint

ちょうどREADMEにも「similar to ESLint」と書かれている通り、使い勝手はESLintとほぼ同じです。VS Code向けの拡張機能もあってとても便利です。

BudouXについて

BudouXは読みやすい改行のための軽量な分かち書き器です。テキストを折り返して表示する際にどこで折り返せばいいのか、機械が判断するためのものです。機械学習の成果が使われています。

https://developers-jp.googleblog.com/2023/09/budoux-adobe.html

https://github.com/google/budoux/

textlintのルールの自作

BudouXを使えば文章の分かち書きができます。そして読点とは基本的に、文章がそれぞれの言葉に分けられたとき、その言葉の間に入れられるものです。つまり、すべての読点をあらかじめ削除しておいた文章をBudouXに入力すれば、読点が入ることのできる場所を特定できるはずなのです。それを実際に読点が入れられた文章と比較すれば、読点が正しい位置にあるかどうかを判定できるでしょう。読点の位置をtypoしてしまっても、それに気付けるというわけです。

実際のルール作成では、craete-textlint-ruleというツールがすでに用意されているようだったため、これを使いました。

https://github.com/textlint/create-textlint-rule

これを使わずに作ることもできるでしょう。しかし途中まで私はこれを使わずに作っていたのですが、テストなどを考えたとき面倒になってしまい、結局素直にこれを使わせてもらうことにした…ということがありました。

textlintでは、文書をパースしてASTにするところはtextlint本体が、実際にルールに照らして判断するところは各ルール側が行うようでした。

// ルールの例(簡略化されています)

// いろいろと型定義が用意されている(省略)
// import type {} from "@textlint/types";

// チェック関数の中身は後述
import { check } from "./check";

const report = (context, options) => {
  return {
    // ASTにある文字列なノードに対し実行する
    [context.Syntax.Str](node) {
      // テキストを得る
      const text = context.getSource(node);

      // チェックして、okではなかった場合はエラーを報告する
      if (!check(text).ok) {
        context.report(new context.RuleError("不正です!"));
      }
  };
};

// デフォルトとしてエクスポート
export default report;

BudouXを使ってチェックする処理は次のように実装してみました。

import * as budoux from "budoux";

const parser = budoux.loadDefaultJapaneseParser();

export const check = (
  text: string,
  comma: string, // 読点として使う文字も受け取る
): { ok: true } | { ok: false; index: number } => {
  // 読点を削除した文章をパースする
  const commaless = text.replace(new RegExp(comma, "g"), "");
  const items: string[] = parser.parse(commaless);

  let i = 0; // どこまで読んだか保存しておくカーソル的な変数
  for (let j = 0; j < items.length; ) {
    const item = items[j];

    if (text.startsWith(item, i)) {
      // 読点が使われず繋げて書かれている:次のアイテムへ
      i += item.length;
      j++;
    } else if (text.startsWith(comma, i)) {
      // 読点が使われている:読点分を読み飛ばす
      i += comma.length;
    } else {
      // 読点が使われているわけでも、繋げて書かれているわけでもない:異常
      return { ok: false, index: text.indexOf(comma, i) };
    }
  }

  return { ok: true };
};

実際のコードはリポジトリを参照してください。

自作ルールの出来栄え

残念ながら、自作ルールはいくつかの正しい文章で、「ここに読点が入るのは異常です」とエラーを出力するようになってしまっています。どうやらBudouXは、読点が完全にない文章を分かち書きするときには少し間違ってしまうようでした。

例えば次のような文章では…。

そのように思っていたところ、textlintなるものを知りました。

読点をなくしてしまうと次のように分かち書きされてしまいます。

["そのように", "思っていた", "ところtextlintなる", "ものを", "知りました。"]

そのため、「『ところ』と『textlintなる』の間に読点が入るのはおかしいです」という風にエラーが表示されてしまいます。あくまでBudouXは、正しく読点の使われた文章をあとから分かち書きするためのものであるということなのでしょう。

(ちなみにこれは、「textlint」という言葉をかぎ括弧で囲うなどすると解消されます。)

おわりに

期待していたような性能は得られませんでしたが、textlintを知れた&ルールの自作方法を学べたという成果は得られたのでよしとします。

Discussion