MON1.2: Utilisation de Python pour automatiser des tâches sur Excel avec Openpyxl

Tags :
  • MON
  • 2023-2024
  • temps 1
  • Excel
  • Python
  • Automatisation
  • Openpyxl
  • Programmation Objet
Auteurs :
  • Duc DANG VU

Ce MON aura pour objectif de s'initier à l'utilisation de la bibliothèque openpyxl de Python, pour lire et modifier des fichiers Excel.

Prérequis

Connaissance basique de la programmation objet avec Python.

Tout les codes présentés ci-dessous ainsi que les fichiers de ce MON (notamment les emplois du temps personnalisés) sont disponibles dans le dossier Github de Do-It: Codes du MON1.2

Introduction

L'outil Excel du pack Office est un outil très répandu dans toutes les entreprises. Tout le monde sait à peu près comment fonctionne ce logiciel, mais peu de gens savent qu'il existe une bibliothèque python qui permet de lire et modifier des fichier Excel: openpyxl. Ce MON aura donc pour objectif d'apprendre à utiliser openpyxl pour automatiser des tâches sur des fichiers Excel qui seraient en temps normal longues à faire à la main.

Dans un premier temps, nous expliquerons les méthodes principales de openpyxl. Ensuite, nous appliquerons ces connaissances en créant des emplois du temps personnalisés pour chaque élève Do-It 2023/2024.

Dans ce MON, seulement une petite partie des commandes de la bibliothèque openpyxl seront expliquées (celles les plus importantes). Le reste est très bien expliqué sur le site de openpyxl.

Dans la suite, quelques lignes de code en python seront présentées. Le résultat de l’exécution des lignes de code est indiqué juste après celles-ci, après 3 chevrons >>>

Apprentissage de la bibliothèque

Tout d'abord, il faut installer openpyxl. Pour cela, il faut rentrer la commande suivante dans une invite de commande:

pip install openpyxl

Une fois la bibliothèque installée, il faut l'importer au début du code python, et nous sommes prêts! Nous allons dans la suite expliquer les méthodes principales qui permettent de lire et modifier des données dans des fichiers Excel. Dans la suite, les fichiers manipulés doivent se trouver dans le même repertoire que le fichier python exécuté. Le fichier Excel que nous allons manipuler en exemple se nomme test.xlsx et contient le simple tableur suivant:

Image test

La feuille qui est remplie se nomme Feuille 1.

Lecture de données

Dans un premier temps, nous allons expliquer comment lire des données. La première classe qui est importante à connaître est la classe Workbook. Pour créer un objet de cette classe, on utilise la méthode openpyxl.load_workbook. Les objets de cette classe correspondent au fichier Excel renseigné en paramètre de la méthode. Ils contiennent toutes les informations du fichier: lignes, colonnes, cellules etc... Par exemple, on peut récupérer les noms des feuilles du fichier Excel avec la méthode sheetnames. Il faut ensuite récupérer la feuille sur laquelle on veut lire les données. Pour cela, on renseigne le nom de la feuille en question comme suit:

import openpyxl

wb = openpyxl.load_workbook('test.xlsx')
sheet = wb['Feuille 1']

