Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Comment se déplacer dans un fichier texte ?

+3 votes

J'ai un fichier de configuration avec des entrées du type :

host gogol {
    mac 04:6F:94:2A:FF:C0;
    ip 10.60.12.1;
}

Je souhaite au final obtenir un dictionnaire de la forme :

d = { "gogol": ["4:6F:94:2A:FF:C0", "10.60.12.1"] }

Donc basiquement je commence quelque chose du genre :

for line in open("monfichier.conf"):
    if "host" in line:
        ...

Donc récupérer cette ligne est facile et en extraire les données aussi...

Mais comment dois-je m'y prendre, proprement, pour obtenir les 2 lignes suivantes (adresse mac et adresse ip) dans ma boucle de parcours ?

Existe-t-il une méthode de "saut" de ligne ?
Est-ce qu'une méthode à base de générateur et de next serait la solution ?
Avec une méthode crade à base de compteurs ?
Une expression régulière avec une balise "host" et une balise "}" ?

Avez-vous d'autres méthodes à me conseiller ?

demandé 11-Mar-2015 par DoubleNain (1,668 points)

4 Réponses

+3 votes
 
Meilleure réponse

En gros il faut parser ligne a ligne le fichier, détecter les début de config pour un host, extraire la config, détecter la fin de config.

Une approche :

result = {}
current_host = None
for line in open("monfichier.conf"):
    # on detecte le debut de la config
    if "host" in line:
        # on recupere le hostname
        current_host = line.strip().split(' ' )[1]
        result[current_host]=[]
    elif '}' in line:
         # fin de config pour l'host, on le reset
         current_host = None
    elif current_host is not None:
         # on split la ligne su les espaces pour recuperer la clef et la valeur
         key, value = line.strip().split(' ')
         # on l'ajoute a la config en supprimant le ';' en fin de ligne.
         result[current_host].append(value.split(';')[0])

PS: non testé.

Pour répondre a certaines de tes questions :
- Existe-t-il une méthode de "saut" de ligne ?
Si tu utilise l'instruction 'continue' dans ta boucle, tu passe a l'élément suivant
Est-ce qu'une méthode à base de générateur et de next serait la solution ?
- un peu bourrin a mon gout mais c'est plus une question d’esthétique
Une expression régulière avec une balise "host" et une balise "}" ? : mauvaise idée

répondu 11-Mar-2015 par jc (2,674 points)
sélectionné 12-Mar-2015 par DoubleNain

Merci, c'est sur cette solution que je me suis basé. Ce qui m'a débloqué est la notion de current host à chaque itération. C'était sur le fait que je devais garder "en mémoire" l'hôte pour l'itération suivante.

def introspection(self, fi):
    curhost = None

    for line in open(fi):
        if "host" in line:
            curhost = line.strip().split()[1]
            self.data[curhost] = [fi]

        elif "}" in line:
            curhost = None

        elif curhost is not None and "hardware ethernet" in line:
            mac = line.strip().split()[2]                
            self.data[curhost].append(mac.split(';')[0])

        elif curhost is not None and "fixed-address" in line:
            ip = line.strip().split()[1]
            self.data[curhost].append(ip.split(';')[0])
0 votes

C'est un truc dans le genre qu'il te faut:

fichier_a_ouvrir = "monfichier.conf"
f = open(fichier_a_ouvrir, 'r+')

# retourne toutes le lignes en tant que liste (array)
lines = f.readlines()
# iterate sur chaque ligne et print la ligne suivante
for line in lines:
    print line
# retourne une ligne donnée du fichier d'entrée
print lines[12]

f.close()

Si tu veux vraiment une méthode pour sauter à la ligne, tu peux utiliser iterator.next()

répondu 11-Mar-2015 par barnumbirr (2,750 points)
+1 vote

J'aurai tendance à utiliser le module re pour ce genre de parsing basique.
Un exemple pour ton cas fait à la va vite:

import re

