Boîte à outil 1 : Extraction de contenu

Préparation de l'environnement

In [ ]:
import os, re
from bs4 import BeautifulSoup

Nous utilisons trois modules ici : os pour naviguer dans l'arborescence de corpus, re pour nettoyer le contenu et extraire certaines informations utiles, et finalement BeautifulSoup pour parser nos fichiers rss.

In [ ]:
def nettoyage(chaine):
    nvl_chaine = re.sub(r"(« | »|« | »)", '"', re.sub(r'<.*?>', '', chaine)).replace("’","'")
    if nvl_chaine and nvl_chaine[-1].isalnum() : 
        nvl_chaine += '.'
    return nvl_chaine

Nous définissons cette fonction pour normaliser le contenu du corpus. Elle prend une phrase en entrée, lui retire les balises <br> (cf corpus 2016) <title> et <description>, et remplace certains caractères spéciaux pour pas qu'ils troublent les traitements suivants. Finalement, elle regarde si la phrase se termine avec une ponctuation, si oui elle renvoie la chaîne, sinon elle rajoute un point final.

In [ ]:
cat_name = {
    '0,2-3236,1-0,0.xml': ['actualite-medias', 0, 0, 0, 0],
    '0,2-3234,1-0,0.xml': ['economie', 0, 0, 0, 0],
    '0,2-3210,1-0,0.xml': ['international', 0, 0, 0, 0],
    '0,57-0,64-823353,0.xml': ['politique', 0, 0, 0, 0],
    '0,2-3242,1-0,0.xml': ['sport', 0, 0, 0, 0],
    '0,2-3238,1-0,0.xml': ['vous', 0, 0, 0, 0],
    '0,2-3476,1-0,0.xml': ['cinema', 0, 0, 0, 0],
    '0,2-3214,1-0,0.xml': ['europe', 0, 0, 0, 0],
    '0,2-3260,1-0,0.xml': ['livres', 0, 0, 0, 0],
    'env_sciences.xml': ['sciences', 0, 0, 0, 0],
    '0,2-651865,1-0,0.xml': ['technologies', 0, 0, 0, 0],
    '0,2-3546,1-0,0.xml': ['voyage', 0, 0, 0, 0],
    '0,2-3246,1-0,0.xml': ['culture', 0, 0, 0, 0],
    '0,2-3232,1-0,0.xml': ['idees', 0, 0, 0, 0],
    '0,2-3244,1-0,0.xml': ['planete', 0, 0, 0, 0],
    '0,2-3224,1-0,0.xml': ['societe', 0, 0, 0, 0],
    '0,2-3208,1-0,0.xml': ['une', 0, 0, 0, 0],
}

Ici nous définissons un dictionnaire cat_name qui est un composant noyau de notre chaîne de traitement. Il permet de gérer les catégories, le compteur des archives et les doublons en fonction des noms de fichier.

Chaque entrée du dictionnaire est composée ainsi : en clé le nom de fichier, en valeur une liste des objets correspondants. Dans la liste, nous avons d'abord une chaîne de caractère pour le nom de rubrique, ensuite un nombre pour compter les archives, un objet set pour gérer les doublons, un objet IOWrapper pour la sortie xml, et un autre objet IOWrapper pour la sortie txt. Voici la structure :

'nom_fichier': ['catégorie', compteur, set_doublons, objet wrapper xml, objet wrapper txt]

Notons que les trois derniers éléments sont générés au cours des traitements à venir, pour l'instant nous leur gardons la place avec un 0 dans la liste.

In [ ]:
if os.path.exists("./SORTIE_V10"):
    for cat in os.listdir('./SORTIE_V10'):
        for ele in cat_name.values():
            if ele[0] == cat:
                with open('./SORTIE_V10/{0}/{0}.xml'.format(cat),'r', encoding = 'UTF-8', newline = '\n') as fichierxml:
                    contenu = fichierxml.read()
                    title_list = re.findall('<title>.*?</title>', contenu)
                    nbarchive = len(title_list)
                    titles = set(title_list)
                    ele[1], ele[2] = nbarchive, titles

Cette partie (facultative) permet de rendre le programme plus 'pérenne', c'est-à-dire qu'il permet de mettre le dictionnaire à jour en fonction du contenu de nos fichiers. Ainsi nous pouvons ajouter quelques flux rss et relancer le programme, les doublons et les compteurs seront gérés correctement.

In [ ]:
months = {
    'Jan': '01',
    'Feb': '02',
    'Mar': '03',
    'Apr': '04',
    'May': '05',
    'Jun': '06',
    'Jul': '07',
    'Aug': '08',
    'Sep': '09',
    'Oct': '10',
    'Nov': '11',
    'Dec': '12',
}

for dir in os.listdir('./2018'):
    try:
        despath = './2018/' + months[dir]
        dirpath = './2018/' + dir
        os.rename(dirpath, despath)
    except:
        pass

L'arborescence d'origine est nommé avec les noms de mois en anglais, les répertoires sont donc triés en ordre alphabétique. Nous les renommons en nombre pour que les traitements puissent se faire dans le bon ordre.

Boucle principale : extraction de contenu

In [ ]:
for i in cat_name.values():
    print(i[0])

demande = input('Indiquez une ou plusieurs catégories (séparées par espace)\n> ')

cat_demande = re.findall(r'[\w-]+', demande.lower())

Nous affichons les noms de rubriques disponibles et nous demandons à l'utilisateur de saisir. Ensuite nous extrayons les catégories demandées avec une expression régulière.

In [ ]:
for root, dirs, files in os.walk('./2018'):
    for name in files :
        if name in cat_name and cat_name[name][0] in cat_demande:

            cat = cat_name[name][0]
            filename = os.path.join(root, name)

            pubdate_list = re.findall('\d+', root)
            pubdate = '-'.join(pubdate_list[:3])

            print(filename + ' : ' + cat)

