19 KiB
Recherche Textuelle
En informatique, il est peut-être utile et interessant de determiner la présence ou non d'un motif au sein d'un texte. Par exemple quand vous cherchez un mot précis dans un long document, si vous voulez vous entrainer pour des chiffres et des lettres...
On se donne un texte et un motif représentés en Python par des chaînes de caractères (type str). La question est de déterminer la présence ou l'absence de ce motif.
Le texte et le motif peuvent être représentés par des espaces, lettres, chiffres, différents types de symboles et autres signes de ponctuation.
En langage Python, chaque caractère d'une chaîne a un indice permettant d'accéder directement à ce caractère. Les indices vont de 0 à n -1 si n est le nombre de caractères de la chaîne, ou la longueur de la chaîne.
La fonction len renvoie la longueur d'une chaîne. Par exemple, si la chaîne ch est définie par ch = "un exemple" alors len(ch) a pour longueur 10.
Il faut faire attention à ne pas oublier de caractère d'espace. Nous utilisons les indices pour obtenir un caractère particulier, par exemple ch[0] a pour valeur le caractère "u" et ch[9] a pour valeur le caractère "e".
1. Recherche d'un caractère dans un texte
Le cas le plus facile à aborder est celui où le motif est un simple caractère. Il s'agit d'une recherche linéaire déjà étudiée en classe de première. Pour le parcours de la chaîne, on peut utiliser une boucle while ou une boucle for.
def recherche(texte, motif):
for car in texte:
if car == motif:
return True
return False
Si une des valeur successives de la variable car est égale à la valeur de la variable motif alors la fonction renvoie True et la fonction est interrompue. Si tous les caractères du texte sont examinés et que la bouclle n'est pas interrompue, la fonction renvoie False.
Nous remplaçons l'instruction return True par return i si nous souhaitons obtenir la place du caractère dans le texte. La fonction renvoie alors la place du caractère s'il a été trouvé et rien sinon (donc la valeur None).
def recherche (texte, motif):
for i in range (len(texte)):
if texte[i] == motif:
return i
Coût
Le coût de cette recherche est linéaire en la longueur de la chaîne, en effet, si n est la longueur de la chapine, on procède à n comparaisons dans le pire des cas, celui où le caractère cherché n'est pas trouvé avant la fin du texte.
2. Recherche naïve d'un motif dans un texte
On parle de recherche naïve car c'est l'une des premières idées qui peut venir à l'esprit
- On recherche la présence du premier caractère du motif dans le texte
- Si on le trouve, on vérifie si les caractères suivants du motif coïncident avec ceux du texte
- Si tous les caractères coïncident, alors le motif est trouvé, sinon, on reprendre la recherche du premier caractère.
Cet algorithme est implémenté dans le programme suivant, où nous pouvons remplacer l'instruction return j par return True si seulement la présence ou non du motif dans le texte nous intéresse.
def recherche(texte, motif):
n = len(texte)
m = len(motif)
for j in range(n - m +1):
i = 0
while i < m and texte [j + i] == motif [i]:
i = i + 1
if i == m :
return j
| indice j | 0 | 1 | ... | ... | j | ... | ... | |||
|---|---|---|---|---|---|---|---|---|---|---|
| texte | u | n | e | x | e | m | ||||
| ↕ | ↕ | ↕ | ||||||||
| motif | e | x | e | r | ||||||
| indice i | 0 | 1 | 2 | 3 |
Coût
Si n est la longueur du texte et m la longueur du motif recherché, alors la boucle for est parcourue (n-m+1) fois.
En effet la recherche du premier caractère du motif s'arrête lorsqu'il ne reste plus assez de place dans le texte pour placer ce motif. Dans le pire des cas, la boucle interne while est parcourue au plus m fois, pour tester chaque caractère du motif.
Le nombre total de comparaisons entre caractères est donc majoré par m(n - m + 1). Par exemple, si m = 5 alors on peut dire que le nombre total de comparaisons est majoré par 5n. Le nombre m est compris entre 0 et n et on peut donc montrer que la valeur maximale du produit m(n - m + 1) est m2 + m = (n2 + 2n) / 4 .
def recherche2(texte, motif):
m = len(motif)
n = len(texte)
i = 0
j = 0
while i < m and j < n :
if motif [i] != texte[j]:
j = j - i + 1
i = 0
else:
i += 1
j += 1
if i >= m:
return j - m
Exemple
Le texte est "abcababcabc" et le motif recherché est "abcabc". Les caractères sont comparés un par un depuis lde début, de la gauche vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ↕ | ↕ | ||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Après six comparaisons de caractères, nous avons un échec (lettres en gras). Le motif est alors décalé d'une unité vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | |||||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Ici, c'est un échec dès la première compraison, il faut donc à nouveau nous décaler vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | |||||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Idem ici, on redécale.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | |||||||||
| Motif | a | b | c | ||||||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Ici, echec à la troisième comparaison. On décale vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | |||||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Encore une fois, un échec à la première comparaison. Le motif est décalé d'une unité vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ↕ | ↕ | ||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Il nous aura donc fallu dix-huit comparaisons pour trouver le motif. Si le dernier caractère du texte n'était pas un "c", nous aurions appris l'absence du motif après le même nombre de comparaisons.
3. Pré-traitement du motif
On peut remarquer qu'il n'est pas judicieux, lorsqu'une comparaison n'est pas trouvée, de décaler le motif d'une seule unité.
Par exemple, après les six premières comparaisons, on constate que les cinq premiers caractères du texte sont les cinq premiers du motif. Donc, en décalant, on va forcément être amené à comparer le début du motif avec une autre partie du motif lui même...
Les informaticiens Morris et Pratt ont eu séparément l'idée d'effectuer un pré-traitement du motif qui permet de déterminer, à partir de quelle place la recherche doit se poursuivre, et éviter ainsi de controler les mêmes caractères plusieurs fois.
Reprenons le texte "abcababcabc" et le motif "abcabc"
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ↕ | ↕ | ||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Après six comparaisons de caractères, nous avons un échec. Sur les cinq caractères correspondants, les deux premiers sont identiques aux deux derniers. Donc le seul décalage qui peut être intéressant, c'est celui qui aligne ces deux premiers caractères avec les deux derniers. Cette étude, ce pré-traitement du motif, peut être réalisée avant de commencer la recherche.
Le motif est donc décalé de trois unités vers la droite.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | |||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Les comparaisons commencent donc au troisième caractère pour lequel nous avons un échec ici.
Il n'est pas interessant de décaler uniquement d'une unité vers la droite. Le caractère est un "a" et nous savons parfaitement où se trouve ce caractère dans le motif.
| Indice j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ↕ | ↕ | ||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Le nombre total de comparaisons est de treize au lieu de dix-huit pour le programme naïf.
def morris_pratt(texte, motif):
m = len (motif)
n = len(texte)
i = 0
j = 0
sol = [] #tableau pour stocker les solutions
s = traitement (motif) #modification du programme naïf recherche
while i < m and j < n :
if i >= 0 and motif[i] != texte[j] :
i = s[i]
else:
i += 1
j += 1
if i >= m : #Si le motif est présent
sol.append(j-m) # La position dans le texte est ajoutée à la liste
i = 0 # Une nouvelle recherche commence après le motif trouvé
return sol
Ici, par rapport à recherche2, on modifie les instructions dans le cas où motif[i] est différent de texte[j]
Dans le programme naïf, les indices i et j sont modifiés par j = j - i + 1 pour décaler le motif d'une unité et i = 0 pour recommencer les comparaisons à partir du premier caractère du motif.
Dans ce nouveau programme, j n'est pas modifié. On reste donc sur le caractère posant problème, on ne revient pas en arrière.
Pour la valeur de i, qui permet d'effectuer le bon décalage, on effectue une fois le traitement s = traitement (motif), s est alors un tableau, et s[i] est la nouvelle valeur de i.
4. Algorithme de Boyer Moore
Boyer, mathématicien et informaticien, et Moore, informaticien, sont tous les deux de l'université d'Austin au Texas. L'algorithme qui porte leurs noms a été inventé en 1977.
Pour observer le comportement de l'algorithme, on peut aller ici
Le principe général des algorithmes de recherche textuelle est de comparer un motif à certaines parties du texte, le motif se décalant vers la droite après chaque échec.
Comme cela est pratiqué avec l'algorithme de Morris et Pratt, le calcul du décalage d'obtient par un pré-traitement du motif. La différence fondamentale avec ce que l'on a vu précédémment, est que la comparaison entre le motif et une partie du texte se fait de droite à gauche en commençant par la fin du motif.
En procédant ainsi, un type de décalage est articulièrement intéresssant : à la premiere différence constatée, on décale le motif vers la droite de manière à faire coïncider le caractère du texte concerné avec un caractère du motif. Si ce n'est pas possible, alors on décale le motif après le caractère.
Exemple
Reprenons le motif "abcabc" et le texte "abcababcabc".
Notons s l'indice d'un caractère du texte (s pour shift, soit décalage en anglais)
| Indice s | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | |||||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
La comparaison commence donc par la fin du motif, de droite à gauche. Nous avons un échec dès la première tentative : on décale donc le motif de manière à faire coïncider le caractère "a" du texte avec sa dernière occurence dans la partie du motif non utilisée dans cette comparaison, soit "abcab".
On décale donc le motif de deux unités vers la droite.
| Indice s | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Reprenons les comparaisons à partir de la fin du motif
| Indice s | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ||||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Ici, on rencontre donc un échec après quatre comparaisons. Nous décalons le motif de nouveau, afin de faire coïncider le caractère "b" du texte avec sa dernière occurence dans la partie du motif non utilisée, soit "ab"
| Indice s | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
On recommence donc à comparer depuis l'élément le plus à droite. C'est un échec après une comparaison. le caractère du texte est un "a" et il faut donc décaler de nouveau le motif pour faire correspondre la dernière occurence dans la partie non utilisée ici du motif.
| Indice s | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Texte | a | b | c | a | b | a | b | c | a | b | c |
| ↕ | ↕ | ↕ | ↕ | ↕ | ↕ | ||||||
| Motif | a | b | c | a | b | c | |||||
| Indice i | 0 | 1 | 2 | 3 | 4 | 5 |
Il nous faudra ici six comparaisons finales pour affirmer la présence du motif au sein du texte. Le nombre total de comparaisons est alors de douze.
Remarque : si la dernière lettre du texte n'était pas un "c", alors nous n'aurions eu besoin que d'une seule comparaison au lieu de six pour affirmer l'absence du motif, soit sept comparaisons au total.
