刪除文件指定行的十種方法及性能分析

1. 問題描述:
  請設計一個程序,通過命令行參數接收一個文件名 filename.txt (純文本文件)和一個整型數字 n,實現從 filename.txt 中刪除第 n 行數據。

2. 解題思路:
  (1) 藉助臨時文件: 將文件逐行讀取,跳過要刪除的行,並將其寫入臨時文件,然後刪除源文件,重命名臨時文件爲源文件,完成刪除指定行數據。
  (2) 不借助臨時文件: 將文件以讀寫方式打開,讀取到要刪除行後,通過移動文件指針將文件後面所有行前移一行,但是最後一行會重複,可以通過截斷文件操作完成在源文件上刪除指定行數據。
  (3) 通過sed 或 awk 刪除文件指定行。

3. 代碼實現:

(1) 通過 fopen 打開文件藉助臨時文件刪除指定行數據

# filename:ques_15a.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{ 
    // 如果參數個數不正確,退出程序
    if (argc != 3) {
        printf("Usage: ./a.out filename num\n");
        exit(EXIT_FAILURE);
    }   

    char buf[4096]; 
    int linenum = atoi(argv[2]); 

    FILE *fp = fopen(argv[1], "r");    
    FILE *fpt = fopen("temp.txt", "w"); 

    // 不能打開文件,退出程序
    if (!fp) {
        printf("File %s not exist!\n", argv[1]);
        exit(EXIT_FAILURE);
    } 

    // 沒有權限寫入文件,退出程序
    char str[100];
    sprintf(str, "%s%s", "test -w ", argv[1]);
    if (system(str)) {
        printf("Can't modify file %s, permission denied!\n", argv[1]);
        exit(EXIT_FAILURE);
    } 

    int total_line = 0;     // 記錄文件總行數
    while (fgets(buf, sizeof(buf), fp)) {  
        total_line++;
    }
    fseek(fp, 0, SEEK_SET);     //將文件指針移到文件頭

    // 如果要刪除文件的行數大於文件總行數,退出程序
    if (linenum > total_line) {
        printf("%d is greater than total_line!\n", linenum);
        exit(EXIT_FAILURE);
    }

    int i = 0;      // 記錄當前行
    while (fgets(buf, sizeof(buf), fp)) {  
        i++;  
        if (i != linenum) {
            fputs(buf, fpt); 
        }
    } 

    remove(argv[1]);                // 刪除源文件
    rename("temp.txt", argv[1]);    // 重命名臨時文件爲源文件

    // 關閉文件指針
    fclose(fp); 
    fclose(fpt);

    return 0;
}

(2) 通過 linux 系統調用 open 打開文件,需要自定義讀取一行的函數,不借助臨時文件刪除指定行數據

# filename:ques_15b.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

// 讀取一行文件
int readline(int fd, char *buf)
{
    int t = 0;
    for (; ;) {
        read(fd, &buf[t], 1);
        t++;
        if (buf[t-1] == '\n') {
            break;
        }
    }
    return t;
}

// 獲取文件的大小和行數信息
int get_file_info(int fd, int *size)
{
    int num = 0;
    char ch;

    while (read(fd, &ch, 1) > 0) {
        (*size)++;
        if (ch == '\n') {
            num++;
        }
    }

    return num;
}

int main(int argc, char *argv[])
{
    // 如果參數個數不正確,退出程序
    if (argc != 3) {
        printf("Usage: ./a.out filename num\n");
        exit(EXIT_FAILURE);
    }

    int fd;         // 文件描述符
    char buf[4096];
    int linenum = atoi(argv[2]);

    // 不能以讀寫權限打開文件,退出程序
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s, file not exist or permission denied!\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    int size = 0;       // 記錄文件大小
    // 獲取文件的大小和行數信息
    int total_line = get_file_info(fd, &size);

    // 如果要刪除文件的行數大於文件總行數,退出程序
    if (linenum > total_line) {
        printf("%d is greater than total_line!\n", linenum);
        exit(EXIT_FAILURE);
    }

    int s = 0;      // 記錄要刪除行大小
    int t = 0;      // 記錄每一行大小
    int i = 0;      // 記錄當前行數
    lseek(fd, 0, SEEK_SET); // 將文件指針移到文件頭

    // 將要刪除行後的每一行前移一行
    while (read(fd, &buf[0], 1) > 0) {

        lseek(fd, -1, SEEK_CUR);
        memset(buf, 0, sizeof(buf));
        readline(fd, buf);
        i++;      
        t = strlen(buf);

        if (i == linenum) {
            s = t;
        }

        if (i > linenum) {
            lseek(fd, -(s+t), SEEK_CUR);
            write(fd, buf, strlen(buf));
            lseek(fd, s, SEEK_CUR);
        }
    }

    ftruncate(fd, size-s);  // 截斷文件
    close(fd);              // 關閉文件描述符

    return 0;
}

