dict-tools.mjs のバグを修正した話

今日は自作の辞書ページ自動生成ツール dict-tools.mjs がうまく動かない問題を調査・修正しました。

症状

bulk-generate コマンドを実行すると、辞書ページが1件も生成されていないにもかかわらず「全件生成済み」と表示されてしまう問題が発生していました。--reset オプションをつけても結果は変わらず、原因の調査が必要な状態でした。

調査と発見したバグ

コードを読み進めると、原因は1つではなく複数のバグが重なっていることがわかりました。

バグ① CSVパースの正規表現が日本語で壊れていた(最重要)

最終的に最も根本的な原因だったのがこれです。用語リストを読み込む loadTermsCsv 関数で、CSV の各行をカラムに分割するのに次の正規表現を使っていました。

lines[i].match(/("(?:[^"]|"")*"|[^,]*)/g)

この正規表現は日本語などのマルチバイト文字を含む列を誤って2つに分割してしまうバグがありました。たとえば quantum-spectral-line,量子スペクトル線,421 という行を読むと、結果が ["quantum-spectral-line", "", "量子スペクトル線", "", "421", ""] のように余分な空文字列が挿入されてしまい、列番号がずれて title が常に空文字になっていました。

その結果、全行が「idまたはtitleが空」として読み飛ばされ、用語リストが0件になっていました。

修正は正規表現をやめて、文字を1つずつ走査するシンプルなパーサーに置き換えました。

const cols = [];
let cur = "", inQuote = false;
for (let j = 0; j <= lines[i].length; j++) {
  const ch = lines[i][j];
  if (j === lines[i].length) { cols.push(cur.trim()); break; }
  if (inQuote) {
    if (ch === '"' && lines[i][j + 1] === '"') { cur += '"'; j++; }
    else if (ch === '"') { inQuote = false; }
    else { cur += ch; }
  } else {
    if (ch === '"') { inQuote = true; }
    else if (ch === ',') { cols.push(cur.trim()); cur = ""; }
    else { cur += ch; }
  }
}

バグ② 既存ファイルのチェックが1万回フルスキャンしていた

bulk-generate は生成済みファイルのスキップ判定に findDictFile() を使っていましたが、この関数は呼ばれるたびにディレクトリ全体を再帰スキャンする実装になっていました。用語リストが1万件あるため、起動時に1万回ものフルスキャンが走り、Node.js のメインスレッドをブロックしていました。

// 修正前:1万件 × 毎回フルスキャン
allTargets.filter(t => !findDictFile(t.id))

修正は、起動時に1回だけスキャンして結果を Set にキャッシュする方式に変えました。

// 修正後:起動時に1回だけスキャン
const existingIds = new Set(
  readdirMdRecursive(DICT_DIR).map(p => path.basename(p, ".md"))
);
allTargets.filter(t => !existingIds.has(t.id))

バグ③ チェックポイントがエラー時にも進んでいた

処理中にAPIエラーやJSONパース失敗が起きても、チェックポイントのインデックスだけが進んでしまう問題もありました。次回実行時にエラーになった用語が「処理済み」とみなされてスキップされてしまうため、ファイルが生成されないまま完了扱いになる原因の一つでした。

succeeded フラグを導入し、成功・スキップ時のみチェックポイントを進めるよう修正しました。

// 成功時のみチェックポイントを進める
if (!DRY_RUN && succeeded) {
  saveCheckpoint(BULK_CHECKPOINT, { ... });
}

まとめ

今回の問題は「全件生成済み」という誤ったメッセージから始まりましたが、調査するとバグが3層になって重なっていました。特に日本語を含むCSVを正規表現でパースするのは落とし穴が多く、シンプルな文字走査パーサーに切り替えるのが確実だと改めて感じました。

修正後は無事に用語の生成が始まり、動作を確認できました。

コメント

タイトルとURLをコピーしました