IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez Python Discussion :

Chercher et remplacer un mot dans plusieurs fichiers "texte"


Sujet :

Contribuez Python

  1. #1
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut Chercher et remplacer un mot dans plusieurs fichiers "texte"
    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!

  2. #2
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 329
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 329
    Points : 36 848
    Points
    36 848
    Par défaut
    Citation Envoyé par tyrtamos Voir le message
    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
    Bel effort!
    Personnellement, depuis 40 ans, j'utilise grep et sed (ou utilitaires équivalents qui viennent avec tout environnement système).

    - W

  3. #3
    Membre chevronné
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2003
    Messages
    1 582
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Industrie

    Informations forums :
    Inscription : Février 2003
    Messages : 1 582
    Points : 2 030
    Points
    2 030
    Par défaut
    100.000 !!!

    Et bé !!!

    Personnellement, ça m'arrive de temps à autre de procéder à des recherches de ce genre. J'utilise (Windows) Git Bash Here, une console installée avec Git et qui permet de faire du grep puisqu'émulant des commandes UNIX.

  4. #4
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 201
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 201
    Points : 4 665
    Points
    4 665
    Par défaut
    bonjour

    petit problème au premier test :
    suiviliens ne s'applique qu'aux répertoires et non aux fichiers python

    J'avais un lien symbolique cassé sur un fichier et l'erreur n'est pas traitée donc arrêt brutal de la recherche

    dans Rechercheremplacemot.__call__()
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
        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 FileNotFoundError:
                lignes = []
    plus ce problème de lien fichier python cassé

    -------------------

    J'ai mon propre script Go qui fait la même chose, j'ai en plus l'utilisation des couleurs (du regex trouvé) lors de l'affichage en console. Cela facilite grandement la lecture du résultat.

    A voir ... Exclure la ligne si elle débute par #.

    -------------------

    Note: je ne suis vraiment pas fan de mettre les paramètres en dur dans le fichier source ...
    Ou alors, faire un :
    script.py cherche=yy dir=xx -r --save # qui modifie le fichier
    script.py --env # qui affiche les paramètres par défaut (-h le fait aussi)

  5. #5
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Citation Envoyé par papajoker Voir le message
    J'avais un lien symbolique cassé sur un fichier et l'erreur n'est pas traitée donc arrêt brutal de la recherche

    dans Rechercheremplacemot.__call__()
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
        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 FileNotFoundError:
                lignes = []
    plus ce problème de lien fichier python cassé

    -------------------

    J'ai mon propre script Go qui fait la même chose, j'ai en plus l'utilisation des couleurs (du regex trouvé) lors de l'affichage en console. Cela facilite grandement la lecture du résultat.

    A voir ... Exclure la ligne si elle débute par #.
    Merci pour les infos, papajoker. Ok pour ajouter le try...except. Il peut encore y avoir d'autres cas de ce genre, mais celui-là est facile à réparer. J'avais bien dit qu'il fallait ajouter des "parachutes"... J'ai corrigé le code complet ci-dessus en conséquence.

    Pour suiviliens: il s'agit simplement de l'argument "followlinks" de os.walk.

    Pour neutraliser les commentaires: je ne sais pas, car d'expérience, le fait que le mot ait été trouvé dans un commentaire m'a déjà rendu service.

    Pour les couleurs dans la console (sous Windows), je suis intéressé: comment fais-tu?


    @ Arioch et wiztricks

    Je sais qu'il existe déjà des utilitaires pour faire ça, comme grep que j'ai pas mal utilisé sous Linux. Mais quand on a accès au code, on peut introduire des particularités qui n'ont aucune chance d'être traitées ailleurs. Et il faut écrire des lignes de commande que je n'aime pas, et c'est pour ça que j'ai soigneusement évité d'utiliser argparse. Comme je l'ai dit au début, je suis en train de généraliser ça pour la plupart des utilitaires que j'utilise souvent, qu'ils soient ou non liés au développement (traitement vidéo, recherche de photos selon leur date de prise de vue, génération de vignettes jpg, etc... ).

  6. #6
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 329
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 329
    Points : 36 848
    Points
    36 848
    Par défaut
    Citation Envoyé par tyrtamos Voir le message
    Je sais qu'il existe déjà des utilitaires pour faire ça, comme grep que j'ai pas mal utilisé sous Linux. Mais quand on a accès au code, on peut introduire des particularités qui n'ont aucune chance d'être traitées ailleurs.
    Il est quand même utile de signaler que des utilitaires existent et que votre code répond a un besoin particulier (le monde selon Tyrtamos).

    Quant à grep et à sed, ce sont des mini-langages assez complets (qui prennent du temps à être maîtrisés) qu'il faut essayer de maîtriser. Imaginez avoir à diagnostiquer une application python sur un système de production, on va triturer le code avec les outils dont on dispose (et ce ne sera pas un environnement de développement).

    - W

  7. #7
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Citation Envoyé par wiztricks Voir le message
    Il est quand même utile de signaler que des utilitaires existent et que votre code répond a un besoin particulier
    Oui! Je le fais d'habitude, et j'ai oublié de le faire ici.

    Citation Envoyé par wiztricks Voir le message
    (le monde selon Tyrtamos).

  8. #8
    Modérateur
    Avatar de N_BaH
    Profil pro
    Inscrit en
    Février 2008
    Messages
    7 586
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2008
    Messages : 7 586
    Points : 19 468
    Points
    19 468
    Par défaut
    Citation Envoyé par wiztricks
    Quant à grep et à sed, ce sont des mini-langages[...]
    sed est un langage, grep, lui, n'est qu'un outil qui recherche des regex.

  9. #9
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 329
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 329
    Points : 36 848
    Points
    36 848
    Par défaut
    Citation Envoyé par N_BaH Voir le message
    sed est un langage, grep, lui, n'est qu'un outil qui recherche des regex.
    Les expressions régulières sont un mini-langage.

    - W

  10. #10
    Modérateur
    Avatar de N_BaH
    Profil pro
    Inscrit en
    Février 2008
    Messages
    7 586
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2008
    Messages : 7 586
    Points : 19 468
    Points
    19 468
    Par défaut
    pas d'accord : les regex ne sont qu'une description normalisée, elles n'ont aucune action. Ce n'est qu'une donnée.

  11. #11
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 201
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 201
    Points : 4 665
    Points
    4 665
    Par défaut
    Citation Envoyé par tyrtamos Voir le message
    Pour les couleurs dans la console (sous Windows), je suis intéressé
    Clairement pas un spécialiste de cet os (utilisateur très très occasionnel)

    - Il semble qu'il n'y a qu'une clé à écrire dans la base de registre pour être compatible avec les codes linux
    HKEY_CURRENT_USER Console VirtualTerminalLevel = 1

    sinon utiliser la lib python : colorama

    existe déjà des utilitaires pour faire ça, comme grep
    Quitte à coder, je préfère coder dans un autre langage que bash un tel script : plus de souplesse/options et surtout le parallélisme

  12. #12
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 329
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 329
    Points : 36 848
    Points
    36 848
    Par défaut
    Citation Envoyé par N_BaH Voir le message
    pas d'accord : les regex ne sont qu'une description normalisée, elles n'ont aucune action. Ce n'est qu'une donnée.
    Les expressions régulières ont alphabet, syntaxe et règles de grammaire, ... à appliquer pour les construire.
    Après vous pensez un peu ce que vous voulez... mais documentez vous d'abord (sur ce que sont les langages formels par exemple).
    note: un script python n'est aussi qu'une donnée: sans interpréteur, pas d'action.

    - W

  13. #13
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Bonjour

    Amélioration importante du programme, particulièrement pour l'accélérer (merci à papajoker qui m'a talonné pour ça ).

    Principales améliorations:


    1- motifs regex

    Les motifs regex ont été simplifiés. Le principe est qu'un mot isolé est entouré par un caractère ne faisant pas partie de ceux autorisés dans le mot.

    La variable "lettres", qui fait partie des données de traitement et qui peut donc être modifiée par l'utilisateur, donne la liste des caractères autorisés dans le mot. Pour un mot clé de Python, on peut se contenter de "a-xA-X0-9_". Pour un nom crée par l'utilisateur, il faut ajouter les caractères accentués puisqu'ils sont permis dans Python v3. Mais si on cherche un mot quelconque de la langue française, rien n'empêche d'ajouter une ligature ("œ" pour trouver "cœur" ), une apostrophe ("'" pour trouver "aujourd'hui") ou un tiret ("-" pour trouver "tête-bêche").

    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
    lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
     
    if motseul:
        # motif pour mot isolé
        motifreg = r"(?:^|[^" + lettres + r"])(" + mot + r")(?:[^" + lettres + r"]|$)"
     
    else:
        # motif pour mot non isolé
        motifreg = r"(?:^|.)(" + mot + r")(?:.|$)"
     
    # 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)

    2- méthode de recherche et remplacement

    Sans changer sa logique, la classe "Rechercheremplacemot" a été accélérée. La lecture du fichier est en binaire, le calcul des lignes du texte ne se fait que si le mot a été détecté, etc...:

    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
    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 est demandé, dateiso est calculé en plus
           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 avant extension (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 demandé
            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, "rb") as fs: # lecture binaire
                    contenu = fs.read() # lit tout le contenu du fichier (bytes)
            except Exception:
                print("Erreur de lecture du fichier:", fichier)
                return [] # fichier non trouvé ou erreur de lecture
     
            # conversion des bytes en str
            texte = str(contenu, encoding=self.encodage, errors='replace')
     
            #====================================================================
            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 "")
                lignes = texte.splitlines() # découpage du texte en lignes
                for i, ligne in enumerate(lignes):
                    ligne2 = ligne.strip()
                    if ligne2 and self.regcomp.search(ligne2):
                        # stocke la ligne trouvée dans la liste fic
                        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 (s'il existe)
                    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.splitlines():
                            fd.write(ligne + '\n')
     
            return fic # si vide => mot non trouvé dans le contenu du fichier

    3- le programme complet

    Pour simplifier la diffusion, les fonctions de bibliothèques, qui faisaient partie d'un autre fichier à importer, ont été intégrées dans le programme principal. Mais, bien sûr, si vous créez d'autres programmes qui utilisent ces fonctions, vous avez intérêt à les mettre en commun dans un fichier bibliothèque.

    Rappelons que pour créer une nouvelle recherche, l'utilisateur ne modifie que la 1ère partie "données de traitement", comme il le ferait si c'était un programme graphique, et que le reste du code (la machinerie de recherche-remplacement) n'a pas besoin d'être vue.

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    # -*- 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 (+ 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"D:\pythondev\Pydev\projetpython"
     
    # motifs wildcard des fichiers à inclure dans la recherche (séparateur=';')
    inclusfics = "*.py;*.pyw"
     
    # motifs wildcard des répertoires à exclure de la recherche (séparateur=';')
    exclusreps = "winpython;utilscripts"
     
    recursion = True # si True => analyse aussi les sous-répertoires non exclus
     
    encodage = "utf-8" # encodage des fichiers à lire
     
    # mot à chercher
    mot = "ProcessPoolExecutor" # du module "concurrent.futures"
     
    motseul = True # si True: ne recherche que le mot isolé
     
    ignorecasse = False # si True: neutralise la casse (majuscules-minuscules)
     
    # lettres permises dans les mots recherchés (langue française)
    lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
    # 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: "Æ Œ"
     
    chrono = True # si True: affiche ordre chronologique (+récents à la fin)
     
    suiviliens = False # si True: suit les liens symboliques
     
    fichierlog = "" # 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
    from datetime import datetime
    from fnmatch import fnmatch
     
    from concurrent.futures import ProcessPoolExecutor # pour calculs parallèles
     
    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) # 3600: 60x60
        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 (avec son chemin)
           - 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 chaînes de motifs wildcard en listes
        if isinstance(inclusfics, str):
            inclusfics = inclusfics.split(';')
        if isinstance(exclusreps, str):
            exclusreps = exclusreps.split(';')
     
        # 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
                if any(fnmatch(sreps[i], exclusrep) for exclusrep in exclusreps):
                    # on retire le sous-répertoire à exclure 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
               - encodage: encodage du fichier log à écrire
            """
            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 dans la console et enregistre dans fichierlog 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()
     
    #############################################################################
     # motif regex
     
    if motseul:
        # motif pour mot isolé
        motifreg = r"(?:^|[^" + lettres + r"])(" + mot + r")(?:[^" + lettres + r"]|$)"
     
    else:
        # motif pour mot non isolé
        motifreg = r"(?:^|.)(" + mot + r")(?:.|$)"
     
    # 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)
     
    #############################################################################
    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 est demandé, dateiso est calculé en plus
           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 avant extension (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 demandé
            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, "rb") as fs: # lecture binaire
                    contenu = fs.read() # lit tout le contenu du fichier (bytes)
            except Exception:
                print("Erreur de lecture du fichier:", fichier)
                return [] # fichier non trouvé ou erreur de lecture
     
            # conversion des bytes en str
            texte = str(contenu, encoding=self.encodage, errors='replace')
     
            #====================================================================
            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 "")
                lignes = texte.splitlines() # découpage du texte en lignes
                for i, ligne in enumerate(lignes):
                    ligne2 = ligne.strip()
                    if ligne2 and self.regcomp.search(ligne2):
                        # stocke la ligne trouvée dans la liste fic
                        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 (s'il existe)
                    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.splitlines():
                            fd.write(ligne + '\n')
     
            return fic # si vide => mot non trouvé dans le contenu du fichier
     
    #############################################################################
    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 trouvés (calculs parallèles)
        fichiers = [] # pour stocker les fichiers avec les lignes concernés
        tps = perf_counter() # pour comptage du temps de traitement
        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 à remplacer 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()

    4- Exemples de résultat

    Je recherche le mot "ProcessPoolExecutor" pour me dire où sont mes codes Python qui utilisent le calcul parallèle (pour utiliser les cœurs de mon CPU), et ceci dans tous mes fichiers de développement Python. J'ai exclus de l'analyse les répertoires "winpython" (python portable sous Windows) et "utilscripts" (le présent répertoire de développement de ce programme). Le résultat sera rangé dans l'ordre chronologique pour me permettre d'accéder aux codes les plus récents. Voilà le résultat obtenu à la 1ère exécution (extrait):

    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
    ===============================================================================
    RECHERCHE & REMPLACE UN MOT DANS DES FICHIERS SELECTIONNES
    Répertoire analysé: D:\pythondev\Pydev\projetpython
    Récursion: oui
    Motifs wildcard d'inclusion des fichiers: *.py;*.pyw
    Motifs wildcard d'exclusion des sous-répertoires: winpython;utilscripts
    Mot à chercher: ProcessPoolExecutor
    Mot isolé: oui
    Casse (maj-min) ignorée: non
    Sans remplacement
     
    ===============================================================================
    01/08/2012_09:50:42
    D:\pythondev\Pydev\projetpython\diff_repert_dircmp\diffrepert06_qprocess_ok\ppython3\Lib\concurrent\futures\process.py
          4 """Implements ProcessPoolExecutor.
         57 # ProcessPoolExecutor's process pool (i.e. shutdown() was not called). However,
        183         executor_reference: A weakref.ref to the ProcessPoolExecutor that owns
        184             this thread. Used to determine if the ProcessPoolExecutor has been
        262 class ProcessPoolExecutor(_base.Executor):
        264         """Initializes a new ProcessPoolExecutor instance.
     
    ===============================================================================
    01/08/2012_09:58:24
    D:\pythondev\Pydev\projetpython\diff_repert_dircmp\diffrepert06_qprocess_ok\ppython3\Lib\test\test_concurrent_futures.py
         88     executor_type = futures.ProcessPoolExecutor
        167         with futures.ProcessPoolExecutor(max_workers=5) as e:
        176         executor = futures.ProcessPoolExecutor(max_workers=5)
     
    ===============================================================================
    22/06/2014_22:50:16
    D:\pythondev\Pydev\projetpython\PyQt5\consolepy\consolepy_trayD_qt5_ok\build\consolepy_exe\Lib\concurrent\futures\process.py
          4 """Implements ProcessPoolExecutor.
         61 # ProcessPoolExecutor's process pool (i.e. shutdown() was not called). However,
        187         executor_reference: A weakref.ref to the ProcessPoolExecutor that owns
        188             this thread. Used to determine if the ProcessPoolExecutor has been
        318     Raised when a process in a ProcessPoolExecutor terminated abruptly
        323 class ProcessPoolExecutor(_base.Executor):
        325         """Initializes a new ProcessPoolExecutor instance.
     
    ...
    ...
    ...
     
    ===============================================================================
    22/10/2022_15:00:18
    D:\pythondev\Pydev\projetpython\concurrent_futures\essai04\test05.py
         40     with concurrent.futures.ProcessPoolExecutor() as executor:
        144     with concurrent.futures.ProcessPoolExecutor() as executor:
     
    ===============================================================================
    27/02/2023_15:40:25
    D:\pythondev\Pydev\projetpython\cherche_fichiers\cherchefichiersdate\bibcherchefichiers.py
         22 from concurrent.futures import ProcessPoolExecutor
        205     with ProcessPoolExecutor() as executor:
     
    ===============================================================================
    30/03/2023_05:17:55
    D:\pythondev\Pydev\projetpython\@programmes\cherchedoublons\doublons_1rep\_bibdoublons.py
         22 from concurrent.futures import ProcessPoolExecutor
        200     with ProcessPoolExecutor() as executor:
     
    ===============================================================================
    Nombre de fichiers trouvés: 63 sur 83421 analysés
    Temps de traitement: 01:43.330878
    On a maintenant une recherche qui a duré 1 minute et 43 secondes, mais c'était la 1ère exécution, qui a demandé à l'OS de lire physiquement le disque dur (normal ici, et non SSD). On n'a donc pas ici la durée de l'algorithme seul.

    Maintenant, on exécute une 2ème fois la même recherche, ou une autre recherche sur le même répertoire:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    ===============================================================================
    Nombre de fichiers trouvés: 63 sur 83421 analysés
    Temps de traitement: 26.838831
    27 secondes ici au lieu de 1 minute et 43 secondes. On a, bien sûr, le même résultat, mais celui-ci a demandé un temps de traitement divisé par 4, parce que l'OS a utilisé ses mémoires cache, plutôt que de relire physiquement le disque dur.

    Dans cette 2ème exécution qui mesure mieux le temps passé par l'algorithme de recherche, il faut avoir conscience de sa rapidité étonnante: on a tout de même analysé 83421 fichiers Python en 27 secondes, soit 0.32 millisecondes par fichier! Qui a dit que Python était lent?

    Bonnes recherches-remplacements !


    [Edit] Ajout d'une fonction supplémentaire pour l'affichage: mettre en couleur sur console les mots trouvés dans les lignes de code.

    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
    def metcouleur(chaine, regcomp, avant="\u001b[32;1m", apres="\u001b[0m"):
        """Retourne une nouvelle chaine dans laquelle tous les mots trouvés par
           le motif regex compilé regcomp, sont précédés par "avant" et suivi
           par "apres".
           Utilisation: afficher la chaine avec les mots en couleur sur console
        """
        result = []
        pos = 0
        while True: # trouve tous les mots avec le motif regex regcomp
            m = regcomp.search(chaine, pos)
            if m is None:
                break
            pos = m.end()
            result.append([m.start(), pos])
        for i1, i2 in result[::-1]: # parcours en sens inverse
            chaine = chaine[:i1] + avant + chaine[i1:i2] + apres + chaine[i2:]
        return chaine
    La correction du code est évidente: au lieu d'afficher la ligne, on affiche metcouleur(ligne, regcomp). La couleur par défaut ici est le vert.

  14. #14
    Membre chevronné
    Homme Profil pro
    Enseignant
    Inscrit en
    Juin 2013
    Messages
    1 609
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Enseignant
    Secteur : Enseignement

    Informations forums :
    Inscription : Juin 2013
    Messages : 1 609
    Points : 2 073
    Points
    2 073
    Par défaut
    Bonjour tyrtamos,
    J'admire à chaque fois ton travail !
    Je me sers d'un programme qui cherche des chaînes de caractères dans des fichiers de tous types, y compris pdf si besoin. J'imagine que mon programme est tout pourri, en même temps, en phase avec mon niveau en Python et je n'oserais pas le partager -)
    Je l'ai fait en version tkinter, que je trouve beaucoup plus agréable à l'utilisation : choix du répertoire, choix des extensions, ...
    Il me semble que tu connais Qt5, cela pourrait être une idée d'amélioration ?
    Bonne semaine à tous et toutes.

  15. #15
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Bonjour marco056,

    Citation Envoyé par marco056 Voir le message
    J'admire à chaque fois ton travail !
    Merci, ça fait plaisir!

    Citation Envoyé par marco056 Voir le message
    ...y compris pdf si besoin..
    Ça, ça peut m'intéresser: comment fais-tu?

    Citation Envoyé par marco056 Voir le message
    Il me semble que tu connais Qt5, cela pourrait être une idée d'amélioration ?
    Mais ce que j'ai fait est un programme graphique! à part que ça s'appelle un éditeur de texte et que ce n'est pas moi qui l'ait développé!

    Il est vrai que je fais à peu près ce que je veux avec PyQt, mais les développements sont longs, et au bout d'un an ou deux, les évolutions des codes en fonction des besoins sont pénibles (les codes graphiques sont complexes). D'autant plus que les versions de PyQt changent, en s'accompagnant d'incompatibilités! Concrètement, il faudra que je change tous mes programmes PyQt5 en PyQt6, ce que je n'ai pas encore fait!
    Par exemple, j'avais développé des programmes en PyQt5 pour la recherche de fichiers, la vérification par pylint et la conversion de vidéos en mp4. Ils fonctionnent très bien, mais dès que je veux les modifier, c'est compliqué, parce qu'il faut que je me rappelle comment j'ai fait il y a deux ou trois ans... Ce n'est pas le problème de l'algorithme du traitement lui-même, mais de la "machinerie" graphique qui manipule les données.

    J'ai une quinzaine d'utilitaires que j'utilise souvent, et pas seulement pour le développement Python: recherche fichiers avec leur date, traitement vidéos, traitement photos, sauvegarde incrémentale, etc... Je suis en train de convertir tous ces utilitaires comme le programme de ce fil. Cela ne concerne, bien sûr, que des petits utilitaires! Et je ne me voyais pas faire une douzaine de développements supplémentaires en PyQt pour ces petits utilitaires!

    Je place tous ces utilitaires dans une structure d'arbre sur disque:

    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
    utilscripts
        cherchefichiers
    	cherche_fichiers.py
    	cherche_fichiers_simil.py
    	cherche_fichiers_date.py
    	recherche_remplacement_mot.py
    	...
        sauvegardes
            sauvincrem.py
            synchronise.py
            ...
        developpements
    	verif_pylint.py
            colorise.py
            ...
        photos
            cherche_photos_date.py
    	crea_vignettes.py
    	liste_exif.py
            supprime_exif.py
    	...
        musiques
    	telech_youtube.py
    	...
        vidéos
    	conversion_mp4.py
    	reparation_video.py
    	extrait_audio.py
    	...
    Je crée ensuite un raccourci sur le bureau pour lancer spyder sur la racine des utilitaires utilscript ('spyder.exe -p r"chemin\utiliscripts"')

    Ensuite, après avoir chargé spyder avec un simple double-clic sur le raccourcis du bureau:
    - Je navigue dans spyder avec la souris pour charger l'utilitaire .py dont j'ai besoin
    - Je suis ensuite dans l'éditeur de texte. J'ai les données de traitement de la dernière fois (mémoire inter-session). Je modifie certaines données pour m'adapter à ce que je veux.
    - Puisque spyder est un IDE, et pas seulement un éditeur de texte, je lance l'utilitaire Python affiché avec la souris!
    - Celui-ci fait le boulot prévu et lance la console pour afficher le résultat (et enregistre un fichier log si demandé).

    Si tu suis bien, tout s'est passé avec la souris, sauf qu'il a bien fallu que j'écrive les données de traitement qu'il fallait. Mais je l'aurais fait de toute façon avec un programme graphique PyQt. En contrepartie, j'ai dans ma solution des commentaires et des petites réserves de codes que je n'aurais pas eu avec le programme graphique.

    J'ai donc une solution graphique d'utilisation aussi pratique que la solution PyQt5, à part que je pourrai faire évoluer mon code plus facilement en fonction de mes besoins!

  16. #16
    Membre chevronné
    Homme Profil pro
    Enseignant
    Inscrit en
    Juin 2013
    Messages
    1 609
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Enseignant
    Secteur : Enseignement

    Informations forums :
    Inscription : Juin 2013
    Messages : 1 609
    Points : 2 073
    Points
    2 073
    Par défaut
    Je suis au boulot et je n'ai pas la dernière version de mon programme mais j'ai un ancien avec :
    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
    from PyPDF2 import PdfFileReader, PdfFileWriter
     
    def pdf_splitter(path):
        """ découpe le fichier pdf page par page """
        fname = os.path.splitext(os.path.basename(path))[0]
     
        pdf = PdfFileReader(path)
        for page in range(pdf.getNumPages()):
            pdf_writer = PdfFileWriter()
            pdf_writer.addPage(pdf.getPage(page))
     
            output_filename = '{}_page_{}.pdf'.format(
                fname, page+1)
     
            with open(output_filename, 'wb') as out:
                pdf_writer.write(out)
     
            print('Created: {}'.format(output_filename))
     
     
    def num_pages(file):
        """ retourne le nombre de pages du fichier pdf """
        pdf = PdfFileReader(file)
        return pdf.getNumPages()
     
    def text_extractor(file,num_page):
        """ extrait le texte du fichier file (pdf) de la page num_page """
        with open(file, 'rb') as f:
            pdf = PdfFileReader(f)
            page = pdf.getPage(num_page)
     
            text = page.extractText()
            return text
     
    def recherche_pdf(chaine, path):
        cpt = 0
        for root, directories, files in os.walk(path):  
            for file in files:
                if file.endswith('.pdf'):
                    nb_pages = num_pages(os.path.join(root,file))
                    for i in range(nb_pages):
                        my_text = str(text_extractor(os.path.join(root,file),i))
                        if chaine.lower().replace(' ', '') in my_text.lower():
                            print("Présent dans le nom du fichier ici :", os.path.join(root,file), "page", i)
                            cpt = cpt + 1
        print(mot, "présent dans ", cpt, "fichiers pdf")

  17. #17
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Bonjour

    Merci marco056 pour ta solution de chercher dans un fichier pdf. Je l'accepte d'autant plus que je connais bien PyPDF2 pour l'avoir souvent utilisé pour faire du "split and merge". J'avais même eu des échanges d'emails avec l'éditeur. En effet, le regroupement de pages nécessite de laisser les fichiers concernés ouverts, mais le nombre de fichiers ouverts en même temps est limité par l'OS (Windows pour moi) à environ 500, et moi j'avais plus de 500 fichiers pdf à combiner. J'ai trouvé comme astuce de faire des assemblages progressifs: par exemple, par groupe de 200, puis assemblage des groupes de 200 entre eux, etc... Ça marche très bien!

  18. #18
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 478
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 478
    Points : 9 278
    Points
    9 278
    Billets dans le blog
    6
    Par défaut
    Bonjour

    Petite amélioration du remplacement. En effet, la solution actuelle a tendance à "manger" les caractères avant et après dans le remplacement, ce qui n'est pas correct.

    Or, il est nécessaire pour ce remplacement, d'utiliser le même motif regex de recherche pour trouver le mot à remplacer, puisque dans le cas d'un mot isolé, il ne faudrait pas que le même mot non isolé soit lui aussi remplacé. Et on ne pourrait pas utiliser le classique "replace" des strings, puisqu'on permet des instructions regex à l'intérieur du mot recherché.

    Voilà la petite astuce que j'utilise désormais, qui ne concerne que la classe "Rechercheremplacemot":

    - ajout d'une nouvelle méthode:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    def remplace(self, x):
        return x.group(0).replace(x.group(1), self.mot2)
    - et modification de l'instruction de remplacement:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    texte2 = self.regcomp.sub(self.remplace, texte)
    Ainsi, l'instruction de remplacement sub appelle la méthode "remplace" qui renvoie le mot à remplacer.

    Voilà le code complet du programme avec cette correction:

    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
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    # -*- 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 (+ 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"D:\pythondev\Pydev\projetpython"
     
    # motifs wildcard des fichiers à inclure dans la recherche (séparateur=';')
    inclusfics = "*.py;*.pyw"
     
    # motifs wildcard des répertoires à exclure de la recherche (séparateur=';')
    exclusreps = "winpython*;utilscripts"
     
    recursion = True # si True => analyse aussi les sous-répertoires non exclus
     
    encodage = "utf-8" # encodage des fichiers à lire
     
    # mot à chercher
    mot = "ProcessPoolExecutor" # du module "concurrent.futures"
     
    motseul = True # si True: ne recherche que le mot isolé
     
    ignorecasse = False # si True: neutralise la casse (majuscules-minuscules)
     
    # lettres permises dans les mots recherchés (langue française)
    lettres = "a-xA-X0-9_àâäçéèêëîïôöùûüÿÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸ"
    # 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: "Æ Œ"
     
    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 sys
    import os
    import re
    from time import perf_counter # pour mesure du temps de traitement
     
    from concurrent.futures import ProcessPoolExecutor # pour calculs parallèles
     
    # importe les fonctions de bibliothèque utilisées
    sys.path.insert(0, os.path.abspath("../")) # ajout chemin de la bibliothèque
    from biblio.bib_gene import (convfr, Print2, affichedelai, secs2tempsiso,
                                 tempsiso2temps)
    from biblio.bib_cherche_fichiers import (icherchefichiers)
     
    import locale # pour tri selon le dictionnaire français
    locale.setlocale(locale.LC_ALL, ('fr_FR', 'UTF-8'))
     
    #############################################################################
     # motif regex
     
    if motseul:
        # motif pour mot isolé
        motifreg = r"(?:^|[^" + lettres + r"])(" + mot + r")(?:[^" + lettres + r"]|$)"
    else:
        # motif pour mot non isolé
        motifreg = r"(?:^|.)(" + mot + r")(?:.|$)"
     
    # 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)
     
    #############################################################################
    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 est demandé, dateiso est calculé en plus
           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 avant extension (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 demandé
            self.mot2 = mot2 # si non vide => remplacera le mot initial
            self.suffixe = suffixe # si non vide => nouveau fichier avec suffixe
     
        #========================================================================
        def remplace(self, x):
            return x.group(0).replace(x.group(1), self.mot2)
     
        #========================================================================
        def __call__(self, fichier):
            """ Méthode de recherche-remplacement
            """
            #====================================================================
            try:
                with open(fichier, "rb") as fs: # lecture binaire
                    contenu = fs.read() # lit tout le contenu du fichier (bytes)
            except Exception:
                print("Erreur de lecture du fichier:", fichier)
                return [] # fichier non trouvé ou erreur de lecture
     
            # conversion des bytes en str
            texte = str(contenu, encoding=self.encodage, errors='replace')
     
            #====================================================================
            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 "")
                lignes = texte.splitlines() # découpage du texte en lignes
                for i, ligne in enumerate(lignes):
                    ligne2 = ligne.strip()
                    if ligne2 and self.regcomp.search(ligne2):
                        # stocke la ligne trouvée dans la liste fic
                        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.remplace, texte)
                    # crée le nouveau nom de fichier avec suffixe (s'il existe)
                    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.splitlines():
                            fd.write(ligne + '\n')
     
            return fic # si vide => mot non trouvé dans le contenu du fichier
     
    #############################################################################
    def metcouleur(chaine, regcomp, avant="\u001b[32;1m", apres="\u001b[0m"):
        """Retourne une nouvelle chaine dans laquelle tous les mots trouvés par
           le motif regex compilé regcomp, sont précédés par "avant" et suivi
           par "apres".
           Utilisation: afficher la chaine avec les mots en couleur sur console
        """
        result = []
        pos = 0
        while True: # trouve tous les mots avec le motif regex regcomp
            m = regcomp.search(chaine, pos)
            if m is None:
                break
            pos = m.end()
            result.append([m.start(), pos])
        for i1, i2 in result[::-1]: # parcours en sens inverse
            chaine = chaine[:i1] + avant + chaine[i1:i2] + apres + chaine[i2:]
        return chaine
     
    #############################################################################
    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 trouvés (calculs parallèles)
        fichiers = [] # pour stocker les fichiers avec les lignes concernés
        tps = perf_counter() # pour comptage du temps de traitement
        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 à remplacer 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(metcouleur(fichier[i], regcomp)) # 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()

  19. #19
    Membre chevronné
    Homme Profil pro
    Enseignant
    Inscrit en
    Juin 2013
    Messages
    1 609
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Enseignant
    Secteur : Enseignement

    Informations forums :
    Inscription : Juin 2013
    Messages : 1 609
    Points : 2 073
    Points
    2 073
    Par défaut
    Citation Envoyé par tyrtamos Voir le message
    Bonjour

    Merci marco056 pour ta solution de chercher dans un fichier pdf. Je l'accepte d'autant plus que je connais bien PyPDF2 pour l'avoir souvent utilisé pour faire du "split and merge". J'avais même eu des échanges d'emails avec l'éditeur. En effet, le regroupement de pages nécessite de laisser les fichiers concernés ouverts, mais le nombre de fichiers ouverts en même temps est limité par l'OS (Windows pour moi) à environ 500, et moi j'avais plus de 500 fichiers pdf à combiner. J'ai trouvé comme astuce de faire des assemblages progressifs: par exemple, par groupe de 200, puis assemblage des groupes de 200 entre eux, etc... Ça marche très bien!
    Hello !
    Le problème de PyPDF2 est parfois la reconnaissance de la police, surtout chez moi, sous Linux où les polices ne sont pas forcément les mêmes que celles utilisées lors de la création du pdf.

Discussions similaires

  1. script shell pour remplacer un mot dans un fichier
    Par MSM_007 dans le forum Linux
    Réponses: 2
    Dernier message: 17/06/2010, 20h37
  2. [RegEx] remplacer des données dans plusieurs fichiers
    Par sam01 dans le forum Langage
    Réponses: 3
    Dernier message: 11/12/2007, 14h03
  3. changer un mot dans plusieurs fichiers
    Par duaner dans le forum Développement
    Réponses: 2
    Dernier message: 27/06/2007, 13h58
  4. Module de recherche de mots dans plusieurs fichiers
    Par hat_et_m dans le forum Autres Logiciels
    Réponses: 1
    Dernier message: 24/09/2006, 20h09
  5. Réponses: 10
    Dernier message: 29/04/2006, 10h40

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo