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,因爲它們不僅支持大文件操作,而且速度還很快。