1- PROBLÉMATIQUE

Voilà plus de 15 ans que je fais du développement en Python, et je commence à avoir beaucoup de fichiers Python (environ 100.000 !). Avec une telle quantité de fichiers, j'ai 2 gros problèmes dont la résolution me fait gagner beaucoup de temps:

- Je voudrais savoir dans quels fichiers j'ai utilisé une fonctionnalité donnée et dans quelles lignes. Par exemple, le mot "ProcessPoolExecutor" du module "concurrent.futures" m'indiquera dans quels fichiers j'ai fait du calcul parallèle, ce qui me donnera rapidement des exemples de codes déjà validés.

- Python ou un module externe (comme PyQt5) a changé de version, en déclenchant des problèmes de compatibilité ascendante. Même s'il existe des "moulinettes" qui facilitent ce passage, il reste toujours des modifications à faire "à la main" sur des milliers de fichiers: comment je fais ?

A noter que j'ai conçu ce qui suit pour les fichiers Python, mais il est applicable à tous les fichiers texte en texte pur (donc pas aux ".doc", ni aux ".pdf", etc...). Par exemple, si vous avez fait une même faute d'orthographe sur plusieurs fichiers ".txt", rien ne vous empêche de la corriger d'une seul coup...

2- PRINCIPES GÉNÉRAUX

Pour résoudre ce problème, j'avais développé il y a quelques années un programme graphique (PyQt5) qui fonctionnait très bien, mais qui était trop compliqué à faire évoluer. Par exemple, je veux maintenant que les résultats puissent être triés selon l'ordre chronologique des fichiers, pour avoir plus vite les développements les plus récents, mais j'ai reculé devant le travail d'adaptation...

L'autre solution que j'avais aussi essayée consiste à créer le code Python avec "argparse" pour qu'il puisse être lancé avec ses arguments dans une (longue) ligne de commande dans la console. Mais c'est vraiment pénible d'écrire une telle ligne de commande à chaque fois, y compris de se rappeler tous les arguments auquels on a droit (=> notice!)!

J'ai aussi essayé une solution intermédiaire avec un fichier de lancement en langage batch console (.bat sous Windows). Mais c'est un langage spécifique peu pratique qui ne facilite pas autant le travail qu'il faudrait.

Alors j'utilise maintenant un code en Python, dont la structure est la suivante. Dans l'ordre:
- données de traitement
- importations
- fonctions spécifiques au traitement
- exécution du traitement
- affichage des résultats

Ainsi, les données de traitement sont au début de la page de code! Pourquoi? Parce que quand on charge dans un éditeur de texte, c'est en général la 1ère chose qu'on voit, et c'est la seule chose qui doit être modifiée pour l'adapter au traitement voulu! De plus, ces données portent les données qu'on a utilisées la dernière fois et dont une partie ne changera pas (mémoire entre sessions), et comportent tous les commentaires utiles pour se rappeler de leur sens 6 mois plus tard. Après cette partie de "données de traitement", il y a la "machinerie du programme" qu'on n'a pas besoin de voir après sa mise au point.

Pour charger ce lanceur et adapter les données de traitement au problème à résoudre; j'utilise comme éditeur de texte "spyder" qui s'installe dans Python avec pip, et qui est donc disponible dans tous les OS. Il fonctionne vraiment très bien! Mais n'importe quel éditeur de texte peut convenir, y compris idle. D'une façon générale, il est pratique qu'on puisse lancer directement le code Python qu'on vient de modifier sans sortir de l'éditeur de texte: il est donc intéressant que cet éditeur fasse partie d'un "IDE" de développement Python.

En fait, il s'agit bien d'un programme "graphique", à part que la partie graphique se limite à l'éditeur de texte utilisé.

A noter, mais c'est important, que j'adopte désormais ce principe pour le lancement de programmes exécutables comme par exemple "ffmpeg" pour le traitement des vidéos! Je ferai peut-être un autre post sur le sujet. En fait, je traite comme ça la plupart des utilitaires que j'utilise souvent, même s'ils ne sont pas liés au développement. Par exemple recherche de photos selon leur date de prise de vue (exif).

Pour la logique du traitement lui-même, voici dans l'ordre:
- recherche des fichiers selon les motifs wildcard fichiers et sous-répertoires
- analyse de ces fichiers en cherchant le mot par expression régulière
- si un fichier contient le mot, on cherche dans quelle(s) ligne(s)
- s'il est prévu de remplacer le mot trouvé par mot2, on crée le nouveau fichier corrigé
- tri des fichiers trouvés pour préparer l'affichage
- on affiche les résultats dans la console et, si demandé, dans un fichier log

3- EXEMPLES D'APPLICATION

Avant d'éplucher le code, voyons des petits exemples de recherche et de résultats, rien que pour vous donner envie d'aller plus loin!

Dans le répertoire Lib du Python installé, on cherche le mot "fnmatch" pas forcément isolé (donc, on trouvera aussi "fnmatchcase" du module fnmatch, qu'il m'arrive d'utiliser aussi). A titre d'exemple, on a exclu de l'analyse les 2 sous-répertoires "site-packages" et "test".

Voilà les données de traitement (recherche-remplacement) telles que l'utilisateur devra les remplir avec son éditeur de texte:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# répertoire à analyser
repertoire = r"C:\Python310\Lib"
 
# motifs wildcard des fichiers à inclure (séparateur=';')
inclusfics = "*.py;*.pyw"
 
# motifs wildcard des sous-répertoires à exclure (séparateur=';')
exclusreps = "site-packages;test"
 
recursion = True # si True => analyse aussi les sous-répertoires non exclus
 
encodage = "utf-8" # encodage des fichiers à lire
 
# mot à chercher
mot = "fnmatch"
# minuscules accentuées en français (signes diacritiques): "àâäçéèêëîïôöùûüÿ"
# majuscules accentuées en français (signes diacritiques): "ÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
# ligatures en français en minuscules : 'æ œ' et en majuscules: "Æ Œ"
 
motseul = False # si True: ne recherche que le mot isolé
 
ignorecasse = True # si True: neutralise la casse (majuscules-minuscules)
 
# lettres permises dans les mots recherchés (langue française)
lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
 
chrono = True # si True: affiche ordre chronologique (+récents à la fin)
 
suiviliens = False # si True: suit les liens symboliques
 
fichierlog = "recherche.txt" # si non vide => enregistre en + dans le fichier
 
# ===== Remplacement =====
mot2 = "" # si non vide => demande de remplacement de mot par mot2
 
suffixe = "_remp" # => ajoute le suffixe au nom de fichier, avant extension

Et voilà l'affichage du résultat:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
===============================================================================
RECHERCHE & REMPLACE UN MOT DANS DES FICHIERS SÉLECTIONNES
Répertoire analysé: C:\Python310\Lib
Récursion: oui
Motifs wildcard d'inclusion des fichiers: *.py;*.pyw
Motifs wildcard d'exclusion des sous-répertoires: site-packages;test
Mot à chercher: fnmatch
Mot isolé: non
Casse (maj-min) ignorée: oui
Sans remplacement
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\bdb.py
      3 import fnmatch
    198             if fnmatch.fnmatch(module_name, pattern):
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\distutils\filelist.py
      8 import fnmatch
    183         are not quite the same as implemented by the 'fnmatch' module: '*'
    272     a string containing the regex.  Differs from 'fnmatch.translate()' in
    276     pattern_re = fnmatch.translate(pattern)
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\fnmatch.py
      3 fnmatch(FILENAME, PATTERN) matches according to the local convention.
      4 fnmatchcase(FILENAME, PATTERN) always takes case in account.
     17 __all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]
     25 def fnmatch(name, pat):
     38     If you don't want this, use fnmatchcase(FILENAME, PATTERN).
     42     return fnmatchcase(name, pat)
     70 def fnmatchcase(name, pat):
     73     This is a version of fnmatch() which doesn't case-normalize
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\glob.py
      6 import fnmatch
     17     fnmatch. However, unlike fnmatch, filenames starting with a
     30     fnmatch. However, unlike fnmatch, filenames starting with a
     97     return fnmatch.filter(names, pattern)
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\idlelib\grep.py
      6 import fnmatch
     59                     if fnmatch.fnmatch(name, pattern))
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\msilib\__init__.py
      4 import fnmatch
    386         files = fnmatch.filter(files, pattern)
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\pathlib.py
      1 import fnmatch
     44     # Whether this pattern needs actual matching using fnmatch, or can
    193         return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
    261         return re.compile(fnmatch.translate(pattern)).fullmatch
    913             if not fnmatch.fnmatchcase(part, pat):
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\shutil.py
     10 import fnmatch
    446             ignored_names.extend(fnmatch.filter(names, pattern))
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\tkinter\filedialog.py
     19 import fnmatch
    189             elif fnmatch.fnmatch(name, pat):
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\tracemalloc.py
      3 import fnmatch
    369         if not fnmatch.fnmatch(filename, self._filename_pattern):
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\unittest\loader.py
     11 from fnmatch import fnmatch, fnmatchcase
    236                 any(fnmatchcase(fullName, pattern) for pattern in self.testNamePatterns)
    382         return fnmatch(path, pattern)
 
