2
votes

Python: correspond à plusieurs sous-chaînes dans une chaîne

Je travaille avec Python et je souhaite faire correspondre une chaîne donnée avec plusieurs sous-chaînes. J'ai essayé de résoudre ce problème de deux manières différentes. Ma première solution a été de faire correspondre la sous-chaîne avec la chaîne comme:

str = "This is a test string from which I want to match multiple substrings"
value = ["test", "match", "multiple", "ring"]
temp = []
temp.extend([x.upper() for x in value if regex.search(r"\b" + regex.escape(x) + r"\b", str,
                                                   regex.IGNORECASE) is not None])
print(temp)

ce qui donne temp = ["TEST", "MATCH", "MULTIPLE", "RING"]

Cependant, ce n'est pas le résultat que j'aimerais. Les sous-chaînes doivent avoir une correspondance exacte, donc "ring" ne doit pas correspondre à "string".

C'est pourquoi j'ai essayé de résoudre ce problème avec des expressions régulières, comme ceci:

str = "This is a test string from which I want to match multiple substrings"
value = ["test", "match", "multiple", "ring"]
temp = []
temp.extend([x.upper() for x in value if x.lower() in str.lower()])
print(temp)

qui aboutit à ["TEST", "MATCH", "MULTIPLE"], la bonne solution. Quoi qu'il en soit, cette solution prend trop de temps à calculer. Je dois faire cette vérification pour environ 1 million de chaînes et la solution utilisant regex prendra des jours pour se terminer par rapport aux 1,5 heures qu'il faut avec la première solution.

Je voudrais savoir s'il existe un moyen de faire fonctionner la première solution ou de faire fonctionner la deuxième solution plus rapidement. Merci d'avance

EDIT: value peut également contenir des nombres, ou une courte phrase comme "test1 test2"


4 commentaires

Vous pouvez probablement gagner beaucoup de temps en compilant votre solution et en exécutant les versions compilées sur vos millions de chaînes


@jeremycg qu'entendez-vous exactement par "compiler votre solution"?


mais cela ne fonctionne pas lorsque la valeur contient des sous-chaînes comme "test1 test2" . Donc, si value a un mot contenu dans str , il devrait y avoir une correspondance?


en utilisant re.compile comme mentionné par @Kevin dans sa réponse


3 Réponses :


0
votes

Vous pouvez diviser la chaîne par un espace, puis faire correspondre les éléments de la valeur avec ==

EDIT:

Vous avez donc dit que certaines chaînes de valeurs peuvent avoir un espace avant ou après elles. Vous pouvez résoudre cela avec cette ligne:

str = ['Hi,', 'how', 'are', 'you?']`
values = ['how', 'you', 'time', 'space']

new_str = []
for word in str:
  for j in values:
    if word.startswith(j):
      new_str.append(word)

# result -> ['how', 'you?']

Cela supprimera tous les espaces avant et après la chaîne (dans votre cas pour chaque élément).

De plus, vous avez mentionné que si vous divisez la chaîne par un espace, certains mots ont des ponctuations laissées par le fractionnement -> «Salut, comment vas-tu?» entraînerait ['Salut', 'comment', 'êtes-vous', 'vous?'] . Vous pouvez résoudre ce problème en utilisant la méthode intégrée de chaîne startswith () pour filtrer tous les mots commençant par des éléments de valeurs comme ceci:

values = [i.strip() for i in values]

Ensuite, vous pouvez supprimer les ponctuations de la liste résultante avec des expressions régulières, mais vous aurez maintenant une liste beaucoup plus petite à parcourir. Après avoir supprimé tous les caractères de ponctuation, vous pouvez faire correspondre les chaînes entières comme je l'ai suggéré dans la réponse d'origine.

J'espère que c'est plus clair maintenant.


6 commentaires

Ne fonctionne pas si un élément de value contient un espace, ou si la phrase contient de la ponctuation à côté d'un mot qui autrement serait mis en correspondance.


Eh bien, vous pouvez faire value = [i.strip () for i in value] et filtrer les éléments de str en utilisant la méthode de chaîne startswith () puis supprimez les ponctuations de la liste filtrée et voyez le résultat


Désolé, je ne suis pas sûr de comprendre. Pouvez-vous modifier un code complet dans votre réponse et montrer comment cela fonctionne?


Merci pour votre modification, mais je pense qu'il me manque encore quelque chose. Lorsque j'exécute ce bloc de code, j'obtiens NameError: le nom 'i' n'est pas défini .


«Vous avez donc dit que certaines chaînes de valeurs peuvent avoir un espace avant ou après elles. Je ne pense pas que ce soit ce qu'il dit. Je pense qu'il dit que les valeurs peuvent avoir un espace en elles, pas nécessairement au début ou à la fin. "test1 test2", par exemple, contient un espace. Et ce serait une erreur de le supprimer, car "test1test2" ne doit pas être mis en correspondance, et il serait erroné de le diviser en plusieurs éléments, car "test1" ne doit pas être mis en correspondance à moins qu'il ne soit immédiatement avant un espace suivi de "test2" .


Merci pour la clarification du problème, je ne savais pas que c'était le cas. En reclassant votre commentaire sur l'erreur, je l'ai corrigé mais c'était clairement l'erreur de dénomination, vous auriez pu le découvrir vous-même.



3
votes

Il est difficile de suggérer une solution optimale sans voir les données réelles, mais vous pouvez essayer ces choses:

  • Générez un modèle unique correspondant à toutes les valeurs. De cette façon, vous n'aurez besoin de rechercher la chaîne qu'une seule fois (au lieu d'une fois par valeur).
  • Ignorez les valeurs d'échappement sauf si elles contiennent des caractères spéciaux (comme '^' ou '*' ).
  • Attribuez le résultat directement à temp , en évitant les copies inutiles avec temp.extend () .
import regex

# 'str' is a built-in name, so use 'string' instead
string = 'This is a Test string from which I want to match multiple substrings'
values = ['test', 'test2', 'Multiple', 'ring', 'match']
pattern = r'\b({})\b'.format('|'.join(map(regex.escape, values)))

# unique matches, lowercased
matches = set(map(str.lower, regex.findall(pattern, string, regex.IGNORECASE)))

# arrange the results as they appear in `values`
temp = [x.upper() for x in values if x.lower() in matches]
print(temp)  # ['TEST', 'MULTIPLE', 'MATCH']


3 commentaires

Merci pour votre réponse. Cela m'a aidé à résoudre mon problème. Je n'ai eu qu'à ajuster if x in matches à if x.lower () in matches et faire de même pour que string ne soit pas sensible à la casse. J'ai également utilisé temp.extend () parce que j'ai d'abord ajouté quelque chose à temp avant de faire la correspondance.


J'utilise ce code depuis un certain temps maintenant, mais j'ai malheureusement trouvé un problème. Si j'ai string = "test1 test2" et values ​​= ['test1', 'test1 test2'] , il doit correspondre aux deux instances. Pour le moment, je n'ai que le premier match. Avez-vous peut-être une idée de la façon dont je peux résoudre ce problème?


@ jv3768 Malheureusement, ce n'est pas possible avec une seule recherche, car le moteur regex renvoie la première correspondance valide en sautant d'autres alternatives. Vous pouvez essayer d'analyser les valeurs d'entrée et de filtre qui sont des sous-chaînes d'autres valeurs, puis de les inclure simplement dans les résultats.



2
votes

Deux optimisations possibles me viennent à l'esprit:

  • précompilez les modèles avec re.compile pour qu'il ne se recompile pas à chaque fois que vous appelez match .
  • plutôt que de comparer avec quatre expressions régulières indépendantes, créez une expression régulière qui correspond à toutes vos valeurs.

['TEST', 'MATCH', 'TEST1 TEST2', 'MULTIPLE']

Résultat:

import re

str = "This is a test string from which I want to match test1 test2 multiple substrings"
values = ["test", "match", "multiple", "ring", "test1 test2"]

pattern = re.compile("|".join(r"\b" + re.escape(x) + r"\b" for x in values))
temp = []

temp.extend([x.upper() for x in pattern.findall(str, re.IGNORECASE)])
print(temp)

Inconvénients potentiels de cette approche:

  • La sortie sera peut-être dans un ordre différent. Votre approche d'origine place les résultats dans l'ordre dans lequel ils apparaissent dans les valeurs . Cette approche place les résultats dans l'ordre dans lequel ils apparaissent dans str .
  • la même valeur apparaîtra plusieurs fois dans temp si elle apparaissait plusieurs fois dans str . Contrairement à votre approche d'origine, où la valeur apparaît au plus une fois dans temp .
  • La

  • recherche se termine dès qu'elle trouve une correspondance. findall recherche toujours la chaîne entière. Si vous vous attendez à ce que la plupart de vos chaînes correspondent à tous les mots de la valeur , et que la plupart des correspondances apparaissent tôt dans la chaîne, alors findall peut être plus lent que la recherche . D'un autre côté, si vous prévoyez que la recherche affichera souvent Aucun , alors findall sera probablement un peu plus rapide.

  • 5 commentaires

    «Compiler» le modèle ne fournira aucun avantage ici car vous ne l'utilisez qu'une seule fois. Même s'il était utilisé plusieurs fois, la différence serait très probablement négligeable.


    Mon interprétation du problème réel est qu'il y a des millions de chaînes qui doivent être recherchées, plutôt que simplement celle présente dans l'exemple de code. Imaginez qu'il y ait une ligne for str dans millions_of_strings: juste après mon temp = [] .


    Merci pour votre réponse, mais votre premier inconvénient potentiel est un réel inconvénient pour mon code. Je veux donner la priorité au premier match par rapport à tous les autres matchs. Je pense que la réponse d'Eugene résoudra mon problème


    @ jv3768 Autant que je sache, la réponse d'Eugene aura également cet inconvénient, car elle utilise également findall.


    @Kevin: On peut trier les résultats en fonction de leur idx en valeur (j'ai ajouté cette étape).