こんにちは滝澤です。

先月(2018年9月16日)に約3年半ぶりに迷惑メールフィルターSpamAssassinの最新版(バージョン3.4.2)がリリースされました。

リリース情報が脆弱性情報とルール更新プログラム(sa-update)の署名アルゴリズムの変更と共にもたらされたため、SpamAssassinを利用している場合には最新版へのアップデートが必要になります。 しかし、以前のバージョンではSpamAssassinの日本語対応は不十分であったため、日本語対応パッチが開発されてきたという経緯があります。 そのため、本記事では最新版の日本語対応状況がどうなっているか確認しました。

要約

SpamAssassin最新版(バージョン3.4.2)の日本語対応状況は次の通りです。

  • パターンテスト: 制約なしに日本語メールで利用できる
  • ベイズフィルター: 日本語メールへの対応は不十分

後者の対応のために、日本語対応トークナイザーを作成しました。

SpamAssassinとは

まず最初に、SpamAssassinとは何かを簡単に紹介します。

SpamAssassinは様々なテストを行って迷惑メールらしさを判定するメールフィルターです。 迷惑メールらしさを判定するテストにはヘッダーや本文のパターンテスト、ベイズフィルター、DNSブラックリスト、協調型データベースなどを使います。 様々なテストの結果を迷惑メールらしさのスコアとして加算していくため、正常なメールを迷惑メールと誤判定することがが少なくなります。

出来ることと出来ないこと

よく勘違いされますが、SpamAssassin単体では迷惑メールを除去したり、振り分けしたり、バウンスメールを送ったりはできません。 SpamAssassinが行うのは迷惑メールらしさのスコアを付け、判定して、ヘッダーに情報を追加したり、書き換えたりすることくらいです。

迷惑メールの除去や振り分けなどを行いたい場合は、別のソフトウェアと組み合わせて使います。

日本語メールへの対応を必要とする機能

SpamAssassinの日本語メールへの対応を必要とする機能としては次の2つになります。

  • パターンテスト
  • ベイズフィルター

パターンテスト

パターンテストとは、メールのヘッダーや本文の文字列が設定ファイルに記述した正規表現のパターンに一致するかを評価する機能です。

次のような設定例を用いて説明します。

header   SUBJECT_CHEAP  Subject =~ /\bcheap\b/i
describe SUBJECT_CHEAP  Subject contains a word 'cheap' 
score    SUBJECT_CHEAP  1.0

Subjectヘッダーフィールドの値に"cheap"という文字列が含まれていれば、ルール SUBJECT_CHEAP に一致したと評価され、そのルールのスコア1.0が加算されます。

body     BODY_JA_DEAI   /出会い/
describe BODY_JA_DEAI   DEAI
score    BODY_JA_DEAI   0.5

本文に「出会い」という文字列が含まれていれば、ルール BODY_JA_DEAI に一致したと評価され、そのルールのスコア0.5が加算されます。

このようなルールを評価して加算されたスコアの合計値が、設定した閾値(required_score)以上になれば迷惑メール(spam)であると判断します。

normalize_charset 機能

SpamAssassin 3.2系から導入された normalize_charset 機能を有効にすると、メールのヘッダーや本文をUTF-8文字エンコーディング(以降、単にUTF-8と略す)に正規化してから、UTF-8で記述されたルールのパターンの評価を行います。

次のように日本語の漢字・ひらがな・カタカナでもパターンを記述できます。

header   SUBJ_MISHODAKU  Subject =~ /(未|末)承諾/
body     DEAI            /出(会|逢)/

しかし、SpamAssassin 3.4.0以前では、実はこの機能は十分には機能していなく、日本語のパターンは正しく評価されませんでした。

SpamAssassin 3.4.1(2015年4月30日リリース)からはうまく動作するようになりましたが、ソースコードの至る所にbytesプラグマが埋め込まれており、UTF-8の文字列をバイト文字列として評価していました。 そのため、ルールのパターンの記述においてUTF-8の文字のブラケット表現(文字クラス)、例えば /[あ-お]+/ のようなパターンが使えませんでした。

