Jak przejść do określonej linii w dużym pliku tekstowym?

107

Czy są jakieś alternatywy dla poniższego kodu:

startFromLine = 141978 # or whatever line I need to jump to

urlsfile = open(filename, "rb", 0)

linesCounter = 1

for line in urlsfile:
    if linesCounter > startFromLine:
        DoSomethingWithThisLine(line)

    linesCounter += 1

Jeśli przetwarzam ogromny plik tekstowy (~15MB)z liniami o nieznanej, ale różnej długości i muszę przeskoczyć do konkretnej linii, którą liczbę znam z góry? Czuję się źle, przetwarzając je jeden po drugim, kiedy wiem, że mógłbym zignorować przynajmniej pierwszą połowę pliku. Szukasz bardziej eleganckiego rozwiązania, jeśli takie istnieje.

user63503
źródło
Skąd wiesz, że pierwsza połowa pliku to nie kilka "\ n", podczas gdy druga połowa to pojedyncza linia? Dlaczego źle się z tym czujesz?
Andrew Dalke
7
Myślę, że tytuł jest mylący - tbh 15MB nie jest tak naprawdę "wielkim plikiem tekstowym", delikatnie mówiąc ...
pms

Odpowiedzi:

30

linecache :

linecacheModuł pozwala na uzyskać dowolną linię z pliku źródłowego Pythona, podczas próby optymalizacji wewnętrznie, używając cache, wspólną sprawę gdzie wiele linii są odczytywane z jednego pliku. Jest to używane przez tracebackmoduł do pobierania wierszy źródłowych do włączenia do sformatowanego śledzenia wstecznego ...

John Ellinwood
źródło
165
Właśnie sprawdziłem kod źródłowy tego modułu: cały plik jest wczytywany do pamięci! Dlatego zdecydowanie odrzuciłbym tę odpowiedź w celu szybkiego dostępu do danej linii w pliku.
MiniQuark
MiniQuark, próbowałem, faktycznie działa i bardzo szybko. Muszę zobaczyć, co się stanie, jeśli będę w ten sposób pracować na kilkunastu plikach jednocześnie, dowiedzieć się, w którym momencie mój system umiera.
user63503
5
Menedżer pamięci wirtualnej Twojego systemu operacyjnego pomaga całkiem sporo, więc wczytywanie dużych plików do pamięci może nie być powolne, jeśli nie generujesz wielu błędów stron :) Wręcz przeciwnie, robiąc to w "głupi sposób" i przydzielając wiele, wiele pamięci może być niesamowicie szybki. Podobał mi się artykuł duńskiego programisty FreeBSD, Poula-Henninga Kampa: queue.acm.org/detail.cfm?id=1814327
Morten Jensen
13
wypróbuj plik 100G, to jest do bani. muszę używać f.tell (), f.seek (), f.readline ()
whi
115

Nie możesz przeskoczyć do przodu bez wczytania się co najmniej raz w pliku, ponieważ nie wiesz, gdzie są podziały wierszy. Możesz zrobić coś takiego:

# Read in the file once and build a list of line offsets
line_offset = []
offset = 0
for line in file:
    line_offset.append(offset)
    offset += len(line)
file.seek(0)

# Now, to skip to line n (with the first line being line 0), just do
file.seek(line_offset[n])
Adam Rosenfield
źródło
2
+1, ale uważaj, jest to przydatne tylko wtedy, gdy przeskoczy do kilku przypadkowych linii! ale jeśli przeskakuje tylko do jednej linii, to jest to marnotrawstwo
zakończył
3
+1: Ponadto, jeśli plik się nie zmieni, indeks numeru wiersza może zostać wytrawiony i ponownie użyty, co dodatkowo amortyzuje początkowy koszt skanowania pliku.
S.Lott,
OK, po tym, jak tam wskoczyłem, jak miałbym przetwarzać to wiersz po wierszu, zaczynając od tej pozycji?
user63503
8
Jedna uwaga (szczególnie w systemie Windows): uważaj, aby otworzyć plik w trybie binarnym lub alternatywnie użyj offset = file.tell (). W trybie tekstowym w systemie Windows linia będzie o bajt krótsza niż jej długość na dysku (\ r \ n zastąpione przez \ n)
Brian,
2
@photographer: użyj read () lub readline (), zaczynają od bieżącej pozycji ustawionej przez seek.
S.Lott,
22

Naprawdę nie masz tak wielu opcji, jeśli linie mają różną długość ... niestety musisz przetworzyć znaki końca linii, aby wiedzieć, kiedy przejdziesz do następnej linii.

