Tokeniser du texte en Python

Dans cet article vous allez apprendre comment tokeniser un chaîne de caractères en Python. Supposons que vous avez une chaîne que vous voulez la parser de gauche à droite dans un flux de jetons.

Imaginons que vous ayez une chaîne de texte comme celle-ci:

texte = 'foto = 23 + 42 * 10'

Pour tokeniser la chaîne de caractères, vous devez faire plus que simplement apparier des motifs. Vous devez avoir un moyen d’identifier le type de motif aussi bien. Par exemple, vous pourriez vouloir transformer la chaîne de caractères en une séquence de paires comme suit:

tokens = [('NAME', 'foo'), ('EQ','='), ('NUM', '23'), ('PLUS','+'),
          ('NUM', '42'), ('TIMES', '*'), ('NUM', '10')]

Pour faire ce type de fractionnement, la première étape consiste à définir tous les jetons possibles, y compris les espaces blancs, par des motifs d’expressions régulières en utilisant des groupes de capture nommés comme celui-ci:

import re
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM  = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ    = r'(?P<EQ>=)'
WS    = r'(?P<WS>\s+)'

master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))

Dans ces motifs, la convention ?P<TOKENNAME> est utilisée pour attribuer un nom au motif. Ceci sera utilisé plus tard.

Ensuite, pour tokeniser, utilisez la méthode peu connue scanner() pour les objets motif. Cette méthode crée un objet scanner dans lequel les appels répétés tomatch() passent d’un élément à l’autre du texte fourni. Voici un exemple interactif du fonctionnement d’un objet scanner:

>>> scanner = master_pat.scanner('foo = 42')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NAME', 'foo')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('EQ', '=')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NUM', '42')
>>> scanner.match()
>>>

Pour prendre cette technique et la mettre dans le code, elle peut être nettoyée et facilement emballée dans un générateur comme celui-ci:

from collections import namedtuple

Token = namedtuple('Token', ['type','value'])

def generate_tokens(pat, text):
    scanner = pat.scanner(text)
    for m in iter(scanner.match, None):
        yield Token(m.lastgroup, m.group())

# Exemple
for tok in generate_tokens(master_pat, 'foo = 42'):
    print(tok)

# Produit la sortie
# Token(type='NAME', value='foo')
# Token(type='WS', value=' ')
# Token(type='EQ', value='=')
# Token(type='WS', value=' ')
# Token(type='NUM', value='42')

Si vous voulez filtrer le flux de jetons d’une manière ou d’une autre, vous pouvez soit définir plus de fonctions de générateur, soit utiliser une expression de générateur. Par exemple, voici comment vous pouvez filtrer tous les jetons d’espaces blancs.

tokens = (tok for tok in generate_tokens(master_pat, text)
          if tok.type != 'WS')
for tok in tokens:
    print(tok)

La tokenisation est souvent la première étape pour les types plus avancés d’analyse et de traitement de texte. Pour utiliser la technique de numérisation (scanning) déja montrée, il y a quelques détails importants à garder à l’esprit.

Tout d’abord, vous devez vous assurer que vous identifiez chaque séquence de texte possible qui pourrait apparaître dans l’entrée avec un motif re correspondant.

Si un texte non correspondant est trouvé, la numérisation s’arrête tout simplement. C’est pourquoi il était nécessaire de spécifier le jeton d’espace blanc (WS) dans l’exemple.

L’ordre des jetons dans l’expression régulière du maître compte aussi. Lors de l’appariement, re essaie d’apparier les motifs dans l’ordre spécifié. Ainsi, si un motif se trouve être une sous-chaîne d’un motif plus long, vous devez vous assurer que le motif plus long passe en premier. Par exemple:

LT = r'(?P<LT><)'
LE = r'(?P<LE><=)'
EQ = r'(?P<EQ>=)'

master_pat = re.compile('|'.join([LE, LT, EQ]))    # Correcte
# master_pat = re.compile('|'.join([LT, LE, EQ]))  # Incorrecte

Le second motif est faux parce qu’il correspondrait au texte <= comme le jeton LT suivi du jeton EQ, et non pas au jeton unique LE, comme cela était probablement souhaité.

Enfin, et ce n’est pas le moins important, vous devez faire attention aux motifs qui forment des chaînes de caractères. Par exemple, supposons que vous ayez deux motifs comme ceux-ci:

PRINT = r'(P<PRINT>print)'
NAME  = r'(P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'

master_pat = re.compile('|'.join([PRINT, NAME]))

for tok in generate_tokens(master_pat, 'printer'):
    print(tok)

# Sorties:
#  Token(type='PRINT', value='print')
#  Token(type='NAME', value='er')

 

Pour des types plus avancés de tokenisation, vous pouvez consulter des modules tels que PyParsing ou PLY.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here