Pythonから使う、大きなテキストファイルのキーワードインデクサをgolangで作った
概要
1ファイルが700MBくらいあるファイル群のうち特定の場所を抜き出す、という事をPythonからやりたいがPythonでファイルを読むと遅かった。
そこで特定のキーワードの含まれている行の一覧を作るlindxrというコマンドと、 大きなファイルのうち特定の領域を抜き出して連番ファイルに書き出すmusectというコマンドをgolangで作ってPythonから呼ぶようにした。
これはそれなりに多くのシチュエーションで使えると思うので公開する事にした。
URL
lindxr
lindxrにキーワードとファイルのパターンを渡すと、 Globで探したファイル達に対して、キーワードが一致した行の行数と行の内容を出力していく。インデックスのファイル名はインデックス元のファイルパスと対応するファイル名が置かれる(元のパスのファイル名+.idxを特定フォルダ以下に置く、という感じ)。
grep -nとfindを組み合わせてだいたい同じ事が出来る。 出力の例などはgithubを参照のこと。
こうして出来た行番号とヒットした中身をいろいろPythonから操作して、 欲しいファイルの行数の範囲を絞る。
例えば”<doc-number>“のインデックスと”<us-patent-grant “のインデクスと”</us-patent-grant>“のインデックスを作って、Pythonからこれらを突き合わせたりして目的の範囲を取り出したりする。
musect
musectは行の範囲のリストをカンマ区切りで保存したファイルとターゲットとするファイルをとり、 この対象とするファイルの指定された範囲を連番で切り出して指定されたフォルダに保存する。
範囲はソートされていてオーバーラップしない、という前提。 これでgolangで大規模ファイルを読み込んでPythonからは切り出されたファイルだけを読めば良くなる。
当初は指定の範囲を切り出して標準出力に出す、というコマンド だったが、いちいち先頭から読むと700MBくらいのファイルを100回くらい読んだら結構遅かったので、そのファイルの必要な範囲を全部一度に受け取り、大きなファイルを読んでいくのは一回で済むように変更した。
基本的なコンセプト
大きなファイル全体は一切Pythonから読まなくても済むように作ってある。 一方でインデックス同士の突き合わせ等の処理はすべてPython側で書けるようになっている。
処理はPythonで、読み書きは外で、というのが基本コンセプト。
動機と既存の解決策の問題点
今回作ったのは幾つかのよくある自然言語処理のデータ分析のシチュエーションで、 既存の解決策があまりうまく行かなかったから。
どういう状況で使う事を想定していて、既存の解決策がどういまいちだったかを以下に書く。
作ろうと思ったシチュエーション
自然言語処理の勉強の為、USの特許データを分析してみよう、と思った。
これは各年に、700MB程度のxmlが連結された一つの大きなファイルが、全部で50個くらいあった。一週間分の出願がconcatされているっぽい。
必要なのはこのうちの特定の特許。ファイルはだいたいASCIIな事が分かっている。
ところがpythonで700MB程度のファイルをreadlineしていくと凄く時間がかかる。
やりたかった事はus-patent-grant要素の下のdoc-numberのinnerTextが特定の値のus-patent-grantのサブツリーを取得する、とかそういう感じだが、 ヘッダの部分のXML構造はかなりかっちり決まっていて、たとえば目的のdoc-numberはus-patent-grant要素の5行下な事が分かっている。
なお、descriptionなどの方には相当複雑なhtml片などが含まれているので、抜き出す部分xmlがvalidなxmlかは知らない。
自然言語処理はPythonでやりたいので、なるべく多くのロジックはPythonのJupyter上に残したい。
なお、これまでもこの手のシチュエーションは何度か遭遇した事がある。 たぶんもともとXMLがデータベースか何かに入っていたのを雑にexportして公開するとこんな感じになるのだろう。
なお、自然言語処理で使いたいので以下の要件は満たしていて欲しい。
- ロジックをなるべくPythonで書きたい
- 機械学習の分析屋がわかりやすい物であって欲しい
- dockerから使いやすいコンパクトな物であって欲しい
汎用indexerの問題点
まず解きたい問題としては、ngramなどの汎用のindexerが欲しい訳では無い。 だからもっとシンプルなgrep -n程度の物で良かった。
ただ汎用のindexerでも十分な速度が出ればそれでもいいかな、と思って幾つか調べたのだが、結構問題が多い。
汎用のindexerは、一ファイルがそんなに大きく無い事を前提にしている
多くのインデクサは、ドキュメントをキーとコンテンツにして食わせる。 で、クエリーを投げるとキーが帰ってくる、という作りになっている。
キーにパスを使えば目的のファイルまでは分かるので、そこからはファイル開いて探してね、という訳だ。 webの文書の検索はこれで良いのだが、 一ファイルがPythonで開くと我慢できないほど遅い、という場合はこれじゃまずい。
一行ずつ食わせてキーをファイル名と行数にすれば行数が取れるが、これだとキーサイズが膨れ上がってしまうパターンが多く、大規模データを食わせると何かしらのバグを突く。
汎用のindexerをPythonから使うと、APIとしてはPythonでファイルを読む事を前提としている
Pythonで700Mのファイルをreadlineするのが遅い、というのが問題なのに、 インデクシングするのに読まないといけない。これは辛い。
普通にネイティブでファイル読んでインデクシングしてPythonからクエリ出来れば良いのだけど、そういうインターフェースになっているインデクサは見つからなかった。
index上での計算があまりいろいろ出来ない
やりたかったのが、us-patent-grantの5行下のdoc-numberの値がXXXXXXの物、とかなので、 この位の事をPython上でインデックスだけでやりたかったが、 そういうインデックスをPythonからいろいろ使う前提のインデクサはあまり無かった。 専用のクエリ言語とかがついていて、これがやりたい事とはだいぶ違う。
JVMとかdockerに用意するのは辛い
もうnvidia-dockerとかいろいろ複雑になっているDockerfileに、あんまり面倒な物を足したくない。 これは我慢しても良いか、とは思っていたのだけれど、 出来たらスタンドアローンで動いてPythonから叩きやすくて、 でもファイルの読み書きの大部分はネイティブ側でやってくれるような物が良い。
Unixコマンドでの解法の問題点
grepやawkなどでは数秒で終わるので、Unixコマンドで特定の行を探す事は出来る。 また、sed -n ‘200000,200030p’などでまぁまぁな速度で切り出す事も出来る。
最初はこれらのコマンドをシェルマジックから実行していたのだけど、
- シェルマジックが凄く遅い
- 他のメンバのUnixコマンド習熟度に差がある
- 出来上がったコードが読みにくくて再利用しづらい
- 当初の想定よりもやりたい事が複雑になっていって結構ひどいスクリプトになりがち
といった問題が出てきた。
これまで似たような事があった時も自分はUnixコマンドを組み合わせて解決していたのだけど、 だいたい自分しか分からない感じになってしまっていた。
もうちょっと機械学習のチームで使っていけるような物が欲しい。
パフォーマンス
あとで書く。
メモ。
だいたい700MB 50ファイルのインデックス生成に、インデックス一つで1min〜7minくらい。 なんでこんなにばらつくのかは良く分かってない。
700MBの50ファイルから2000ファイルくらいを切り出して30sec〜5minくらい。 これまたなんでこんなにばらつくのかは良く分かってない。