Position in einer Datei feststellen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Hallo,

ich habe große Text-Dateien (>2000000 Zeilen), die ich umformatieren möchte. Dazu lese ich diese zeilenweise ein und verändere diese dann, soweit so gut.
Leider dauert die ganze Prozedur seehr lange.

Wie kann ich nun zwischendruch feststellen, wie viele Bytes denn schon verarbeitet wurden bzw. an welcher Stelle ich mich im File befinde? tell() funktioniert nicht, da ich zeilenweise über File iteriere und das sich irgendwie beißt.

Ich könnte natürlich byteweise einlesen, doch finde ich dies extrem unschön.
Hat jemand vielleicht noch Tipps für den Umgang mit sehr großen Dateien (mehrere GB)?

Danke schonmal
BlackJack

@RedSharky: Du kannst doch einfach die Zeilen mitzählen oder die Länge der einzelnen Zeilen aufaddieren.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Hm, ich möchte aber doch die Zeilenanzahl relativ zur Gesamt-Zeilenzahl wissen. Die Gesamt-Zeilenzahl weiß ich doch aber erst am Ende, oder? Tatsächlich gebe ich mir alle zehn Sekunden die abgearbeitete Zeilenzahl aus. Jedoch bringt mir das nicht wirklich was, wenn nicht bekannt ist ob auf Zeile 204054 noch 2000000 oder 4000000 folgen.

Oder kann ich die Gesamt-Zeilenzahl schon ganz am Anfang ermitteln, etwa so wie die Dateigröße?
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Noch eine Ergänzende Frage.

Auf meinem recht aktuellen Computer werden nur ca. 220 Text-Zeilen pro Sekunde abgearbeitet. Ist das normal oder ist das doch recht mickrig? Immerhin habe ich Millionen von Zeilen umzuschreiben.
BlackJack

@RedSharky: Wenn die Zeilen nicht alle gleich lang sind, kann man die Gesamtzeilenzahl nicht ermitteln ohne die Datei einmal komplett gelesen zu haben. Man kann mit `tell()` die aktuelle Position ermitteln. Warum geht das bei Dir nicht?

Über die Geschwindigkeit kann man nicht viel sagen solange man nicht weiss um wieviele Daten es sich handelt und was Du damit genau machst.

Wenn Du wissen willst wo in Deinem Programm die meiste Zeit verbracht wird, musst Du mit einem entsprechenden Profiling-Modul nachmessen.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Wenn ich tell() einfüge, kommt die Fehlermeldung:
IOError: telling position disabled by next() call

Ich hab mal mein aktuelles Script rangehängt. Eigentlich soll es nur Dateien aus einer DNA-Sequenz-Datenbank einlesen und den Inhalt getrennt in zwei Dateien speichern. Wenn die Zeile mit "@" beginnt, kommt alles folgende in Datei1, bei "+" wird alles in Datei2 gespeichert.

Code: Alles auswählen

import time

infile = "seq.fastq"
fasta_file = "seq.fas"
qual_file = "seq.q"

########################################################################

def get_file(infile):
    try:
        f = open(infile, "r")
        print("File opened...")

        op1 = ""
        op2 = ""
        counter = 0
        t1 = time.time()
        
        for line in f:
            print(f.tell())
            
            counter += 1
            
            t2 = time.time()
            if (t2-t1) > 10:
                print(time.asctime(time.localtime()), ":", counter, "lines processed")
                t1 = t2
            
            
            if line.startswith('@'):
                op1 += line
                dest = 1
            elif line.startswith('+'):
                op2 += line
                dest = 2
            else:
                if dest == 1:
                    op1 += line
                elif dest == 2:
                    op2 += line
            
    finally:
        f.close()
        print("File closed.")
    
    return op1, op2
    
    
    
def write_to_file(op_file, op):
    try:
        f = open(op_file, "w")
        f.write(op)
    finally:
        f.close()
        print("Data writen to:", op_file)
    

########################################################################


print("This script splits the data of fastq files into two seperate files.")

output1, output2 = get_file(infile)

write_to_file(fasta_file, output1)
write_to_file(qual_file, output2)

print ("End of script.")
Meine Fragen:

Wie kann ich die "Länge der Zeilen aufaddieren", sodass diese dem Speicherverbrauch in der Datei (bzw. der Position des Cursors) entspricht?

Warum geht tell() nicht, bzw. was mache ich falsch?

Warum ist der Code so lahm?

Wie messe ich am besten die Zeit der kritischen for-Schleife?
BlackJack

@RedSharky: Anscheinend wird `tell()` bei neueren Python-Versionen "abgeschaltet" wenn man das Dateiobjekt als Iterator verwendet. Dann stimmt die `tell()`-Position nämlich nicht zwingend mit der aktuellen Zeile überein, weil nicht mehr zeilenweise über die C-Standardbibliothek gelesen wird, sondern Python selbst das einlesen und aufteilen in Zeilen übernimmt. Das ist gut, denn das ist schneller als das `readline()` auf C-Ebene. Aber halt auch "schlecht" weil man die Dateiposition nicht mehr mit der aktuellen Zeile in Verbindung bringen kann.

Du liest ja alles erst ein und verwendest dann auch noch ``+=`` auf den Daten. Das ist sicher langsamer und speicherhungriger als nötig. Zeichenketten sind unveränderbar, dass heisst für jede dieser Operationen wird neuer Speicher angefordert und die alte und die neue Zeichenkette werden dort hinkopiert. Je länger das Programm läuft um so mehr Daten werden unnötig pro Zeile im Speicher herumkopiert und um so langsamer wird es.

