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
Merci pour l'intérêt de tout le monde jusqu'à présent.
3 Réponses :
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) .
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}
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:
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))
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.
Les identifiants contenus dans les dictionnaires peuvent-ils se répéter dans d'autres dictionnaires?
Votre exemple de valeur
'id'123456est unint, donc le testi ['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
listBpour correspondre à un nom de fichier danslistA? Sinon, vous pouvezfaire apparaîtreles éléments trouvés dans (une copie de)listBchaque 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.