===============================================================================
07/02/2023_18:25:14
C:\Python310\Lib\urllib\request.py
   2573     from fnmatch import fnmatch
   2620         elif fnmatch(host, value):
 
===============================================================================
Nombre de fichiers trouvés: 12 sur 856 analysés
Temps de traitement:  0.799861
856 fichiers analysés en moins d'une seconde!



Autre exemple: je recherche le mot "ProcessPoolExecutor" dans le répertoire de tous mes fichiers Python afin de retrouver mes codes de calculs parallèles déjà écrits.
Voilà l'affichage des résultats:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
===============================================================================
RECHERCHE & REMPLACE UN MOT DANS DES FICHIERS SÉLECTIONNES
Répertoire: D:\pythondev\Pydev\projetpython
Récursion: oui
Motif(s) wildcard d'inclusion des fichiers: *.py;*.pyw
Motif(s) wildcard d'exclusion des sous-répertoires:
Mot à chercher: ProcessPoolExecutor
Mot isolé: oui
Casse (maj-min) ignorée: non
Sans remplacement
 
........
........
........
 
===============================================================================
Nombre de fichiers trouvés: 69 sur 96422 analysés
Temps de traitement: 04:09.149266
96.422 fichiers analysés en 4 minutes environ! Qui a dit que Python était lent? Essayez donc de faire ça à la main!


4- FONCTION PRINCIPALE DE RECHERCHE-REMPLACEMENT

On est là au cœur du processus de recherche et de remplacement d'un mot dans un fichier par expressions régulières.
Voilà la fonction de recherche-remplacement:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Rechercheremplacemot:
    """Recherche dans le fichier le mot intégré dans le motif regex compilé
       regcomp (multilignes). Si le mot a été trouvé au moins une fois,
       retourne la liste avec les lignes concernées et leur numéro:
           [fichier, dateiso, 1ère ligne trouvée, 2ème ligne trouvée, ...]
       Si l'ordre chronologique n'est pas demandé, dateiso n'est pas calculé
       Si mot2 n'est pas vide, mot est remplacé par mot2, et le texte
       corrigé est enregistré dans le fichier, ou dans un nouveau fichier dont
       le nom a été modifié par un suffixe (option)
    """
    #========================================================================
    def __init__(self, regcomp, encodage, chrono, mot2, suffixe):
        self.regcomp = regcomp # motif regex compilé ayant le mot à chercher
        self.encodage = encodage # encodage des fichiers à lire
        self.chrono = chrono # dit si tri chronologique des résultats
        self.mot2 = mot2 # si non vide => remplacera le mot initial
        self.suffixe = suffixe # si non vide => nouveau fichier avec suffixe
 
    #========================================================================
    def __call__(self, fichier):
        """ Méthode de recherche-remplacement
        """
        with open(fichier, "r", encoding=self.encodage, errors='replace') as fs:
            lignes = fs.readlines() # lit toutes les lignes (list)
 
        texte = "".join(lignes) # recrée le texte complet du fichier (str)
 
        fic = [] # contiendra la liste des résultats
        if self.regcomp.search(texte):
            # => il y a au moins une fois le mot recherché dans tout le texte
 
            # stocke le fichier et les lignes concernées par la recherche
            fic.append(fichier)
            fic.append(secs2tempsiso(os.path.getmtime(fichier)) if self.chrono else "")
            for i, ligne in enumerate(lignes):
                if self.regcomp.search(ligne):
                    fic.append("{:7d} {}".format(i+1, ligne.rstrip()))
 
            # si remplacement demandé, enregistre le texte corrigé => fichier2
            if self.mot2: # si mot2 n'est pas vide
                # fait le remplacement
                texte2 = self.regcomp.sub(self.mot2, texte)
                # crée le nouveau nom de fichier avec suffixe
                nom, ext = os.path.splitext(fichier)
                fichier2 = nom + self.suffixe + ext
                # enregistre le texte corrigé dans fichier2 (même encodage)
                with open(fichier2, "w", encoding=self.encodage, errors='replace') as fd:
                    for ligne in texte2.split('\n'):
                        fd.write(ligne + '\n')
 
        return fic # si vide => mot non trouvé dans le contenu du fichier
