Python讀寫檔案你真的瞭解嗎?

昀溪發表於2018-07-29

內容概述

Python檔案操作

針對大檔案如何操作

為什麼不能修改檔案?

你需要知道的基本知識

1. Python檔案操作

這一部分內容不是重點,因為很簡單網上很多,主要看看檔案操作的步驟就可以了。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

import os
import time
import sys

FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt"
if os.path.exists(FILE_PATH):
    # 把檔案物件給一個變數,這樣後續才能操作這個檔案物件,這裡預設是以只讀方式開啟
    FILE_HANDLER = open(FILE_PATH, encoding="utf-8")
    # 讀取
    """
    read(size) 不加SIZE可以是整個檔案讀取,加上SIZE可以一部分一部分的讀取,對於大檔案要使用SIZE分段讀取,對於小檔案可以整體讀取
    readline() 一次讀取一行
    readlines() 一次讀取全部檔案到一個列表,適合讀取配置檔案,因為配置檔案通常都是一行一行的,對於配置檔案這種非常適合因為它比較小而且
    配置檔案都是一行一行的
    """
    data = FILE_HANDLER.read()
    print(FILE_HANDLER.fileno())
    time.sleep(600)
    print(data)
    # 關閉
    FILE_HANDLER.close()
else:
    print("檔案:", FILE_PATH, "不存在。")

# 一行一行讀取
if os.path.exists(FILE_PATH):
    FILE_HANDLER = open(FILE_PATH, encoding="utf-8")
    dataList = FILE_HANDLER.readlines()
    for line in dataList:
        print(line)
    FILE_HANDLER.close()

# 寫入一行檔案
if os.path.exists(FILE_PATH):
    """
    mode  r 是隻讀模式  
          w 是隻寫模式開啟檔案,其實是建立,所以如果原來的檔案有內容它就給它清空了,所以在原有的內容寫就不能用這種模式
          a 追加模式,只能追加
          r+ 讀寫模式,可以讀取也能追加,只能追加
          rb 讀取二進位制檔案
          wb 寫入二進位制檔案
    """
    FILE_HANDLER = open(FILE_PATH, encoding="utf-8", mode="a")
    FILE_HANDLER.write("你好嗎\n")
    # 這個語句是立即落盤, 因為延遲寫入,上面的write只是寫入快取並沒有真正寫入磁碟,如果對於寫入內容比較多,你不去呼叫flush的話可能如果執行完write語句後剛好計算機
    # 斷電,那麼就容易出現資料丟失的情況。
    FILE_HANDLER.flush()
    # 關閉已開啟的檔案流
    FILE_HANDLER.close()

更加簡單的建立檔案物件的方式,上面的方式需要手動關閉檔案流,下面的方式它會自動關閉檔案流

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

import os
import sys

FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt"

if os.path.exists(FILE_PATH):
    with open(FILE_PATH, encoding="utf-8", mode="a") as FILE_HANDLER:
        FILE_HANDLER.write("你好嗎\n")

 透過with open還可以開啟多個檔案

with open (FILE1, "r") as f1, open(FILE2, "w") as f2:
    pass

2. 針對大檔案如何操作

如何操作大檔案比如10G一個檔案,顯然不能使用read()和readlines(),就算你的記憶體夠大,這樣一個檔案都讀取到記憶體裡顯然不是一個好辦法,太浪費了記憶體了,另外從磁碟載入到記憶體頁需要時間。是不是可以使用readline()?可以一行行讀取,不過讀的時候雖然是一行,可是不斷的讀取到記憶體最終還是會佔用很大記憶體,所以一個好的方法是,讀取一行到記憶體,處理完後把記憶體的那一行內容刪除,然後再讀取一行,也就是記憶體只保留一行。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com

import os
import sys

FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt"

if os.path.exists(FILE_PATH):
    with open(FILE_PATH, encoding="utf-8", mode="r") as FILE_HANDLER:
# 這裡並沒有呼叫檔案物件的read方法,此時這裡這個檔案物件是一個迭代器 for line in FILE_HANDLER: print(line)

 

說明:由於上面使用的是迭代器而不是一個列表,所以你想控制輸出N行或者第N行你就需要自己來做一個計數器,也就自己記錄當前是第幾行。

3. 為什麼不能修改檔案

你透過r+開啟檔案發現即便你使用當前讀取到第N行(假設有M行)你呼叫write()還是無法插入,這個新增的內容只會顯示在最後一行

你透過w開啟檔案發現原始檔被清空了,感覺好危險的樣子。

感覺vi編輯器、word這些都能直接修改啊?

這都是為什麼呢?

資料儲存簡要原理

磁碟分割槽的最小單位是柱面,但是資料儲存的最小單位是block也就是塊,這個是檔案系統相關內容,不是磁碟的物理特性。一個檔案通常會佔用1個或多個塊。下面簡要畫圖來看:

為了效能通常都會尋找連續的塊來存放資料(讀寫效率最高,減少定址時間),但是隨著時間的推移(刪除、新建、更新檔案等操作)導致很多檔案存放的塊並不是連續的,就想文件2。假設我們現在修改文件1

而且需要修改的資料正好在文件1的塊2中。如果這時候允許你直接修改,那就意味著原來的資料存放位置要想後挪動,可是你看如果挪動,block03的內容就容易覆蓋了block04也就是文件2的內容,顯然這是不允許的,所以肯定不允許你從資料物理存放的角度來插入資料,所以就算你透過r+模式開啟也無法插入。如果你以w模式開啟系統會當做新檔案來處理所以清空原來的資料。

那麼為什麼r+可以追加呢?追加是在檔案末尾,假設block03還有空間那麼就繼續使用這個塊,如果發現這個塊空間不夠,那麼除了使用這個塊剩餘的空間它在最後儲存的時候還會去申請新的塊來儲存內容,總之原有資料順序不能變。

