目录
前言
本文是基于Linux文件编程的一个小实验,用文件IO来读取Excel表格的数据,处理后写入另一个文件,本文只是对文件IO的基本应用,巩固文件编程的知识。Linux一切皆文件。本文的侧重点是对于数据的处理。
(图1)
来看这个简单的表格,要计算三人的总分,对于熟练应用Excel表格的人来说很简单,在 E2 输入计算公式 =sum(b2:d2) 然后下拉就能计算三人的总分;如果再加上一个评分栏,根据总分给每个人评级,对于不熟悉Excel的人来说就有点难了,但我们可以通过编程实现这个功能。
(上图为最终实现的效果,将图1的Excel文件另存为 .csv 格式)
.csv格式是用逗号分割的文件,用记事本打开可以看到第一行的数据为",语文,数学,英语,总分,评价",之所以第一个字符是逗号,是因为表格的第一行第一列为空,csv格式会用逗号分隔每个数据,“总分”和“评价”那一列也没有数据,但是空出来的数据之间也有逗号分隔开,将score.csv文件上传到虚拟机,就可以开始编写代码了。
整体的代码框架
对于Linux文件编程不了解的,可以看我之前的博客Linux文件编程,里面有详细的函数介绍和代码示例。
我们只需要用三个函数:
open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
read函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
write函数
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
整体的函数框架
打开数据文件
创建结果文件
在while循环里
读取一行的数据
处理数据
写入结果文件
最后关闭文件
以上就是整个程序的大致框架,现在来写程序,难点还是数据的处理
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>/*打印错误信息所要包含的头文件*/
/* 最终编译出来的可执行文件的用法,以及main函数的参数
* ./process_excel data.csv result.csv
* argc = 3
* argv[0] = "./process_excel"
* argv[1] = "data.csv"
* argv[2] = "result.csv"
*/
int main(int argc, char **argv)
{
/*数据文件和结果文件的文件描述符*/
int fd_data, fd_result;
/*记录用户自己编写的函数的返回值*/
int len;
/*存放从数据文件读到的数据*/
unsigned char data_buf[1000];
/*存放处理好即将写入结果文件的数据*/
unsigned char result_buf[1000];
/*如果main函数传入的参数不对,则打印用法*/
if (argc != 3)
{
printf("Usage: %s <data.csv> <result.csv>\n", argv[0]);
return -1;
}
/*以只读的方式打开数据文件*/
fd_data = open(argv[1], O_RDONLY);
if (fd_data < 0)
{
/*打开失败就打印失败信息*/
printf("can not open file %s\n", argv[1]);
perror("open");
return -1;
}
else
{
/*成功打开打印数据文件的文件描述符*/
printf("data file fd = %d\n", fd_data);
}
/*可读可写方式打开(不存在就创建),截断文件,权限为0644(文件所有者具有读写权限,
其他用户具有读权限)*/
fd_result = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd_result < 0)
{
printf("can not creat file %s\n", argv[2]);
perror("creat");
return -1;
}
else
{
printf("result file fd = %d\n", fd_result);
}
/*------------------------------------重点------------------------------------*/
while(1)
{
/*从数据文件里读取1行*/
len = read_line(fd_data, data_buf);
if(len == -1)
{
break;
}
if(len != 0)
{
/*处理数据,将处理好的数据存入result_buf*/
process_data(data_buf, result_buf);
/*写入结果文件,要写入的每一行数据都是不带回车换行的,需要我们自己写入*/
write(fd_result, result_buf, strlen(result_buf));
write(fd_result, "\r\n", 2);
}
}
/*养成好习惯,关闭打开的文件,避免造成损坏*/
close(fd_data);
close(fd_result);
return 0;
}
整个看下来,思路还是比较清晰的,就是文件IO的基本应用,重点就在于main函数的while循环里,有两个函数需要我们自己编写,read_line 和 process_data ,根据read_line函数的返回值决定是否对读取到的数据进行处理。
如何读取数据文件的数据
读取数据文件后直接拿来判断显然不现实,在每一行数据的结尾都有一个回车换行才能从下一行开始写新的数据,但这些回车换行我们看不到,我们可以将这些内容转换成十六进制的数据。
在终端输入以下命令 hexdump -C score.csv 后面的是要转换的文件名,可以看到十六进制的数据
可以看到,回车换行的十六进制是 0x0d 和 0x0a ,就是我们常用的 '\r' 和 '\n' ,现在来编写读取每一行数据的函数
read_line
/* 返回值: n 表示读到了一行的数据的个数(n >= 0)
* -1 表示读到了文件的尾部或者出错
*/
static int read_line(int fd, unsigned char *buf)
{
/*循环读入一个字符*/
/*如何判断已经读完一行?读到 0x0d , 0x0a*/
/*读到一个字符放入c*/
unsigned char c;
/*记录read函数的返回值*/
int len;
/*传入的字符串的元素指针*/
int i = 0;
/*根据err变量判断该函数的返回值*/
int err = 0;
while(1)
{
/*read函数读取成功会返回读取到的字节数,失败返回-1*/
len = read(fd, &c, 1);
if(len <= 0)
{
/*读到文件末尾或发生错误*/
err = -1;
break;
}
else
{
if(c != '\n' && c != '\r')
{
buf[i++] = c;
}
else
{
/*碰到回车换行*/
err = 0;
break;
}
}
}
/*表示字符串的结束*/
buf[i] = '\0';
if(err && (i == 0))
{
/*读到文件尾部并且一个数据都没有读到*/
return -1;
}
else
{
/*返回读取到的数据的个数+1,因为我们手动添加了结束符 '\0'*/
return i;
}
}
这个函数还是需要点时间看的,建议结合转换后的十六进制表示的文件来看,思路会更加清晰,一直循环读取数据文件,一个字节一个字节的读
读到0x0d的时候,跳出while循环,执行 buf[i] = '\0'; 这句代码,构造出一个字符串存入data_buf里,然后去处理数据。读到0x0a也立马跳出循环,构造出一个空的字符串。
如何处理读取到的数据
直接看代码
process_data
void process_data(unsigned char *data_buf, unsigned char *result_buf)
{
/* 示例1:data_buf=",语文,数学,英语,总分,评价"
* result_buf=",语文,数学,英语,总分,评价"
* 示例2:data_buf="张三,90,91,92"
* result_buf="张三,90,91,92,273,A+"
*/
char name[100];
int scores[3];
int sum;
char *levels[] = {"A+", "A", "B"};
int level;
if (data_buf[0] == 0xef) /* 对于UTF-8编码的文件,它的前3个字符是0xef 0xbb 0xbf */
{
/*进入这个分支是因为我们不需要处理第一行的数据*/
/*将data_buf文件的数据拷贝到result_buf*/
strcpy(result_buf, data_buf);
}
else
{
/*用sscanf拆分字符串,第一个为%[^,],是因为如果用%s会分割失败*/
sscanf(data_buf, "%[^,],%d,%d,%d,", name, &scores[0], &scores[1], &scores[2]);
/*计算总分*/
sum = scores[0] + scores[1] + scores[2];
if (sum >= 270)
level = 0;
else if (sum >= 240)
level = 1;
else
level = 2;
/*构造字符串*/
sprintf(result_buf, "%s,%d,%d,%d,%d,%s", name, scores[0], scores[1], scores[2], sum, levels[level]);
}
}
处理数据就比较简单了,第一行照搬不用处理,之后用sscanf拆分数据,分类再组合就可以写入结果文件了。
运行结果
编译运行后cat一下 result.csv ,查看文件的内容,可以看到正是我们想要的效果,传回Windows看看Excel表格,非常完美。
总结
因为是一行一行的处理数据,有些小伙伴可能会疑惑,数据为什么不会被覆盖,为什么不会得到同样的数据?
我们要知道read函数的原理,你读一个字符,文件的光标就会移动一个位置,你下次再打开这个文件,他的光标位置是不变的,除非你用lseek函数来改变光标的位置,所以我们每读一行,再加上结束符'\0'就相当于一个字符串,然后立马处理数据并且写入结果文件;下次再去读数据文件,是从上一次读的位置的下一个开始读,所以才能得到一行一行的数据。
什么时候读完?
读到文件尾部就读完了,就会跳出main函数的while循环,程序结束。