Fooling API と実装

Back


  1. 基本的な流れ
    1. 検索
    2. インデックス
  2. 各クラスの使い方
  3. 拡張方法
    1. 新しい種類の文書集合 (Corpus) をサポートする
    2. 新しい種類の文書 (Document) をサポートする
    3. 新しい種類の述語 (Predicate) をサポートする
  4. Webアプリケーションから使う際の注意点

1. 基本的な流れ

a. 検索

Python プログラム中で文書を検索する場合、 以下のクラスを使います。

DocumentCorpus は抽象的な 文書および文書の集合を表すクラスです (実際には HTMLDocumentFilesystemCorpus などの派生クラスを使います)。 Corpus オブジェクトと複数の Predicate オブジェクトから Selection (該当する文書) オブジェクトを作成します。 ここから Document オブジェクトをひとつずつ取り出し、検索結果として表示します。

# 基底ディレクトリ /doc/Python-2.4-ja にある HTML 文書と、
# /tmp/index にあるインデックスを使って検索する例

from fooling.corpus import FilesystemCorpus
from fooling.document import HTMLDocument
from fooling.selection import Predicate, Selection

# 検索する文書集合の基底ディレクトリとインデックス用ディレクトリ、および文書の種類を指定する:
corpus = FilesystemCorpus('/doc/Python-2.4-ja', '/tmp/index', HTMLDocument)

# 検索条件を作成する (これらは AND で結合される) :
predicates = [ Predicate(u'Python'), Predicate(u'オブジェクト') ]

# 該当する文書の一覧を得る:
selection = Selection(corpus, predicates)

# すべての該当文書のタイトルと snippet を表示する:
for (found, doc) in selection:
  print found, doc.get_title(), doc.get_snippet(selection)

Selection オブジェクトはリストとしても扱えますが、 内部ではイテレータになっており、 文書をひとつ取得するたびに次の文書を漸進的に検索します。 すべての文書を表示すると時間がかかるため、途中でやめてもかまいません。 Selection オブジェクトは pickle可能なので、 途中まで検索した Selectionオブジェクトを pickle 保存しておき、 あとで検索を再開することが可能です。 CGI から使う場合は SelectionWithContinuation クラスを参照してください。

b. インデックス

インデックスの際には Corpus クラスと Indexer クラスを使います:

# 基底ディレクトリ /doc/Python-2.4-ja にある HTML 文書から
# /tmp/index 以下のディレクトリにインデックスを作成する例

from fooling.corpus import FilesystemCorpus
from fooling.document import HTMLDocument
from fooling.indexer import Indexer

# 検索する文書集合の基底ディレクトリとインデックス用ディレクトリ、および文書の種類を指定する:
corpus = FilesystemCorpus('/doc/Python-2.4-ja', '/tmp/index', HTMLDocument)

# 文書集合に対する Indexer オブジェクトを作成する:
indexer = Indexer(corpus)

# 文書をインデックスに追加する。
# これらは基底ディレクトリからの相対パス名で指定する:
indexer.index_doc('a.html')
indexer.index_doc('b.html')
indexer.index_doc('c.html')

# インデックスを完了する:
indexer.finish()

最後の indexer.finish() を実行した時点で インデックスファイルがディスクに書き込まれます (これ以前にも、文書数または単語数が規定の値を超えると そこまでのインデックスファイルが順次、作成されていきます)。


2. 各クラスの使い方

Corpus クラス

Corpus クラスは仮想的な文書集合を定義し、 各文書とそのインデックスにアクセスするためのインターフェイスを提供します。 各文書は location (位置) によって一意に指定されます。 Location は文字列で、この値はたとえば FilesystemCorpus では文書ファイルのパス名として使われ、 BerkeleyDBCorpus では文書データ (value) を取り出すためのキーとして使われます。

Corpus クラスは fooling.corpus モジュール内で定義されています。 Corpus クラスは抽象クラスであるため、実際には以下のサブクラスのどれかを作成します。 これらすべての Corpus オブジェクトは pickle が可能です。

