4
votes

Recherche efficace de chaînes partielles dans les listes Python

Recherche d'une méthode efficace pour rechercher des chaînes partielles dans les listes Python (3.6+).

J'ai deux listes. listA est une liste de chaînes de chemin + un nom de fichier unique:

{j:i['create_date'] for j in listA for i in listB  if i['id'] in j}

(créé en utilisant glob (), les noms de fichiers sont tous donnés et existent déjà)

listB est une liste de dictionnaires. Chaque dictionnaire a le même ensemble de clés, mais des valeurs uniques.

listA = ['/pathname/abdce_654321.ext', '/pathname/a3b4c5_123456.ext', '/pathname/cbeebie_645321_abcde.ext', ...]

listB = [{"id": "123456", "create_date": "23/05/2014"}, ...]

new_dict = {"/pathname/a3b4c5_123456.ext": "23/05/2014, ...}

(également déjà donné)

Une paire clé: valeur dans chaque dictionnaire de la listeB aura une valeur qui est contenue dans un élément unique de listA.

Cependant, la position de la valeur telle qu'elle apparaît dans chaque élément de listA est indéterminée.

Ce que je voulais, c'était: pour chaque élément de listB, trouver l'élément dans listA qui contient une sous-chaîne correspondant à la paire k: v dans le dict, et créez un nouveau dict (ou une liste de tuples) comme "table de recherche" (le but était de corriger une date de création exif corrompue dans un ensemble de fichiers image).

Exemple:

XXX

J'ai exactement ce que je veux d'une composition de dict comme suit:

[{key1:value1, key2:value2}, {key1:value3, key2:value4}, ...]

Mais , même pour mes très petits fichiers (~ 5500 éléments), cela prend 12 secondes sur mon ordinateur portable (certes assez ancien).

C'est probablement parce que je dois parcourir l'ensemble de la listeB ~ 5500 fois en utilisant ma méthode. / p>

Existe-t-il un moyen plus efficace de faire cela en Python?

(nb je ne cherche pas de conseils sur la façon de corriger les données exif avec python; c'est un q généralisé sur les recherches de chaînes dans les listes)

CORRECTIONS ET CLARIFICATIONS

  1. J'ai négligé de placer des guillemets autour de la valeur «123456» dans mon exemple, ce qui implique bien sûr qu'il s'agit d'un entier; Dans les données du monde réel, ce n'est pas le cas et aucune des valeurs équivalentes dans les données réelles avec lesquelles j'ai traité.
  2. La sous-chaîne 'id' telle qu'elle apparaît dans une liste Un élément est presque toujours délimité par des traits de soulignement, mais n'apparaît pas toujours à la même position dans toute la chaîne; Ainsi, effectuer un fractionnement ('_') par exemple sur chaque élément ne placera pas toujours la chaîne 'id' à la position [-1] ou [-2] ou [-3], bien que [-1] fasse attention d'environ 80% des cas.
  3. Tous les identifiants sont uniques, ils n'apparaissent pas plus d'une fois dans l'une ou l'autre des listes; chaque nom de fichier est unique dans listA; chaque "id" n'apparaît jamais dans plus d'un dictionnaire.

Merci pour l'intérêt de tout le monde jusqu'à présent.


4 commentaires

Les identifiants contenus dans les dictionnaires peuvent-ils se répéter dans d'autres dictionnaires?


Votre exemple de valeur 'id' 123456 est un int , donc le test i ['id'] en j échouerait ici. Pour la partie id dans le nom de fichier, l’identifiant est-il toujours délimité , soit par des traits de soulignement, soit par la partie _ ?


Peut-il y avoir plus d'une entrée dans listB pour correspondre à un nom de fichier dans listA ? Sinon, vous pouvez faire apparaître les éléments trouvés dans (une copie de) listB chaque fois que vous trouvez une correspondance pour un nom de fichier donné.


@MartijnPieters - eh bien, oui, mais vous devrez pardonner mon oubli en ne stringifying 123456; dans le cas réel, la valeur «id» est une chaîne et le code fonctionne parfaitement.


3 Réponses :


2
votes

Je peux voir à quoi servent les deux commentaires. La grande question est: devons-nous utiliser dans parce que cela n'est nécessaire que si nous ne savons pas où l'id apparaît dans la chaîne de chemin? Si c'est toujours à un endroit particulier, nous pouvons l'extraire et utiliser une recherche à temps constant:

def extract_id(path):
    # todo
ids = {item['id']: item['create_date'] for item in listB}
new_dict = {path: ids[extract_id(path)] for path in listA}

qui est seulement O (N) par opposition à votre O (N ** 2) .


0 commentaires

0
votes

Tout d'abord, voici des listes généralisées pour vous aider à tester:

cpdef dict main():
    cdef int x
    cdef int number
    cdef char j
    cdef dict i

    listA = ['/pathname/abdce_%s.ext' % str(x) for x in range(10000)]

    listB = [{'id': str(number), "create_date": "23/05/2014"} for number in range(10000)]

    hello = {j: i['create_date'] for j in listA for i in listB if i['id'] in j}

    return hello

L'exécution de cela, avec 10 000 valeurs, a pris en moyenne 8,8 secondes à ma machine. (9,5 secondes si j'imprime le dictionnaire après)

Maintenant, si nous compilons ce code en Cython (un sur-ensemble python qui fonctionne sur C), ce temps est descendu à 4,4 secondes pour moi.

Voir le code ci-dessous

listA = ['/pathname/abdce_%s.ext' % str(x) for x in range(10000)]

listB = [{'id': str(number), "create_date": "23/05/2014"} for number in range(10000)]

hello = {j: i['create_date'] for j in listA for i in listB if i['id'] in j}


0 commentaires

0
votes

J'ai écrit un petit banc de test qui génère des données aléatoires qui sont un peu comme les vôtres, et essaie votre compréhension originale du dictionnaire et une version qui a des optimisations telles que la sortie précoce quand il trouve une correspondance et la suppression des balises utilisées.

match (votre original) et match2 (le mien) affichent le nombre de résultats, pour essayer de s'assurer qu'ils fonctionnent de manière équivalente.

Les résultats sont plutôt révélateur ... j'espère que cela vous aidera.

Chiffres pour 5000/10 000 éléments sur mon MBP:

  • original: 1.771 / 7.391
  • optimisé: 0,054 / 0,203
  • sans supprimer les balises utilisées (si ce n'est pas une règle métier acceptable): 0,917 / 3,789

import random
import timeit
import string

random.seed(42)


def genrand(n):
    return "".join(
        random.choice(string.ascii_lowercase + string.digits) for x in range(n)
    )


filenames = []
tags = []

for x in range(5000):
    id = genrand(8)
    filenames.append("/pathname/%s_%s.ext" % (genrand(6), id))
    if random.random() < 0.95:
        tags.append({"id": id, "date": "date for %s" % id})


def match():
    x = {j: i["date"] for j in filenames for i in tags if i["id"] in j}
    print(len(x))


def match2():
    x = {}
    available_tags = tags[:]
    for filename in filenames:
        for tag in available_tags:
            if tag["id"] in filename:
                x[filename] = tag
                available_tags.remove(tag)  # we've used this tag, remove it
                break
    print(len(x))


print(timeit.timeit(match, number=1))
print(timeit.timeit(match2, number=1))


2 commentaires

Votre réponse a le même bug @Martijn Pieters signalé dans le code de l'OP.


@martineau En fait, non, puisque l ' id généré est une chaîne.