使用Python讀取大文件

今天有個朋友問了我一個問題,如何使用Python讀取大文件?覺得這個問題挺有意思的,就記錄下來。

大部分時間我們處理小文件的時候(1g以內?),可以直接用f.read()或readlines()直接把全部內容讀取到內存裏面來。但當文件非常大,比如10g,100g的時候,文件的大小一般已經超出了機器的內存大小,就沒法直接按小文件的方式處理了。那應該怎麼辦呢?

首先,選一個文件做演示,就用上一篇博客的代碼吧。我們現在有一個文件,名爲replace_content.py。我們要做的事是讀取並打印這個文件的每一行。這個文件可能太小了,但不影響我們演示大文件的讀取。

一、讀取小文件

我們先看一下讀取小文件的時候是怎麼做的:

def get_lines(file_path):
    """
    給定一個文件路徑,讀取文件並返回一個迭代器,這個迭代器將順序返回文件的每一行
    """
    with open(file_path, 'rb') as f:
        lines = f.readlines()
    return lines


for e in get_lines('replace_content.py'):
    print(e)

這個很簡單,沒什麼好說的,只是貼上來作爲參考。

二、讀取大文件

讀取大文件有幾種方法,我們逐個來說一下,不過都是通過生成器實現的。

1. f.read(block_size)

在Python中,文件對象有一個方法f.read(block_size),可以用於從文件流中讀取block_size大小的字節塊。那麼,我們只要每次從文件中讀取一點,保證內存裝得下就行了。一個循環下去,總有全部讀取完的一天。

但是,我們還有個要求是,每次輸出文件中的一行。而f.read(block_size)是按字節大小算的,並不會理會文件有沒有換行。所以,我們需要手動處理。

# -*- coding: utf-8 -*-
# @File  : read_big_file.py
# @Author: AaronJny
# @Date  : 2019/11/22
# @Desc  :


def get_lines(file_path):
    """
    給定一個文件路徑,讀取文件並返回一個迭代器,這個迭代器將順序返回文件的每一行
    """
    with open(file_path, 'rb') as f:
        while True:
            # 用於緩存一行字節數據
            file_bytes = []
            # 每次讀取一個字節
            while True:
                block = f.read(1)
                # 判斷讀取到的是不是換行
                if block and block != b'\n':
                    # 不是換行,就說明這一行還沒有讀完,就加入到緩存列表中
                    file_bytes.append(block)
                else:
                    # 否則的話,這一行緩存完了,可以返回了
                    break
            # 判斷這次讀取一行有沒有讀取到內容
            if block:
                # 讀取到了
                file_bytes.append(b'\n')
                # yield一下
                yield b''.join(file_bytes)
            else:
                # 文件已經讀取完啦
                break


for e in get_lines('replace_content.py'):
    print(e)

2. f.readline()

第一個示例的實現比較繁瑣,但其實Python中的文件對象已經封裝了類似的方法,那就是f.readline()。我們現在用f.readline()來實現一下:

# -*- coding: utf-8 -*-
# @File  : read_big_file.py
# @Author: AaronJny
# @Date  : 2019/11/22
# @Desc  :


def get_lines(file_path):
    """
    給定一個文件路徑,讀取文件並返回一個迭代器,這個迭代器將順序返回文件的每一行
    """
    with open(file_path, 'rb') as f:
        while True:
            # 讀取一行
            line = f.readline()
            # 判斷有沒有讀取到
            if line:
                # 讀取到了,yield
                yield line
            # 文件讀取完了
            else:
                break


for e in get_lines('replace_content.py'):
    print(e)

是不是簡單多了?但這仍然不是最好的方法。請往下看。

3.把f作爲迭代對象

事實上,Python中的文件對象是可以被直接作爲可迭代對象的,文件的IO緩衝和內存管理對我們來說是透明的,解釋器會自行處理,我們可以不用理會。

那麼,這個問題可以簡單地寫成這樣:

# -*- coding: utf-8 -*-
# @File  : read_big_file.py
# @Author: AaronJny
# @Date  : 2019/11/22
# @Desc  :


def get_lines(file_path):
    """
    給定一個文件路徑,讀取文件並返回一個迭代器,這個迭代器將順序返回文件的每一行
    """
    with open(file_path, 'rb') as f:
        for line in f:
            yield line


for e in get_lines('replace_content.py'):
    print(e)

我們可以運行測試一下,三種代碼的輸出結果是完全一致的,以下爲輸出結果:

image.png

因爲是直接輸出的字節編碼,所以可能不是很直觀,我們改一下輸出的部分:

for e in get_lines('replace_content.py'):
    # 因爲print本身就會換行,所以我把每一行最後的換行符去掉了
    print(e.decode('utf8')[:-1])

image.png

ok,到這裏就結束了,希望對你有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章