web-dev-qa-db-ja.com

大きなテキストファイルで文字列を検索する-python

この質問は何度も尋ねられました。答えを読んだ後、少し時間をかけてプロファイリングを行い、前述のさまざまな方法を試しました...

  • 600 MB600万の文字列の行があるファイル(DMOZプロジェクトのカテゴリパス)があります。
  • 各行のエントリは一意です。
  • loadファイルoncekeep searchでデータの一致を確認したい

以下で試した3つの方法は、ファイルのロードにかかる時間、の検索時間、タスクマネージャーでの負の一致とメモリ使用量を検索します


1) set :
    (i)  data   = set(f.read().splitlines())
    (ii) result = search_str in data   

読み込み時間〜10秒、検索時間〜0.0秒、メモリ使用量〜1.2 GB


2) list :
    (i)  data   = f.read().splitlines()
    (ii) result = search_str in data

読み込み時間〜6秒、検索時間〜0.36秒、メモリ使用量〜1.2 GB


3) mmap :
    (i)  data   = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
    (ii) result = data.find(search_str)

読み込み時間〜0秒、検索時間〜5.4秒、メモリ使用量〜NA


4) Hash lookup (using code from @alienhard below):   

読み込み時間〜65秒、検索時間〜0.0秒、メモリ使用量〜250 MB


5) File search (using code from @EOL below):   
   with open('input.txt') as f:
       print search_str in f #search_str ends with the ('\n' or '\r\n') as in the file

読み込み時間〜0秒、検索時間〜3.2秒、メモリ使用量〜NA


6) sqlite (with primary index on url): 

読み込み時間〜0秒、検索時間〜0.0秒、メモリ使用量〜NA


私の使用例では、十分なメモリが利用可能である限り、セットを使用することが最良のオプションのようです。私はこれらの質問についていくつかのコメントを得ることを望んでいました:

  1. A より良い代替案例sqlite?
  2. 方法mmapを使用して検索時間を改善する。 64ビットのセットアップがあります。 [編集]例ブルームフィルター
  3. ファイルサイズが数GBに増えると、「設定」を使い続ける方法はありますか。バッチで分割してください。

[編集1]追伸頻繁に検索し、値を追加/削除する必要があります。変更した値を後で取得する必要があるため、ハッシュテーブルだけを使用することはできません。

コメント/提案は大歓迎です!

[編集2]回答で提案された方法の結果で更新[編集3] SQLiteの結果で更新

ソリューション:すべてのプロファイリングとフィードバックに基づいて、私はsqliteを使用すると思います。 2番目の代替案はメソッド4です。sqliteの欠点の1つは、データベースのサイズが、URLを含む元のcsvファイルの2倍以上であることです。これは、URLのプライマリインデックスが原因です

41
user

バリアント1は、多くの順次検索を起動する必要がある場合に最適です。 setは内部的にはハッシュテーブルであるため、検索には適しています。ただし、ビルドには時間がかかり、データがRAMに収まる場合にのみ機能します。

Variant 3は、それらをマップするための十分なアドレス空間があり、OSが十分なデータをキャッシュするため、非常に大きなファイルに適しています。フルスキャンを実行します。データがRAMに収まらなくなると、かなり遅くなる可能性があります。

複数の検索を続けて行う必要があり、データをRAMに収めることができない場合、SQLiteは間違いなく素晴らしいアイデアです。文字列をテーブルに読み込み、インデックスを作成します。SQLiteは、Nice bツリーを作成します。ツリーはRAMに適合しますが、データが適合しない場合(@alienhardが提案したものと少し似ています)、適合しない場合でも、必要なI/Oの量は劇的に少なくなります) 。もちろん、ディスクベースのSQLiteデータベースを作成する必要がありますが、メモリベースのSQLiteがVariant 1を大幅に上回るとは思えません。

13
9000

外部化された文字列を使用したカスタムハッシュテーブル検索

