web-dev-qa-db-ja.com

pythonでファイルの最後のx行を検索する最も効率的な方法

ファイルがあり、それがどれくらいの大きさになるかわかりません(かなり大きくなる可能性がありますが、サイズは大きく異なります)。最後の10行程度を検索して、それらが文字列と一致するかどうかを確認します。私はこれを可能な限り迅速かつ効率的に行う必要があり、次のものより良いものがあるかどうか疑問に思っていました:

s = "foo"
last_bit = fileObj.readlines()[-10:]
for line in last_bit:
    if line == s:
        print "FOUND"
31
Harley Holcombe
# Tail
from __future__ import with_statement

find_str = "FIREFOX"                    # String to find
fname = "g:/autoIt/ActiveWin.log_2"     # File to check

with open(fname, "r") as f:
    f.seek (0, 2)           # Seek @ EOF
    fsize = f.tell()        # Get Size
    f.seek (max (fsize-1024, 0), 0) # Set pos @ last n chars
    lines = f.readlines()       # Read to end

lines = lines[-10:]    # Get last 10 lines

# This returns True if any line is exactly find_str + "\n"
print find_str + "\n" in lines

# If you're searching for a substring
for line in lines:
    if find_str in line:
        print True
        break
33
PabloG

これはMizardXのような答えですが、最悪の場合、チャンクが追加されたときに改行のために作業文字列を繰り返し再スキャンすることから二次時間を取るという明らかな問題はありません。

Activestateソリューション(これも2次のように見える)と比較すると、空のファイルが指定されている場合は爆発せず、ブロックの読み取りごとに2つではなく1つのシークを実行します。

「尾」を産むのと比較して、これは自己完結型です。 (ただし、「テール」がある場合はそれが最適です。)

最後から数kBを取得してそれで十分であることを期待するのと比較して、これはどのような線の長さでも機能します。

import os

def reversed_lines(file):
    "Generate the lines of file in reverse order."
    part = ''
    for block in reversed_blocks(file):
        for c in reversed(block):
            if c == '\n' and part:
                yield part[::-1]
                part = ''
            part += c
    if part: yield part[::-1]

def reversed_blocks(file, blocksize=4096):
    "Generate blocks of file's contents in reverse order."
    file.seek(0, os.SEEK_END)
    here = file.tell()
    while 0 < here:
        delta = min(blocksize, here)
        here -= delta
        file.seek(here, os.SEEK_SET)
        yield file.read(delta)

要求どおりに使用するには:

from itertools import islice

def check_last_10_lines(file, key):
    for line in islice(reversed_lines(file), 10):
        if line.rstrip('\n') == key:
            print 'FOUND'
            break

編集: head()でmap()をitertools.imap()に変更しました。 編集2:簡略化されたreverse_blocks()。 編集3:改行のテールの再スキャンを回避します。 編集4: BrianBが気づいたようにstr.splitlines()が最後の '\ n'を無視するため、reversed_lines()を書き直しました(ありがとう)。

非常に古いPythonバージョンでは、ここでのループ内の文字列連結には2次時間がかかることに注意してください。少なくとも過去数年間のCPythonは、この問題を自動的に回避します。

34
Darius Bacon

POSIXシステムでPythonを実行している場合は、「tail -10」を使用して最後の数行を取得できます。これは、最後の10行を取得するために独自のPythonコードを記述するよりも高速な場合があります。ファイルを直接開くのではなく、コマンド「tail -10 filename」からパイプを開きます。ただし、ログ出力が確実である場合(たとえば、never数百または数千文字の非常に長い行があることがわかっている場合)、「最後の2KBを読み取る」のいずれかを使用しますリストされているアプローチで結構です。

8
Myrddin Emrys

ファイルの最後の2 KB程度を読み取ると、10行が確実に得られるはずであり、リソースを大量に消費するべきではないと思います。

file_handle = open("somefile")
file_size = file_handle.tell()
file_handle.seek(max(file_size - 2*1024, 0))

# this will get rid of trailing newlines, unlike readlines()
last_10 = file_handle.read().splitlines()[-10:]

assert len(last_10) == 10, "Only read %d lines" % len(last_10)
7
Ryan Ginstrom

これは、かなり効率的に見えるmmapを使用したバージョンです。大きなプラスは、mmapが自動的にファイルからメモリへのページング要件を自動的に処理することです。

import os
from mmap import mmap

def lastn(filename, n):
    # open the file and mmap it
    f = open(filename, 'r+')
    m = mmap(f.fileno(), os.path.getsize(f.name))

    nlcount = 0
    i = m.size() - 1 
    if m[i] == '\n': n += 1
    while nlcount < n and i > 0:
        if m[i] == '\n': nlcount += 1
        i -= 1
    if i > 0: i += 2

    return m[i:].splitlines()

target = "target string"
print [l for l in lastn('somefile', 10) if l == target]
5
mhawke

私はその問題に遭遇し、LARGE Syslogファイルの最後の1時間を解析し、activestateのレシピサイトからこの関数を使用しました...( http://code.activestate.com/recipes/439045/

!/usr/bin/env python
# -*-mode: python; coding: iso-8859-1 -*-
#
# Copyright (c) Peter Astrand <[email protected]>

import os
import string

class BackwardsReader:
    """Read a file line by line, backwards"""
    BLKSIZE = 4096

    def readline(self):
        while 1:
            newline_pos = string.rfind(self.buf, "\n")
            pos = self.file.tell()
            if newline_pos != -1:
                # Found a newline
                line = self.buf[newline_pos+1:]
                self.buf = self.buf[:newline_pos]
                if pos != 0 or newline_pos != 0 or self.trailing_newline:
                    line += "\n"
                return line
            else:
                if pos == 0:
                    # Start-of-file
                    return ""
                else:
                    # Need to fill buffer
                    toread = min(self.BLKSIZE, pos)
                    self.file.seek(-toread, 1)
                    self.buf = self.file.read(toread) + self.buf
                    self.file.seek(-toread, 1)
                    if pos - toread == 0:
                        self.buf = "\n" + self.buf

    def __init__(self, file):
        self.file = file
        self.buf = ""
        self.file.seek(-1, 2)
        self.trailing_newline = 0
        lastchar = self.file.read(1)
        if lastchar == "\n":
            self.trailing_newline = 1
            self.file.seek(-1, 2)

# Example usage
br = BackwardsReader(open('bar'))

while 1:
    line = br.readline()
    if not line:
        break
    print repr(line)

これは本当にうまく機能し、fileObj.readlines()[-10:]のようなものよりもはるかに効率的です。これにより、pythonはファイル全体をメモリに読み取り、最後の10行を切り捨てます)それ。

2
user32716

UNIXボックスを使用している場合は、os.popen("tail -10 " + filepath).readlines()がおそらく最速の方法です。それ以外の場合は、どの程度堅牢にするかによって異なります。これまでに提案された方法は、いずれにせよすべて落ちるでしょう。最も一般的なケースで堅牢性と速度を得るには、おそらく対数検索のようなものが必要です。file.seekを使用してファイルの末尾から1000文字を引いたところに移動し、読み込んで、含まれる行数を確認してから、EOFマイナス3000文字、2000文字で読み取り、行を数え、次にEOFマイナス7000、4000文字で読み取り、行を数え、以下同様に行数を増やします。ただし、常に適切な行長のファイルで実行されることが確実にわかっている場合は、必要ない場合もあります。

また、unix tailコマンドの ソースコード からインスピレーションを得ることもあります。

2
Alex Coventry

私は似たようなことをしなければならなかったときに、 Manu Gargからのこのブログ投稿 のコードを採用したことを覚えていると思います。

2
Daryl Spitzer

私はmhawkeの提案に従ってmmapを使用し、rfindを使用するバージョンを作成しました。

from mmap import mmap
import sys

def reverse_file(f):
    mm = mmap(f.fileno(), 0)
    nl = mm.size() - 1
    prev_nl = mm.size()
    while nl > -1:
        nl = mm.rfind('\n', 0, nl)
        yield mm[nl + 1:prev_nl]
        prev_nl = nl + 1

def main():
    # Example usage
    with open('test.txt', 'r+') as infile:
        for line in reverse_file(infile):
            sys.stdout.write(line)
1
Edd

また、バイトオフセットで推測する代わりに、ファイルを逆にたどるときに行を数えることもできます。

_lines = 0
chunk_size = 1024

f = file('filename')
f.seek(0, 2)
f.seek(f.tell() - chunk_size)

while True:
    s = f.read(chunk_size)
    lines += s.count('\n')
    if lines > NUM_OF_LINES:
        break
    f.seek(f.tell() - chunk_size*2)
_

これで、ファイルはreadlines()を実行するのに適した位置にあります。また、最初に読み取った文字列をキャッシュして、ファイルの同じ部分を2回読み取るのを防ぐこともできます。

1
JimB

1,000行程度のチャンクをファイルの終わりから10行になるまでバッファーに読み込むことができます。

1
Robert Gamble

まず、リストを返す関数:

def lastNLines(file, N=10, chunksize=1024):
    lines = None
    file.seek(0,2) # go to eof
    size = file.tell()
    for pos in xrange(chunksize,size-1,chunksize):
        # read a chunk
        file.seek(pos,2)
        chunk = file.read(chunksize)
        if lines is None:
            # first time
            lines = chunk.splitlines()
        else:
            # other times, update the 'first' line with
            # the new data, and re-split
            lines[0:1] = (chunk + lines[0]).splitlines()
        if len(lines) > N:
            return lines[-N:]
    file.seek(0)
    chunk = file.read(size-pos)
    lines[0:1] = (chunk + lines[0]).splitlines()
    return lines[-N:]

次に、逆の順序で行を反復する関数:

def iter_lines_reversed(file, chunksize=1024):
    file.seek(0,2)
    size = file.tell()
    last_line = ""
    for pos in xrange(chunksize,size-1,chunksize):
        # read a chunk
        file.seek(pos,2)
        chunk = file.read(chunksize) + last_line
        # split into lines
        lines = chunk.splitlines()
        last_line = lines[0]
        # iterate in reverse order
        for index,line in enumerate(reversed(lines)):
            if index > 0:
                yield line
    # handle the remaining data at the beginning of the file
    file.seek(0)
    chunk = file.read(size-pos) + last_line
    lines = chunk.splitlines()
    for line in reversed(lines):
        yield line

あなたの例のために:

s = "foo"
for index, line in enumerate(iter_lines_reversed(fileObj)):
    if line == s:
        print "FOUND"
        break
    Elif index+1 >= 10:
        break

編集:ファイルサイズを自動的に取得するようになりました
Edit2:現在は10行のみ反復します。

0
Markus Jarderot

このソリューションはファイルを1回だけ読み取りますが、2つのファイルオブジェクトポインターを使用して、ファイルを再読み取りせずにファイルの最後のN行を取得できます。

def getLastLines (path, n):
    # return the las N lines from the file indicated in path

    fp = open(path)
    for i in range(n):
        line = fp.readline()
        if line == '':
            return []

    back = open(path)
    for each in fp:
        back.readline()

    result = []
    for line in back:
        result.append(line[:-1])

    return result




s = "foo"
last_bit = getLastLines(r'C:\Documents and Settings\ricardo.m.reyes\My Documents\desarrollo\tail.py', 10)
for line in last_bit:
    if line == s:
        print "FOUND"
0
Ricardo Reyes

ファイルの最後の数Kを読み取り、それを行に分割して、最後の10のみを返します。

そのチャンクが行の境界に落ちるのはほとんどありませんが、とにかく最初の行を破棄します。

0
Javier

18ダライアスベーコンによるソリューションのおかげで、実装が30%高速化され、io.BaseIOクラスにラップされています。

class ReverseFile(io.IOBase):
    def __init__ (self, filename, headers=1):
        self.fp = open(filename)
        self.headers = headers
        self.reverse = self.reversed_lines()
        self.end_position = -1
        self.current_position = -1

    def readline(self, size=-1):
        if self.headers > 0:
            self.headers -= 1
            raw = self.fp.readline(size)
            self.end_position = self.fp.tell()
            return raw

        raw = next(self.reverse)
        if self.current_position > self.end_position:
            return raw

        raise StopIteration

    def reversed_lines(self):
        """Generate the lines of file in reverse order.
        """
        part = ''
        for block in self.reversed_blocks():
            block = block + part
            block = block.split('\n')
            block.reverse()
            part = block.pop()
            if block[0] == '':
                block.pop(0)

            for line in block:
                yield line + '\n'

        if part:
            yield part

    def reversed_blocks(self, blocksize=0xFFFF):
        "Generate blocks of file's contents in reverse order."
        file = self.fp
        file.seek(0, os.SEEK_END)
        here = file.tell()
        while 0 < here:
            delta = min(blocksize, here)
            here -= delta
            file.seek(here, os.SEEK_SET)
            self.current_position = file.tell()
            yield file.read(delta)

rev = ReverseFile(filename)
for i, line in enumerate(rev):
        print("{0}: {1}".format(i, line.strip()))
0

多分これは役に立つかもしれません:

import os.path

path = 'path_to_file'
os.system('tail -n1 ' + path)
0
AM01

個人的には、シェルにブレークアウトし、tail -n10を呼び出してファイルをロードしたくなるでしょう。しかし、私は実際にはPythonプログラマーではありません;)

0
Gareth