VI或者word為什麼可以修改?

我們用VI開啟檔案尤其是一個大檔案你會感覺慢,其實是因為它要把內容載入記憶體,然後你在記憶體中修改,也就是可以在檔案任意位置修改,當修改完成儲存磁碟的時候它會覆蓋原有內容,同時如果本次修改增加了更多內容則會申請新的磁碟塊用於存放。對於word來說也是一樣的,你發現開啟一個10M以上的word檔案也並不快它也是載入到記憶體裡,有人說如果檔案很大,我的記憶體不是很大,能不能開啟呢?可以,你是否還記得一個叫做虛擬記憶體的東西麼,當然如果實體記憶體+虛擬記憶體都不夠了,你也就打不開了。我記得之前用Windows電腦的時候出現過開啟檔案提示說記憶體不夠。

4. 如何實現修改檔案呢

在python中其實也一樣,因為它底層呼叫的就是庫函式。如果你想實現修改的效果怎麼辦?你要麼讀取都讀取到記憶體中然後修改,最後再儲存其實也就是覆蓋原始檔

我這裡有一個之前替換線上配置檔案某些資訊的例子,main()方法不是重點,可以忽略,後面的replacepath才是檔案修改部分。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import sys
import os
import re


def main(argv=None):
    if argv is None:
        argv = sys.argv

    # 規範化路徑
    path = os.path.normpath(r"/Users/rex.chen/Downloads/apps/")
    config_path = os.path.normpath(r"product/conf")

    # 獲取目錄列表
    appnamelist = os.listdir(path)
    modifyappname = []
    # 遍歷目錄
    for appname in appnamelist:
        print appname
        # 拼接路徑
        app_config_full_path = os.path.join(path, appname, config_path)
        filename = r"log4j.properties"
        filefullpath = os.path.join(app_config_full_path, filename)
        # 判斷路徑是否存在
        if os.path.exists(filefullpath):
            print filefullpath
            key_word = r"${CATALINA_OUT}"
            new_word = r"/work/logs/{{appname}}-{{port}}"
            # 呼叫自定義函式並獲取返回值
            result = replacepath(appname, filefullpath, key_word, new_word)
            if result:
                modifyappname.append(appname)
            else:
                pass
        else:
            print "檔案路徑:%s 不存在" % filefullpath

    print "需要修改日誌檔案的程式個數為:%s 名稱如下:" % len(modifyappname)
    for tempname in modifyappname:
        print tempname


def replacepath(temp_appname, temp_filepath, temp_keyword, temp_newword, ismodify=False):

    # 開啟檔案
    file1 = open(temp_filepath, 'r')

    # 這裡不能直接使用傳遞過來的keyword進行搜尋是因為這裡需要一個正規表示式。字串雖然是一個特殊的正規表示式,但是因為有特殊符號需要轉義
    # 這裡的含義是讀取檔案整體內容,並對內容進行搜尋查詢符合表示式的內容,把找到的放到變數中,這是一個列表
    allchars = re.findall("\$\{CATALINA_OUT\}", file1.read())
    # 統計列表長度,這樣就獲取了匹配的個數
    count = len(allchars)
    print "關鍵字 %s 在程式 %s 出現次數: %s" % (temp_keyword, temp_appname, count)

    # 如果count大於0,表示這個檔案裡面有我們要替換的字元,如果不大於0則退出該函式
    if count > 0:
        ismodify = True
    else:
        file1.close()
        return ismodify

    # 移動指標到檔案首部
    file1.seek(0)
    # 讀取所有內容
    alllines = file1.readlines()
    # 關閉檔案流
    file1.close()

    # 設定正則
    p = re.compile("\$\{CATALINA_OUT\}")
    # 以寫模式開啟檔案,因為這裡是同一個檔案,所以w引數會清空該檔案,這樣就實現了在同一個檔案中查詢和替換
    file2 = open(temp_filepath, 'w')
    # 遍歷之前的所有內容
    for line in alllines:
        try:
            # 使用sub進行替換,p物件包含正則也就是需要匹配的內容,temp_newword是新內容,line是在哪裡進行查詢匹配
            c = p.sub(temp_newword, line)
            # c 物件為替換後的新內容,然後寫入檔案
            file2.write(c)
        except IOError as err:
            print "檔案寫入錯誤:"+str(err)
            file2.close()

    ismodify = True
    return ismodify


if __name__ == "__main__":
    sys.exit(main())

  

我上面採取的方式就是全部讀取到記憶體,然後進行處理最後寫入。如果檔案很大怎麼辦呢?

如果檔案過大你可以一次讀取一部分然後進行修改或者用上面提到的檔案迭代器,修改後儲存到一個新檔案,然後在讀取一部分修改後追加到那個新檔案,所有修改和儲存完成之後,刪除原檔案把新檔案重新命名為原來的檔名。

5. 額外的基礎知識

延遲寫入:磁碟是低速裝置,記憶體是高速裝置,如果高速裝置向低速裝置寫入資料那根據木桶原理其速度不會高於短板,所以程式就會等待,那麼系統中通常有很多服務如果是一個對外提供服務的伺服器那麼寫入資料的操作一定少不了,如果都實時寫入磁碟那效能將會非常地下。所以所有寫入都先寫入buffer也就是快取,然後由系統來覺得什麼時候把快取中的資料同步到磁碟上。通常來說快取寫滿了就會進行一次同步或者在到了一個同步週期也會進行同步。這就是為什麼會有flush()的原因。你可以嘗試一下,當你呼叫完write(),然後讓程式睡眠一下,你去檢視原來的檔案一定沒有你寫入的資料。

相關文章