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:
856 fichiers analysés en moins d'une seconde!
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
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:
96.422 fichiers analysés en 4 minutes environ! Qui a dit que Python était lent? Essayez donc de faire ça à la main!
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
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:
On initialise cette classe avec les données que la méthode de recherche utilisera:
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
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:
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
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 + ")(?:.*)"
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:
Code : Sélectionner tout - Visualiser dans une fenêtre à part àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜ = 5
- 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!
Partager