รับ n บรรทัดสุดท้ายของไฟล์ซึ่งคล้ายกับ tail


181

ฉันกำลังเขียนโปรแกรมดูไฟล์บันทึกสำหรับเว็บแอปพลิเคชันและฉันต้องการให้เลขหน้าบรรทัดของไฟล์บันทึก รายการในไฟล์เป็นบรรทัดที่มีรายการใหม่ล่าสุดที่ด้านล่าง

ดังนั้นฉันต้องการtail()วิธีที่สามารถอ่านnบรรทัดจากด้านล่างและรองรับออฟเซ็ต สิ่งที่ฉันคิดขึ้นมามีลักษณะเช่นนี้:

def tail(f, n, offset=0):
    """Reads a n lines from f with an offset of offset lines."""
    avg_line_length = 74
    to_read = n + offset
    while 1:
        try:
            f.seek(-(avg_line_length * to_read), 2)
        except IOError:
            # woops.  apparently file is smaller than what we want
            # to step back, go to the beginning instead
            f.seek(0)
        pos = f.tell()
        lines = f.read().splitlines()
        if len(lines) >= to_read or pos == 0:
            return lines[-to_read:offset and -offset or None]
        avg_line_length *= 1.3

นี่เป็นแนวทางที่สมเหตุสมผลหรือไม่? วิธีที่แนะนำในการตัดต่อไฟล์บันทึกด้วยออฟเซ็ต


ในระบบของฉัน (linux SLES 10) การค้นหาความสัมพันธ์กับจุดสิ้นสุดทำให้ IOError "ไม่สามารถทำการค้นหาแบบ end-ญาติที่ไม่ใช่ศูนย์" ฉันชอบโซลูชันนี้ แต่ได้แก้ไขเพื่อให้ได้ความยาวของไฟล์ ( seek(0,2)จากนั้นtell()) และใช้ค่านั้นเพื่อค้นหาสัมพันธ์กับจุดเริ่มต้น
แอนน์

2
ยินดีด้วย - คำถามนี้ทำให้มันกลายเป็นซอร์สโค้ด Kippo
ไมล์

พารามิเตอร์ของopenคำสั่งที่ใช้ในการสร้างfวัตถุไฟล์ควรมีการระบุเพราะขึ้นอยู่กับว่า f=open(..., 'rb')หรือจะต้องดำเนินการที่แตกต่างกันf=open(..., 'rt')f
อิกอร์ Fobia

คำตอบ:


123

นี่อาจเร็วกว่าของคุณ ไม่มีข้อสมมติฐานเกี่ยวกับความยาวบรรทัด ย้อนกลับไปที่ไฟล์ครั้งละหนึ่งบล็อกจนกระทั่งพบจำนวนอักขระ '\ n' ที่ถูกต้อง

def tail( f, lines=20 ):
    total_lines_wanted = lines

    BLOCK_SIZE = 1024
    f.seek(0, 2)
    block_end_byte = f.tell()
    lines_to_go = total_lines_wanted
    block_number = -1
    blocks = [] # blocks of size BLOCK_SIZE, in reverse order starting
                # from the end of the file
    while lines_to_go > 0 and block_end_byte > 0:
        if (block_end_byte - BLOCK_SIZE > 0):
            # read the last block we haven't yet read
            f.seek(block_number*BLOCK_SIZE, 2)
            blocks.append(f.read(BLOCK_SIZE))
        else:
            # file too small, start from begining
            f.seek(0,0)
            # only read what was not read
            blocks.append(f.read(block_end_byte))
        lines_found = blocks[-1].count('\n')
        lines_to_go -= lines_found
        block_end_byte -= BLOCK_SIZE
        block_number -= 1
    all_read_text = ''.join(reversed(blocks))
    return '\n'.join(all_read_text.splitlines()[-total_lines_wanted:])

ฉันไม่ชอบสมมติฐานที่ยุ่งยากเกี่ยวกับความยาวบรรทัดเมื่อ - เป็นเรื่องจริง - คุณไม่สามารถรู้สิ่งต่าง ๆ เช่นนั้นได้

โดยทั่วไปสิ่งนี้จะค้นหา 20 บรรทัดสุดท้ายของการผ่านครั้งแรกหรือครั้งที่สองผ่านการวนซ้ำ หากสิ่งที่ตัวละครของคุณ 74 มีความถูกต้องจริงคุณสร้างขนาดบล็อก 2048 และคุณจะลาก 20 บรรทัดเกือบจะในทันที

นอกจากนี้ฉันไม่ได้เผาผลาญแคลอรี่ในสมองจำนวนมากเพื่อพยายามปรับแนวให้เข้ากับบล็อก OS จริง การใช้แพ็คเกจ I / O ระดับสูงเหล่านี้ฉันสงสัยว่าคุณจะเห็นผลการทำงานใด ๆ ที่เกิดขึ้นจากการพยายามจัดแนวบนขอบเขตบล็อกระบบปฏิบัติการ หากคุณใช้ I / O ระดับต่ำกว่าคุณอาจเห็นการเร่งความเร็ว


UPDATE

สำหรับ Python 3.2 ขึ้นไปให้ทำตามขั้นตอนเป็นไบต์เช่นในไฟล์ข้อความ (ที่เปิดโดยไม่มี"b"ในสตริงโหมด) อนุญาตเฉพาะการค้นหาที่สัมพันธ์กับจุดเริ่มต้นของไฟล์เท่านั้น (ยกเว้นการค้นหาไฟล์ที่ส่วนท้ายสุด) ด้วยการแสวงหา (0, 2)):