Il est également possible de récupérer la feuille active (c'est-à-dire celle qui s'affiche lorsqu'on ouvre le fichier Excel) par l'attribut active de la classe Workbook: sheet = wb.active

La variable sheet est un objet de la classe Worksheet. Cet objet contient notamment les informations de toutes les cellules de la feuille. On récupère une cellule en indiquant directement son nom, ou bien en indiquant son numéro de ligne et de colonne via la méthode cell. Notons que ces numéros commencent à 1 (et pas 0) et le numéro de colonne 27 correspond à la colonne "AA". On peut récupérer le contenu de la cellule avec l'attribut value. On peut également récupérer les coordonnées d'une cellule avec l'attribut coordinate.

cell = sheet['A1']
# ou cell = sheet.cell(row = 1, column = 1)
print(cell.value)
print(cell.coordinate)

>>> 'Pommes'
>>> 'A1'

Il est possible de convertir un nombre vers la lettre d'une colonne et vice-versa. Par exemple, 1 correspond à "A" et 28 correspond à "AB". Les fonctions utilisées sont get_column_letter et column_index_from_string. Ces fonctions doivent être importées avant d'être utilisées:

from openpyxl.utils import get_column_letter, column_index_from_string

print(get_column_letter(30))
print(column_index_from_string('E'))

>>> 'AD'
>>> 5

Écriture de données

En plus de lire des données, il est possible de les modifier, et d'en rajouter dans les fichiers Excel. Par exemple, on peut modifier le titre d'une feuille, ou encore modifier le contenu d'une cellule. Après chaque modification, il faut enregistrer le workbook avec la méthode save. Notons qu'on ne peut pas sauvegarder un fichier qui est ouvert dans un autre onglet. Il est recommandé d'enregistrer le fichier modifié sous un autre nom afin de ne pas écraser le fichier original. Ainsi, si jamais il y a des erreurs dans le programme, il est possible de récupérer le fichier original.

wb = openpyxl.load_workbook('test.xlsx')
sheet = wb['Feuille 1']
cell = sheet['A1']

sheet.title = 'Inventaire fruits'
cell.value = 'Kiwis'

wb.save('test_copy.xlsx')

Voici à quoi ressemble le fichier test_copy.xlsx, qui a été enregistré dans le même repertoire que le code python exécuté:

Image test2

La cellule A1 a bien été modifiée, ainsi que le nom de la feuille.

On peut également modifier la hauteur et la largeur des cellules en modifiant les attributs row_dimensions.height et column_dimensions.width des objets de la classe Worksheet. Les valeurs indiquées sont en "pt".

cell = sheet['A2']
cell.value = 'Pommes de rainette'
sheet.column_dimensions['A'].width = 30

wb.save('test_copy.xlsx')

Le fichier test_copy.xlsx produit est le suivant:

Image test3

La largeur de la colonne A s'est bien modifiée.

On peut aussi centrer horizontalement et verticalement le contenu d'une cellule avec la classe Alignment, changer le style de police du contenu d'une cellule avec la classe Font ou encore changer la couleur de fond d'une cellule avec la classe PatternFill. Ces classes doivent être importées de la bibliothèque openpyxl.styles. Enfin, il est possible de créer des bordures sur les cellules en modifiant leurs attributs border de la classe Cell. Encore une fois, il faut importer la classe Border depuis la bibliothèque openpyxl.styles.borders.

from openpyxl.styles import Font, PatternFill, Alignment

wb = openpyxl.load_workbook('test.xlsx')
sheet = wb['Feuille 1']

cell = sheet['A2']
cell.value = 'Pommes de rainette'
sheet.column_dimensions['A'].width = 30
sheet.row_dimensions[2].height = 30
cell.font = Font(size = 15, bold = True)
cell.alignment = Alignment(horizontal = "center", vertical = "center")
cell.fill = PatternFill("solid", start_color = "0ebf3e")
cell.border = Border(left = Side(style='thick'), right = Side(style='thick'), top = Side(style='thick'), bottom = Side(style='thick'))

wb.save('test_copy.xlsx')

Le résultat est le suivant:

Image

Le texte "Pomme de rainette" est bien centré horizontalement et verticalement dans sa cellule et il est bien en gras et de taille 15. De plus, la couleur de la cellule a bien changé et la cellule a bien des bordures épaisses.

La couleur renseignée en paramètres de la classe PatternFill est sous la forme d'un code hex. Ce code peut être déterminé grâce au site suivant: htmlcolorcode.com

Enfin, il est possible de fusionner des cellules ou de les séparer, grâce aux méthodes merge_cells et unmerge_cells. Par exemple, si l'on ajoute au code précédent la commande sheet.merge_cells('A3:A4'), on obtient le résultat suivant:

Image merge

On remarque que seul le contenu de la première cellule est conservé. De plus, les autres cellules de la cellule fusionnée sont des objets de la classe MergedCell. Il est possible de récupérer tout ces objets grâce à l'attribut merged_cell.ranges de la classe Worksheet.

Application des connaissances acquises

Afin d'appliquer ces connaissances de openpyxl, nous allons travailler sur les fichiers Excel disponibles dans le Drive Do-It 2023/2024. Nous allons récupérer le fichier contenant la liste des élèves ainsi que leur choix de cours, ainsi que le fichier contenant l'emploi du temps de l'année. L'objectif est de créer un emploi du temps personnalisé pour chaque élève, en fonction de ses choix de cours. Il faudra donc lire les données de la première feuille puis écrire de nouvelles données sur la deuxième feuille. Ensuite, nous allons implémenter des méthodes pour permettre à l'utilisateur de rajouter ou supprimer manuellement des cours sur son emploi du temps.

Création automatique d'emplois du temps personnalisés

Le premier fichier Excel sur lequel nous allons travailler est celui contenant la liste des élèves ainsi que leurs choix de cours. Ce fichier est nommé étudiants_Do-It_23_24.xlsx. Le deuxième fichier est nommé edt Do_It.23-24.xlsx, et contient l'emploi du temps de la promo Do-It 2023/2024 sous forme d'un tableau. On va créer une classe Create_timetable qui va contenir les méthodes pour manipuler les emplois du temps. Plus particulièrement, la méthode create_timetable_automatic va prendre en argument le nom et le prénom d'un étudiant et va enregistrer son emploi du temps personnalisé. Voici les différentes étapes que le programme va suivre:

On créé également une méthode create_all_timetable, qui itère la méthode précédente 24 fois (une fois pour chaque élève) pour créer chaque emploi du temps pour chaque élèves. Ces emplois du temps sont disponibles dans le dossier GitHub de ce MON: Codes du MON1.2

Pour executer ce code, il faut créer un objet de la classe Create_timetable et appeler la méthode create_timetable_automatic, en renseignant les nom et prénom de l'élève (sous forme de chaîne de caractères):

test = Create_timetable()
test.create_timetable_automatic("Dang Vu", "Duc")

L'exécution de ces lignes produit le fichier Excel suivant:

edt Duc

Les emplois du temps produits sont basés sur le fichier original datant du 14/10/23. Il est possible que d'ici cette date, de nouveaux cours soient rajoutés, ou bien modifiés voire même supprimés. C'est pourquoi le temps 3 n'est pas complet pour tout les emplois du temps car à l'heure où j'écris ces lignes, les cours du temps 3 ne sont pas encore fixés. De plus, il est très important que les noms des cours indiqués dans le fichier étudiants_Do-It_23_24.xlsx correspondent mot pour mot à ceux présents dans le fichier edt Do_It.23-24.xlsx. Même un espace à la fin du nom posera des problèmes.

Ajout et suppression automatisé d'un cours

La classe Create_timetable contient également des méthodes pour ajouter ou supprimer des cours dans un fichier Excel renseigné en argument. Voici les étapes que suit la méthode add_course pour ajouter des cours:

La méthode remove_course suit des étapes similaires.

Voici ce que produit la méthode add_course quand on exécute la commande test.add_course("edt de Duc Dang Vu.xlsx"), après avoir rentré les informations du cours à rajouter:

Image commande

Image edt modifié

Et voici ce que produit la méthode remove_course quand on exécute la commande test.remove_course("edt de Duc Dang Vu.xlsx"), après avoir rentré les informations du cours à supprimer:

Image commande 2

Image edt modifié 2

Le cours a bien été ajouté, puis supprimé comme on le voulait.

Cliquer pour voir le code de la classe Create_timetable

import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
from openpyxl.styles.borders import Border, Side

class Create_timetable():
    def __init__(self):
        self.create_new_sheets()
        self.weeks = []
        self.possible_times = [8, 9, 10, 11, 12, 14, 15, 16, 17, 18]
        self.wb = openpyxl.load_workbook('edt Do_It.23-24.xlsx')
        self.sheet = self.wb['année']
        for i in range(3,35):
            cell_begin = self.sheet.cell(row = i, column = 3)
            cell_end = self.sheet.cell(row = i, column = 4)
            self.weeks.append([cell_begin.value, cell_end.value])
    
    def create_new_sheets(self):
        self.wb2 = openpyxl.load_workbook('étudiants_Do-It_23_24.xlsx')
        self.sheet2 = self.wb2['effectif']
        self.wb3 = openpyxl.load_workbook('edt Do_It.23-24.xlsx')
        self.sheet3 = self.wb3['année']
    
    def get_cell_reference(self, day, month, time):
        nb_week = None
        nb_day = None
        for k in range(len(self.weeks)):
            date_begin = self.weeks[k][0]
            date_end = self.weeks[k][1]
            if date_begin.month == date_end.month and month == date_begin.month:
                if day in range(date_begin.day, date_end.day + 1):
                    nb_week = k
                    nb_day = day - date_begin.day
                    break
            if date_begin.month != date_end.month and month == date_begin.month:
                if day >= date_begin.day:
                    nb_week = k
                    nb_day = day - date_begin.day
                    break
            if date_begin.month != date_end.month and month == date_end.month:
                if day <= date_end.day:
                    nb_week = k
                    virtual_day_begin = date_end.day - 4
                    nb_day = day - virtual_day_begin
                    break
        if nb_week == None:
            print("La date indiquée n'est pas valide")
            return(False)
        nb_row = nb_week + 3
        if time not in self.possible_times:
            print("L'heure indiquée n'est pas valide")
            return(False)
        else:
            index = self.possible_times.index(time)
            nb_column = nb_day * 8 + 5 + index
        return(get_column_letter(nb_column) + str(nb_row))

    def add_course(self, file_name):
        current_wb = openpyxl.load_workbook(file_name)
        current_sheet = current_wb['année']
        day = int(input("Entrez le jour du cours: "))
        month = int(input("Entrez le mois du cours: "))
        time = None
        while time not in self.possible_times:
            time = int(input("Entrez l'heure du cours: "))
        duration = int(input("Entrez la durée du cours: "))
        name = str(input("Entrez le nom du cours: "))
        professor = str(input("Entrez le nom du professeur: "))
        room = str(input("Entrez le numéro de salle: "))
        color = str(input("Entrez le code hex de la couleur du cours: "))
        cell_reference = self.get_cell_reference(day, month, time)
        cell_reference2 = self.get_cell_reference(day, month, time + duration - 1)
        current_sheet.merge_cells(cell_reference + ':' + cell_reference2)
        cell = current_sheet[cell_reference]
        cell.fill = PatternFill("solid", start_color = color)
        cell.value = f"{name}\n{professor}\n{room}"
        cell.alignment = Alignment(wrapText=True, horizontal = "center", vertical = "center")
        current_sheet.row_dimensions[cell.row].height = 10 * 3 + 10
        current_wb.save(file_name)
    
    def remove_course(self, file_name):
        current_wb = openpyxl.load_workbook(file_name)
        current_sheet = current_wb['année']
        day = int(input("Entrez le jour du cours qui doit être supprimé: "))
        month = int(input("Entrez le mois du cours qui doit être supprimé: "))
        time = int(input("Entrez l'heure du cours qui doit être supprimé: "))
        duration = int(input("Entrez la durée du cours qui doit être supprimé: "))
        cell_reference = self.get_cell_reference(day, month, time)
        cell_reference2 = self.get_cell_reference(day, month, time + duration - 1)
        border = Border(left = Side(style='thin'), right = Side(style='thin'), top = Side(style='thin'), bottom = Side(style='thin'))
        current_sheet.unmerge_cells(cell_reference + ':' + cell_reference2)
        for k in range(duration):
            cell_reference = self.get_cell_reference(day, month, time + k)
            cell = current_sheet[cell_reference]
            cell.border = border
            cell.fill = PatternFill("solid", start_color = "FFFFFF")
            cell.value = None
        current_wb.save(file_name)
    
    def remove_course_automatic(self, cell, duration):
        border = Border(left = Side(style='thin'), right = Side(style='thin'), top = Side(style='thin'), bottom = Side(style='thin'))
        if duration == 1:
            cell.border = border
            cell.fill = PatternFill("solid", start_color = "FFFFFF")
            cell.value = None
        else:
            cell_end_coordinate = str(get_column_letter(cell.column + duration - 1)) + str(cell.row)
            self.sheet3.unmerge_cells(cell.coordinate + ':' + cell_end_coordinate)
            for k in range(duration):
                current_cell_coordinate = str(get_column_letter(cell.column + k)) + str(cell.row)
                current_cell = self.sheet3[current_cell_coordinate]
                current_cell.border = border
                current_cell.fill = PatternFill("solid", start_color = "FFFFFF")
                current_cell.value = None

    def get_course_duration(self, cell):
        for mergedCell in self.sheet3.merged_cells.ranges:
            if cell.coordinate in mergedCell:
                return(mergedCell.max_col - mergedCell.min_col + 1)
        return(1)
        


    def create_timetable_automatic(self, nom, prénom):
        courses_name = ["bonjour !", "tc1 : agilité", "lancement projet 3a", "tc1 : gestion des sources", "tc1 : service design", "langues", "projet 3a", "vacances", "filière métier", "tronc commun 3a", "prez mon 2", "prez pok", "point pok sprint 1", "point pok sprint 2", "prez mon 1", "pok&mon", "cap 1a/3a/conception si", "prez projet", "rencontre w3g", "débrief", "conférence métier"]
        row_number = None
        for k in range(3,27):
            cell_nom = self.sheet2.cell(row = k, column = 1)
            cell_prénom = self.sheet2.cell(row = k, column = 2)
            if prénom.lower() == cell_prénom.value.lower() and nom.lower() == cell_nom.value.lower():
                row_number = k
                break
        if row_number == None:
            print("Nom ou prénom invalide")
            return()
        for k in range(7,30):
            cell = self.sheet2.cell(row = row_number, column = k)
            if cell.value == 'x' or cell.value == "X":
                course = self.sheet2.cell(row = 2, column = k).value
                course = course.lower()
                if course == 'bonnes pratiques':
                    courses_name.append("structuration d'un projet info")
                    courses_name.append("programmation par les tests")
                else:
                    courses_name.append(course)
        border_medium_left =  Border(left = Side(style='medium'), right = Side(style='thin'), top = Side(style='thin'), bottom = Side(style='thin'))
        border = Border(left = Side(style='thin'), right = Side(style='thin'), top = Side(style='thin'), bottom = Side(style='thin'))
        border_thick_left = Border(left = Side(style='thick'), right = Side(style='thin'), top = Side(style='thin'), bottom = Side(style='thin'))
        for nb_row in range(5, 35):
            for nb_column in range(5, 45):
                cell = self.sheet3.cell(row = nb_row, column = nb_column)
                course_name = ''
                if cell.value != None:
                    for char in cell.value:
                        course_name += char.lower()
                        if "\n" in course_name:
                            course_name = course_name[:len(course_name) - 1]
                            break
                    if course_name not in courses_name:
                        course_duration = self.get_course_duration(cell)
                        self.remove_course_automatic(cell, course_duration)
                    else:
                        cell.alignment = Alignment(wrapText=True, horizontal = "center", vertical = "center")
                        cell.font = Font(size = 10)
                        self.sheet3.row_dimensions[cell.row].height = 10 * 3 + 10
                if (nb_column - 5) % 8 == 0:
                    cell.border = border_thick_left
                elif (nb_column - 5) % 8 == 4:
                    cell.border = border_medium_left
                else:
                    cell.border = border
        self.wb3.save(f'edt de {cell_prénom.value} {cell_nom.value}.xlsx')
        print(f'Emploi du temps enregistré sous le nom de "edt de {cell_prénom.value} {cell_nom.value}.xlsx"')

    def create_all_timetable(self):
        for k in range(3,27):
            self.create_new_sheets()
            nom = self.sheet2.cell(row = k, column = 1).value
            prénom = self.sheet2.cell(row = k, column = 2).value
            self.create_timetable_automatic(nom, prénom)
        print("Tout les edt ont été enregistrés")

Conclusion

Ce MON fut très intéressant. Je ne savais pas qu'il était possible d'automatiser des opérations sur Excel avec Python, et j'ai donc beaucoup appris. Ces connaissances peuvent très certainement m'être utiles dans ma future carrière.

Sources