On initialise cette classe avec les données que la méthode de recherche utilisera:

regcomp: motif regex compilé ayant le mot à chercher
encodage: encodage des fichiers à lire et écrire
chrono: dit si on veut un tri chronologique des résultats selon les dates des fichiers
mot2: si non vide => remplacera le mot initial
suffixe: si non vide => nouveau fichier avec suffixe

La méthode de recherche est appelée avec le nom du fichier dont on va analyser son contenu. Dans l'ordre de cette recherche:
- lecture de toutes les lignes du fichier (=> list)
- reconstitution du texte complet (=> str)
- recherche dans le texte complet du mot à trouver (.search)
- si le mot y est, on reprend toutes lignes pour stocker les lignes qui ont le mot avec leur numéro de ligne
- s'il y a remplacement, on calcule le texte corrigé (.sub), et on l'enregistre dans le même fichier ou un autre dont le nom a été modifié par un suffixe


Expression régulières pour chercher-remplacer le mot dans les fichiers

Voilà ce que j'utilise:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
if motseul:
    # motif regex pour mot isolé
    motifreg = "(?:^|.*[^" + lettres + "])(" + mot + ")(?:[^" + lettres + "].*|$)"
else:
    # motif regex pour mot non isolé
    motifreg = "(?:.*)(" + mot + ")(?:.*)"
La variable "lettres" permettra de trouver les mots isolés, puisque ceux-ci seront nécessairement précédés et suivis par des lettres différents. Cette variable "lettres" peut être adaptée aux recherches particulières, mais n'oubliez pas que les noms de classes, fonctions et variables créés par l'utilisateur avec la version 3 Python actuelle, peuvent comporter des caractères accentués! Ainsi, même si c'est rare (et peu recommandable!), une instruction Python comme la suivante est tout à fait correcte (essayez!):

Code : Sélectionner tout - Visualiser dans une fenêtre à part
àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ = 5
Il y a en plus quelques subtilités pour écrire le mot. Comme, on le voit, le mot est intégré dans le script du regex. On peut donc en l'écrivant, y ajouter des instructions regex à l'intérieur, un peu comme on le fait avec des motifs "wildcard" de recherche de noms de fichiers! Par exemple:

- On cherche le mot ayant un accent: "Présent", mais on n'est pas sûr d'avoir l'accent partout, ou même si on ne s'est pas trompé dans l'accent. On peut l'écrire comme "Pr[eéè]sent" qui trouvera "Present", "Présent" et "Prèsent". On peut même après avoir trouvé tous ces mots, les remplacer dans tous les fichiers trouvés par le nom correct "Présent".

- On peut utiliser des caractères "joker" comme on le fait couramment avec les motifs wildcard ('?' et '*' ). Pour remplacer une seule position par n'importe quel caractère, on peut utiliser '.'. Et pour en remplacer plusieurs, on peut utiliser '.*' (le '*' signifiant '0 ou plusieurs'), ou utiliser '.+' (le '+' signifiant '1 ou plusieurs'). On peut même être plus précis en demandant "n'importe quelle lettre permise dans la liste des caractères autorisés". Dans les noms Python sans caractère accentué, c'est "[a-xA-X0-9_]" (sans les chiffres si c'est la 1ère lettre du mot).

- On cherche un terme de plusieurs mots séparés par un espace, mais on n'est pas sûr de la quantité d'espaces. Par exemple "from math import". On peut l'écrire comme: "from[ ]+math[ ]+import", le [ ]+ indiquant un espace ou plusieurs.

- On cherche des fichiers ayant un ou deux mots comme "toto" et "titi": on peut écrire le mot comme "toto|titi". Les lignes trouvées pourront comporter un des deux mots ou les deux. Attention cependant, le code n'est pas actuellement prévu pour le remplacement dans ce cas, mais il peut être adapté!

-etc...

Il faut bien sûr pour ça, apprendre un peu les expressions régulières (module Python "re"): c'est puissant et rapide, mais c'est loin d'être toujours facile...

Appel de la fonction de recherche-remplacement

Dans la mesure où la recherche-remplacement dans chaque fichier est indépendante des autres fichiers, j'ai trouvé utile de faire ça en calculs parallèles avec "ProcessPoolExecutor" du module "concurrent.futures" pour exploiter les CPU multicores. En fait ici, on ne gagne pas autant qu'on le souhaiterait parce qu'une partie importante, les lectures-écritures sur disque, n'est pas "parallélisable". Chez moi, je divise tout de même avec ça le temps de traitement par 2, et c'est toujours ça de pris...


