2012-05-31 4 views
2

J'ai un projet qui a un format de fichier non standard quelque chose comme:Quelle est la façon pythonique d'implémenter une arborescence d'analyse pour un format personnalisé?

var foo = 5 
load 'filename.txt' 
var bar = 6 
list baz = [1, 2, 3, 4] 

Et je veux analyser cela en une structure de données un peu comme le fait BeautifulSoup. Mais ce format n'est pas supporté par BeatifulSoup. Quelle est la manière pythonique de construire un arbre d'analyse pour pouvoir modifier les valeurs et les réécrire? En fin de compte, je voudrais faire quelque chose comme:

data = parse_file('file.txt') 
data.foo = data.foo * 2 
data.write_file('file_new.txt') 

Merci,

Glen

+1

Avez-vous essayé le module 'shlex'? – JBernardo

+0

@JBernardo: 'shlex' est seulement le lexer, cependant. Cela ressemble à une langue nécessitant un analyseur –

+0

@EliBendersky semble avoir un tas de commandes sur chaque ligne. Les choses nécessitant un analyseur (dernier élément) ressemblent à du code Python valide, sinon cela n'aurait pas de sens. Ainsi, même regex + 'ast.literal_eval' peut résoudre le problème – JBernardo

Répondre

4

Voici une solution utilisant pyparsing ... cela fonctionne dans votre cas. Prenez garde que je ne suis pas un expert en fonction donc vos normes, le code pourrait être laid ... hourras

class ConfigFile (dict): 
    """ 
    Configuration file data 
    """ 

    def __init__ (self, filename): 
     """ 
     Parses config file. 
     """ 

     from pyparsing import Suppress, Word, alphas, alphanums, nums, \ 
      delimitedList, restOfLine, printables, ZeroOrMore, Group, \ 
      Combine 

     equal = Suppress ("=") 
     lbrack = Suppress ("[") 
     rbrack = Suppress ("]") 
     delim = Suppress ("'") 

     string = Word (printables, excludeChars = "'") 
     identifier = Word (alphas, alphanums + '_') 

     integer = Word (nums).setParseAction (lambda t: int (t[0])) 
     real = Combine(Word(nums) + '.' + Word(nums)).setParseAction (lambda t: float(t[0])) 
     value = real | integer 

     var_kwd = Suppress ("var")   
     load_kwd = Suppress ("load") 
     list_kwd = Suppress ("list")    

     var_stm = Group (var_kwd + identifier + equal + value + 
         restOfLine.suppress()).setParseAction (
          lambda tok: tok[0].insert(len(tok[0]), 0)) 

     load_stm = Group (load_kwd + delim + string + delim + 
          restOfLine.suppress()).setParseAction (
           lambda tok: tok[0].insert(len(tok[0]), 1)) 

     list_stm = Group (list_kwd + identifier + equal + lbrack + 
          Group (delimitedList (value, ",")) + 
          rbrack + restOfLine.suppress()).setParseAction (
           lambda tok: tok[0].insert(len(tok[0]), 2)) 


     cnf_file = ZeroOrMore (var_stm | load_stm | list_stm) 

     lines = cnf_file.parseFile (filename) 
     self._lines = [] 
     for line in lines: 
      self._lines.append ((line[-1], line[0])) 
      if line[-1] != 1: dict.__setitem__(self, line[0], line[1])    
     self.__initialized = True 
     # after initialisation, setting attributes is the same as setting an item 

    def __getattr__ (self, key): 
     try: 
      return dict.__getitem__ (self, key) 
     except KeyError: 
      return None 


    def __setattr__ (self, key, value): 
     """Maps attributes to values. Only if we are initialised""" 

     # this test allows attributes to be set in the __init__ method 
     if not self.__dict__.has_key ('_ConfigFile__initialized'): 
      return dict.__setattr__(self, key, value) 

     # any normal attributes are handled normally 
     elif self.__dict__.has_key (key): 
      dict.__setattr__(self, key, value) 

     # takes care of including new 'load' statements 
     elif key == 'load': 
      if not isinstance (value, str): 
       raise ValueError, "Invalid data type" 
      self._lines.append ((1, value)) 

     # this is called when setting new attributes after __init__ 
     else: 
      if not isinstance (value, int) and \ 
       not isinstance (value, float) and \ 
       not isinstance (value, list): 
       raise ValueError, "Invalid data type" 

      if dict.has_key (self, key): 
       if type(dict.__getitem__(self, key)) != type (value): 
        raise ValueError, "Cannot modify data type." 
      elif not isinstance (value, list): self._lines.append ((0, key)) 
      else: self._lines.append ((2, key))    
      dict.__setitem__(self, key, value) 


    def Write (self, filename): 
     """ 
     Write config file. 
     """ 
     fid = open (filename, 'w') 
     for d in self._lines: 
      if d[0] == 0: fid.write ("var %s = %s\n" % (d[1], str(dict.__getitem__(self, d[1])))) 
      elif d[0] == 1: fid.write ("file '%s'\n" % (d[1])) 
      else: fid.write ("list %s = %s\n" % (d[1], str(dict.__getitem__(self, d[1])))) 


if __name__ == "__main__": 

    input="""var foo = 5 
load 'filename.txt' 
var bar = 6 
list baz = [1, 2, 3, 4]""" 

    file ("test.txt", 'w').write (input) 
    config = ConfigFile ("test.txt") 
    # Modify existent items 
    config.foo = config.foo * 2 
    # Add new items 
    config.foo2 = [4,5,6,7] 
    config.foo3 = 12.3456 
    config.load = 'filenameX.txt' 
    config.load = 'filenameXX.txt' 
    config.Write ("test_new.txt") 

EDIT

j'ai modifié la classe à utiliser

__getitem__, __setitem__ 

méthodes pour imiter la syntaxe 'accès au membre' aux éléments analysés comme requis par notre poster. Prendre plaisir!

PS

Surcharge de la méthode

__setitem__ 

doit être fait avec soin pour éviter les interférences entre le réglage de la « normale » des attributs (les membres de la classe) et les éléments analysés (qui sont des accès comme attributs) . Le code est maintenant corrigé pour éviter ces problèmes. Voir la référence suivante http://code.activestate.com/recipes/389916/ pour plus d'informations. C'était marrant de le découvrir!

+0

Cela semble très bien. Je vais essayer. L'autre option que je vois est Lepl. Lepl semble avoir une meilleure documentation mais votre exemple est très bien. Merci d'avoir fourni cela. – dailyglen

+0

jamais entendu parler de LEPL, mais oui je suis d'accord que la documentation de pyparsing pourrait être grandement améliorée. OTOH, en utilisant pyparsing est si intuitif que vous avez seulement besoin de comprendre quelques exemples pour être productif. Quoi qu'il en soit, la syntaxe de ce LEPL est très similaire à pyparsing. – Giuliano

+0

@CnrAltCanc C'est un très beau travail. Pensé que d'autres pourraient sauter dans des alternatives, mais cela semble très agréable et facile à travailler avec ce que je cherchais. Merci encore. – dailyglen

1

Ce que vous avez est un langage personnalisé, vous devez analyser. Utilisez l'une des nombreuses bibliothèques d'analyse existantes pour Python. Personnellement, je recommande PLY. Alternativement, Pyparsing est également bon et largement utilisé & pris en charge.

Si votre langage est relativement simple, vous pouvez également implémenter un analyseur manuscrit. Here is an example

+0

+1 pour PLY. J'ai construit des parseurs pour quelques formats différents en PLY et c'est indolore et facile à comprendre. –

Questions connexes