(3) 通過 fopen 打開文件,不借助臨時文件刪除指定行數據

# filename:ques_15c.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <unistd.h>

int main(int argc, char *argv[])
{ 
    // 如果參數個數不正確,退出程序
    if (argc != 3) {
        printf("Usage: ./a.out filename num\n");
        exit(EXIT_FAILURE);
    }

    int linenum = atoi(argv[2]); 
    char buf[4096];

    FILE *fp = fopen(argv[1], "r+");    
    // 不能以讀寫權限打開文件,退出程序
    if (!fp) {
        printf("Can't open file %s, file not exist or permission denied!\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    int total_line = 0; // 記錄文件總行數
    int size = 0;       // 記錄文件總大小

    while (fgets(buf, sizeof(buf), fp)) {  
        size += strlen(buf);
        total_line++;
    }

    // 如果要刪除文件的行數大於文件總行數,退出程序
    if (linenum > total_line) {
        printf("%d is greater than total_line!\n", linenum);
        exit(EXIT_FAILURE);
    }


    int s = 0;      // 記錄要刪除行大小
    int t = 0;      // 記錄每一行大小
    int i = 0;      // 記錄當前行數
    fseek(fp, 0L, SEEK_SET);    // 將文件指針移到文件頭

    // 將要刪除行後的每一行前移一行
    while (fgets(buf, sizeof(buf), fp)) {  

        i++;
        t = strlen(buf);

        if (i == linenum) {
            s = t;
        }

        if (i > linenum) {
            fseek(fp, -(s+t), SEEK_CUR);
            fputs(buf, fp);
            fseek(fp, s, SEEK_CUR);
        }
    } 

    truncate(argv[1], size-s);  // 截斷文件
    fclose(fp);                 // 關閉文件指針

    return 0;
}

(4) 這三個刪除文件指定行的函數都需藉助臨時文件完成

#!/usr/bin/env python
#-*- coding: UTF-8 -*-
# filename: ques_15a.py

import sys
import os

def removeLine1(filename, linenum):

    try:
    fro = open(filename, "r")
    frw = open("temp.txt", "w")
    # 如果文件不存在或者沒有權限操作文件,拋出異常並退出程序
    except IOError as e:
    print "An error has occurred ==> " + str(e)
        sys.exit()

    total_line = 0
    # 獲取文件的總行數
    while True:
    line = fro.readline()
    if line == '':
        break
    total_line += 1
    # 將文件指針移到文件頭
    fro.seek(0, 0)

    # 如果要刪除文件的行數大於文件總行數,退出程序
    if linenum > total_line:
        print str(linenum) + " is greater than total_line!"
        sys.exit()

    # 將除過要刪除文件行外的其他行寫入臨時文件
    current_line = 1
    for line in fro:
    if current_line != linenum:
        frw.write(line)
    current_line += 1

    fro.close()
    frw.close()

    try: 
        os.remove(filename)             # 刪除源文件
        os.rename("temp.txt", filename) # 重命名臨時文件爲源文件
    except OSError as e:
    print "An error has occurred ==> " + str(e)
        sys.exit()


def removeLine2(filename, linenum):

    try:
    fro = open(filename, "r")
    frw = open("temp.txt", "w")
    # 如果文件不存在或者沒有權限操作文件,拋出異常並退出程序
    except IOError as e:
    print "An error has occurred ==> " + str(e)
        sys.exit()

    total_line = 0
    # 獲取文件的總行數
    while True:
    line = fro.readline()
    if line == '':
        break
    total_line += 1
    # 將文件指針移到文件頭
    fro.seek(0, 0)

    # 如果要刪除文件的行數大於文件總行數,退出程序
    if linenum > total_line:
        print linenum + " is greater than total_line!"
        sys.exit()

    # 將除過要刪除文件行外的其他行寫入臨時文件
    current_line = 0
    while True:
        line = fro.readline()
        if line == '':
            break
        current_line += 1
        if current_line != linenum:
            frw.write(line)

    fro.close()
    frw.close()

    try: 
        os.remove(filename)             # 刪除源文件
        os.rename("temp.txt", filename) # 重命名臨時文件爲源文件
    except OSError as e:
    print "An error has occurred ==> " + str(e)
        sys.exit()

def removeLine3(filename, linenum):
    with open(filename, 'r+') as ouf:
        with open(filename, 'r') as inf:

        total_line = 0
        # 獲取文件的總行數
        while True:
            line = fro.readline()
            if line == '':
            break
        total_line += 1
        # 將文件指針移到文件頭
        fro.seek(0, 0)

        # 如果要刪除文件的行數大於文件總行數,退出程序
        if linenum > total_line:
        print linenum + " is greater than total_line!"
        sys.exit()

            # 跳過要刪除行前的所有行
            current_line = 0 
            while current_line < linenum:
                inf.readline()
                current_line += 1

            seekpos = inf.tell()
            # 把ouf文件指針從開始向後移動seekpos個字節
            ouf.seek(seekpos, 0)

            # 跳過要刪除行
            inf.readline()

            # 將後面的所有行寫入ouf
            current_line = inf.readline()
            while current_line:
                ouf.writelines(current_line)
                current_line = inf.readline()

        ouf.truncate() # 截斷文件

if __name__ == '__main__':

    # 如果參數個數不正確,退出程序
    if len(sys.argv) != 3:
        print "Usage: python " + sys.argv[0] + " filename linenum"
        sys.exit()
    removeLine1(sys.argv[1],int(sys.argv[2]))
    #removeLine2(sys.argv[1],int(sys.argv[2]))
    #removeLine3(sys.argv[1],int(sys.argv[2]))

(5) 不借助臨時文件刪除指定行數據

#!/usr/bin/env python
# coding=utf-8

import sys
import os

def removeLine(filename, linenum):

    try:
    fro = open(filename, "r+")
    # 如果文件不存在或者沒有權限操作文件,拋出異常並退出程序
    except IOError as e:
    print "An error has occurred ==> " + str(e)
        sys.exit()

    current_line = 0    # 記錄當前行數
    total_line = 0      # 記錄文件總行數
    size = 0            # 記錄文件總大小
    s = 0               # 記錄刪除行文件大小

    # 獲取文件的總行數
    while True:
        line = fro.readline()
        if line == '':
            break
        size += len(line)
        total_line += 1
    # 將文件指針移到文件頭
    fro.seek(0, 0)

    # 如果要刪除文件的行數大於文件總行數,退出程序
    if linenum > total_line:
        print str(linenum) + " is greater than total_line!"
        sys.exit()

    # 將要刪除行後的每一行前移一行
    while True:
        line = fro.readline()       
        if line == '':
            break
        t = len(line)
        current_line += 1

        if current_line == linenum:
            fro.seek(-t, 1)
            line = fro.readline()
            s = len(line)

        if current_line > linenum:
            fro.seek(-(s+t), 1)
            fro.write(line)
            fro.seek(s, 1)

    fro.truncate(size-s)        # 截斷文件
    fro.close()                 # 關閉文件

if __name__ == '__main__':

    # 如果參數個數不正確,退出程序
    if len(sys.argv) != 3:
        print "Usage: python " + sys.argv[0] + " filename linenum"
        sys.exit()
    removeLine(sys.argv[1],int(sys.argv[2]))

(6) 通過 sed 刪除文件指定行數據

# filename: ques_15a.sh

#!/bin/bash

# 如果參數個數不正確,退出程序
if [ $# -ne 2 ];
then
    echo "Usage: $0 filename linenum"
    exit
fi

# 如果文件不存在,退出程序
if [ ! -f $1 ];
then
    echo "File $1 not exist!"
    exit
fi

# 如果沒有文件寫入權限,退出程序
if [ ! -w "$1" ];
then
    echo "Can't modify file $1, permission denied!"
    exit
fi

total_line=`sed -n '$=' $1`

# 如果要刪除文件的行數大於文件總行數,退出程序
if [ $2 -gt $total_line ];
then
    echo "$2 is greater than total_line!"
    exit
fi

sed -i $2d $1

(7) 通過 awk 刪除文件指定行數據

# filename: ques_15b.sh

#!/bin/bash

# 如果參數個數不正確,退出程序
if [ $# -ne 2 ];
then
    echo "Usage: $0 filename linenum"
    exit
fi

# 如果文件不存在,退出程序
if [ ! -f $1 ];
then
    echo "File $1 not exist!"
    exit
fi

# 如果沒有文件寫入權限,退出程序
if [ ! -w "$1" ];
then
    echo "Can't modify file $1, permission denied!"
    exit
fi

total_line=`awk 'END{print NR}' $1`

# 如果要刪除文件的行數大於文件總行數,退出程序
if [ $2 -gt $total_line ];
then
    echo "$2 is greater than total_line!"
    exit
fi

awk 'BEGIN{printf "" > "temp"}
    {if(NR!='$2')print $0 >> "temp";}
    END{t="mv temp ";cmd=t""ARGV[1];system(cmd)}' $1

(8) 通過 read 直接讀取文件刪除指定行數據

# filename: ques_15c.sh

#!/bin/bash

# 如果參數個數不正確,退出程序
if [ $# -ne 2 ];
then
    echo "Usage: $0 filename linenum"
    exit
fi

# 如果文件不存在,退出程序
if [ ! -f $1 ];
then
    echo "File $1 not exist!"
    exit
fi

# 如果沒有文件寫入權限,退出程序
if [ ! -w "$1" ];
then
    echo "Can't modify file $1, permission denied!"
    exit
fi

total_line=`sed -n '$=' $1`

# 如果要刪除文件的行數大於文件總行數,退出程序
if [ $2 -gt $total_line ];
then
    echo "$2 is greater than total_line!"
    exit
fi

current_line=0
while read line
do 
    current_line=$(($current_line+1))
    if [ $current_line == $2 ];
    then
        continue
    fi
    echo $line >> temp
done < $1

mv temp $1

(9) 通過 cat 顯示文件然後用 read 讀取的方法刪除指定行數據

# filename: ques_15d.sh

#!/bin/bash

# 如果參數個數不正確,退出程序
if [ $# -ne 2 ];
then
    echo "Usage: $0 filename linenum"
    exit
fi

# 如果文件不存在,退出程序
if [ ! -f $1 ];
then
    echo "File $1 not exist!"
    exit
fi

# 如果沒有文件寫入權限,退出程序
if [ ! -w "$1" ];
then
    echo "Can't modify file $1, permission denied!"
    exit
fi

total_line=`sed -n '$=' $1`

# 如果要刪除文件的行數大於文件總行數,退出程序
if [ $2 -gt $total_line ];
then
    echo "$2 is greater than total_line!"
    exit
fi

current_line=0
while read line
do 
    current_line=$(($current_line+1))
    if [ $current_line == $2 ];
    then
        continue
    fi
    echo $line >> temp
done < $1

mv temp $1

(10) 通過 awk 腳本操作文件刪除指定行數據

# filename: ques_15.awk

#!/bin/awk -f

BEGIN{
    ARGC = 2            # 指定接收命令行參數個數
    f = "test -f "
    cmd1 = f""ARGV[1]
    # 如果文件不存在,退出程序
    if(system(cmd1)) {
        print "File",ARGV[1],"not exist!"
        exit
    }

    w = "test -w "
    cmd2 = w""ARGV[1]
    # 如果沒有寫入文件權限,退出程序
    if(system(cmd2)) {
        print "Can't modify file",ARGV[1],", permission denied!"
        exit
    }
    printf "" > "temp"
}
{
    # 如果不是要刪除的行,將其寫入臨時文件
    if(NR != ARGV[2])
        print >> "temp"
}
END{
    # 如果要刪除行號大於文件總行數,退出程序
    if(ARGV[2] > NR) {
        print ARGV[2],"is greater than the number of lines!"
        exit
    }
    # 重命名臨時文件爲源文件
    t1 = "mv temp "
    t2 = " 2>/dev/null"
    cmd = t1""ARGV[1]""t2
    system(cmd)
}

4. 性能分析  

我們在這兒用一個日誌文件來測試這些代碼的運行時間,以下是這個日誌文件的基本信息(大小和行數):

這裏寫圖片描述

  爲公平起見,已將此日誌文件備份,保證每段代碼操作時源文件都是一樣的,以下是這些代碼運行所需的時間和運行結果:

(1) 運行第一段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(2) 運行第二段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(3) 運行第三段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(4) 運行第四段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(5) 運行第五段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(6) 運行第六段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(7) 運行第七段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(8) 運行第八段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(9) 運行第九段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

(10) 運行第十段代碼分別刪除文件第一行,中間一行(274080)和最後一行(548160)的結果和所需的時間:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

非法輸入處理:

這裏寫圖片描述

總結: 如果通過臨時文件刪除文件指定行數據,那麼刪除文件開頭,中間,或者末尾所需時間基本相同;如果通過文件指針移動而不借助臨時文件刪除文件指定行數據,刪除文件開頭最慢,其次是中間,刪除文件末尾速度最快,因爲刪除文件末尾只需少數行移動,而刪除文件開頭後面每一行都要移動,所以耗時相對較長。

Note: 對於文件修改刪除這類操作,推薦使用 sed 和 awk,因爲它們不僅支持大文件操作,而且速度還很快。

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