5- FONCTIONS DE BIBLIOTHÈQUE

Pour faire le traitement, il y a d'abord quelques fonctions de bibliothèques nécessaires pour cette application, mais qui peuvent aussi être utilisées dans d'autres programmes. Je les place ici dans un fichier que s'appelle "bib_recherche_fichiers.py", que j'importe dans le programme principal. Pour les codes: voir le code complet à la fin.


convfr(chaine)

Fonction nécessaire pour trier les fichiers selon l'ordre alphanumérique. En effet, si on ne fait rien, l'ordre est incorrect, parce que l'ordre "ASCII étendu" ou "ANSI" classe par exemple les majuscules après les minuscules (donc 'A' est après le 'x'), et les caractères accentués sont après les majuscules ('à' est après le 'X'), ce qui est déconcertant pour un français (et d'autres qui ont aussi des "signes diacritiques" dans leur langue...).


secs2tempsiso(secondes)

Fonction nécessaire pour convertir les secondes renvoyées par "os.path.getmtime" par exemple, en temps de type ISO comme "aaaa-mm-jj_hh-mm-ss". Le grand avantage d'écrire la date à l'envers, c'est que l'ordre alphanumérique de ces chaînes ISO coincide avec l'ordre chronologique!


tempsiso2temps(tempsiso)

Même si le temps des fichiers est calculé en temps de type ISO par la fonction précédente pour permettre le tri chronologique, il n'est pas facile à lire. Aussi cette fonction convertit en temps "normal" comme "jj/mm/aaaa_hh:mm:ss" uniquement pour l'affichage.


affichedelai(secs)

On affiche le délai du traitement à la fin, mais en seconde, il n'est vraiment pas parlant. Par exemple, 2748.562 secondes, ça fait beaucoup, mais encore? Cette fonction va convertir ce délai donné en secondes, en une chaîne comme "45:48.562000", soit 45 mn et 48.562000 sec. Selon le nombre de secondes, on pourrait avoir des jours, heures, minutes et secondes.


icherchefichiers(repertoire, inclusfics="*", exclusreps="", recursion=True, fnerreur=None, suiviliens=False)