高速なアクセス時間およびのメモリ消費量を減らすには、次のようにします。

  • 各行について文字列ハッシュを計算し、それをハッシュテーブルに追加します(例:index[hash] = position(文字列を保存するしない)。衝突がある場合は、そのキーのすべてのファイル位置をリストに格納します。
  • 文字列を検索し、そのハッシュを計算して、テーブルで検索します。キーが見つかった場合は、ファイルからpositionの文字列を読み取り、本当に一致していることを確認します。複数のポジションがある場合は、一致が見つかるか、見つからなくなるまで、それぞれを確認してください。

編集1:line_numberを位置で置き換えました(コメント投稿者から指摘されたように、実際には行番号ではなく実際の位置が必要です)

編集2:カスタムハッシュテーブルを使用して実装のコードを提供します。これは、このアプローチが前述のその他のアプローチよりもメモリ効率が良いことを示しています。

from collections import namedtuple 
Node = namedtuple('Node', ['pos', 'next'])

def build_table(f, size):
    table = [ None ] * size
    while True:
        pos = f.tell()
        line = f.readline()
        if not line: break
        i = hash(line) % size
        if table[i] is None:
            table[i] = pos
        else:
            table[i] = Node(pos, table[i])
    return table

def search(string, table, f):
    i = hash(string) % len(table)
    entry = table[i]
    while entry is not None:
        pos = entry.pos if isinstance(entry, Node) else entry
        f.seek(pos)
        if f.readline() == string:
            return True
        entry = entry.next if isinstance(entry, Node) else None
    return False

SIZE = 2**24
with open('data.txt', 'r') as f:
    table = build_table(f, SIZE)
    print search('Some test string\n', table, f)

行のハッシュは、テーブルへのインデックス付けにのみ使用されます(通常の辞書を使用した場合、ハッシュもキーとして格納されます)。行のファイル位置は、指定されたインデックスに格納されます。衝突は連鎖によって解決されます。つまり、リンクリストが作成されます。ただし、最初のエントリがノードにラップされることはありません(この最適化により、コードは少し複雑になりますが、スペースがかなり節約されます)。

600万行のファイルの場合、ハッシュテーブルのサイズとして2 ^ 24を選択しました。テストデータを使用すると、933132回の衝突が発生しました。 (半分のサイズのハッシュテーブルはメモリ消費量では同等でしたが、衝突が多くなりました。衝突が増えると検索のためのファイルアクセスが増えるため、むしろ大きなテーブルを使用します。)

Hash table: 128MB (sys.getsizeof([None]*(2**24)))
Nodes:       64MB (sys.getsizeof(Node(None, None)) * 933132)
Pos ints:   138MB (6000000 * 24)
-----------------
TOTAL:      330MB (real memory usage of python process was ~350MB)
9
alienhard

あなたも試すことができます

with open('input.txt') as f:
    # search_str is matched against each line in turn; returns on the first match:
    print search_str in f

search_str適切な改行シーケンス('\n'または'\r\n')。ファイルは段階的に読み込まれるため、これはほとんどメモリを使用しません。また、ファイルの一部のみが読み取られるため、非常に高速になるはずです。

4
Eric O Lebigot

パスの多くはDMOZでも同じように始まると思います。 トライデータ構造 を使用して、個々の文字をノードに格納する必要があります。

O(m)ルックアップ時間(mはキーの長さ))を使用すると、大きな辞書やツリーのようなデータを保存するときに、多くのスペースを節約できます。

ノードにパスパーツを保存してノード数を減らすこともできます。これはPatricia Trieと呼ばれます。ただし、これにより、平均文字列長の比較時間だけ検索が遅くなります。実装の詳細については、SO質問 Pythonの検索(プレフィックスツリー) を参照してください。

Python Package Indexにはいくつかのトライ実装がありますが、それらはあまり良くありません。RubyとCommon LISPに1つ書いています。このタスクに特に適しています。もしよろしければ、オープンソースとして公開することもできます... :-)

3
peterhil

インデックスファイルを作成しないと、検索が遅くなり、これはそれほど簡単な作業ではありません。すでに開発されたソフトウェアを使用する方が良いでしょう。最良の方法は Sphinx Search Engine を使用することです。

1

テキスト索引付けソリューションはどうですか?

私はJava=世界でLuceneを使用しますが、python Whooshというエンジンがあります

https://bitbucket.org/mchaput/whoosh/wiki/Home

1
rlawson