web-dev-qa-db-fra.com

Recherche d'une chaîne dans un fichier texte volumineux - profilage de diverses méthodes dans python

Cette question a été posée à plusieurs reprises. Après avoir passé un peu de temps à lire les réponses, j'ai fait un profilage rapide pour essayer les différentes méthodes mentionnées précédemment ...

  • J'ai un fichier 600 Mo avec 6 millions lignes de chaînes (chemins de catégorie du projet DMOZ).
  • L'entrée sur chaque ligne est unique.
  • Je veux charger le fichier ne fois & continuer à chercher pour les correspondances dans les données

Les trois méthodes que j'ai essayées ci-dessous indiquent le temps nécessaire pour charger le fichier, le temps de recherche d'une correspondance négative et l'utilisation de la mémoire dans le gestionnaire de tâches


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

Temps de chargement ~ 10 s, temps de recherche ~ 0,0 s, utilisation de la mémoire ~ 1,2 Go


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

Temps de chargement ~ 6 s, temps de recherche ~ 0,36 s, utilisation de la mémoire ~ 1,2 Go


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

Temps de chargement ~ 0 s, temps de recherche ~ 5,4 s, utilisation de la mémoire ~ NA


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

Temps de chargement ~ 65 s, temps de recherche ~ 0,0 s, utilisation de la mémoire ~ 250 Mo


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

Temps de chargement ~ 0s, temps de recherche ~ 3.2s, utilisation de la mémoire ~ NA


6) sqlite (with primary index on url): 

Temps de chargement ~ 0s, temps de recherche ~ 0,0s, utilisation de la mémoire ~ NA


Pour mon cas d'utilisation, il semble que le choix de l'ensemble soit la meilleure option tant que j'ai suffisamment de mémoire disponible. J'espérais obtenir quelques commentaires sur ces questions:

  1. A meilleure alternative ex. sqlite?
  2. Façons de améliorer le temps de recherche en utilisant mmap. J'ai une configuration 64 bits. [modifier] par exemple filtres de floraison
  3. Au fur et à mesure que la taille du fichier atteint quelques Go, est-il possible de continuer à utiliser `` set '', par exemple le diviser en lots ..

[modifier 1] P.S. J'ai besoin de rechercher fréquemment, d'ajouter/supprimer des valeurs et je ne peux pas utiliser une table de hachage seule car j'ai besoin de récupérer les valeurs modifiées plus tard.

Tous les commentaires/suggestions sont les bienvenus!

[modifier 2] Mettre à jour avec les résultats des méthodes suggérées dans les réponses [modifier 3] Mettre à jour avec les résultats sqlite

Solution: Sur la base de tout le profilage et le retour sur investissement, je pense que j'irai avec sqlite. La deuxième alternative est la méthode 4. Un inconvénient de sqlite est que la taille de la base de données est plus du double du fichier csv d'origine avec des URL. Cela est dû à l'index principal sur l'URL

41
user

La variante 1 est idéale si vous devez lancer de nombreuses recherches séquentielles. Puisque set est en interne une table de hachage, il est plutôt bon pour la recherche. Cependant, la construction prend du temps et ne fonctionne bien que si vos données tiennent dans la RAM.

La variante 3 convient aux très gros fichiers, car vous disposez de beaucoup d'espace d'adressage pour les mapper et le système d'exploitation met suffisamment en cache les données. Vous effectuez une analyse complète; il peut devenir assez lent une fois que vos données s'arrêtent pour tenir dans la RAM.

SQLite est certainement une bonne idée si vous avez besoin de plusieurs recherches en ligne et que vous ne pouvez pas insérer les données dans la RAM. Chargez vos chaînes dans une table, construisez un index et SQLite crée un joli b-tree pour vous. L'arbre peut tenir dans RAM même si les données ne le font pas (c'est un peu comme ce que @alienhard a proposé), et même si ce n'est pas le cas, le nombre d'E/S nécessaires est considérablement inférieur Bien sûr, vous devez créer une base de données SQLite sur disque. Je doute que SQLite basé sur la mémoire batte la variante 1 de manière significative.

