Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Supprimer des lignes spécifiques dans de nombreux fichiers

+6 votes

Je souhaite enlever plusieurs lignes de texte sur un nombre très important de fichiers enregistrés en ASCII. Ces fichiers sont des scènes Maya (un soft de 3D) pesant de quelques Ko à 300Mo.
A la louche, il y a une dizaine de milliers de fichiers à traiter.

Je chercher à enlever ces lignes si elles sont présentes:

requires "mayall_maya70" "0.9.1(Beta)";
requires "elastikSolver" "0.991";
requires "RenderMan_for_Maya" "3.0.1";

Pourquoi?
Si elles sont là, Maya essaie de trouver les plugins en question et met une 30aine de secondes en plus pour charger la scène. Les artistes râlent du temps d'ouverture des scènes, ça me retombe dessus etc...

J'ai fait un premier jet d'un script permettant de parser chaque scène et enlever les lignes indésirables. Le hic, c'est que ça met du temps et vu la masse de fichier à traiter, j'aimerais bien optimiser ça.

from pathlib import Path

skip_list = ['requires "mayall_maya70" "0.9.1(Beta)";\n', 'requires "elastikSolver" "0.991";\n', 'requires "RenderMan_for_Maya" "3.0.1";\n', 'requires "maxwell" "2.6.17";\n', 'requires "maxwell" "2.7.11";\n']

for maya_scene in Path(r'T:\Michelin_Surete').glob('**/*.ma'): #Les scenes Maya ont l'extension .ma
    maya_scene = str(maya_scene)

    print("Processing ", maya_scene) 

    file_content = []
    with open(maya_scene,"r") as input:
        for line in input:
            if line in skip_list:
                print("Erased :", line)
            else:
                file_content.append( line )

    with open(maya_scene,"w") as output: 
        output.writelines(file_content)    

Est-ce que vous pouvez me donner quelques pistes pour améliorer ça? N'y a t'il pas un meilleur moyen que d'ouvrir en lecture puis ouvrir en écriture le fichier pour appliquer les modifs? Aurais-je un gain de perf si je multithread tout ça?

demandé 14-Mar-2016 par DrHaze (144 points)

Une ligne de bash et c'est bouclé (rien à voir avec python du coup) :