作成可能なクラス
FilesystemCorpus(basedir, idxdir, prefix='', doctype=None, encoding='euc-jp')
通常のディレクトリツリー構造を使った文書集合を表現します。 すべての文書は basedir 以下のディレクトリに格納され、 各文書は basedir からの相対パス名で指定されます。 検索時には idxdir ディレクトリ中の prefix のプレフィックスをもつ インデックスファイルが使われます (prefix が空の場合はすべてのインデックスファイルが使われます)。 すべての文書は同一の文書クラス doctype および同一の文字コード encoding を もつと仮定されています。 doctype には Document クラス (後述) の クラスオブジェクトを与える必要があります。

BerkeleyDBCorpus(dbfile, idxdir, prefix='', doctype=None, encoding='euc-jp')
Berkeley DB ファイルに格納されている文書集合を表現します。 各文書は dbfile 内のキーにより指定されます。

CDBCorpus(dbfile, idxdir, prefix='', doctype=None, encoding='euc-jp')
CDB ファイルに格納されている文書集合を表現します。 各文書は dbfile 内のキーにより指定されます。

SQLiteCorpus(dbfile, idxdir, prefix='', doctype=None, encoding='euc-jp', table='documents', key='docid', text='doctext', mtime='mtime'):
SQLite テーブルに格納されている文書集合を表現します (pysqlite2 が必要です)。 各文書はテーブル table 中の主キー key で表され、 text フィールド内には文書データ (文字列) が、 mtime フィールド内には文書の更新時刻 (整数) が格納されると仮定しています。

(注意: 新山は SQLite をまともに使ったことがないため、このクラスは まだしっかりテストしていません。ぜひご協力を!)

公開されている属性・メソッド

以下の属性・メソッドはすべての Corpus クラスで使えます:

Corpus オブジェクトを pickle する際の注意点

Predicate クラス

Predicate オブジェクトは検索条件を表現します。 ひとつの検索フレーズがひとつの Predicate オブジェクトに相当します。 Predicate オブジェクトの内部には単語列と正規表現パターンが含まれており、 ユーザは Corpus オブジェクトと Predicate オブジェクトのリストを 使って検索結果である Selection オブジェクト (後述) を作成します。

Predicate クラス自体は抽象クラスであり、これは fooling.selection モジュール内で定義されています。 ユーザは Predicate から派生した、以下のクラスを作成できます。

作成可能なクラス
KeywordPredicate(phrase)
phrase にはひとつの検索条件式 (文字列) を与えます。 KeywordPredicate オブジェクトは これを内部表現 (2.2-グラムのトークンおよび正規表現パターン) に変換して保存します。 条件式の文法は 検索機能ヘルプ にあるのと同じで、 フレーズtitle:フレーズ、 または |フレーズ| という形式が使えます。 "フレーズ" という形式はサポートしていません。 で囲まれたフレーズを KeywordPredicate オブジェクトのリストに分割するには fooling.selection モジュールにある parse_preds()関数を使ってください。

StrictKeywordPredicate(phrase)
KeywordPredicateオブジェクトと同じですが、検索フレーズ中の 記号や句読点まで (空白以外の) 一字一句にマッチする文書を検索します。

注意: Fooling では記号や句読点はインデックスされていません。 そのため、StrictKeywordPredicate を使った検索では最初に「意味のある (約物以外の)」 フレーズ検索をしたあと、各フレーズの周囲を調べて与えられたパターンにマッチするかどうか 絞りこみが行われます。文書集合が特定のフレーズを大量に含んでおり、なおかつ その中でパターンに正確に合致するようなものが非常に少なかった場合、これはほとんど 逐次検索と同じなるため、検索に非常に時間がかかる場合があります。

EMailPredicate(phrase)
KeywordPredicate オブジェクトと同じく検索条件式を表現しますが、 この述語は電子メール (EMailDocument オブジェクト) の検索専用です。 上にあげた通常のフレーズ検索に加えて、以下のような条件式をサポートしています:

YomiKeywordPredicate(phrase)
KeywordPredicateオブジェクトと同じですが、 漢字をふくむ日本語文字列の読みがなにマッチする文書を検索します。 phraseはローマ字あるいはひらがな、カタカナで 書かれた読みがなとみなされます。

ローマ字変換で使用する規則は、現在のところ romm.py 内の PARSE_DEFAULT 値で直接指定されています。ここには PARSE_OFFICIAL (訓令式 + "nn"で「ん」を入力)、 PARSE_OFFICIAL_ANNA (訓令式 + "n"で「ん」を入力)、 PARSE_ENGLISH (英語式)、のいずれかの値を指定できます。

YomiEMailPredicate(phrase)
EMailPredicateオブジェクトに 読みがな検索の機能を加えたものです。

Selection クラス

Selection オブジェクトは、与えられた Corpusオブジェクトと Predicateオブジェクトに対する検索結果の一覧を表現します。 実際にはこのオブジェクトは最初からすべての該当文書を含んでいるわけではなく、 ユーザが要素を参照するに従って漸進的に構築されていきます。

Selection クラスは fooling.selection モジュール内で定義されています。 Selection オブジェクトはこれまでに見つかった文書 (の location) 一覧を含んでおり、 Document オブジェクト (後述) のリストまたはイテレータとして扱うことができます。 また、Selection オブジェクトは pickle が可能です。 Selection クラスの使い方については fooling/selection.pymain関数も参照してください。

作成可能なクラス
Selection(corpus, predicates, doc_preds=None, safe=True, start_loc=None, end_loc=None, disjunctive=False)
与えられた Corpusオブジェクト corpus と、 Predicateオブジェクトのリスト predicates に対する 検索結果の一覧を作成します。引数 doc_preds が指定されている場合は、 得られた各文書の location に対して doc_preds 内の各関数を呼び出し、文書のさらなる絞り込みを行います。 引数 safeTrue の場合、インデックスの最終更新時刻よりも後に 変更された文書は検索結果から除かれます。 disjunctiveTrue の場合、検索条件は and ではなく or として組み合わされます。

Selection オブジェクトは __iter____getitem__ を サポートしており、リストと同様に扱えますが、リストとして参照したときと イテレータとして使用したときでは返す結果が違うので注意してください。 リストとして参照したときは i番目の Documentオブジェクトを返しますが、 イテレータとして使用すると (順位, Documentオブジェクト) という形式のタプルを返します。

SelectionWithContinuation(corpus, predicates)
Fooling の検索は漸進的におこなわれるため、 検索結果はかならず最初から取得する必要があります。 いわゆる「20位目からの結果を表示」というような、 該当文書中の途中の位置から検索を開始することはできません。 Selectionオブジェクトを pickle 保存しておくことで 「次の 10件」のような機能を提供することができますが、 Webアプリケーション上でサービスを提供する場合はこの方法は不便です。

SelectionWithContinuation クラスには、 Selection オブジェクトを pickle 保存しなくても続きから検索が再開できるような メソッドを追加してあります。これらのメソッドは現在の Selection オブジェクトの状態を、 文字列として符号化・復号します。pickle で保存したのとは異なり、 この文字列にはこれまでに見つかった文書の一覧は保存されていませんが、 この情報を使うことで「続き」から検索を再開できます。 ただし、Selection オブジェクトと違い、 SelectionWithContinuation オブジェクトはイテレータとしては使えません。 これは SelectionWithContinuation オブジェクトが過去の検索結果を 保存していないため、検索結果を最初からすべて返すイテレータの意味論が適用できないためです。 途中から検索を再開するには iter_start() メソッドを使ってください。

DummySelection(corpus, locations)
手動で作成した文書の集合 (locations に文書の位置のリストを与えます) を あたかも Selection の検索結果と同じように扱うオブジェクトです。なお、このオブジェクトから返された Documentオブジェクトの get_snippet() メソッドを呼び出すと、 各文書の先頭 (あるいはそのクラスの default_snippet_pos で指定された位置) からの 内容をデフォルトの snippet として表示します。

公開されている属性・メソッド

以下の属性・メソッドはすべての Selection クラスで使えます:

Document クラス

Document オブジェクトはひとつの文書を表現します。 このクラスは必ずしも内部に具体的なデータを持っているわけではなく、 Corpus オブジェクトから file-like オブジェクト (実際の fileオブジェクト または StringIO) を取得して文書データにアクセスします。 また、このクラスは各文書からインデックスするための単語列を抽出したり、 文書のタイトルを取得したりするインターフェイスを提供します。

検索機能を使うユーザは、通常 Selection オブジェクトを介して Document オブジェクトを受け取り、以下のメソッドあるいは属性にアクセスします:

公開されている属性・メソッド

以下の属性・メソッドはすべての Document クラスで使えます:

定義されているクラスオブジェクト

以下のクラスが fooling.document モジュール内で定義されています。

ユーザが Document オブジェクトを直接作成しては *いけません*。 Document オブジェクトはつねに Corpus オブジェクトによって作成されます。 ただし Corpus オブジェクト を作成するさい、ユーザはその文書形式として 引数 doctype に以下のクラスオブジェクトのどれかを与える必要があります。 これらのクラスは、文書データ (生のバイト列) の解釈と snippet の表示方法が 異なっています。新しい Document サブクラスを定義するさいは 新しい種類の文書 (Document) をサポートする を参照してください。

PlainTextDocument
プレインテキスト形式の文書。

HTMLDocument
HTML文書。

EMailDocument
電子メール文書 (RFC 2822形式のテキスト)。

SourceCodeDocument
プログラムのソースコード。(PlainTextDocument とほとんど同じ)

Indexer クラス

Indexer オブジェクトは指定された Corpus 中の文書をインデックスし、 インデックスファイルを作成します。ユーザは作成した Indexer オブジェクトの index_doc()メソッドを呼び出して文書を追加していきます。 文書数 (あるいはその合計単語数) がある既定値を超えると、 Indexer は 随時インデックスファイルを (Corpus オブジェクトで指定された) インデックス用ディレクトリに 書き出していきます。Indexer クラスの使い方については、 fooling/indexer.pymain関数も参照してください。

作成可能なクラス
Indexer(corpus, max_docs_threshold=2000, max_terms_threshold=50000)
与えられた Corpusオブジェクト corpus を対象とした Indexer オブジェクトを作成します。オプション引数 max_docs_threshold は 1インデックスファイル中に含まれる文書の最大数を指定し、 max_terms_threshold は 1インデックスファイル中に含まれる単語の (おおまかな) 最大数を指定します。 毎回ひとつの文書をインデックスに追加したあと、Indexer はこれらの値が ここで指定されたスレッショルドを超えているかどうかを調べ、 超えていればそれまでの結果をひとつのインデックスファイル中に書き出し次に進みます。

デフォルトの値では、ひとつのインデックスファイルはだいたい 3〜10MBytes になり、 生成時にはその約2倍のメモリを必要とします。個々のインデックスファイルを大きくする (max_docs_threshold あるいは max_terms_threshold の値を上げる) と 場所の効率はよくなりますが、検索時間が必ずしも早くなるとはかぎりません (検索フレーズによってはかえって遅くなることもあります)。

公開されている属性・メソッド

Merger クラス

Merger オブジェクトは指定された Corpus 中に属する インデックスファイルを統合・再構成します。ほとんどの操作は自動で行われるため、 ユーザはただ Merger オブジェクトを作成して run() メソッドを 呼び出すだけです。Merger クラスの使い方については fooling/merger.pymain関数も参照してください。

作成可能なクラス
Merger(corpus, max_docs_threshold=2000, max_terms_threshold=50000)
与えられた Corpusオブジェクト corpus を対象とした Merger オブジェクトを作成します。引数 max_docs_thresholdmax_terms_threshold の値は Indexer オブジェクトと同じです。
公開されている属性・メソッド


3. 拡張方法

Fooling では以下のクラスを拡張して 新しい文書集合や文書の形式、検索方法などをサポートすることができます。

a. 新しい種類の文書集合 (Corpus) をサポートする

Corpus クラスは文書の実体データにアクセスする手段を提供します。 また、このクラスは個々の文書に関連したメタ情報 (タイトル、更新日時など) や、 文書の形式を決定するのにも使われます。文書のデータを保存するコンテナとして 別のものが利用したい場合や、ひとつの文書集合中で異なる文書形式・文字コードを 混在させたい場合は、Corpus のサブクラスを定義してください。

必ず実装しなければならないメソッド
オーバーライド可能なメソッド

b. 新しい種類の文書 (Document) をサポートする

Document オブジェクトは Corpus オブジェクトから 得られたバイト列データを解析し、検索単語の抽出や snippet の生成を行います。 あるバイト列データを特定の文書形式として解析したい場合は、このサブクラスを作成します。

必ず実装しなければならないメソッド
オーバーライド可能なメソッド

c. 新しい種類の述語 (Predicate) をサポートする

