目录
前言
本文是基于Linux文件编程的一个小实验,用文件IO来读取Excel表格的数据,处理后写入另一个文件,本文只是对文件IO的基本应用,巩固文件编程的知识。Linux一切皆文件。本文的侧重点是对于数据的处理。
(图1)
来看这个简单的表格,要计算三人的总分 ,对于熟练应用Excel表格的人来说很简单,在 E2输入计算公式 =sum(b2:d2) 然后下拉就能计算三人的总分;如果再加上一个评分栏,根据总分给每个人评级,对于不熟悉Excel的人来说就有点难了,但我们可以通过编程实现这个功能。
(上图为最终实现的效果,将图1的Excel文件另存为 .csv 格式)
.csv格式是用逗号分割的文件,用记事本打开可以看到第一行的数据为",语文,数学,英语,总分,评价",之所以第一个字符是逗号,是因为表格的第一行第一列为空,csv格式会用逗号分隔每个数据,"总分"和"评价"那一列也没有数据,但是空出来的数据之间也有逗号分隔开,将score.csv文件上传到虚拟机,就可以开始编写代码了。
整体的代码框架
对于Linux文件编程不了解的,可以看我之前的博客Linux文件编程,里面有详细的函数介绍和代码示例。
我们只需要用三个函数:
open函数
cpp
#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函数
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
write函数
cpp
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
整体的函数框架
打开数据文件
创建结果文件
在while循环里
读取一行的数据
处理数据
写入结果文件
最后关闭文件
以上就是整个程序的大致框架,现在来写程序,难点还是数据的处理
cpp
#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
cpp
/* 返回值: 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
cpp
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循环,程序结束。