res = {}
with open('file.conf', 'r') as f: # on ouvre le fichier
    data = f.read() # on le lit entierement
    hosts = re.findall("host\s+(\S+)\s*\{"
                       "((\s*(\S+)\s+([^;]+);\s*)+)"
                       "\}", data, re.VERBOSE)
    # hosts est ici une liste de chaque "host" dans la config
    for host in hosts: # host est un tuple
        # host[0] est le nom de l'host
        # host[1] est le contenu de l'host
        res[host[0]] = {}
        for prop in re.findall("\s*(\S+)\s+([^;]+);\s*", host[1]):
            res[host[0]][prop[0]] = prop[1]
            # on associe chaque nom de propriété avec son contenu

print res # {'gogol': {'ip': '10.60', 'mac': '04:6F:94:2A:FF:C0'}}

Tu peux sinon regarder différentes libs de parsing. Je pense à pyrser qui te permet de parser en fonction d'une BNF que tu écris.

répondu 11-Mar-2015 par Arza (726 points)
edité 12-Mar-2015 par Arza

Autre solution (adaptation d'un de mes codes) : voir la fonction lit_fichier().
J'utilise un indicateur déterminant si oui ou non on doit ajouter la ligne lue à la liste qui contient les informations nécessaires (mac, ip, etc.).

J'ai découper en plusieurs fonctions :
slurp_file() : qui lit le fichier en entier (on suppose qu'il ne prend pas beaucoup de place en mémoire)
lit_fichier() : (précisément la partie qui répond à la question) la fonction de parsing. Deux versions : une avec break si on a qu'une occurence ou qu'on recherche la première et une version 'continue' si on doit rechercher toutes les occurences (faut commenter/décommenter). La première ligne contenant "host gogol..." n'est PAS copiée dans la liste (décommenter la ligne ad hoc si nécessaire).
monsieur_propre() : qui nettoie le résultat ou fait les trucs qu'on veut faire sur la chaîne extraite.

Fichier texte à analyser :

mon code de CB est ... Aaaargh ...
host gogol {
    mac 04:6F:94:2A:FF:C0;
    ip 10.60;
    hardware AACCVVGGFFF7322;
    fixed-address 192.168.12.1
}
C'est un lapin, un lapin de bois

Second host trouvé seulement si on 'continue'
host gugul {
    mac 05:6F:94:2A:FF:C0;
    ip 10.61;
    hardware RRCCVVGGFFF7322;
    fixed-address 192.168.12.2
}
Ni ! Ni !

Et le code python3 :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import pathlib


def slurp_file():
    '''
    Lis le contenu du fichier 'host.txt' et retourne celui-ci dans une liste.
    '''
    liste_retour = []  # la liste à retourner
    liste_tmp = []  # liste temporaire

    # On instantie 'fic' à la classe Path
    fic = pathlib.Path('host.txt')

    # Test existence fichier
    if not fic.exists():
        raise Exception("Fichier inexistant.")

    with open(str(fic)) as f:
        # on lit tout en virant les \n au passage
        liste_tmp = [ligne.strip() for ligne in f.readlines()]

    # on nettoie : retrait des ";"
    # on pourrait mettre le nettoyage, etc. dans la fonction associée à cette
    # tâche cf monsieur_propre().
    liste_retour = [item.replace(";", "") for item in liste_tmp]

    # on retourne le contenu nettoyé sous forme de liste
    return liste_retour


def lit_fichier(my_content):
    '''
    Analyse le contenu du fichier et retourne uniquement ce qui est situé
    entre les lignes "host ...." et la ligne contenant '}'.

    Attention, la ligne contenant '}' n'est PAS ajoutée ; il convient, par
    conséquent, de l'avoir sur une ligne seule comme dans ton exemple.

    La variable my_content est censée être le retour de la fonction
    slurp_file().
    '''
    # ma liste de retour
    list_ret = []

    # drapeau "oui, je lis" ou "non, je ne lis pas/plus"
    # On ne 'lit' pas par défaut.
    recording = False

    # Pour chaque ligne de mon fichier ...
    for line in my_content:
        # On met la ligne lue en majuscule pour la comparaison
        line_up = line.upper()
        # Si ma ligne contient 'HOST' (ou 'Host' ou 'hOst' ou ...
        # d'où l'utilité de faire la comparaison avec soit upper()
        # soit lower().
        # si la ligne ne débute pas par ce que l'on recherche, on peut
        # utiliser à la place:
        # if 'HOST' in line_up:
        if line_up.startswith('HOST'):
            # On démarre l'enregistrement
            recording = True

            # si on veut ajouter la première ligne : décommenter
            # list_ret.append(line)

        # Sinon, si '}' dans la ligne ET qu'on est en train
        # d'enregistrer, on sort de l'itération
        elif ('}' in line) and recording:
            # recording = False  # si 'continue' au lieu de 'break'
            # continue <-- si on veut continuer le parsing
            break  # stop le parsing
        # Sinon, si on est en mode d'enregistrement seulement
        elif recording:
            # on ajoute notre ligne à la liste
            list_ret.append(line)
        # on reboucle sur la 'line' suivante.

    # on retourne la liste
    return list_ret


def monsieur_propre(ma_liste):
    '''
    Fonction de nettoyage et d'assemblage des informations.
    '''
    # faire ici des trucs avec mes_info_host comme par exemple :
    for item in mes_info_host:
        # item est une str. On coupe sur l'espace, par défaut.
        cle, valeur = item.split()
        # Et zou ! on affiche.
        print("Clé = <{}> :: Valeur = <{}>".format(cle, valeur))

# === MAIN ===
# on récupère le contenu filtré, lavé et séché
mon_contenu = slurp_file()  # nom du fichier encodé 'en dur' dans la fct
# On appelle la fonction d'analyse + fabrication des info qu'on désire
mes_info_host = lit_fichier(mon_contenu)
# et on fait ce qu'on veut des informations (on les nettoie, on les
# transforme, etc.
monsieur_propre(mes_info_host)

Et enfin, voici ce que ça sort, au final, de monsieur_propre() :
(attention, j'ai vu que tu recherchais des 'hardware configuration', ici faut élaborer un peu le script qui ne prend pas en compte le titre-contenant-des-espaces (c'est pas le but de la question non plus)).

Version 'break'

Clé = <mac> :: Valeur = <04:6F:94:2A:FF:C0>
Clé = <ip> :: Valeur = <10.60>
Clé = <hardware> :: Valeur = <AACCVVGGFFF7322>
Clé = <fixed-address> :: Valeur = <192.168.12.1>

Version 'continue'

Clé = <mac> :: Valeur = <04:6F:94:2A:FF:C0>
Clé = <ip> :: Valeur = <10.60>
Clé = <hardware> :: Valeur = <AACCVVGGFFF7322>
Clé = <fixed-address> :: Valeur = <192.168.12.1>
Clé = <mac> :: Valeur = <05:6F:94:2A:FF:C0>
Clé = <ip> :: Valeur = <10.61>
Clé = <hardware> :: Valeur = <RRCCVVGGFFF7322>
Clé = <fixed-address> :: Valeur = <192.168.12.2>

Remarque : dans ton code, tu compare avec :

.../... and "hardware ethernet" in line:

Je ne suis pas sûr mais il me semble que ça comparera avec cette chaine précisément, casse des caractères incluses (si je me trompe, on me corrigera, je ne peux le faire maintenant).
Si tu es sûr que tu n'auras jamais de "Hardware ethernet" ou "HARDwAre ETHernet" (casse mélangée), tout va bien, sinon il est préférable de passer la chaîne à comparer en .upper() ou .lower() et d'effectuer la comparaison avec soit "HARDWARE ETHERNET" soit "hardware ethernet".

+1 vote

(dette-rage)
tu supprimais tous les sauts de ligne du fichier, puis tu remplacais les } par saut de ligne et { par ;. et tu obtenais proprement 1 ligne par information sans code de 20 lignes.

host gogol ;mac 04:6F:94:2A:FF:C0; ip 10.60.12.1; ( csv a peu pret)

ou même tu supprimes tous les sauts de ligne et ta barre d'information c'est de host à }. Pour bosser en production, le + simple c'est le mieux même en sacrifiant un peu en temps de traitement.

rep={}
with open('test', 'r') as content_file:
    for n in content_file.read().replace('\n','').replace('\r','').replace(' ','').replace('}','\n').replace('{',';').split('\n') :
        if (n!=''): 
            rep[n.split(';')[0][4:]] = [n.split(';')[1][3:],n.split(';')[2][2:]]
print (rep )
répondu 22-Avr-2016 par LonGdueZBOO (184 points)
edité 23-Avr-2016 par LonGdueZBOO
...