C再学习 02 - 输入与输出

C再学习 02 - 输入与输出

上一次学C的时候没有看标准库,这一次看了才发现,原来printf实际上是fprintf的简写,相当于自动传入了stdout作为参数。 输入与输出 这一次就借助一个程序来学习一下标准输入输出与重定向吧。 程序来自于Head First C的第三章的示例程序。用格式化的方式读入数据并且转换成JSON格式

上一次学C的时候没有看标准库,这一次看了才发现,原来printf实际上是fprintf的简写,相当于自动传入了stdout作为参数。

输入与输出

这一次就借助一个程序来学习一下标准输入输出与重定向吧。 程序来自于Head First C的第三章的示例程序。用格式化的方式读入数据并且转换成JSON格式。

重定向标准输入

一开始的程序很简单,通过读入一行的三个数字,然后以JSON格式输出:
#include <stdio.h>

int parse(void){
    float latitude;
    float longitude;
    char info[80];
    int started = 0;

    puts("data=[");

    while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
        if (started) {
            printf(",\n");
        } else {
            started = 1;
        }
        printf("{latitude: %f, longitude: %f, info: \"%s\"}", latitude, longitude, info);
    }

    puts("\n]");
    return 0;
}
但是单独运行这个程序的时候,会等待键盘的输入,然后输出一行结果。这是因为标准输入和输出默认情况下是键盘和显示器。 现在我们希望用一个csv文件作为输入,其中的内容是复核程序读取格式的一行一行的数据:
42.363400,-71.098465,Speed = 21
43.363327,-72.097588,Speed = 23
44.363255,-73.096710,Speed = 17
45.363182,-74.095833,Speed = 22
46.363110,-75.094955,Speed = 14
47.363037,-76.094078,Speed = 16
48.362965,-77.093201,Speed = 18
49.362892,-78.092323,Speed = 22
50.362820,-79.091446,Speed = 17
51.362747,-80.090569,Speed = 23
52.362675,-81.089691,Speed = 14
53.362602,-82.088814,Speed = 19
54.362530,-83.087936,Speed = 16
55.362457,-84.087059,Speed = 16
56.362385,-85.086182,Speed = 21
这个时候无需重新编译程序,实际上现在学习的是操作系统的命令,也就是重定向标准输入。 将csv文件和编译出的exe文件放在同一个目录之下,然后输入c < gpsdata.csv<表示重定向标准输入到这个文件,即将csv文件的内容输入给c.exe文件。 之后程序成功的输出了结果:
[
{"latitude": 42.363400, "longitude": -71.098465, "info": "Speed = 21"},
{"latitude": 43.363327, "longitude": -72.097588, "info": "Speed = 23"},
{"latitude": 44.363255, "longitude": -73.096710, "info": "Speed = 17"},
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
{"latitude": 48.362965, "longitude": -77.093201, "info": "Speed = 18"},
{"latitude": 49.362892, "longitude": -78.092323, "info": "Speed = 22"},
{"latitude": 50.362820, "longitude": -79.091446, "info": "Speed = 17"},
{"latitude": 51.362747, "longitude": -80.090569, "info": "Speed = 23"},
{"latitude": 52.362675, "longitude": -81.089691, "info": "Speed = 14"},
{"latitude": 53.362602, "longitude": -82.088814, "info": "Speed = 19"},
{"latitude": 54.362530, "longitude": -83.087936, "info": "Speed = 16"},
{"latitude": 55.362457, "longitude": -84.087059, "info": "Speed = 16"},
{"latitude": 56.362385, "longitude": -85.086182, "info": "Speed = 21"}
]

有了重定向输入,就可以处理以文件形式的数据,而不是每次都一个一个从键盘上输入了。

重定向标准输出

肯定立刻就会想到,那重定向输出应该也可以,是的,可以把上边程序的结果输出成一个.json文件。 输入c < gpsdata.csv >output.json即可。>是重定向标准输出符号。这里就会将本来应该打印在显示器上的结果重定向输出到output.json文件。执行之后就在同一目录下找到这个文件。 当然,熟悉JSON格式的肯定知道这个输出结果其实并不是一个JSON文件,只需要略微修改一下原来的程序即可:
#include <stdio.h>

