Ok, alors j’ai posé beaucoup de petites questions sur ce projet, mais je n’ai toujours pas confiance dans les designs que je vais proposer, je vais donc poser une question à une plus grande échelle.
J'analyse des descriptions préalables pour un catalogue de cours. Les descriptions suivent presque toujours une certaine forme, ce qui me fait penser que je peux en analyser la plupart.
À partir du texte, j'aimerais générer un graphique des relations préalables au parcours. (Cette partie sera facile, après avoir analysé les données.)
Quelques exemples d'entrées et de sorties:
"CS 2110" => ("CS", 2110) # 0
"CS 2110 and INFO 3300" => [("CS", 2110), ("INFO", 3300)] # 1
"CS 2110, INFO 3300" => [("CS", 2110), ("INFO", 3300)] # 1
"CS 2110, 3300, 3140" => [("CS", 2110), ("CS", 3300), ("CS", 3140)] # 1
"CS 2110 or INFO 3300" => [[("CS", 2110)], [("INFO", 3300)]] # 2
"MATH 2210, 2230, 2310, or 2940" => [[("MATH", 2210), ("MATH", 2230), ("MATH", 2310)], [("MATH", 2940)]] # 3
Si la description complète est juste un cours, elle est sortie directement.
Si les cours sont associés ("et"), ils sont tous affichés dans la même liste
Si les cours sont disjoints ("ou"), ils sont dans des listes séparées
Ici, nous avons à la fois "et" et "ou".
Une mise en garde qui facilite les choses: il semble que l’imbrication des phrases "et"/"ou" n’est jamais plus grande que celle illustrée dans l’exemple 3.
Quelle est la meilleure façon de procéder? J'ai commencé avec PLY, mais je ne savais pas comment résoudre le problème de la réduction/réduction des conflits. L'avantage de PLY est qu'il est facile de manipuler ce que chaque règle d'analyse génère:
def p_course(p):
'course : DEPT_CODE COURSE_NUMBER'
p[0] = (p[1], int(p[2]))
Avec PyParse, il est moins clair comment modifier la sortie de parseString()
. J'envisageais de développer l'idée de @Alex Martelli de conserver l'état dans un objet et d'en tirer le résultat, mais je ne sais pas exactement comment faire.
def addCourse(self, str, location, tokens):
self.result.append((tokens[0][0], tokens[0][1]))
def makeCourseList(self, str, location, tokens):
dept = tokens[0][0]
new_tokens = [(dept, tokens[0][1])]
new_tokens.extend((dept, tok) for tok in tokens[1:])
self.result.append(new_tokens)
Par exemple, pour traiter "ou" les cas:
def __init__(self):
self.result = []
# ...
self.statement = (course_data + Optional(OR_CONJ + course_data)).setParseAction(self.disjunctionCourses)
def disjunctionCourses(self, str, location, tokens):
if len(tokens) == 1:
return tokens
print "disjunction tokens: %s" % tokens
Comment disjunctionCourses()
sait-il quelles phrases plus petites doivent être disjointes? Tout ce qui est obtenu est constitué de jetons, mais ce qui a été analysé jusqu'à présent est stocké dans result
. Comment la fonction peut-elle indiquer quelles données dans result
correspondent à quels éléments de token
? Je suppose que je pourrais chercher dans les jetons, puis trouver un élément de result
avec les mêmes données, mais cela semble compliqué ...
En outre, de nombreuses descriptions incluent du texte divers, comme:
"CS 2110 or permission of instructor"
"INFO 3140 or equivalent experience"
"PYSCH 2210 and sophomore standing"
Mais il n'est pas essentiel que j'analyse ce texte.
Quelle est la meilleure façon d'aborder ce problème?
def parse(astr):
astr=astr.replace(',','')
astr=astr.replace('and','')
tokens=astr.split()
dept=None
number=None
result=[]
option=[]
for tok in tokens:
if tok=='or':
result.append(option)
option=[]
continue
if tok.isalpha():
dept=tok
number=None
else:
number=int(tok)
if dept and number:
option.append((dept,number))
else:
if option:
result.append(option)
return result
if __name__=='__main__':
tests=[ ("CS 2110" , [[("CS", 2110)]]),
("CS 2110 and INFO 3300" , [[("CS", 2110), ("INFO", 3300)]]),
("CS 2110, INFO 3300" , [[("CS", 2110), ("INFO", 3300)]]),
("CS 2110, 3300, 3140", [[("CS", 2110), ("CS", 3300), ("CS", 3140)]]),
("CS 2110 or INFO 3300", [[("CS", 2110)], [("INFO", 3300)]]),
("MATH 2210, 2230, 2310, or 2940", [[("MATH", 2210), ("MATH", 2230), ("MATH", 2310)], [("MATH", 2940)]])]
for test,answer in tests:
result=parse(test)
if result==answer:
print('GOOD: {0} => {1}'.format(test,answer))
else:
print('ERROR: {0} => {1} != {2}'.format(test,result,answer))
break
les rendements
GOOD: CS 2110 => [[('CS', 2110)]]
GOOD: CS 2110 and INFO 3300 => [[('CS', 2110), ('INFO', 3300)]]
GOOD: CS 2110, INFO 3300 => [[('CS', 2110), ('INFO', 3300)]]
GOOD: CS 2110, 3300, 3140 => [[('CS', 2110), ('CS', 3300), ('CS', 3140)]]
GOOD: CS 2110 or INFO 3300 => [[('CS', 2110)], [('INFO', 3300)]]
GOOD: MATH 2210, 2230, 2310, or 2940 => [[('MATH', 2210), ('MATH', 2230), ('MATH', 2310)], [('MATH', 2940)]]
Pour les grammaires simples, j'aime beaucoup les grammaires d'analyse syntaxique (PEG), qui constituent une manière disciplinée et structurée d'écrire un analyseur syntaxique à descente récursive. Dans un langage à typage dynamique comme Python, vous pouvez effectuer des tâches utiles sans disposer d'un "générateur d'analyseur" distinct. Cela signifie pas de bêtises avec réduire/réduire les conflits ou autres arcanes de l'analyse syntaxique LR.
J'ai fait une petite recherche et pyPEG semble être une belle bibliothèque pour Python.
Si vous obtenez des conflits de réduction/réduction, vous devez spécifier la priorité de "ou" et "et". Je devine "et" le plus étroit, ce qui signifie "CS 101 et CS 102 ou CS 201" signifie [[CS 101, CS 102] [CS 201]].
Si vous pouvez trouver des exemples des deux, la grammaire est ambiguë et vous n’avez pas de chance. Cependant, vous pouvez peut-être laisser cette ambiguïté sous-spécifiée, tout dépend de ce que vous allez faire avec les résultats.
PS, on dirait que la langue est régulière, vous pourriez envisager un DFA.
Je ne prétends pas en savoir beaucoup sur l'analyse syntaxique d'une grammaire, et pour vous, la solution proposée par unutbu est tout ce dont vous avez besoin. Mais j'ai beaucoup appris sur l'analyse syntaxique d'Eric Lippert dans sa récente série d'articles de blog.
http://blogs.msdn.com/b/ericlippert/archive/2010/04/26/every-program-there-is-part-one.aspx
Il s’agit d’une série en 7 parties qui passe par la création et l’analyse d’une grammaire, puis optimisée pour rendre l’analyse plus facile et plus performante. Il produit du code C # pour générer toutes les combinaisons de grammaires particulières, mais il ne devrait pas être trop long de convertir cela en python pour analyser une grammaire assez simple.