เช่น: f = open('C:/.../../apache_logs.txt', 'rb')

 def tail(f, lines=20):
    total_lines_wanted = lines

    BLOCK_SIZE = 1024
    f.seek(0, 2)
    block_end_byte = f.tell()
    lines_to_go = total_lines_wanted
    block_number = -1
    blocks = []
    while lines_to_go > 0 and block_end_byte > 0:
        if (block_end_byte - BLOCK_SIZE > 0):
            f.seek(block_number*BLOCK_SIZE, 2)
            blocks.append(f.read(BLOCK_SIZE))
        else:
            f.seek(0,0)
            blocks.append(f.read(block_end_byte))
        lines_found = blocks[-1].count(b'\n')
        lines_to_go -= lines_found
        block_end_byte -= BLOCK_SIZE
        block_number -= 1
    all_read_text = b''.join(reversed(blocks))
    return b'\n'.join(all_read_text.splitlines()[-total_lines_wanted:])

13
สิ่งนี้ล้มเหลวในล็อกไฟล์ขนาดเล็ก - IOError: อาร์กิวเมนต์ที่ไม่ถูกต้อง - f.seek (บล็อก * 1024, 2)
ohnoes

1
วิธีการที่ดีมากอย่างแน่นอน ฉันใช้เวอร์ชันแก้ไขเล็กน้อยของโค้ดด้านบนและมาพร้อมกับสูตรนี้: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà

6
ไม่สามารถใช้งานได้ใน python 3.2 อีกต่อไป ฉันได้รับio.UnsupportedOperation: can't do nonzero end-relative seeksฉันสามารถเปลี่ยนการชดเชยเป็น 0 แต่นั่นเอาชนะวัตถุประสงค์ของฟังก์ชัน
ตรรกะการเข้าใจผิด

4
@DavidEnglund เหตุผลที่นี่ กล่าวโดยย่อ: การค้นหาที่สัมพันธ์กับจุดสิ้นสุดของไฟล์ไม่ได้รับอนุญาตในโหมดข้อความน่าจะเป็นเพราะเนื้อหาไฟล์จะต้องมีการถอดรหัสและโดยทั่วไปการค้นหาตำแหน่งโดยพลการภายในลำดับไบต์ที่เข้ารหัสสามารถมีผลลัพธ์ที่ไม่ได้กำหนดไว้เมื่อคุณ พยายามถอดรหัสเป็น Unicode โดยเริ่มจากตำแหน่งนั้น ข้อเสนอแนะที่ลิงก์คือลองเปิดไฟล์ในโหมดไบนารีและทำการถอดรหัสด้วยตัวเองโดยจับข้อยกเว้น DecodeError
สูงสุด

6
อย่าใช้รหัสนี้ มันทำให้เกิดความเสียหายในหลาย ๆ กรณีชายแดนในหลาม 2.7 คำตอบจาก @papercrane ด้านล่างช่วยแก้ไข
xApple

88

สมมติว่าระบบเหมือนยูนิกซ์บน Python 2 ที่คุณสามารถทำได้:

import os
def tail(f, n, offset=0):
  stdin,stdout = os.popen2("tail -n "+n+offset+" "+f)
  stdin.close()
  lines = stdout.readlines(); stdout.close()
  return lines[:,-offset]

สำหรับ python 3 คุณสามารถทำได้:

import subprocess
def tail(f, n, offset=0):
    proc = subprocess.Popen(['tail', '-n', n + offset, f], stdout=subprocess.PIPE)
    lines = proc.stdout.readlines()
    return lines[:, -offset]

5
ควรเป็นแพลตฟอร์มอิสระ นอกจากนี้หากคุณอ่านคำถามคุณจะเห็นว่า f เป็นไฟล์เหมือนวัตถุ
Armin Ronacher

40
คำถามไม่ได้บอกว่าการพึ่งพาแพลตฟอร์มนั้นไม่สามารถยอมรับได้ ฉันล้มเหลวที่จะดูว่าทำไมสิ่งนี้สมควรได้รับสอง downvotes เมื่อมันให้ unixy มาก (อาจเป็นสิ่งที่คุณกำลังมองหา ... แน่นอนสำหรับฉัน) วิธีการทำสิ่งที่คำถามถาม
Shabbyrobe

3
ขอบคุณฉันคิดว่าฉันต้องแก้ปัญหานี้ด้วย Python บริสุทธิ์ แต่ก็ไม่มีเหตุผลที่จะไม่ใช้ยูทิลิตี UNIX เมื่อมันถึงมือฉันจึงไปกับมัน FWIW ใน Python ที่ทันสมัย ​​subprocess.check_output มีแนวโน้มว่าจะดีกว่า os.popen2; มันลดความซับซ้อนของสิ่งเล็กน้อยเพียงแค่ส่งกลับผลลัพธ์เป็นสตริงและเพิ่มรหัสออกที่ไม่ใช่ศูนย์
mrooney

3
แม้ว่านี่จะขึ้นอยู่กับแพลตฟอร์ม แต่มันเป็นวิธีที่มีประสิทธิภาพมากในการทำสิ่งที่ถูกถามเช่นเดียวกับวิธีที่รวดเร็วมากในการทำ (คุณไม่ต้องโหลดไฟล์ทั้งหมดลงในหน่วยความจำ) @Shabbyrobe
earthmeLon

