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,因为它们不仅支持大文件操作,而且速度还很快。