Możesz jednak znacznie przyspieszyć to i zmniejszyć zużycie pamięci, zmieniając ostatni parametr na „otwórz” na coś innego niż 0.

0 oznacza, że ​​operacja odczytu pliku jest niebuforowana, co jest bardzo powolne i zajmuje dużo miejsca na dysku. 1 oznacza, że ​​plik jest buforowany wierszami, co byłoby poprawą. Cokolwiek powyżej 1 (powiedzmy 8k .. tj .: 8096 lub więcej) odczytuje fragmenty pliku do pamięci. Nadal masz do niego dostęp for line in open(etc):, ale Python działa tylko po trochu naraz, odrzucając każdy buforowany fragment po jego przetworzeniu.

Jarret Hardie
źródło
6
8K to 8192, być może lepiej jest napisać 8 << 10 na wszelki wypadek. :)
odpręż się
Czy przypadkiem wiesz, że rozmiar bufora jest określony w bajtach? Jaki jest odpowiedni format? Czy mógłbym napisać „8k”? A może powinno to być „8096”?
user63503
1
HAHAHA ... musi być piątek ... Wyraźnie nie umiem matematyki. Rozmiar bufora jest rzeczywiście liczbą całkowitą wyrażającą bajty, więc napisz 8192 (nie 8096 :-)), a nie 8
Jarret Hardie
Z przyjemnością - mam nadzieję, że się uda. W nowoczesnym systemie prawdopodobnie można znacznie zwiększyć rozmiar bufora. 8k jest po prostu pozostałością w mojej pamięci z jakiegoś powodu, którego nie mogę zidentyfikować.
Jarret Hardie
Zrobiłem tutaj kilka testów i ustawienie go na -1 (domyślny system operacyjny, często 8k, ale często trudno powiedzieć), wydaje się działać tak szybko, jak to tylko możliwe. To powiedziawszy, po części może być to, że testuję na serwerze wirtualnym.
Oscar Smith
12

Prawdopodobnie rozpieszcza mnie obfity baran, ale 15 M nie jest ogromny. Czytanie do pamięci readlines() jest tym, co zwykle robię z plikami tego rozmiaru. Uzyskanie dostępu do następnej linii jest banalne.

SilentGhost
źródło
Dlaczego trochę się wahałem przed odczytaniem całego pliku - mogłem mieć uruchomionych kilka z tych procesów, a jeśli kilkanaście z nich odczytuje 12 plików po 15 MB każdy, może to nie być dobre. Ale muszę to przetestować, aby dowiedzieć się, czy zadziała. Dziękuję Ci.
user63503
4
Hrm, a co jeśli to plik 1 GB?
Noah
@photographer: nawet „kilka” procesów odczytujących pliki 15MB nie powinno mieć znaczenia na typowym nowoczesnym komputerze (oczywiście w zależności od tego, co dokładnie z nimi robisz).
Jacob Gabrielson
Jacob, tak, powinienem po prostu spróbować. Procesy są uruchomione na maszynie wirtualnej przez tygodnie, jeśli maszyna wirtualna nie uległa awarii. Niestety ostatnio uległ awarii po 6 dniach. Muszę kontynuować od miejsca, w którym nagle się zatrzymał. Nadal muszę dowiedzieć się, jak znaleźć miejsce, w którym został.
user63503
@Noah: ale tak nie jest! Dlaczego nie pójdziesz dalej? Co jeśli plik 128 TB? Niż wiele systemów operacyjnych nie byłoby w stanie go obsłużyć. Dlaczego nie rozwiązać problemu na bieżąco?
SilentGhost
7

Dziwię się, że nikt nie wspomniał o islice

line = next(itertools.islice(Fhandle,index_of_interest,index_of_interest+1),None) # just the one line

lub jeśli chcesz mieć całą resztę pliku

rest_of_file = itertools.islice(Fhandle,index_of_interest)
for line in rest_of_file:
    print line

lub jeśli chcesz co drugą linię z pliku

rest_of_file = itertools.islice(Fhandle,index_of_interest,None,2)
for odd_line in rest_of_file:
    print odd_line
Joran Beasley
źródło
5

Ponieważ nie ma sposobu na określenie długości wszystkich linii bez ich przeczytania, nie masz innego wyjścia, jak tylko powtórzyć wszystkie linie przed linią startową. Wszystko, co możesz zrobić, to ładnie wyglądać. Jeśli plik jest naprawdę duży, możesz użyć podejścia opartego na generatorze:

from itertools import dropwhile

def iterate_from_line(f, start_from_line):
    return (l for i, l in dropwhile(lambda x: x[0] < start_from_line, enumerate(f)))

for line in iterate_from_line(open(filename, "r", 0), 141978):
    DoSomethingWithThisLine(line)

Uwaga: w tym podejściu indeks jest zerowy.


źródło
4

Jeśli nie chcesz czytać całego pliku w pamięci ... może być konieczne wymyślenie innego formatu niż zwykły tekst.

oczywiście wszystko zależy od tego, co próbujesz zrobić i jak często będziesz przeskakiwać przez plik.

Na przykład, jeśli masz zamiar wielokrotnie przeskakiwać do wierszy w tym samym pliku i wiesz, że plik nie zmienia się podczas pracy z nim, możesz to zrobić:
Najpierw przejdź przez cały plik i nagraj „ szukaj lokalizacji "niektórych kluczowych numerów linii (takich jak kiedykolwiek 1000 linii).
Następnie, jeśli chcesz linii 12005, przeskocz do pozycji 12000 (którą zapisałeś), a następnie przeczytaj 5 linii i będziesz wiedzieć są w linii 12005 i tak dalej

hasen
źródło
3

Jeśli znasz z góry pozycję w pliku (raczej numer wiersza), możesz użyć file.seek (), aby przejść do tej pozycji.

Edycja : możesz użyć funkcji linecache.getline (nazwa pliku, lineno) , która zwróci zawartość linii lineno, ale tylko po wczytaniu całego pliku do pamięci. Dobrze, jeśli uzyskujesz losowy dostęp do wierszy z pliku (ponieważ sam Python może chcieć zrobić, aby wydrukować śledzenie), ale nie jest dobry dla pliku o wielkości 15 MB.

Noe
źródło
Na pewno nie użyłbym do tego celu pliku linecache, ponieważ czyta on cały plik z pamięci przed zwróceniem żądanej linii.
MiniQuark
Tak, to brzmiało zbyt dobrze, aby mogło być prawdziwe. Nadal chciałbym, żeby istniał moduł, który robiłby to wydajnie, ale zamiast tego używam metody file.seek ().
Noah
3

Co generuje plik, który chcesz przetworzyć? Jeśli jest to coś pod twoją kontrolą, możesz wygenerować indeks (w której linii znajduje się pozycja) w momencie dołączania pliku. Plik indeksu może mieć stały rozmiar linii (wypełnione spacjami lub 0 liczb dopełnianych) i na pewno będzie mniejszy. Dzięki temu można je szybko czytać i przetwarzać.

  • Którą linię chcesz?
  • Oblicz offset bajtowy odpowiedniego numeru linii w pliku indeksowym (możliwe, ponieważ rozmiar linii w pliku indeksowym jest stały).
  • Użyj seek lub cokolwiek innego, aby bezpośrednio przeskoczyć i pobrać wiersz z pliku indeksu.
  • Przeanalizuj, aby uzyskać przesunięcie bajtów dla odpowiedniej linii rzeczywistego pliku.
kamathln
źródło
3

Miałem ten sam problem (trzeba pobrać konkretną linię z ogromnego pliku).

Z pewnością mogę za każdym razem przeglądać wszystkie rekordy w pliku i zatrzymywać je, gdy licznik będzie równy docelowej linii, ale nie działa to skutecznie w przypadku, gdy chcesz uzyskać liczbę mnogą określonych wierszy. To spowodowało, że główny problem został rozwiązany - jak dotrzeć bezpośrednio do potrzebnego miejsca pliku.

Dowiedziałem się kolejnej decyzji: Najpierw uzupełniłem słownik z pozycją początkową każdego wiersza (klucz to numer wiersza, a wartość - skumulowana długość poprzednich wierszy).

t = open(file,’r’)
dict_pos = {}

kolvo = 0
length = 0
for each in t:
    dict_pos[kolvo] = length
    length = length+len(each)
    kolvo = kolvo+1

ostatecznie funkcja celu:

def give_line(line_number):
    t.seek(dict_pos.get(line_number))
    line = t.readline()
    return line

t.seek (numer_linii) - polecenie, które wykonuje czyszczenie pliku do początku linii. Tak więc, jeśli następnym razem zatwierdzisz readline - otrzymasz swoją docelową linię.

Stosując takie podejście zaoszczędziłem znaczną część czasu.

user3810114
źródło
3