Nous parcourons toute l'arborescence et nous excluons les fichiers non pertinents. Nous ne traitons que les fichiers dont le nom est présent dans les clés du dictionnaire cat_name, ainsi les fichiers de travail comme fil1514829634-v1.xml ne seront pas concernés. A chaque rencontre d'un fichier, nous cherchons dans cat_name la catégorie correspondante et nous regardons si elle est demandée par l'utilisateur. Si oui, nous extrayons le chemin complet du fichier, la catégorie et la fate de publication, et nous imprimons le nom de fichier traité pour avoir un contrôle sur le processus ; sinon nous passons au fichier prochain.

Variables générées : cat (nom de catégorie), filename (chemin complet du ficheir à traiter), pubdate (date de publication du fichier)

In [ ]:
            try:
                os.makedirs("./SORTIE_V10/" + cat)
                xmltmp = open('./SORTIE_V10/{0}/{0}.xml'.format(cat), 'w+', encoding = 'UTF-8', newline = '\n')
                txttmp = open('./SORTIE_V10/{0}/{0}.txt'.format(cat), 'w+', encoding = 'UTF-8', newline = '\n')
                xmltmp.write('<?xml version="1.0" encoding="UTF8"?>\n<file>\n</file>')
                cat_name[name][2] = set()

            except:
                xmltmp = open('./SORTIE_V10/{0}/{0}.xml'.format(cat), 'r+', encoding = 'UTF-8', newline = '\n')
                txttmp = open('./SORTIE_V10/{0}/{0}.txt'.format(cat), 'r+', encoding = 'UTF-8', newline = '\n')

            cat_name[name][3] = xmltmp
            cat_name[name][4] = txttmp

            cat, counter, titles, sortiexml, sortietxt = cat_name[name]

Les résultats d'extraction sont stockés dans un répertoire SORTIE_V10, triés par catégorie. Nous regardons s'il existe déjà un sous-répertoire correspondant à la catégorie du fichier en cours. Si non, nous créons un nouveau répertoire ainsi que les fichiers de sortie (ceci en les ouvrant en mode w+, création, lecture et écriture) ; si oui nous ouvrons directement les sorties en mode r+ (lecture et écriture). Nous remplissons les objets manquants dans le dictionnaire cat_name, ainsi l'entrée est complète. Ensuite nous téléchargeons les cinq objets nécessaires dans le processus suivant.

Variables générées : cat (nom de catégorie), counter (compteur des archives), titles (set de doublons), sortiexml (IOwrapper de la sortie xml), sortietxt (IOWrapper de la sortie txt)

In [ ]:
            with open(filename, 'rb') as fichier:

                soup = BeautifulSoup(fichier, 'lxml-xml')
            
                items = soup.find_all('item')

Nous ouvrons le fichier à traiter et le passons au parser BeautifulSoup. Ce dernier construit une arborescence du contenu de fichier, nous pouvons ensuite naviguer dedans pour récupérer des informations. Ici nous traitons les balises <item>, donc nous disons au parser de renvoyer une liste de toutes les <item> dans le fichier.

Variables générées : items (liste des balises <item>)

In [ ]:
                for item in items:

                    name_title = nettoyage(str(item.title))
                    name_title_xml = '<title>' + name_title + '</title>'

                    prev_len = len(titles)

                    titles.add(name_title_xml)

                    if len(titles) != prev_len:
                        sortietxt.seek(0, 2)
                        sortiexml.seek(sortiexml.seek(0,2) - 7)
                        sortiexml.write('<archive n="{0}">\n{1}\n'.format(counter, name_title_xml))
                        sortietxt.write(name_title + '\n')
                        counter += 1

                        if item.description and item.description.string:
                            name_description = nettoyage(str(item.description)) 
                            sortiexml.write('<description>' + name_description + '</description>\n')
                            sortietxt.write(name_description + '\n\n')
                        else:
                            sortietxt.write('\n')

                        sortiexml.write('<pubDate>' + pubdate + '</pubDate>\n</archive>\n</file>')

            cat_name[name][1] = counter
            cat_name[name][2] = titles

Pour chaque balise <item>, nous nettoyons d'abord le texte de la balise <title>, le mettons en format xml, puis le passons à la gestion de doublons. Nous sommes obligés de traiter dans cet ordre parce que ce sont les titres xml "nettoyés" qui sont stockés dans notre set titles (cf Préparation d'environnement 4). BeautifulSoup propose une fonction pour récupérer le texte d'une balise, cependant c'est le texte interprété qu'il nous renvoie. Les caractères spéciaux comme & sont transformés en caractères normaux alors que ce n'est pas autorisé dans la sortie xml que nous allons produire. Donc nous avons pris la représentation textuelle en appelant la fonction str().

Donc nous avons obtenu une phrase au format xml, nous l'ajoutons dans le se de doublons titles. L'astuce de la gestion de doublons est que si l'on ajoutait un élément déjà existant dans un set, la longueur de set ne changerait pas. S'il s'agit d'un doublon, nous passons à la prochaine balise; sinon nous les écrivons dans les deux sorties avec les informations nécessaires tels que le numéro d'archive et la date de publication. Nous avons également mis une structure de condition pour gérer les descriptions manquantes.

Après l'écriture, nous mettons à jour le compteur counter et le set de doublons titles et nous les remettons dans le dictionnaire cat_name.

In [ ]:
for fermeture in cat_name.values():
    if fermeture[3] != 0:
        fermeture[3].close()
        fermeture[4].close()

La dernière étape consiste à fermer les fichiers de sortie que nous avons ouverts au cours des traitements, et ceci en parcourant cat_name.