(attention, c'est juste l'idée, je n'ai pas testé, et ce script modifie le fichier existant)

find **/*.ma -exec sed -ir -e '/requires "mayall_maya70" "0.9.1(Beta)";/d ; /requires "elastikSolver" "0.991";/d ; /requires "RenderMan_for_Maya" "3.0.1";/d' chrome {} \;

J'ai très envie de tester ça.

5 Réponses

+3 votes

On a déjà pas mal discuté de l'optimisation des I/O sur les fichiers, par exemple ici, ou encore .

En fait une petite recherche sur IE avec le bon tag et t'auras pas mal d'infos

répondu 15-Mar-2016 par boblinux (3,092 points)

Désolé pour le duplicate, merci pour tous ces pointeurs, je reviens une fois que j'ai un peu plus planché sur le sujet.

rassure toi ce n'est pas un duplicate, chaque problème est unique :) , ce sont juste quelques pistes intéressantes histoire de capitaliser les Q/R déjà résolus

+2 votes

C'est marrant, on fait un peu le même boulot :]

Pour le moment j'ai pas cherché plus loin qu'une bonne vieille modif du fichier à la volée, aucune idée si c'est plus rapide que ton process ou pas mais ça peut déjà te permettre de gratter un peu :

    with open(maya_scene,"r+") as input:
        maya_lines = input.readlines()
        for line in skip_list :
            try:
                # On essaie de virer la ligne. Attention, la valeur doit être exactement égale à la ligne!
                maya_lines.remove(line)
                print("Erased :", line)
            except:
                #Ligne a conserver, on ne fait rien
                pass
        # On remonte en haut du fichier et on efface
        input.seek(0)
        input.truncate()
        # On sauve
        input.writelines(maya_lines)
répondu 16-Mar-2016 par furankun (1,434 points)

Dans son cas, c’est une mauvaise solution car tu charges tout le fichier en mémoire vive.

Si le programme crashe entre l'avant dernière et la dernière ligne, c'est la catastrophe...

Effectivement, d'un autre côté est-ce qu'ouvrir le fichier deux fois plutôt qu'une est moins gourmand? et les machines sur lesquels le code sera lancé sont (a priori) de grosses brutes question RAM donc je ne suis pas sûr que charger 300Mo soit un problème.

Il est question de performance ici, utiliser autant de mémoire va jouer un rôle là dedans. Par ailleurs, s'il veut paralléliser, ça risque de monter à bien plus que 300 Mo.

Au moins ça donnera un exemple à ne pas suivre :P
Les enfants, ne faites pas ça chez vous!

+7 votes

Pour améliorer ton code, la première chose est de ne surtout pas traiter tout le fichier avant de commencer à écrire, tu dois effectuer la modification à la volée, par exemple en utilisant des générateurs et en écrivant dans une copie (quitte à supprimer l'ancien fichier et renommer le nouveau à la fin), ce sera beaucoup plus efficace du point de vue mémoire.

Quelque chose comme ça :

with open(filename, 'r'), open(filename+'.tmp', 'w') as reader, writer:
    for line in reader:
        if line not in skip_list:
            writer.write(line)
        else:
            print(filename, 'skipped:', line)
    # on n'est jamais trop prudent...
    writer.flush()
    os.fsync(writer.fileno())
# si tout est OK
os.rename(filename+'.tmp', filename)  # opération atomique

Pour le multi-threading, c'est possible que ça aide vu que le programme est IO-bound, mais je ne saurais pas te dire quelle est la meilleure approche possible ici.

répondu 16-Mar-2016 par yoch (2,510 points)
edité 16-Mar-2016 par yoch

Quel est l'intérêt du flush et du fsync ? Avec la fin du contexte manager, ça devrait le faire automatiquement non ?

Effectivement, c'est plus propre d'instancier le fichier ainsi! Merci :)

@jev: il me semble que seul close() est appelé, mais ça n'implique pas de fsync(). Or pour pouvoir faire fsync(), il faut que le fichier soit encore ouvert, et je dois appeler flush() d'abord.

D'accord, j'imaginais bien que close() appelle flush() mais je ne savais pas (et je suis un peu surpris aussi) que ça n'implique pas de fsync() pour autant.

Concernant flush et fsync, je suis tombé sur ce fil SO: what exactly the python's file.flush() is doing?
La dernière phrase de la réponse acceptée résume bien ma situation: Typically you don't need to bother with either method, but if you're in a scenario where paranoia about what actually ends up on disk is a good thing, you should make both calls as instructed.

Oui, c'est un peu de la parano, d'où mon commentaire comme quoi on n'est jamais trop prudent (puisque le but est aussi de faire une opération atomique, autant être parano jusqu'au bout).

Tu peux par exemple consulter ce post sur SO pour plus d'infos.

+3 votes

Je suis aussi infographiste. Pour patcher les fichiers maya, j'utilise ce script qui accepte les expressions regulieres. C'est bourrin par ce que ca charge tout en memoire mais c'est rapide et ca depanne. Apres je reconnais que ca pourrait etre ameliore et plus subtile mais les machines de prod ont minimum 16 go de ram...

#!/usr/bin/python

# remplace dans le fichier fournit le/les patterns par une chaine
# by blue
# 16/08/2011

import os
import sys
import re

if len(sys.argv)<4:
    print "patchFile <file> <replace pattern> <search expression 1> <search expression 2> <search expression 3>..."
    print "  Usage: dans un fichier, remplace par un pattern un ou plusieurs motifs de recherche via des expressions regulieres"
    sys.exit()

fileName = sys.argv[1]
print "file: >"+fileName+"<"
if not os.path.exists(fileName):
    print "file not found"
    sys.exit()

# read file
canalRead = open(fileName, "r")
lines = canalRead.read()
canalRead.close()

print "read size:"+(str)(len(lines))

patternReplace = sys.argv[2]
for expReg in sys.argv[3:]:
    print " patch " + expReg
    print " by " + patternReplace
    lines = re.sub(expReg, patternReplace, lines)

print "write size:"+(str)(len(lines))

# write file
canalWrite = open(fileName+".patch", "w")
canalWrite.write(lines)
canalWrite.close()
répondu 22-Mar-2016 par blue (176 points)
+1 vote

J'ai mixé un peu toutes les infos que j'ai pu glaner à gauche et à droite et ai pu faire quelque chose d'assez rapide et pas trop gourmand en ressources.
Voilà une version un peu allégée du résultat:

PROJECT_PATH = r"T:\ProjectName"
REQUIRES_LIST   = [
    'requires "mayall_maya70" "0.9.1(Beta)";\n'                       ,
    'requires "elastikSolver" "0.991";\n'                             ,
    'requires "RenderMan_for_Maya" "3.0.1";\n'                        ,
    'requires "maxwell" "2.6.17";\n'                                  ,
    'requires "maxwell" "2.7.11";\n'                                  ,
    'requires "tfbUVanim2013" "1.0";\n'                               
    ... ]

def processMayaScene( maya_scene ):
    # Apparently there are some folders on the network ending with .ma ...
    if not os.path.isdir( maya_scene ):
        #Don't bother with permissions
        os.chmod( maya_scene, 0o777 )

        # Retrieve the 200 first lines of the scene
        with open(maya_scene, "r", encoding="latin1") as input:
            hundred_first_lines = [next(input) for x in range(200)] #On recupere les 200 premieres lignes

        # Si on a aucun match avec REQUIRES_LIST on passe a la scene suivante
        if len(set(hundred_first_lines).intersection( REQUIRES_LIST ) ) == 0:
            return None

        # Si il y a des matchs, on ouvre la scene en lecture et on cree une scene temporaire en ecriture
        with open(maya_scene, 'r') as reader:
            with open(maya_scene+'.tmp', 'w') as writer:
                for line in reader:
                    if line not in REQUIRES_LIST:
                        writer.write( line )
                # on n'est jamais trop prudent...
                # http://stackoverflow.com/questions/7127075/what-exactly-the-pythons-file-flush-is-doing
                writer.flush()
                os.fsync(writer.fileno())

        # si tout est OK, on remplace la scene originale par la scene tampon
        os.remove(maya_scene) # A enlever si on est sous unix
        # On Unix, if dst exists and is a file, it will be replaced silently if the user has permission.
        # The operation may fail on some Unix flavors if src and dst are on different filesystems.
        # If successful, the renaming will be an atomic operation (this is a POSIX requirement).
        # On Windows, if dst already exists, OSError will be raised even if it is a file;
        # there may be no way to implement an atomic rename when dst names an existing file.
        # http://docs.python.org/library/os.html#os.rename
        os.rename(maya_scene+'.tmp', maya_scene)  # Operation atomique

        return maya_scene
    else:
        return None


if __name__=='__main__':
    # Start timer for logger
    start_time = time.time()

    # Create the pool of workers
    pool = Pool(processes=8)  # start 8 worker processes
    # Assign a job for them
    results = pool.imap(processMayaScene, glob.iglob( os.path.join(PROJECT_PATH, "**", "*.ma"), recursive=True), chunksize=30)

    #For each scene modified
    for maya_scene in results:
        if maya_scene: print("Changes done on:", maya_scene)

    print("Time   : %s seconds" % (time.time() - start_time))

Pour environ 1500 scènes dont une 100aine qui ont été modifiée le script a pris 2 minutes à s'exécuter sur une vieille machine du parc.

Merci à tous pour votre aide.

répondu 14-Mai-2016 par DrHaze (144 points)
...