6
คุณอาจต้องการคำนวณการชดเชยเช่น: offset_total = str(n+offset)และแทนที่บรรทัดนี้stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)เพื่อหลีกเลี่ยงTypeErrors (cannot concatenate int+str)
เพิ่มสี

32

นี่คือคำตอบของฉัน หลามบริสุทธิ์ ใช้ timeit มันดูค่อนข้างเร็ว ทำตามไฟล์บันทึก 100 บรรทัดที่มี 100,000 บรรทัด

>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10)
0.0014600753784179688
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100)
0.00899195671081543
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=1000)
0.05842900276184082
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10000)
0.5394978523254395
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100000)
5.377126932144165

นี่คือรหัส:

import os


def tail(f, lines=1, _buffer=4098):
    """Tail a file and get X lines from the end"""
    # place holder for the lines found
    lines_found = []

    # block counter will be multiplied by buffer
    # to get the block size from the end
    block_counter = -1

    # loop until we find X lines
    while len(lines_found) < lines:
        try:
            f.seek(block_counter * _buffer, os.SEEK_END)
        except IOError:  # either file is too small, or too many lines requested
            f.seek(0)
            lines_found = f.readlines()
            break

        lines_found = f.readlines()

        # we found enough lines, get out
        # Removed this line because it was redundant the while will catch
        # it, I left it for history
        # if len(lines_found) > lines:
        #    break

        # decrement the block counter to get the
        # next X bytes
        block_counter -= 1

    return lines_found[-lines:]

3
ทางออกที่สง่างาม! เป็น if len(lines_found) > lines:สิ่งที่จำเป็นจริงๆ? loopเงื่อนไขจะไม่จับเช่นกันหรือไม่
แมกซีมีเลียนปีเตอร์

คำถามเพื่อความเข้าใจของฉัน: os.SEEK_ENDใช้เพื่อความชัดเจนเท่านั้น? เท่าที่ฉันได้พบค่าของมันคือค่าคงที่ (= 2) ฉันสงสัยว่าจะทิ้งมันไว้เพื่อให้สามารถออกไปimport osได้ ขอบคุณสำหรับทางออกที่ยอดเยี่ยม!
n1k31t4

2
@ MaximilianPeters ใช่ มันไม่จำเป็น. ฉันแสดงความคิดเห็นมันออกมา
glenbot

@DexterMorgan คุณสามารถแทนที่os.SEEK_ENDด้วยจำนวนเต็มเทียบเท่า ส่วนใหญ่มันอยู่ที่นั่นเพื่อการอ่าน
glenbot

1
ฉัน upvoting แต่มีจู้จี้เล็กน้อย หลังจากที่แสวงหาบรรทัดอ่านครั้งแรกอาจไม่สมบูรณ์เพื่อที่จะได้รับ _complete_lines N ผมเปลี่ยนwhile len(lines_found) < linesไปwhile len(lines_found) <= linesในสำเนาของฉัน ขอบคุณ!
เกรแฮม Klyne

30

หากการอ่านไฟล์ทั้งหมดเป็นที่ยอมรับก็ให้ใช้ deque

from collections import deque
deque(f, maxlen=n)

ก่อนหน้า 2.6, deques ไม่ได้มีตัวเลือก maxlen แต่มันง่ายพอที่จะใช้

import itertools
def maxque(items, size):
    items = iter(items)
    q = deque(itertools.islice(items, size))
    for item in items:
        del q[0]
        q.append(item)
    return q

หากต้องการอ่านไฟล์จากท้ายที่สุดให้ใช้การค้นหาแบบควบ (aka เลขชี้กำลัง)

def tail(f, n):
    assert n >= 0
    pos, lines = n+1, []
    while len(lines) <= n:
        try:
            f.seek(-pos, 2)
        except IOError:
            f.seek(0)
            break
        finally:
            lines = list(f)
        pos *= 2
    return lines[-n:]

ทำไมฟังค์ชั่นด้านล่างนั้นจึงใช้งานได้? pos *= 2ดูเหมือนโดยพลการอย่างสมบูรณ์ ความสำคัญของมันคืออะไร?
2mac

1
@ 2mac การค้นหาแบบเอ็กซ์โพเนนเชียล มันจะอ่านจากจุดสิ้นสุดของไฟล์ซ้ำ ๆ กันเป็นสองเท่าของจำนวนที่อ่านในแต่ละครั้งจนกว่าจะพบบรรทัดเพียงพอ
A. Coady

ฉันคิดว่าวิธีการอ่านจากตอนท้ายจะไม่รองรับไฟล์ที่เข้ารหัสด้วย UTF-8 เนื่องจากความยาวของอักขระเป็นตัวแปรและคุณสามารถ (อาจจะ) ตกที่ออฟเซ็ตแปลก ๆ ที่ไม่สามารถตีความได้อย่างถูกต้อง
Mike

น่าเสียดายที่คุณ ควบโซลูชันการค้นหาไม่ทำงานสำหรับหลาม 3. f.seek () ไม่ได้ใช้ชดเชยลบ ฉันได้อัปเดตรหัสของคุณแล้วทำให้ลิงก์
itsjwala

25