Wenn das alles mal im Speicher sein muss, dann solltest Du die Zeilen in Listen sammeln und am Ende die Listen mit `writelines()` rausschreiben. Allerdings nur wenn die Daten wirklich alle auch im Speicher benötigt werden, sonst könntest Du doch gleich die Zeilen wegschreiben!?

Das manuelle hochzählen von `counter` könnte man mit `enumerate()` vermeiden. Und `dest` als Zahlen ist eine unnötige Indirektion. Man könnte auch direkt die Liste oder das Dateiobjekt, welches das Ziel der Zeile ist, an den Namen binden.

Die Entscheidung bei '@' und '+' ist zu umständlich. Die erste Zeile in den beiden ersten Zweigen kann weg wenn man den Code im ``else`` nicht in das ``else`` sondern einfach nach dem ``if``/``elif`` stehen hat. Und da ist wie gesagt das ``if``/``elif`` überflüssig wenn man gleich `dest` an das entsprechende Objekt bindet.
BlackJack

Mal eine ungetestete Variante wie man das effizienter lösen könnte:

Code: Alles auswählen

def process(lines, process_at, process_plus):
    process_line = lambda line: None
    for line in lines:
        if line.startswith('@'):
            process_line = process_at
        elif line.startswith('+'):
            process_line = process_plus
        process_line(line)


def main():
    with open(IN_FILENAME, 'r') as lines:
        with open(FASTA_FILENAME, 'w') as fasta_file:
            with open(QUAL_FILENAME, 'w') as qual_file:
                process(lines, fasta_file.write, qual_file.write)
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Ich hab einige gute Tipps eingearbeitet. Das Programm rennt nun in 10 Sekunden durch - ist also um Dimensionen schneller als zuvor! Danke!! So richtig verstanden hab ich das aber nicht.

Code: Alles auswählen

import time
import string

IN_FILENAME = "sulcia.fastq"
FASTA_FILENAME = "sulcia.fas"
QUAL_FILENAME = "sulcia.q"


def process(lines, fasta_file, qual_file):
    t1 = time.time()
    
    for i,line in enumerate(lines):
        t2 = time.time()
        
        if (t2-t1) > 1:
            print (time.strftime('%X'), ":", i, "lines processed")
            t1 = t2
        
        if line.startswith('@') and not line.startswith('@@'):
            fasta_file.write(line.replace('@','>'))
            destination = "fasta"
        elif line.startswith('+'):
            qual_file.write(line)
            destination = "qual"
        else:
            if destination == "fasta": 
                fasta_file.write(line)
            if destination == "qual": 
                qual_file.write(line)

def main():
    with open(IN_FILENAME, 'r') as lines:
        with open(FASTA_FILENAME, 'w') as fasta_file:
            with open(QUAL_FILENAME, 'w') as qual_file:
                process(lines, fasta_file, qual_file)

if __name__ == "__main__":
    print("fastq2fasANDq v0.5")
    print("This script splits the data of fastq files into two seperate files.")
    
    main()
    
    print ("End of script.")
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Noch eine kleine Vereinfachung:

Code: Alles auswählen

        if line.startswith('@') and not line.startswith('@@'):
            fasta_file.write(line.replace('@','>'))
            destination = fasta_file
        elif line.startswith('+'):
            qual_file.write(line)
            destination = qual_file
        else:
            destination.write(line)
Das Leben ist wie ein Tennisball.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Danke, EyDu. Hab ich eingebaut.
RedSharky
User
Beiträge: 99
Registriert: Donnerstag 13. April 2006, 15:38

Noch mal zurück zum eigentlichen Thema:
In der for-Schleife addiere ich nun einfach die aktuelle Zeilen-Größe auf (cur_size). Das stimmt dann am Ende mit der Dateigröße überein - allerdings nicht immer hundertprozentig genau.

Code: Alles auswählen

import os
import time

IN_FILENAME = "sequence.fastq"
FASTA_FILENAME = "sequence.fas"
QUAL_FILENAME = "sequence.q"


def process(lines, fasta_file, qual_file):
    cur_size = 0
    file_size = os.path.getsize(IN_FILENAME)
    
    t1 = time.time()
    for i,line in enumerate(lines):
        
        #current progress
        cur_size += len(line)
        
        #feedback 
        t2 = time.time()
        if (t2-t1) > 1:
            print (time.strftime('%X'), ":", i, \
                "lines processed ("+str('{0:.1f}'.format(cur_size*100/file_size))+" %)")
            t1 = t2
        
        #logic
        if line.startswith('@') and not line.startswith('@@'):
            fasta_file.write(line.replace('@','>'))
            destination = fasta_file
        elif line.startswith('+'):
            qual_file.write(line)
            destination = qual_file
        else:
            destination.write(line)
            
    print (time.strftime('%X'), ":", i, \
                "lines processed ("+str('{0:.1f}'.format(cur_size*100/file_size))+" %)")


def main():
    with open(IN_FILENAME, 'r') as lines:
        with open(FASTA_FILENAME, 'w') as fasta_file:
            with open(QUAL_FILENAME, 'w') as qual_file:
                process(lines, fasta_file, qual_file)


if __name__ == "__main__":
    print("fastq2fasANDq v0.7")
    print("This script splits the data of fastq files into two seperate files.")
    
    main()
    
    print ("End of script.")
Antworten