Predicate クラスを拡張するためには、 Fooling の検索ルーチンでこれがどのように使われているかを理解している必要があります:

  1. Selection オブジェクトに渡されたひとつ (あるいは複数) の Predicate オブジェクトは、まず優先度高→優先度低の順にソートされます。 各 Predicate オブジェクトは優先度 (priority) をもっています。 これは AND 検索で複数の述語を使う場合、なるべく文書を絞りこめるような述語を先に 適用したほうが処理速度が上がるためです。

  2. 次に、否定をあらわす述語 (- あるいは ! が先頭についているもの) は後で処理されるよう並べ替えられます。 Fooling 検索では「まず肯定的な条件に合致する文書を集め、 そこから否定的な条件に合致する文章を引く」という戦略をとっています。 その述語が否定かどうかは neg 属性によって決定されます。

  3. 各述語の中では、検索するキーワードは 2.2-グラム の列に分解され、 さらに検索文字列の「先頭」「中央」「末尾」部分それぞれのトークンを含んだ 3つのリストに分けて返されます。これは、2.2-グラムが境界の文字種を区別するため、 たとえば「日本列島」などのキーワードを境界を無視して検索するさいには、 以下に示す 3つの条件を見たす文を検索しなければならないからです: インデックスファイルからこれらのトークンを検索するさい、 先頭と末尾のトークン列は OR 結合され、中央のトークン列は AND 結合として扱われます。

  4. インデックスファイルの中から、検索キーワードをあらわす 2.2-グラムのトークンを含んだ (文書ID, 文ID) の リストが取得されます。各文書内の文 ID は Predicate オブジェクトpos_filter 関数に渡され、条件に合致しない文 ID はふるい落とされます。 たとえば電子メール (EMailDocument オブジェクト) 中で ヘッダに含まれる文字列だけを検索したい場合、ヘッダに現れる文字列は必ず 100未満の文 ID をもち、 ボディ中に現れる文字列は必ず 100以上の文 ID をもつようになっているので、 pos_filterlambda pos: pos < 100 に設定してやれば ヘッダ内の文字列のみにマッチするような述語を作成することができます。 この判定は実際の文書ファイルにシークする前におこなわれるため、 これによって 5. の段階で文書ファイルを取得する回数を減らすことができます。

  5. 該当した文書ファイルが実際にアクセスされ、文 ID で表される文のリストが取得されます。 ここで Predicate オブジェクトreg_pat 属性に格納された 正規表現オブジェクトが使われ、最終的にこの検索文字列に正確にマッチするかどうかがテストされます。
Predicate オブジェクトの基本的なふるまいは、 ほとんど Predicate.__init__ メソッドが行います。 Predicate クラスを拡張する場合は setup メソッドをオーバーライドしてください。


4. Webアプリケーションから使う際の注意点

Webアプリケーションなどから不特定多数のユーザに Fooling の検索サービスを提供する場合、 以下のことに注意する必要があります:

SelectionWithContinuation クラスを使う

Webアプリケーション上で「次の 10件」のような機能を提供する場合は SelectionWithContinuation クラスの save_continuation() メソッドを使って 次の検索開始位置を 12文字の base64 文字列として符号化します。 この文字列を URL またはフォームに入れておき、次の HTTPリクエスト時に 受けとって load_continuation() メソッドを使うと、 それまでの続きから検索できます。なお、ここで保存・復元される情報は 検索の開始位置と「おおよその該当件数」を計算するために必要な 統計情報のみです。そのため、悪意あるユーザがこの文字列を改ざんしても オブジェクトが不正な状態になることはありません。

検索タイムアウトを設定する

Fooling は検索のたびに数百ミリ秒〜数秒の CPU 時間を消費するので、 状況によってはこれはマシンの負荷を増大させます。 そのため、あまり時間のかかる検索は避けたほうが無難です。 Selection オブジェクト (または SelectionWithContinuationオブジェクト) でキーワード引数 timeout (秒数) を指定すると、一定時間後に検索は自動的にタイムアウトします。

StrictPredicate クラスを使わない

上の項目と関連したことですが、StrictPredicate オブジェクトを使った検索は、 場合によっては非常に時間がかかる場合があります。たとえば JavaDoc 日本語版で「オブジェクト-」という文字列を検索すると、 Fooling は「オブジェクト」を検索したあと、各文書の該当部分が 「オブジェクト-」というパターンにマッチするかどうかを調べるため、 実際には数千件の文書をスキャンすることになります。そのため Webアプリケーションでは とくに必要がない限り StrictPredicate は使わないほうがよいでしょう。


付録. Fooling 実装上の特徴

a. 2.2-グラム

Fooling で使われている文字列検索の基本的なアイデアは、 ある文字列から、となりあった 2文字の組 (2-グラム) を単語列として 分解しインデックスするというものです。たとえば「東京都」という文字列は 「東京」と「京都」に分解されることになりますが、これに加えて Fooling では 各文字種 (漢字、ひらがな、カタカナ) の境界を考慮することにより 各単語をもう少し細かく区別するようにしました。たとえば:

この例に示された「京都」は、どれも通常の 2-グラムでは 区別されませんが、2.2-グラムでは各「京都」の両どなりにある 文字が異なる種類のものは別の語として識別します。 左右にある文字が同じ種類 (漢字) である場合は "-" で、 異なる種類の場合は "|" で表すとすると、上の「京都」は それぞれ以下のようなトークンとして表されます:

「2.2-グラム」という名前は、これらの各トークンが通常の 2-グラムよりも 2ビット分余計な情報を含んでいることからつけました。

b. 複数のインデックスファイルによる検索

Fooling で使われているインデックスは、文中の各トークン (2.2-グラムで分割されたもの) に対して、それが現れる文書 ID と文 ID を羅列したものです。 Fooling における「文」とは、フレーズを検索する際の最小単位です。 異なる「文」をまたいだ文字列は一個のフレーズとはみなされません。 複数のトークンが同じ文中に現れていれば、 その文は検索フレーズを含んでいる可能性が高くなります。 しかしこの操作はインデックス中に含まれる文書 ID と文 ID の数が 多くなると遅くなるため、Fooling ではインデックスをいくつものファイルに分割し、 優先順位の高い方から検索していきます (これは、ランキングがあらかじめ 決まっていると仮定しているために可能になっています)。また文 ID として 文書ファイル中でその文が現れるバイトオフセットを使っているため、 snippets を表示するときもすばやく文中の該当する位置にシークできます。

c. インデックスファイルの形式

Fooling ではインデックスファイルの形式として cdb を使っています。 cdb はある「キー」に対応する値をすばやく (ディスクの平均シーク回数が約3回で) 見つけることができます。 cdb は Berkeley DB などと比べてファイルサイズが小さく高速ですが、 いちど作成したら追加・修正ができないという欠点があります。そのため Fooling では 新しく追加された文書に対する cdb ファイルを毎回新たに作成します。 この方法では cdb ファイルは時間がたつにつれて増えていくので、 定期的にこれらをマージすることによってコンパクトなファイルを保持します。

以下の図はインデックスファイルの構造を示しています。 マージ操作の効率を上げるため、 すべてのキーは最後のひとつを除き辞書順にソートされています。 すべての文書には インデックスファイル中でローカルな文書 ID がつけられています。 最初に各文書 ID とその文書の location に対応するマッピングがあり、 そのあとに 2.2-グラムで分割されたトークンが続きます。 各トークンの前にある文字 c は、トークンの境界情報を表します。 各トークンに対応する値は降順にソートされた (文書ID, 文ID) のリストであり、 最初にリストの要素数 n が記録されています。 Fooling では n が 4以上の場合、以後の文字列は zlib.compress で圧縮されます。 つづいて文書の location から文書 ID へのマッピングがあります。 最後に、空文字列をキーとしてインデックスファイル全体の情報が記録されています。

キー
'\x00'+docid1location1
'\x00'+docid2location2
...
c + トークン1(n, [(docid, sentid), ...])
c + トークン2(n, [(docid, sentid), ...])
c + トークン3(n, [(docid, sentid), ...])
...
'\xff'+location1docid1
'\xff'+location2docid2
...
''(総文書数, 総トークン数)

d. トークンの境界情報

インデックスファイル中の各トークンは、境界情報を表す 1バイトの文字のあとに UTF-8で表されたトークン本体の文字列が連結された形で表されています。 この境界情報を表す 1バイトの意味は以下のとおりです:

読みがなを使ったインデックスの場合には、以下の識別文字が使われます。

なお、境界情報が '\x10'〜 以上の文字である場合、 このトークンは文書に関するメタ情報を表す特殊なトークンとして扱われます。


Yusuke Shinyama