今回リリースされたSpamAssassin 3.4.2ではbytesプラグマが全てコメントアウトされており、UTF-8の文字列として評価できるようになりました。つまり、 /[あ-お]+/ のようなパターンが使えるようになりました。

そのため、今回リリースされたバージョンからは何の制約もなしに日本語のパターンを評価できます。

ベイズフィルター

ベイズフィルターとは、ベイズ推定(WikiPedia)を用いた学習型フィルターです。

学習の際に、メールのヘッダーや本文の文字列から主に単語をトークンとして抽出します。

英語のように分かち書きされていれば単語をそのままトークンとして抽出できます。

I have a dream.

この場合は'I', 'have', 'a', 'dream'がトークンとして抽出されます。 実際には迷惑メールにも正常なメールにも共通で使われる単語はストップワードとして除去されるため、'dream'のみがトークンとして抽出されます。

日本語の文字列の場合

日本語の文字列の場合にどうなるかを確認してみました。

日本語の文字列をSpamAssassin 3.4.2のベイズフィルター(normalize_charset 機能有効)に通すと、UTF-8の16バイト以上のバイト文字列の場合は、3バイトUTF-8と4バイトUTF-8の文字を1文字毎に抽出するという抽出ルールが実行されます。

私の名前は中野です。

この文字列をベイズフィルターに通してみると、'u8:私', 'u8:の', 'u8:名', 'u8:前', 'u8:は', 'u8:中', 'u8:野', 'u8:で', 'u8:す', 'u8:。'のようなトークンが抽出されます。1文字ずつ抽出されていることとがわかります。ここで、'u8:'はUTF-8の文字を1文字ずつ抽出したことを示すプレフィックスです。

ひらがなやカタカナの1文字は迷惑メールにも正常なメールにも共通で使われる文字であるため、役に立たないトークンになります。 漢字1文字の場合、熟語の文字列から漢字1文字ずつ抽出されたものは、元の熟語に比べて迷惑メールや正常なメールを特徴付ける性質が弱くなります。

以上より、ベイズフィルターは日本語メールへの対応は不十分であることがわかります。

それでは、どういう改善を行ったらよいかを考えると、ベイズフィルターの処理の前に日本語の文章を分かち書きしたらよいのではないかと考えました。

先の例では、次のように分かち書きできれば適切なトークンとして抽出できます。

私 の 名前 は 中野 です 。

今回は、このような分かち書きを行う日本語トークナイザーのパッチを作成したので紹介します。

※ 以前から開発されてきた日本語対応パッチも同様な考えで開発されてきましたが、今回は日本語トークナイザーに特化して開発し直しました。

日本語対応トークナイザー

日本語対応トークナイザーのパッチを次の場所で公開しています。

このパッチを適応すると、SpamAssassinのベイズフィルターにおいて、日本語の文章の分かち書きを行い、適切に日本語のトークンを学習できるようになります。この結果として、日本語メールのベイズフィルターの判定精度が向上します。

行っていることは次のことです。

  • ベイズフィルターの前処理としてトークナイザー機能を追加する Tokenizer プラグインの追加
  • Tokenizerプラグインの日本語対応の実装として、 Tokenizer::MeCab プラグインを追加
    • 形態素解析器MeCabにより分かち書きを行うプラグイン
  • Tokenizerプラグインの日本語対応の実装として、 Tokenizer::SimpleJA プラグインを追加
    • 文字種(漢字、ひらがな、カタカナ、それ以外)により分かち書きを行うプラグイン
  • ベイズフィルターにおける日本語処理の改善
  • 日本語の文字エンコーディングの検出処理の改善

なお、このパッチは日本語対応に特化したものであるため、開発本家にマージすることは全く考えていません。

処理の流れ

本パッチの実行処理の流れを紹介します。

  1. メールをUTF-8に変換(normalize_charset機能により実行)
  2. 言語判定の実施
  3. 言語に対応したトークナイザーの取得
  4. トークナイザーにより分かち書きの実施
  5. ベイズフィルターの実施