คำตอบของ S.Lott ด้านบนเกือบจะเหมาะกับฉัน แต่สุดท้ายก็ทำให้ฉันมีบางส่วน ปรากฎว่ามันทำลายข้อมูลบนขอบเขตบล็อกเนื่องจากข้อมูลเก็บบล็อกการอ่านตามลำดับที่กลับกัน เมื่อเรียกว่า '' .join (data) บล็อกจะเรียงลำดับผิด เป็นการแก้ไขที่

def tail(f, window=20):
    """
    Returns the last `window` lines of file `f` as a list.
    f - a byte file-like object
    """
    if window == 0:
        return []
    BUFSIZ = 1024
    f.seek(0, 2)
    bytes = f.tell()
    size = window + 1
    block = -1
    data = []
    while size > 0 and bytes > 0:
        if bytes - BUFSIZ > 0:
            # Seek back one whole BUFSIZ
            f.seek(block * BUFSIZ, 2)
            # read BUFFER
            data.insert(0, f.read(BUFSIZ))
        else:
            # file too small, start from begining
            f.seek(0,0)
            # only read what was not read
            data.insert(0, f.read(bytes))
        linesFound = data[0].count('\n')
        size -= linesFound
        bytes -= BUFSIZ
        block -= 1
    return ''.join(data).splitlines()[-window:]

1
การแทรกที่จุดเริ่มต้นของรายการเป็นความคิดที่ไม่ดี ทำไมไม่ใช้โครงสร้าง deque?
Sergey11g

1
น่าเสียดายที่ไม่รองรับ Python 3 ... พยายามคิดดูว่าทำไม
Sherlock70

20

รหัสที่ฉันใช้ ฉันคิดว่านี่ดีที่สุดแล้ว:

def tail(f, n, offset=None):
    """Reads a n lines from f with an offset of offset lines.  The return
    value is a tuple in the form ``(lines, has_more)`` where `has_more` is
    an indicator that is `True` if there are more lines in the file.
    """
    avg_line_length = 74
    to_read = n + (offset or 0)

    while 1:
        try:
            f.seek(-(avg_line_length * to_read), 2)
        except IOError:
            # woops.  apparently file is smaller than what we want
            # to step back, go to the beginning instead
            f.seek(0)
        pos = f.tell()
        lines = f.read().splitlines()
        if len(lines) >= to_read or pos == 0:
            return lines[-to_read:offset and -offset or None], \
                   len(lines) > to_read or pos > 0
        avg_line_length *= 1.3

5
ไม่ตอบคำถาม
sheki

13

วิธีที่ง่ายและรวดเร็วด้วย mmap:

import mmap
import os

def tail(filename, n):
    """Returns last n lines from the filename. No exception handling"""
    size = os.path.getsize(filename)
    with open(filename, "rb") as f:
        # for Windows the mmap parameters are different
        fm = mmap.mmap(f.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ)
        try:
            for i in xrange(size - 1, -1, -1):
                if fm[i] == '\n':
                    n -= 1
                    if n == -1:
                        break
            return fm[i + 1 if i else 0:].splitlines()
        finally:
            fm.close()

1
นี่อาจเป็นคำตอบที่เร็วที่สุดเมื่ออินพุทอาจมีขนาดใหญ่ (หรืออาจเป็นถ้าใช้.rfindวิธีการสแกนย้อนหลังเพื่อขึ้นบรรทัดใหม่แทนที่จะทำการไบต์ในเวลาที่ตรวจสอบระดับ Python ใน CPython แทนที่รหัสระดับ Python ด้วย การโทรภายใน C มักจะชนะมาก) สำหรับอินพุตที่เล็กกว่านั้นdequeด้วย a maxlenนั้นง่ายกว่าและเร็วกว่าในทำนองเดียวกัน
ShadowRanger

4

python3 รุ่นที่เข้ากันได้ที่สะอาดยิ่งขึ้นซึ่งไม่ได้แทรก แต่จะผนวก & ย้อนกลับ:

def tail(f, window=1):
    """
    Returns the last `window` lines of file `f` as a list of bytes.
    """
    if window == 0:
        return b''
    BUFSIZE = 1024
    f.seek(0, 2)
    end = f.tell()
    nlines = window + 1
    data = []
    while nlines > 0 and end > 0:
        i = max(0, end - BUFSIZE)
        nread = min(end, BUFSIZE)

        f.seek(i)
        chunk = f.read(nread)
        data.append(chunk)
        nlines -= chunk.count(b'\n')
        end -= nread
    return b'\n'.join(b''.join(reversed(data)).splitlines()[-window:])

ใช้มันแบบนี้:

with open(path, 'rb') as f:
    last_lines = tail(f, 3).decode('utf-8')

ไม่โทรมเกินไป - แต่โดยทั่วไปฉันแนะนำไม่ให้เพิ่มคำตอบสำหรับคำถามอายุ 10 ปีที่มีคำตอบมากมาย แต่ช่วยฉันด้วย: Python 3 ในโค้ดของคุณมีอะไรจำเพาะ?
usr2564301

คำตอบอื่น ๆ ก็ทำงานได้ไม่ดีเหมือนกัน :-) py3: ดูstackoverflow.com/questions/136168/ …
Hauke ​​Rehfeld

3

อัพเดตโซลูชัน @ papercrane เป็น python3 เปิดไฟล์ด้วยopen(filename, 'rb')และ:

def tail(f, window=20):
    """Returns the last `window` lines of file `f` as a list.
    """
    if window == 0:
        return []

    BUFSIZ = 1024
    f.seek(0, 2)
    remaining_bytes = f.tell()
    size = window + 1
    block = -1
    data = []

    while size > 0 and remaining_bytes > 0:
        if remaining_bytes - BUFSIZ > 0:
            # Seek back one whole BUFSIZ
            f.seek(block * BUFSIZ, 2)
            # read BUFFER
            bunch = f.read(BUFSIZ)
        else:
            # file too small, start from beginning
            f.seek(0, 0)
            # only read what was not read
            bunch = f.read(remaining_bytes)

        bunch = bunch.decode('utf-8')
        data.insert(0, bunch)
        size -= bunch.count('\n')
        remaining_bytes -= BUFSIZ
        block -= 1

    return ''.join(data).splitlines()[-window:]

3

การโพสต์คำตอบตามคำสั่งของผู้แสดงความคิดเห็นในคำตอบของคำถามที่คล้ายกันที่ซึ่งใช้เทคนิคเดียวกันในการแปลงบรรทัดสุดท้ายของไฟล์ไม่ใช่แค่เอามาใช้

สำหรับไฟล์ขนาดใหญ่mmapเป็นวิธีที่ดีที่สุดในการทำเช่นนี้ เพื่อปรับปรุงmmapคำตอบที่มีอยู่เวอร์ชันนี้เป็นแบบพกพาระหว่าง Windows และ Linux และควรทำงานได้เร็วขึ้น (แม้ว่ามันจะไม่ทำงานหากไม่มีการแก้ไขบางอย่างใน Python 32 บิตพร้อมไฟล์ในช่วง GB ดูคำตอบอื่น ๆ สำหรับคำแนะนำในการจัดการนี้ และสำหรับการดัดแปลงเพื่อทำงานบน Python 2 )

import io  # Gets consistent version of open for both Py2.7 and Py3.x
import itertools
import mmap

def skip_back_lines(mm, numlines, startidx):
    '''Factored out to simplify handling of n and offset'''
    for _ in itertools.repeat(None, numlines):
        startidx = mm.rfind(b'\n', 0, startidx)
        if startidx < 0:
            break
    return startidx

def tail(f, n, offset=0):
    # Reopen file in binary mode
    with io.open(f.name, 'rb') as binf, mmap.mmap(binf.fileno(), 0, access=mmap.ACCESS_READ) as mm:
        # len(mm) - 1 handles files ending w/newline by getting the prior line
        startofline = skip_back_lines(mm, offset, len(mm) - 1)
        if startofline < 0:
            return []  # Offset lines consumed whole file, nothing to return
            # If using a generator function (yield-ing, see below),
            # this should be a plain return, no empty list

        endoflines = startofline + 1  # Slice end to omit offset lines

        # Find start of lines to capture (add 1 to move from newline to beginning of following line)
        startofline = skip_back_lines(mm, n, startofline) + 1

        # Passing True to splitlines makes it return the list of lines without
        # removing the trailing newline (if any), so list mimics f.readlines()
        return mm[startofline:endoflines].splitlines(True)
        # If Windows style \r\n newlines need to be normalized to \n, and input
        # is ASCII compatible, can normalize newlines with:
        # return mm[startofline:endoflines].replace(os.linesep.encode('ascii'), b'\n').splitlines(True)

นี่จะถือว่าจำนวนบรรทัดที่มีขนาดเล็กพอที่คุณสามารถอ่านได้อย่างปลอดภัยทั้งหมดในหน่วยความจำทันที คุณสามารถทำให้ฟังก์ชั่นเครื่องกำเนิดไฟฟ้านี้และอ่านบรรทัดด้วยตนเองในเวลาเดียวกันโดยแทนที่บรรทัดสุดท้ายด้วย:

        mm.seek(startofline)
        # Call mm.readline n times, or until EOF, whichever comes first
        # Python 3.2 and earlier:
        for line in itertools.islice(iter(mm.readline, b''), n):
            yield line

        # 3.3+:
        yield from itertools.islice(iter(mm.readline, b''), n)

สุดท้ายนี้การอ่านในโหมดไบนารี (จำเป็นต้องใช้mmap) ดังนั้นมันจึงให้strบรรทัด (Py2) และbytesบรรทัด (Py3); หากคุณต้องการunicode(Py2) หรือstr(Py3) วิธีการวนซ้ำอาจถูกปรับแต่งเพื่อถอดรหัสให้คุณและ / หรือแก้ไขบรรทัดใหม่:

        lines = itertools.islice(iter(mm.readline, b''), n)
        if f.encoding:  # Decode if the passed file was opened with a specific encoding
            lines = (line.decode(f.encoding) for line in lines)
        if 'b' not in f.mode:  # Fix line breaks if passed file opened in text mode
            lines = (line.replace(os.linesep, '\n') for line in lines)
        # Python 3.2 and earlier:
        for line in lines:
            yield line
        # 3.3+:
        yield from lines

หมายเหตุ: ฉันพิมพ์สิ่งนี้ทั้งหมดบนเครื่องที่ฉันไม่สามารถเข้าถึง Python เพื่อทดสอบ กรุณาแจ้งให้เราทราบหากฉันพิมพ์อะไร; นี่คล้ายกับคำตอบอื่น ๆ ของฉันที่ฉันคิดว่าควรใช้งานได้ แต่การปรับแต่ง (เช่นการจัดการoffset) อาจทำให้เกิดข้อผิดพลาดเล็กน้อย โปรดแจ้งให้เราทราบในความคิดเห็นหากมีข้อผิดพลาดใด ๆ


3

ฉันพบว่า Popen ด้านบนเป็นทางออกที่ดีที่สุด มันรวดเร็วและสกปรกและใช้งานได้กับ python 2.6 บนเครื่อง Unix ฉันใช้สิ่งต่อไปนี้

def GetLastNLines(self, n, fileName):
    """
    Name:           Get LastNLines
    Description:        Gets last n lines using Unix tail
    Output:         returns last n lines of a file
    Keyword argument:
    n -- number of last lines to return
    filename -- Name of the file you need to tail into
    """
    p = subprocess.Popen(['tail','-n',str(n),self.__fileName], stdout=subprocess.PIPE)
    soutput, sinput = p.communicate()
    return soutput

soutput จะมี n บรรทัดสุดท้ายของรหัส เพื่อทำซ้ำผ่าน soutput ทีละบรรทัดทำ:

for line in GetLastNLines(50,'myfile.log').split('\n'):
    print line

2

ขึ้นอยู่กับคำตอบที่ได้รับการโหวตสูงสุดของ S.Lott (25 ก.ย. 2551 เวลา 21:43 น.) แต่ได้รับการแก้ไขสำหรับไฟล์ขนาดเล็ก

def tail(the_file, lines_2find=20):  
    the_file.seek(0, 2)                         #go to end of file
    bytes_in_file = the_file.tell()             
    lines_found, total_bytes_scanned = 0, 0
    while lines_2find+1 > lines_found and bytes_in_file > total_bytes_scanned: 
        byte_block = min(1024, bytes_in_file-total_bytes_scanned)
        the_file.seek(-(byte_block+total_bytes_scanned), 2)
        total_bytes_scanned += byte_block
        lines_found += the_file.read(1024).count('\n')
    the_file.seek(-total_bytes_scanned, 2)
    line_list = list(the_file.readlines())
    return line_list[-lines_2find:]

    #we read at least 21 line breaks from the bottom, block by block for speed
    #21 to ensure we don't get a half line

หวังว่านี่จะเป็นประโยชน์


2

มีการนำไปใช้งานบางส่วนของ tail บน pypi ซึ่งคุณสามารถติดตั้งได้โดยใช้ pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

ขึ้นอยู่กับสถานการณ์ของคุณอาจมีข้อดีในการใช้หนึ่งในเครื่องมือที่มีอยู่เหล่านี้


คุณรู้จักโมดูลที่ทำงานบน Windows หรือไม่? ฉันพยายามtailhead, tailerแต่พวกเขาไม่ได้ทำงาน mtFileUtilนอกจากนี้ยังพยายาม มันเริ่มโยนข้อผิดพลาดเนื่องจากprintคำสั่งไม่มีวงเล็บ (ฉันอยู่ใน Python 3.6) ฉันเพิ่มสิ่งเหล่านั้นในreverse.pyและข้อความแสดงข้อผิดพลาดหายไป แต่เมื่อสคริปต์ของฉันเรียกโมดูล ( mtFileUtil.tail(open(logfile_path), 5)) มันไม่ได้พิมพ์อะไรเลย
Technext

2

ง่าย ๆ :

with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)

นี่เป็นการดำเนินการที่ไม่ดีอย่างสิ้นเชิง พิจารณาการจัดการไฟล์ขนาดใหญ่และในกรณีที่ n มีขนาดใหญ่เกินไปการดำเนินการที่มีราคาแพงเกินไป
Nivesh Krishna

1

เพื่อประสิทธิภาพที่มีไฟล์ขนาดใหญ่มาก (โดยทั่วไปในสถานการณ์ logfile ที่คุณอาจต้องการใช้ tail) โดยทั่วไปคุณต้องการหลีกเลี่ยงการอ่านไฟล์ทั้งหมด (แม้ว่าคุณจะทำโดยไม่ต้องอ่านไฟล์ทั้งหมดลงในหน่วยความจำพร้อมกัน) อย่างไรก็ตามคุณทำ จำเป็นต้องทำงานอย่างใดชดเชยออฟไลน์ในบรรทัดมากกว่าตัวอักษร ความเป็นไปได้อย่างหนึ่งคือการอ่านย้อนหลังด้วยการค้นหา () ถ่านด้วยถ่าน แต่มันช้ามาก แต่จะดีกว่าในการประมวลผลในบล็อกขนาดใหญ่

ฉันมีฟังก์ชั่นยูทิลิตี้ฉันได้เขียนสักครู่แล้วเพื่ออ่านไฟล์ย้อนหลังที่สามารถใช้ได้ที่นี่

import os, itertools

def rblocks(f, blocksize=4096):
    """Read file as series of blocks from end of file to start.

    The data itself is in normal order, only the order of the blocks is reversed.
    ie. "hello world" -> ["ld","wor", "lo ", "hel"]
    Note that the file must be opened in binary mode.
    """
    if 'b' not in f.mode.lower():
        raise Exception("File must be opened using binary mode.")
    size = os.stat(f.name).st_size
    fullblocks, lastblock = divmod(size, blocksize)

    # The first(end of file) block will be short, since this leaves 
    # the rest aligned on a blocksize boundary.  This may be more 
    # efficient than having the last (first in file) block be short
    f.seek(-lastblock,2)
    yield f.read(lastblock)

    for i in range(fullblocks-1,-1, -1):
        f.seek(i * blocksize)
        yield f.read(blocksize)