C'est la fonction de recherche des fichiers selon les motifs wildcard donnés (fichiers et sous-répertoires). On pourrait avec un code plus simple avec le module glob, mais je préfère "os.walk" pour plusieurs raisons, et entre autres:
- récupération possible des éventuelles erreurs de lecture (alors que glob est "silencieux" en cas d'erreur)
- possibilité d'éliminer des sous-répertoires des analyses récursives suivantes (pas prévu avec glob)

On pourrait, bien sûr, ajouter des complexités comme la neutralisation des accents et/ou de la casse (majuscules - minuscules) dans la recherche des fichiers, etc... Je suis cependant resté ici dans une version simple mais suffisament évoluée pour un usage courant.

Dans les arguments, il faudra veiller à utiliser si nécessaire l'exclusion des sous-répertoires dans le cas des remplacements, parce qu'il ne faudrait pas modifier des codes Python qui doivent rester intacts.


Print2

Voilà une classe plutôt pratique. Elle affiche dans la console comme dans un simple "print", mais elle permet en plus d'enregistrer dans un fichier log s'il est donné comme argument au lancement. Et la correction du code est facile puisqu'il suffit de remplacer "print(" par "print2(" avec le traitement de texte. On peut même faire ça avec le programme que l'on décrit ici!



6- CODE COMPLET

Fonctions de bibliothèque à importer: bib_recherche_fichiers.py

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# -*- coding: utf-8 -*-
 
import os
from datetime import datetime
from fnmatch import fnmatch
 
import locale # pour tri selon le dictionnaire français
locale.setlocale(locale.LC_ALL, ('fr_FR', 'UTF-8'))
 
##############################################################################
def convfr(chaine):
    """convertit la chaine pour la trier selon le dictionnaire français
       demande avant: locale.setlocale(locale.LC_ALL, ('fr_FR', 'UTF-8'))
       exemple de tri français d'une liste': xxx.sort(key=convfr)
    """
    chaine2 = chaine.replace('\xA0', '') # supprime les blancs insécables
    chaine2 = chaine2.replace(' ', '') # supprime les espaces internes
    return locale.strxfrm(chaine2)
 
#############################################################################
def secs2tempsiso(secondes):
    """Convertit le temps en secondes depuis l'epoch, sous le format
       type ISO "aaaa-mm-jj_hh-mm-ss" (sans les microsecondes)
    """
    return str(datetime.fromtimestamp(secondes,
            tz=None).replace(microsecond=0).isoformat('_')).replace(':', '-')
 
#############################################################################
def tempsiso2temps(tempsiso):
    """Convertit le temps type ISO "aaaa-mm-jj_hh-mm-ss" en temps
       "normal" "jj/mm/aaaa_hh:mm:ss" pour en faciliter la lecture.
       Le temps ISO peut comporter ou non l'heure
    """
    an, mois, jour = tempsiso[0:4], tempsiso[5:7], tempsiso[8:10]
    return "/".join([jour, mois, an]) + tempsiso[10:].replace('-', ':')
 
#############################################################################
def affichedelai(secs):
    """Retourne une chaîne pour affichage d'un délai exprimé en secondes. Le
    résultat peut comporter jours, heures, minutes et secondes.
       Exemple: 2748.562 sec  => "45:48.562000" soit 45 mn et 48.562000 sec
       Un éventuel signe négatif est neutralisé dans le calcul mais restitué
       au résultat final
    """
    sign = "" if secs>=0 else "-"
    secs = abs(secs)
    j, r = divmod(secs, 86400) # 86400: 3600x24
    h, r = divmod(r, 3600)
    m, s = divmod(r, 60)
    j, h, m = int(j), int(h), int(m)
    if j>0:
        return "{}{:d}:{:02d}:{:02d}:{:09.6f}".format(sign, j, h, m, s)
    elif h>0:
        return "{}{:02d}:{:02d}:{:09.6f}".format(sign, h, m, s)
    elif m>0:
        return "{}{:02d}:{:09.6f}".format(sign, m, s)
    else:
        return "{}{:9.6f}".format(sign, s)
 
##############################################################################
def icherchefichiers(repertoire, inclusfics="*", exclusreps="",
                             recursion=True, fnerreur=None, suiviliens=False):
    """Retourne un par un les fichiers du répertoire, qui satisfont aux motifs
       de sélection wildcard.
       - repertoire: nom du répertoire à analyser
       - inclusfics: chaîne des motifs wildcard pour inclure des noms de
           fichier. Si plusieurs motifs => séparateur=';'.
       - exclusreps: chaîne des motifs wildcard pour exclure des noms de
           sous-répertoire. Si plusieurs motifs => séparateur=';'
       - recursion: si True, analyse aussi les sous-répertoires non exclus
       - fnerreur: fonction "callback" qui enregistre les erreurs de os.walk
       - suiviliens: si True, l'analyse suit les liens symboliques
    """
    # conversion si nécessaire des chaines de motifs wildcard en listes
    inclusfics = inclusfics.split(';') if isinstance(inclusfics, str) else inclusfics
    exclusreps = exclusreps.split(';') if isinstance(exclusreps, str) else exclusreps
 
    # recherche et retourne les fichiers trouvés un par un
    for repert, sreps, fics in os.walk(repertoire, onerror=fnerreur,
                                                     followlinks=suiviliens):
        for fic in fics:
            if any(fnmatch(fic, inclusfic) for inclusfic in inclusfics):
                yield os.path.join(repert, fic) # nom du fichier avec chemin
 
        if not recursion:
            break # récursion non demandée => arrêt de la boucle os.walk
 
        # retire les sous-répertoires à exclure des analyses suivantes
        for i in range(len(sreps[:])-1, -1, -1): # parcours à l'envers de sreps
            if any(fnmatch(sreps[i], exclusrep) for exclusrep in exclusreps):
                # on retire le sous-répertoire de la liste sreps
                sreps.pop(i)
 
##############################################################################
class Print2:
    """Affiche le déroulement du traitement dans la console
       Si demandé (fichierlog non vide), enregistre en plus sur disque
    """
 
    #=========================================================================
    def __init__(self, fichierlog="", modw="w", encodage="utf-8"):
        """Initialise l'affichage et l'ouverture du fichier log (fichierlog)
           - modw: possibilité avec 'a' d'ajouter au fichier existant'
        """
        self.flog = None
        if fichierlog:
            try:
                self.flog = open(fichierlog, modw, encoding=encodage)
            except Exception:
                # fichier log donné non conforme ou inaccessible
                self.flog = None # aucun enregistrement ne sera fait
 
    #=========================================================================
    def __call__(self, *v):
        """Affiche et enregistre dans le fichier log si demandé
        """
        print(*v, end='\n') # affiche sur le périphérique de sortie
        if self.flog:
            # enregistre sur disque
            print(*v, end='\n', file=self.flog, flush=True)
 
    #=========================================================================
    def ferme(self):
        """Ferme le fichier log s'il est ouvert
        """
        if self.flog:
            self.flog.close()

Programme principal: recherche_remplace_mot.py

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# -*- coding: utf-8 -*-
"""Dans un répertoire donné, trouve tous les fichiers qui contiennent un mot,
et les affiche avec toutes les lignes ayant ce mot, avec leur numéro de ligne.
Si demandé, remplace ce mot par un autre et enregistre le texte corrigé.
"""
 
#############################################################################
# Données de traitement
 
# répertoire à analyser
repertoire = r"C:\Python310\Lib"
 
# motifs wildcard des fichiers à inclure (séparateur=';')
inclusfics = "*.py;*.pyw"
 
# motifs wildcard des sous-répertoires à exclure (séparateur=';')
exclusreps = "site-packages;test"
 
recursion = True # si True => analyse aussi les sous-répertoires non exclus
 
encodage = "utf-8" # encodage des fichiers à lire
 
# mot à chercher
mot = "fnmatch"
# minuscules accentuées en français (signes diacritiques): "àâäçéèêëîïôöùûüÿ"
# majuscules accentuées en français (signes diacritiques): "ÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
# ligatures en français en minuscules : 'æ œ' et en majuscules: "Æ Œ"
 
motseul = False # si True: ne recherche que le mot isolé
 
ignorecasse = True # si True: neutralise la casse (majuscules-minuscules)
 
# lettres permises dans les mots recherchés (langue française)
lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
 
chrono = True # si True: affiche ordre chronologique (+récents à la fin)
 
suiviliens = False # si True: suit les liens symboliques
 
fichierlog = "recherche.txt" # si non vide => enregistre en + dans le fichier
 
# ===== Remplacement =====
mot2 = "" # si non vide => demande de remplacement de mot par mot2
 
suffixe = "_remp" # => ajoute le suffixe au nom de fichier, avant extension
 
#############################################################################
#############################################################################
# Exécution
 
import os
import re
from time import perf_counter, sleep
from concurrent.futures import ProcessPoolExecutor # pour calculs parallèles
 
# importation des fonctions de bibliothèque
from bib_recherche_fichiers import (secs2tempsiso, tempsiso2temps,
                              affichedelai, icherchefichiers, convfr, Print2)
 
#############################################################################
class Rechercheremplacemot:
    """Recherche dans le fichier le mot intégré dans le motif regex compilé
       regcomp (multilignes). Si le mot a été trouvé au moins une fois,
       retourne la liste avec les lignes concernées et leur numéro:
           [fichier, dateiso, 1ère ligne trouvée, 2ème ligne trouvée, ...]
       Si l'ordre chronologique n'est pas demandé, dateiso n'est pas calculé
       Si mot2 n'est pas vide, mot est remplacé par mot2, et le texte
       corrigé est enregistré dans le fichier, ou dans un nouveau fichier dont
       le nom a été modifié par un suffixe (option)
    """
    #========================================================================
    def __init__(self, regcomp, encodage, chrono, mot2, suffixe):
        self.regcomp = regcomp # motif regex compilé ayant le mot à chercher
        self.encodage = encodage # encodage des fichiers à lire
        self.chrono = chrono # dit si tri chronologique des résultats
        self.mot2 = mot2 # si non vide => remplacera le mot initial
        self.suffixe = suffixe # si non vide => nouveau fichier avec suffixe
 
    #========================================================================
    def __call__(self, fichier):
        """ Méthode de recherche-remplacement
        """
        try:
            with open(fichier, "r", encoding=self.encodage, errors='replace') as fs:
                lignes = fs.readlines() # lit toutes les lignes (list)
        except Exception:
            return [] # fichier non trouvé ou erreur de lecture
 
        texte = "".join(lignes) # recrée le texte complet du fichier (str)
 
        fic = [] # contiendra la liste des résultats
        if self.regcomp.search(texte):
            # => il y a au moins une fois le mot recherché dans tout le texte
 
            # stocke le fichier et les lignes concernées par la recherche
            fic.append(fichier)
            fic.append(secs2tempsiso(os.path.getmtime(fichier)) if self.chrono else "")
            for i, ligne in enumerate(lignes):
                if self.regcomp.search(ligne):
                    fic.append("{:7d} {}".format(i+1, ligne.rstrip()))
 
            # si remplacement demandé, enregistre le texte corrigé => fichier2
            if self.mot2: # si mot2 n'est pas vide
                # fait le remplacement
                texte2 = self.regcomp.sub(self.mot2, texte)
                # crée le nouveau nom de fichier avec suffixe
                nom, ext = os.path.splitext(fichier)
                fichier2 = nom + self.suffixe + ext
                # enregistre le texte corrigé dans fichier2 (même encodage)
                with open(fichier2, "w", encoding=self.encodage, errors='replace') as fd:
                    for ligne in texte2.split('\n'):
                        fd.write(ligne + '\n')
 
        return fic # si vide => mot non trouvé dans le contenu du fichier
 
#############################################################################
 # motif regex
 
if motseul:
    # motif pour mot isolé
    motifreg = "(?:^|.*[^" + lettres + "])(" + mot + ")(?:[^" + lettres + "].*|$)"
else:
    # motif pour mot non isolé
    motifreg = "(?:.*)(" + mot + ")(?:.*)"
 
# options regex
options = re.MULTILINE # => la recherche ne s'arrête pas en fin de ligne
if ignorecasse:
    options |= re.IGNORECASE # ignore la casse (majuscules-minuscules)
 
# compile le motif regex avec ses options
regcomp = re.compile(motifreg, options)
 
#############################################################################
if __name__ == "__main__":
 
    #========================================================================
    # recherche des fichiers du répertoire satisfaisant aux motifs wildcard
    erreurs = [] # pour collecter les éventuelles erreurs de os.walk
    fics = list(icherchefichiers(repertoire, inclusfics=inclusfics,
                    exclusreps=exclusreps, recursion=recursion,
                    fnerreur=erreurs.append, suiviliens=suiviliens))
    cpt = len(fics) # compte le nombre de fichiers à analyser
 
    #========================================================================
    # Analyse tous les fichiers à analyser (calculs parallèles)
    fichiers = []
    tps = perf_counter()
    rechercheremplacemot = Rechercheremplacemot(regcomp, encodage, chrono,
                                                               mot2, suffixe)
    with ProcessPoolExecutor() as executor:
        for fichier in executor.map(rechercheremplacemot, fics):
            if fichier: # si fichier non vide
                fichiers.append(fichier)
    tps = perf_counter()-tps
 
    #========================================================================
    # tri(s) pour l'affichage
 
    # tri pour affichage dans l'ordre alphanumérique français des fichiers
    fichiers.sort(key=lambda v: convfr(v[0]))
    if chrono:
        # tri pour affichage dans l'ordre chronologique des fichiers
        fichiers.sort(key=lambda v: v[1]) # +récents à la fin
 
    #========================================================================
    # Initialise la fonction d'affichage
    print2 = Print2(fichierlog) # si fichierlog non vide => enregistre en +
 
    #========================================================================
    # Affichage du titre
    print2()
    print2("="*79)
    print2("RECHERCHE & REMPLACE UN MOT DANS DES FICHIERS SELECTIONNES ")
    print2("Répertoire analysé:", repertoire)
    print2("Récursion:", "oui" if recursion else "non")
    print2("Motifs wildcard d'inclusion des fichiers:", inclusfics)
    print2("Motifs wildcard d'exclusion des sous-répertoires:", exclusreps)
    print2("Mot à chercher:", mot)
    print2("Mot isolé:", "oui" if motseul else "non")
    print2("Casse (maj-min) ignorée:", "oui" if ignorecasse else "non")
    if mot2:
        print2("Mot remplacé par:", mot2)
        if suffixe:
            print2("Nouveau fichier avec suffixe:", suffixe)
        else:
            print2("Mise à jour du fichier d'origine")
    else:
        print2("Sans remplacement")
 
    #========================================================================
    # affiche les résultats
    for fichier in fichiers:
        print2()
        print2("="*79)
        if chrono:
            print2(tempsiso2temps(fichier[1]))
        print2(fichier[0])
        for i in range(2, len(fichier)):
            print2(fichier[i]) # affiche la ligne contenant le mot cherché
    print2()
 
    if erreurs: # erreurs rencontrées pendant la recherche des fichiers
        print2("="*79)
        print2("Erreur(s) rencontrée(s):", len(erreurs))
        for erreur in erreurs:
            print2(erreur)
        print2()
 
    print2("="*79)
    print2("Nombre de fichiers trouvés:", len(fichiers), "sur", cpt, "analysés")
    print2("Temps de traitement:", affichedelai(tps))
    print2()
 
    #========================================================================
    # Ferme le fichier log s'il a été demandé
    print2.ferme()

Bonnes recherches-remplacements!