int parse(void) {
    float latitude;
    float longitude;
    char info[80];
    int started = 0;

    puts("[");

    while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
        if (started) {
            printf(",\n");
        } else {
            started = 1;
        }
        printf("{\"latitude\": %f, \"longitude\": %f, \"info\": \"%s\"}", latitude, longitude, info);
    }

    puts("\n]");
    return 0;
}

重定向标准错误输出

现在又有了新需求,我们的程序在处理数据的时候,至少要具备的功能就是对数据进行检测是否符合要求,如果不符合要求,需要提醒使用者,而不是闷头继续处理。从错误的输入得到的结果没有意义。 现在给输入加上一些判断,如果出错的话,就直接返回:
#include <stdio.h>

int parse(void) {
    float latitude;
    float longitude;
    char info[80];
    int started = 0;

    puts("[");

    while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
        if (started) {
            printf(",\n");
        } else {
            started = 1;
        }

        if(latitude<-90.0 || latitude> 90.0){
            printf("Invalid latitude: %f\n", latitude);
            return 2;
        }

        if(longitude < -180.0 || longitude>180.0){
            printf("Invalid longitude: %f\n", longitude);
            return 2;
        }

        printf("{\"latitude\": %f, \"longitude\": %f, \"info\": \"%s\"}", latitude, longitude, info);
    }

    puts("\n]");
    return 0;
}
现在判断经度和纬度超出范围的时候,会停止处理程序并且报错。 在csv文件中插入一条错误的数据:
342.362965,-712.093201,Speed = 18
然后再执行同样的命令,可以看到控制台没有任何错误提示,打开output.json,发现错误信息写在了里边:
[
{"latitude": 42.363400, "longitude": -71.098465, "info": "Speed = 21"},
{"latitude": 43.363327, "longitude": -72.097588, "info": "Speed = 23"},
{"latitude": 44.363255, "longitude": -73.096710, "info": "Speed = 17"},
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
Invalid latitude: 342.362976
这是因为在重定向标准输出的时候,其实还有一个错误输出,也一并重定向到文件中了。操作系统可以使用>2来单独指定标准错误输出的重定向。 想重定向这个,就不能使用printf函数了,而要使用fprintf函数,printf只是fprintf的一个简化形式。 用fprintf来修改一下程序:
#include <stdio.h>

int parse(void) {
    float latitude;
    float longitude;
    char info[80];
    int started = 0;

    puts("[");

    while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
        if (started) {
            printf(",\n");
        } else {
            started = 1;
        }

        if(latitude<-90.0 || latitude> 90.0){
//            printf("Invalid latitude: %f\n", latitude);
            fprintf(stderr,"Invalid latitude: %f\n", latitude);
            return 2;
        }

        if(longitude < -180.0 || longitude>180.0){
//            printf("Invalid longitude: %f\n", longitude);
            fprintf(stderr,"Invalid longitude: %f\n", longitude);
            return 2;
        }

        printf("{\"latitude\": %f, \"longitude\": %f, \"info\": \"%s\"}", latitude, longitude, info);
    }

    puts("\n]");
    return 0;
}
现在再运行程序,错误信息就会被打印到标准错误输出,默认的标准错误输出也是屏幕,则数据会写到文件中,而错误会显示到屏幕上。 如果还想要重定向标准错误输出,就使用2>来重定向,比如:
c < gpsdata.csv >output.json 2>error.txt
这就会将输出写入output.json,将错误信息写入error.txt。

管道符号

假设我们的csv文件是其他程序生成的,不会有错误。现在我们专注于解决问题。 现在需要从结果中过滤出所有的在一定范围内的数据,然后生成对应的JSON文件。考虑一下我们现在的程序,是一个接受CSV然后输出JSON的程序。 我们只要想办法过滤一下所有的csv文件,从中取出符合要求的数据传递给我们现在的程序就可以了。 这个时候就要用到管道符|,表示将一个程序的标准输出连接到另外一个程序的标准输入。而最开始的输入和最后的输出,可以通过重定向来控制。 先来编写一个过滤CSV的程序:
int filter(void){
    float latitude;
    float longitude;
    char info[80];
    while (scanf("%f,%f,%79[^\n]", &latitude, &longitude, info) == 3) {
        if(latitude>45.0 && latitude<50.0){
            if(longitude<-73.0 && longitude > -80.0){
                printf("%f,%f,%s\n", latitude, longitude, info);
            }
        }
    }
    return 0;
}
然后把这个编译生成一个filter.exe文件,刚才的程序我们叫json.exe文件,将两个文件和csv文件放在同一个目录下,然后执行(windows下):
(filter | json) < gpsdata.csv >output.json
检查生成的JSON文件:
[
{"latitude": 45.363182, "longitude": -74.095833, "info": "Speed = 22"},
{"latitude": 46.363110, "longitude": -75.094955, "info": "Speed = 14"},
{"latitude": 47.363037, "longitude": -76.094078, "info": "Speed = 16"},
{"latitude": 48.362965, "longitude": -77.093201, "info": "Speed = 18"},
{"latitude": 49.362892, "longitude": -78.092323, "info": "Speed = 22"}
]
可以看到成功的过滤出来了结果。这里注意的是无论在windows下还是linux下,都需要用括号包起来管道内的所有程序,这样才能知道是一整个管道,如果不包起来,则认为对每个程序设置重定向。 看来linux的bash编程之后也要学了,学无止境啊。

创建自己的数据流

在一个程序运行的时候,操作系统会为其创建三个数据流,分别是标准输入,标准输出和标准错误输出。 除了重定向标准输入输出,还可以创建自己的数据流,其实就是自定义输入和输出,一般输入没有太多的需要自定义的内容,主要是输出。 所以自定义的数据流可以用一个指向文件的指针来表示:
FILE *in_file = fopen("input.txt","r");
r代表模式,已经很熟悉了。w表示写,r表示读,a表示追加。 创建了这个文件指针之后,就可以和stdio之类的流一样当做fprintf函数的第一个参数来往里边写东西了,fscanf也可以从其中进行输入。 只是要记得在完成之后使用fclose(in_file)来关闭数据流。 来写一个简单的例子,现在有一个文本文件input.txt如下:
a,b,c
d,e,f
g,h,i
想让其中所有逗号分隔的内容,用|分割并且追加到一个output.txt的尾部:
#include <stdio.h>

int custom(void){

    char a[5];
    char b[5];
    char c[5];

    FILE *in_file = fopen("input.txt", "r");
    FILE *out_file = fopen("output.txt", "a");

    //读逗号分隔的字符串真不容易,要先读到逗号之前的部分,之后逗号放回缓冲区。
    //再读的时候逗号匹配逗号,再去按照%[^,]来读取
    //如果直接要逗号分隔,不加%[^,]是不行的,因为%s会一次读完所有的空白
//    fscanf(in_file, "%[^,],%[^,],%[^\n]\n", a, b, c);
//    printf("%s|%s|%s|", a, b, c);
//
//    fscanf(in_file, "%[^,],%[^,],%[^\n]", a, b, c);
//    printf("%s|%s|%s|", a, b, c);

    while (fscanf(in_file, "%[^,],%[^,],%[^\n]\n", a, b, c) == 3) {
        fprintf(out_file, "%s|%s|%s|", a, b, c);
    }


    fclose(in_file);
    fclose(out_file);

    return 0;
}
这里比较搞的是用扫描用逗号分隔的字符串。由于默认情况下%s是会一直扫描到空白字符为止,所以这里要使用类似正则的匹配到逗号为止。 然而注意,逗号匹配不成功后会放回到缓冲区,所以必须添加一个逗号分隔符用于匹配逗号。在最末尾,会在匹配到\n之前,然后把\n放回缓冲区。会匹配到下一行开始的新字符串上,导致使用a模式的时候会换行。所以必须再添加一个\n来匹配掉\n。 这样执行程序的时候,就不会添加上换行字符。 运行程序后产生output.txt文件,结果是:
a|b|c|d|e|f|g|h|i|
再运行一次,变成:
a|b|c|d|e|f|g|h|i|a|b|c|d|e|f|g|h|i|
scanf函数的花头实在太多了,在匹配的时候一定要当心。 还需要注意的是,fopen函数如果打开错误,会返回0,也就是判断是否打开成功:
FILE *in;
if (!(in = fopen("我不存在.txt", "r"))) {
    fprintf(stderr, "无法打开文件.\n");
    return 1;
}

运行时候获取参数

为了写一个完整的在命令行下可以使用的小程序,还需要知道最后一点,就是可以从命令行中获取参数来执行任务。 之前写main(void)已经形成习惯,其实真正的main()函数要写成如下方式:
int main(int argc, char *argv[]){

}
int argc是记录命令行一共有几个参数,其中自己的可执行文件的名称也算一个参数。 而char *argv[]记录了所有参数的内容,是一个字符串数组或者说是一个char * 指针数组。 这样知道了argc,和数组,就可以遍历其中来获取参数,之后可以进行各种操作。 比如先写一个获取所有参数的命令:
int getparam(int argc, char *argv[]){
    int i = 0;
    for (i = 0; i < argc; i++) {
        printf("顺序为 %d 的参数是 %s\n", i, argv[i]);
    }
}
然后在main执行的时候把参数传给这个函数用来打印:
int getparam(int , char *[]);

int main(int argc, char *argv[]) {

    getparam(argc, argv);

}
编译之后,在命令行下随便输入一些参数:
c fdsj 98 fdk -312 s9d8
可以看到结果是:
顺序为 0 的参数是 c
顺序为 1 的参数是 fdsj
顺序为 2 的参数是 98
顺序为 3 的参数是 fdk
顺序为 4 的参数是 -312
顺序为 5 的参数是 s9d8
这样就可以根据其中的内容,在程序中进行逻辑处理了,可以规定argc的长度,强制用户输入一些参数。这里边的逻辑就会更复杂了,但是万变不离其宗。

命令行选项库

如果我们的工具写的比较死,比如我们规定argc为3,去掉自己的文件名之后,还有两个参数,必须是源文件和目标文件名,这样是可以的。但是不够灵活。 如果用过一些操作系统工具就会知道,这些工具常常是以提供选项的方式来操作,比如经典的ls命令:
ls -l
就会列出与单独使用ls不同的结果,使用了-l的结果更加详细。 这个通过一个一个搜索数组去进行判断固然可以,不过C语言在unistd.h标准库中提供了一个函数getopt()可以方便的来进行操作。 要使用这个库函数,就要把argc和argv交给这个库函数,然后就可以进行操作了。看一个最简单的例子,我们自己编写的程序支持-a name -b age两个选项,如果-a生效会打印名字,-b生效会打印年龄。
int getparam(int , char *[]);

int main(int argc, char *argv[]) {

    char ch;

    while ((ch = getopt(argc, argv, "a:b:")) != EOF) {
        switch (ch) {
            case 'a':
                printf("Your name is %s\n", optarg);
                break;
            case 'b':
                printf("Your age is %s\n", optarg);
                break;
            default:
                fprintf(stderr, "You don't give name or age!\n");
                return 1;
        }
    }

    argc -= optind;
    argv += optind;

    getparam(argc, argv);
}
这里的核心是使用getopt函数不断循环来处理所有的选项和传入的值,之后才是剩余的没有处理的普通的参数值。 其中的getopt的模式很重要,"a:b:"表示有效的选项是a和b,这两个选项都需要带参数。 注意,我们的程序如果使用这个库,就必须要先输入选项的部分,再输入普通参数,否则不会匹配成功。 在循环结束之后,optind表示成功匹配的数量,只要跳过匹配的数量,之后就是剩余的参数,所以继续利用上一节里的getparam(argc, argv);打印出剩余的参数。 在命令行里可以各种实验一下,其中有一点和Head First C上说的不同,不传入任何选项不会报错,只是什么都没有做。 一些尝试和结果如下:
>c -a cony
Your name is cony

>c -b 5
Your age is 5

>c -b 5 -a cony
Your age is 5
Your name is cony

>c -t 32
c: unknown option -- t
You don't give name or age!

>c -a cony -b 5 confidence optimal happy birthday
Your name is cony
Your age is 5
顺序为 0 的参数是 confidence
顺序为 1 的参数是 optimal
顺序为 2 的参数是 happy
顺序为 3 的参数是 birthday
有了输入输出和参数控制,现在就可以写控制台下有用的程序了。 今天还是可爱的女儿5岁生日,超级乐观和自信的小家伙,继续努力成为更厉害的人吧。老爸在此祝你生日快乐。也许某一天你会看到这里哦,那也意外着你应该很厉害啦。
LICENSED UNDER CC BY-NC-SA 4.0
Comment