def tail(f, nlines):
    buf = ''
    result = []
    for block in rblocks(f):
        buf = block + buf
        lines = buf.splitlines()

        # Return all lines except the first (since may be partial)
        if lines:
            result.extend(lines[1:]) # First line may not be complete
            if(len(result) >= nlines):
                return result[-nlines:]

            buf = lines[0]

    return ([buf]+result)[-nlines:]


f=open('file_to_tail.txt','rb')
for line in tail(f, 20):
    print line

[แก้ไข] เพิ่มรุ่นที่เฉพาะเจาะจงมากขึ้น (หลีกเลี่ยงต้องย้อนกลับสองครั้ง)


การทดสอบอย่างรวดเร็วแสดงให้เห็นว่าสิ่งนี้ทำงานได้แย่กว่ารุ่นของฉันจากด้านบนมาก อาจเป็นเพราะบัฟเฟอร์ของคุณ
Armin Ronacher

ฉันสงสัยว่าเป็นเพราะฉันกำลังมองหาหลาย ๆ แบบย้อนหลังดังนั้นจึงไม่ได้รับการใช้งานที่ดีของบัฟเฟอร์ readahead อย่างไรก็ตามฉันคิดว่ามันอาจจะดีกว่าเมื่อคุณเดาความยาวบรรทัดไม่ถูกต้อง (เช่นบรรทัดที่มีขนาดใหญ่มาก) เนื่องจากมันหลีกเลี่ยงการอ่านข้อมูลอีกครั้งในกรณีนี้
Brian

1

คุณสามารถไปที่ท้ายไฟล์ด้วย f.seek (0, 2) จากนั้นอ่านบรรทัดทีละบรรทัดด้วยการแทนที่ readline () ต่อไปนี้:

def readline_backwards(self, f):
    backline = ''
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    backline = last
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    f.seek(1, 1)
    return backline

1

ขึ้นอยู่กับคำตอบของ Eyecue (10 มิถุนายน '10 เวลา 21:28): วิธีนี้เพิ่มส่วนหัว () และ tail () ไปยังไฟล์วัตถุ

class File(file):
    def head(self, lines_2find=1):
        self.seek(0)                            #Rewind file
        return [self.next() for x in xrange(lines_2find)]

    def tail(self, lines_2find=1):  
        self.seek(0, 2)                         #go to end of file
        bytes_in_file = self.tell()             
        lines_found, total_bytes_scanned = 0, 0
        while (lines_2find+1 > lines_found and
               bytes_in_file > total_bytes_scanned): 
            byte_block = min(1024, bytes_in_file-total_bytes_scanned)
            self.seek(-(byte_block+total_bytes_scanned), 2)
            total_bytes_scanned += byte_block
            lines_found += self.read(1024).count('\n')
        self.seek(-total_bytes_scanned, 2)
        line_list = list(self.readlines())
        return line_list[-lines_2find:]

การใช้งาน:

f = File('path/to/file', 'r')
f.head(3)
f.tail(3)

1

วิธีแก้ปัญหาเหล่านี้หลายอย่างมีปัญหาหากไฟล์ไม่สิ้นสุดใน \ n หรือตรวจสอบให้แน่ใจว่าอ่านบรรทัดแรกเสร็จสมบูรณ์

def tail(file, n=1, bs=1024):
    f = open(file)
    f.seek(-1,2)
    l = 1-f.read(1).count('\n') # If file doesn't end in \n, count it anyway.
    B = f.tell()
    while n >= l and B > 0:
            block = min(bs, B)
            B -= block
            f.seek(B, 0)
            l += f.read(block).count('\n')
    f.seek(B, 0)
    l = min(l,n) # discard first (incomplete) line if l > n
    lines = f.readlines()[-l:]
    f.close()
    return lines

1

นี่คือการใช้งานที่เรียบง่าย:

with open('/etc/passwd', 'r') as f:
  try:
    f.seek(0,2)
    s = ''
    while s.count('\n') < 11:
      cur = f.tell()
      f.seek((cur - 10))
      s = f.read(10) + s
      f.seek((cur - 10))
    print s
  except Exception as e:
    f.readlines()

เยี่ยมมาก! คุณช่วยอธิบายการใช้งานลองก่อนได้f.seekไหม? ทำไมไม่มาก่อนwith open? นอกจากนี้ทำไมexceptคุณถึงทำf.readlines()?

ความจริงแล้วความพยายามน่าจะเป็นอันดับแรก .. ฉันจำไม่ได้ว่ามีเหตุผลที่จะไม่เปิด () นอกเหนือจากระบบ Linux มาตรฐานที่มีสุขภาพดี / etc / passwd ควรอ่านได้เสมอ ลองแล้วด้วยเป็นคำสั่งทั่วไป
GL2014

1

มีโมดูลที่มีประโยชน์มากที่สามารถทำได้:

from file_read_backwards import FileReadBackwards

with FileReadBackwards("/tmp/file", encoding="utf-8") as frb:

# getting lines by lines starting from the last line up
for l in frb:
    print(l)

1

ทางออกอื่น

หากไฟล์ txt ของคุณมีลักษณะเช่นนี้: mouse snake cat lizard wolf dog

คุณสามารถย้อนกลับไฟล์นี้ได้โดยเพียงแค่ใช้การจัดทำดัชนีอาร์เรย์ในไพ ธ อน

contents=[]
def tail(contents,n):
    with open('file.txt') as file:
        for i in file.readlines():
            contents.append(i)

    for i in contents[:n:-1]:
        print(i)

tail(contents,-5)

ผล: สุนัขหมาป่าแมวจิ้งจก



0

ฉันต้องอ่านค่าเฉพาะจากบรรทัดสุดท้ายของไฟล์และสะดุดกับหัวข้อนี้ แทนที่จะสร้างวงล้อใหม่ใน Python ฉันลงเอยด้วยสคริปต์เชลล์ตัวเล็ก ๆ ซึ่งบันทึกเป็น / usr / local / bin / get_last_netp:

#! /bin/bash
tail -n1 /home/leif/projects/transfer/export.log | awk {'print $14'}

และในโปรแกรม Python:

from subprocess import check_output

last_netp = int(check_output("/usr/local/bin/get_last_netp"))

0

ไม่ใช่ตัวอย่างแรกที่ใช้ deque แต่เป็นตัวอย่างที่ง่ายกว่า อันนี้เป็นแบบทั่วไป: มันทำงานบนวัตถุ iterable ใด ๆ ไม่ใช่แค่ไฟล์

#!/usr/bin/env python
import sys
import collections
def tail(iterable, N):
    deq = collections.deque()
    for thing in iterable:
        if len(deq) >= N:
            deq.popleft()
        deq.append(thing)
    for thing in deq:
        yield thing
if __name__ == '__main__':
    for line in tail(sys.stdin,10):
        sys.stdout.write(line)

0
This is my version of tailf

import sys, time, os

filename = 'path to file'

try:
    with open(filename) as f:
        size = os.path.getsize(filename)
        if size < 1024:
            s = size
        else:
            s = 999
        f.seek(-s, 2)
        l = f.read()
        print l
        while True:
            line = f.readline()
            if not line:
                time.sleep(1)
                continue
            print line
except IOError:
    pass

0
import time

attemps = 600
wait_sec = 5
fname = "YOUR_PATH"

with open(fname, "r") as f:
    where = f.tell()
    for i in range(attemps):
        line = f.readline()
        if not line:
            time.sleep(wait_sec)
            f.seek(where)
        else:
            print line, # already has newline



0

อัปเดตสำหรับคำตอบที่ได้รับจากA.Coady

ทำงานร่วมกับงูหลาม 3

สิ่งนี้ใช้การค้นหาแบบเอ็กซ์โปแนนเชียลและจะบัฟเฟอร์เฉพาะNบรรทัดจากด้านหลังและมีประสิทธิภาพมาก

import time
import os
import sys

def tail(f, n):
    assert n >= 0
    pos, lines = n+1, []

    # set file pointer to end

    f.seek(0, os.SEEK_END)

    isFileSmall = False

    while len(lines) <= n:
        try:
            f.seek(f.tell() - pos, os.SEEK_SET)
        except ValueError as e:
            # lines greater than file seeking size
            # seek to start
            f.seek(0,os.SEEK_SET)
            isFileSmall = True
        except IOError:
            print("Some problem reading/seeking the file")
            sys.exit(-1)
        finally:
            lines = f.readlines()
            if isFileSmall:
                break

        pos *= 2

    print(lines)

    return lines[-n:]




with open("stream_logs.txt") as f:
    while(True):
        time.sleep(0.5)
        print(tail(f,2))

-1

ในความคิดที่สองนี่อาจเร็วพอ ๆ กับทุกอย่างที่นี่

def tail( f, window=20 ):
    lines= ['']*window
    count= 0
    for l in f:
        lines[count%window]= l
        count += 1
    print lines[count%window:], lines[:count%window]

มันง่ายกว่ามาก และดูเหมือนว่าจะฉีกไปด้วยกันในจังหวะที่ดี


เพราะเกือบทุกอย่างที่นี่ใช้ไม่ได้กับไฟล์บันทึกที่มีมากกว่า 30 MB หรือมากกว่านั้นโดยไม่ต้องโหลดหน่วยความจำใน RAM เท่ากัน;) เวอร์ชั่นแรกของคุณดีกว่ามาก แต่สำหรับไฟล์ทดสอบที่นี่มันทำงานได้แย่กว่าของฉันเล็กน้อย และมันไม่ทำงานกับอักขระขึ้นบรรทัดใหม่ที่แตกต่างกัน
Armin Ronacher

3
ฉันผิดไป. เวอร์ชัน 1 ใช้ 0.00248908996582 สำหรับ 10 ก้อยผ่านพจนานุกรม เวอร์ชัน 2 ใช้ 1.2963051796 สำหรับ 10 ก้อยผ่านพจนานุกรม ฉันเกือบจะลงคะแนนให้ตัวเองแล้ว
S.Lott

"ไม่ทำงานกับอักขระขึ้นบรรทัดใหม่ที่แตกต่างกัน" แทนที่ดาต้าเคานต์ ('\ n') ด้วย len (data.splitlines ()) หากมีปัญหา
S.Lott
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.