13
9000

Recherche de table de hachage personnalisée avec des chaînes externalisées

Pour obtenir un temps d'accès rapide et une consommation de mémoire inférieure, vous pouvez procéder comme suit:

  • pour chaque ligne, calculez un hachage de chaîne et ajoutez-le à une table de hachage, par exemple, index[hash] = position (ne pas stocker la chaîne). En cas de collision, stockez toutes les positions de fichier pour cette clé dans une liste.
  • pour rechercher une chaîne, calculer son hachage et la rechercher dans le tableau. Si la clé est trouvée, lisez la chaîne à position dans le fichier pour vérifier que vous avez vraiment une correspondance. S'il y a plusieurs positions, vérifiez chacune jusqu'à ce que vous trouviez une correspondance ou aucune.

Edit 1: line_number remplacé par la position (comme l'a souligné un commentateur, on a évidemment besoin de la position réelle et non des numéros de ligne)

Edit 2: fournir du code pour une implémentation avec une table de hachage personnalisée, ce qui montre que cette approche est plus efficace en mémoire que les autres approches mentionnées:

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)

Le hachage d'une ligne est uniquement utilisé pour indexer dans la table (si nous utilisions un dict normal, les hachages seraient également stockés sous forme de clés). La position du fichier de la ligne est stockée à l'index donné. Les collisions sont résolues avec le chaînage, c'est-à-dire que nous créons une liste chaînée. Cependant, la première entrée n'est jamais encapsulée dans un nœud (cette optimisation rend le code un peu plus compliqué mais économise beaucoup d'espace).

Pour un fichier de 6 millions de lignes, j'ai choisi une taille de table de hachage de 2 ^ 24. Avec mes données de test, j'ai eu 933132 collisions. (Une table de hachage de la moitié de la taille était comparable en consommation de mémoire, mais entraînait plus de collisions. Comme plus de collisions signifie plus d'accès aux fichiers pour les recherches, je préfère utiliser une grande table.)

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

Vous pouvez également essayer

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

avec search_str se terminant par la séquence de nouvelle ligne appropriée ('\n' ou '\r\n'). Cela devrait utiliser peu de mémoire, car le fichier est lu progressivement. Il devrait également être assez rapide, car seule une partie du fichier est lue.

4
Eric O Lebigot

Je suppose que beaucoup de chemins commencent de la même manière sur DMOZ. Vous devez utiliser une structure de données trie et stocker les caractères individuels sur les nœuds.

Les tentatives ont O(m) temps de recherche (où m est la longueur de la clé) économise également beaucoup d'espace, lors de l'enregistrement de grands dictionnaires ou de données de type arborescence.

Vous pouvez également stocker des parties de chemin sur des nœuds pour réduire le nombre de nœuds - cela s'appelle Patricia Trie. Mais cela rend la recherche plus lente par le temps de comparaison de longueur de chaîne moyenne. Voir SO question Trie (Prefix Tree) en Python pour plus d'informations sur les implémentations.

Il y a quelques implémentations de trie sur Python Index du package, mais elles ne sont pas très bonnes. J'en ai écrit une en Ruby et en Common LISP, qui est particulièrement bien adapté à cette tâche - si vous le demandez bien, je pourrais peut-être le publier en open source ... :-)

3
peterhil

Sans construire un fichier d'index, votre recherche sera lente, et ce n'est pas une tâche aussi simple. Il vaut donc mieux utiliser un logiciel déjà développé. La meilleure façon sera d'utiliser Sphinx Search Engine .

1
Andrey Nikishaev

qu'en est-il d'une solution d'indexation de texte?

J'utiliserais Lucene dans le monde Java mais il y a un moteur python appelé Whoosh

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

1
rlawson