2〜5の処理について簡単に説明していきます。

言語判定の実施

言語判定処理としては、メール本文にひらがなやカタカナや漢字が含まれていれば日本語であると判定するという単純な実装を行いました。

言語に対応したトークナイザーの取得

トークナイザーをプラグインとして実装しており、ロードしたTokenizerプラグインの中から言語に対応したものを利用するように実装しました。

日本語のTokenizerプラグインとして次の2つを用意しました。

  • Tokenizer::MeCab
    • 形態素解析器MeCabにより分かち書きを行うプラグイン
  • Tokenizer::SimpleJA
    • 文字種(漢字、ひらがな、カタカナ、それ以外)により分かち書きを行うプラグイン

例えば、設定ファイル中に次のような記述を行うと、日本語トークナイザーのTokenizer::MeCabプラグインをロードできます。

# Tokenizer::MeCab
#
loadplugin Mail::SpamAssassin::Plugin::Tokenizer::MeCab

処理速度はどちらもほとんど変わらないので、解析精度を考慮するとTokenizer::MeCabプラグインを使うのがよいでしょう。

トークナイザーにより分かち書きの実施

Tokenizer::MeCabプラグイン

Tokenizer::MeCabプラグインは、形態素解析器MeCabを利用して、日本語メールの分かち書きを行います。

私の名前は中野です。

この文章は次のように分かち書きされます。

私 の 名前 は 中野 です 。 

さらに、後述するストップワードを除去すると次のようになります。

名前 中野

文章から特徴的なトークンが抽出されたことがわかります。

ストップワードとして、単語のリストを用意するのではなく、次の2種類を用いました。

  • 品詞が「その他,間投」、「フィラー」、「記号」(括弧と句読点)、「助詞」、「助動詞」、「接続詞」、「名詞,代名詞」、「名詞,非自立」であるもの
  • 1文字の「ひらがな」と「カタカナ」

品詞については、MeCabの解析結果の形態素IDを利用して、判断しています。

Tokenizer::SimpleJAプラグイン

Tokenizer::SimpleJAプラグインは、文字種(「ひらがな」「カタカナ」「漢字」)毎に文字列を分けます。 MeCabをインストールできない環境向けに用意しました。

私の名前は中野です。

この文章は次のように文字列が分けられます。

私 の 名前 は 中野 です 。 

さらに、後述するストップワードを除去すると次のようになります。

私 名前 中野

それなりに、文章から特徴的なトークンが抽出されたことがわかります。

ストップワードとして、単語のリストを用意するのではなく、次のものを用いました。

  • 2文字以下の「ひらがな」と「カタカナ」

かなり雑な実装ですが、助詞の多くはこれで除去できます。

ベイズフィルターの実施

日本語トークナイザーにより分かち書きされた文字列をトークンとして抽出できます。

なお、このパッチでは上述したUTF-8の文字列を1文字ずつ抽出する処理を無効にしました。

さらに、デバッグ用のコードを埋め込んでおり、デバッグを有効にして学習するコマンド sa-learn を実行するとトークンの抽出結果がわかります。

$ ~/bin/sa-learn -D --spam < sample.txt 2>&1 | less
中略
Oct 15 08:16:43.640 [5877] dbg: bayes: _tokenize_line: line=名前 中野
Oct 15 08:16:43.640 [5877] dbg: bayes: _tokenize_line: tokens=名前,中野
中略

最後に

今まで開発されてきた日本語対応パッチは12年前に開発されたものです。 根本的な見直しをせずに前回のSpamAssassin 3.4.1のリリース時まで保守されてきました。

SpamAssassin 3.4.2では以前のパッチがそのままではうまく動かなかったこともあり、UTF-8のメールがほぼ標準になっている現状も考慮して、根本的に見直しを行うことにしました。 その結果として、日本語対応トークナイザーの処理に特化して、変更箇所をできるだけ少なくなるように全面的に見直しました。

参考サイト

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。