Możesz użyć mmap, aby znaleźć przesunięcie linii. Wydaje się, że MMap jest najszybszym sposobem przetwarzania pliku

przykład:

with open('input_file', "r+b") as f:
    mapped = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
    i = 1
    for line in iter(mapped.readline, ""):
        if i == Line_I_want_to_jump:
            offsets = mapped.tell()
        i+=1

następnie użyj funkcji f.seek (przesunięcia), aby przejść do potrzebnej linii

Jerzy
źródło
2

Czy same wiersze zawierają jakieś informacje o indeksie? Gdyby zawartość każdego wiersza brzmiała jak „ <line index>:Data”, to seek()podejście mogłoby zostać użyte do przeszukania pliku binarnego, nawet jeśli ilość Datajest zmienna. Szukałbyś środka pliku, czytał wiersz, sprawdzał, czy jego indeks jest wyższy lub niższy niż ten, którego chcesz, itp.

W przeciwnym razie najlepsze, co możesz zrobić, to po prostu readlines(). Jeśli nie chcesz czytać wszystkich 15 MB, możesz użyć sizehintargumentu, aby przynajmniej zamienić wiele readline()s na mniejszą liczbę wywołań funkcji readlines().

DNS
źródło
2

Jeśli masz do czynienia z plikiem tekstowym i opartym na systemie linux , możesz użyć poleceń linux.
Dla mnie to działało dobrze!

import commands

def read_line(path, line=1):
    return commands.getoutput('head -%s %s | tail -1' % (line, path))

line_to_jump = 141978
read_line("path_to_large_text_file", line_to_jump)
HongKun Yoo
źródło
oczywiście nie jest kompatybilne z Windows lub jakimś rodzajem powłok Linuksa, które nie obsługują head / tail.
Wizmann
Czy to jest szybsze niż robienie tego w Pythonie?
Shamoon
Czy to może uzyskać wiele linii?
Shamoon
1

Oto przykład użycia „readlines (sizehint)” do odczytywania fragmentów wierszy naraz. DNS wskazał na to rozwiązanie. Napisałem ten przykład, ponieważ inne przykłady tutaj są zorientowane jednowierszowo.

def getlineno(filename, lineno):
    if lineno < 1:
        raise TypeError("First line is line 1")
    f = open(filename)
    lines_read = 0
    while 1:
        lines = f.readlines(100000)
        if not lines:
            return None
        if lines_read + len(lines) >= lineno:
            return lines[lineno-lines_read-1]
        lines_read += len(lines)

print getlineno("nci_09425001_09450000.smi", 12000)
Andrew Dalke
źródło
0

Żadna z odpowiedzi nie jest szczególnie satysfakcjonująca, więc oto mały fragment, który pomoże.

class LineSeekableFile:
    def __init__(self, seekable):
        self.fin = seekable
        self.line_map = list() # Map from line index -> file position.
        self.line_map.append(0)
        while seekable.readline():
            self.line_map.append(seekable.tell())

    def __getitem__(self, index):
        # NOTE: This assumes that you're not reading the file sequentially.  
        # For that, just use 'for line in file'.
        self.fin.seek(self.line_map[index])
        return self.fin.readline()

Przykładowe użycie:

In: !cat /tmp/test.txt

Out:
Line zero.
Line one!

Line three.
End of file, line four.

In:
with open("/tmp/test.txt", 'rt') as fin:
    seeker = LineSeekableFile(fin)    
    print(seeker[1])
Out:
Line one!

Wymaga to wykonywania wielu operacji wyszukiwania plików, ale jest przydatne w przypadkach, gdy nie można zmieścić całego pliku w pamięci. Wykonuje jeden wstępny odczyt, aby uzyskać lokalizacje wierszy (więc czyta cały plik, ale nie przechowuje go w całości w pamięci), a następnie przy każdym dostępie szuka pliku po fakcie.

Oferuję powyższy fragment w ramach licencji MIT lub Apache według uznania użytkownika.

Joseph Catrambone
źródło
-1

Można użyć tej funkcji, aby zwrócić wiersz n:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()
ksed
źródło
Ta logika nie działa, jeśli istnieją ciągłe puste wiersze, fi.next () pomija wszystkie puste wiersze naraz, w przeciwnym razie jest dobrze :)
Anvesh Yalamarthy
OP nie wspomina, że ​​wiersze mają wiersze z niestandardowymi znakami końca wiersza. W takim przypadku musiałbyś przeanalizować każdy wiersz z co najmniej jedną instrukcją if dla częściowych podziałów wierszy.
wysłano