☰
用C语言实现钉钉直播观看时长统计:CSV解析与编码处理实战
2026/10/16 1:09:51 网站建设 项目流程

如果要统计一场钉钉直播里每个人的观看时长,很多人第一反应是打开表格软件,或者让脚本语言跑一下。但我这次用C语言把这个事做完了,整个工具就是一个单文件、零依赖的exe,拷到任何一台机器上都能跑。这篇就把从钉钉直播后台导出数据、到用C语言完成观看时长统计的完整过程拆开讲清楚,包括CSV解析、中文编码、时间换算、聚合排序和实际踩过的坑。

先说明一下,我这里说的“钉钉直播导出观看时长”,指的是直播结束后从后台导出的观看明细数据。不同版本的钉钉后台,导出能力略有差异,有的给的是汇总表,有的给的是“姓名、进入时间、离开时间、观看时长”这种明细表。但不管哪种,最终落到本地通常都是Excel或CSV文件,而CSV是C语言最容易处理的格式。所以本文默认你的导出结果是CSV,如果不是,先用表格软件另存为CSV。

1. 直播导出的原始数据长什么样:字段、编码与时间口径

1.1 后台导出的入口和文件格式

钉钉直播结束后,有权限的管理者通常在直播详情页能看到“导出”相关入口。导出的文件会包含参与者的观看记录。我这里不写具体按钮路径,因为产品版本经常变,但导出结果基本会落入两种形态:

  • 一种是“观看明细”,每一行是一条观看会话,字段类似:姓名、部门、进入时间、离开时间、观看时长(秒或分钟)。
  • 另一种是“人员汇总”,按人聚合好,一行一个人,直接给出累计观看时长。

建议优先导出明细表。原因后面会说到:明细表可以自己定义统计口径,比如只看有效观看、过滤掉少于某时长的记录;汇总表通常只有一个固定口径,拿到手也没法二次加工。

导出的文件如果是xlsx,建议在表格软件里另存为“CSV UTF-8”格式。如果直接另存为“CSV(逗号分隔)”,在简体中文Windows上很可能是GBK编码,C语言读进来中文会乱。这个点很关键,后面会专门讲。

1.2 字段顺序、编码与时间格式的预先确认

拿到CSV后,第一件事不是写代码,而是用文本编辑器打开看三样东西:分隔符、编码、时间格式。

分隔符通常是逗号,但有些导出会因为字段内带有逗号而自动给字段加上双引号。比如部门字段写成"研发,一部",这种情况解析时不能简单地按逗号切,必须处理引号。

编码方面,常见的三种情况:

  • UTF-8带BOM:文件头三个字节是EF BB BF。
  • UTF-8无BOM:直接用文本编辑器打开正常。
  • GBK/ANSI:中文在文本编辑器里正常,但在C程序里按UTF-8解析会乱码。

我的建议是统一转换成UTF-8无BOM再喂给程序。Windows下可以用记事本打开另存为UTF-8,或直接在程序里做编码转换。C语言做GBK转UTF-8不是不行,但要接iconv,处理起来稍麻烦。既然是一次性工具,我选择在输入侧做规范化,程序只认UTF-8。

时间格式通常有两种典型写法:

2025-03-12 14:30:05 2025/3/12 14:30:05

C语言里的sscanf都能解析,但必须写两个分支。还要注意如果观看时长跨天,比如进入时间是23:50,离开时间是次日00:20,按“离开-进入”算就不能直接用分钟差,要落到秒再减。这里有个隐含坑:观看时长到底用后台给的字段,还是自己用进出时间算?我的实践是:如果明细里直接有“观看时长(秒)”,优先用这个字段;如果只有进出时间,用秒级时间差,且要考虑跨天。

1.3 这份数据常见的脏数据情况

导出的CSV并不总是干干净净:

  • 文件末尾可能没有换行符,导致最后一行在按行读取时被漏掉。
  • 可能存在完全空白的行,尤其是表格软件转CSV时偶尔会多出空行。
  • 同一人可能有多条观看记录,因为中途退出再进入会拆成多行。
  • 观看时长为0的记录也可能存在,比如只点开直播间就退出去的人。

这些在写解析逻辑前都要想好。C语言不像脚本语言那样有现成的CSV库,所以容错逻辑自己要扛起来。我的原则是:解析阶段尽量宽容,统计阶段再做清洗。

2. 为什么用C语言做这件事:选型判断与工具结构设计

2.1 这个场景下C语言的实际优势

有人会问:统计CSV,Python不是一上午就写完吗?确实,如果有Python环境,用pandas或者csv模块非常快。但我这次的场景是:统计工具要发给不配开发环境的同事,对方机器上可能只有办公软件和浏览器,不允许装运行时。C语言编译出来是一个独立exe,不需要任何依赖。gcc编完,拷走,双击或命令行就能跑。这就把“写代码”和“跑工具”彻底分开了。

另外,直播间观看人数一般几十到几千,数据量不大,C语言处理CSV完全够用。字符串操作是C语言的硬功夫,但这个场景需要的无非就是按分隔符切串、字符串比较、整数累加。用C语言并不会比Python慢多少开发效率,因为逻辑本身很简单。

当然,如果数据量到了十万行以上,C语言的优势会更明显。实测几万行数据也是毫秒级处理完,完全不存在性能问题。

2.2 程序的输入输出与命令行设计

我给这个小工具定的需求是:

watchstat.exe -i input.csv -o result.txt -t 60

参数含义:

  • -i:输入的CSV文件路径。
  • -o:输出的统计报告路径。
  • -t:有效观看阈值(秒),低于该时长的记录直接不计入统计。

这里-t是一个很实用的设计。直播观看时长统计往往不是“看了就算”,而是“超过多少秒才算有效观看”。比如培训直播,要求每人至少观看30分钟,那么-t 1800就能把不合格的过滤掉。阈值在命令行传入,比写在代码里改来改去舒服得多。

如果命令行参数缺省,程序可以走默认值:输入输出路径必填,阈值默认为0表示不过滤。这样工具的使用门槛低,同事不用记复杂规则。

2.3 模块划分和数据流

程序整体按四个模块划分:

  1. 读取模块:负责打开文件、跳过BOM、按行读取。
  2. 解析模块:把一行CSV按要求切成字段,提取姓名、观看时长、进入/离开时间。
  3. 聚合模块:按姓名累加观看时长,维护人员列表。
  4. 输出模块:按总时长排序,过滤低于阈值的记录,输出报告。

这四个模块之间用函数接口连接,数据流是单向的,不搞全局变量满天飞。整个程序大约400行。C语言写小工具,最忌讳一开始就上复杂架构。我通常只用一个头文件、一个C文件,用结构体数组装所有数据。

3. 文件读取与CSV解析的C语言实现细节

3.1 读取方式选择与换行符处理

读取CSV的方式有三种:fgetc逐字符读、fgets按行读、fread一次全读。我推荐fgets,它天然保留行语义,处理大文件也不太占内存。行缓冲设大一点,比如8192字节,中文姓名和部门字段再长也够。

注意Windows下CSV的换行是\r\n,而Linux下是\n。fgets会把整行包括换行符读进来,所以要手动去掉末尾的\r和\n。

while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\r\n")] = 0; // 同时去掉 \r 和 \n if (line[0] == 0) continue; // 跳过空行 // 处理该行 }

strcspn返回字符串中第一次出现\r或\n的位置,把它置为\0,这一行就干净了。这个写法比先找\n再判断\r简洁。

3.2 字符串切割:为什么不用strtok

很多初学者会用strtok按逗号切字段,但它在CSV解析中有两个硬伤:

  • 连续两个逗号表示空字段,strtok会直接跳过,导致字段错位。
  • strtok会修改原字符串,把分隔符替换成\0,数据行被破坏。

所以我选择手写一个按逗号切分的函数,逐字节扫描。如果遇到双引号,就跳过引号内的逗号,直到引号闭合。这样可以正确处理"研发,一部"这类字段。

char *nextField(char **cursor) { if (*cursor == NULL || **cursor == '\0') return NULL; char *start = *cursor; if (**cursor == '"') { (*cursor)++; start++; while (**cursor && **cursor != '"') (*cursor)++; if (**cursor == '"') (*cursor)++; } else { while (**cursor && **cursor != ',') (*cursor)++; } if (**cursor == ',') { **cursor = '\0'; (*cursor)++; } else { *cursor = NULL; } return start; }

用的时候:

char *p = line; char *name = nextField(&p); char *dept = nextField(&p); char *enterTime = nextField(&p); char *leaveTime = nextField(&p); char *durationStr = nextField(&p);

这样切出来的字段是原始字符串,不会因为多余空格而错位。要注意字段起始位置可能有空格,比如张三,切出来之后最好做一次跳空格处理。我的习惯是加一个trim函数,把字段首尾空格去掉再比较。

3.3 中文乱码问题与UTF-8 BOM

C语言的字符串本质是字节数组。UTF-8下的中文,一个字是3个字节,但strcmp、strcpy这些函数对多字节字符串依然有效,因为它们处理的是“直到遇到0字节”的字节序列。

真正会翻车的地方有两个:

  • 文件带BOM,第一行第一列字段前面会多出EF BB BF三个字节,导致和“张三”比较时不同。
  • 字段在按\0切分时,把某个汉字的最后一个字节截断,会造成乱码,但这种情况只在按固定长度截取时发生。按分隔符切分不会碰到。

所以解析前先跳过BOM:

unsigned char bom[3]; fread(bom, 1, 3, fp); if (!(bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF)) { rewind(fp); // 没有BOM,回到文件头重新读 }

如果文件是GBK,程序读进来姓名是乱码。这种情况下我建议单独处理:要么用记事本转成UTF-8,要么在代码里用iconv。iconv在Windows下不是标准库,要给程序配一个转换库,多少有点麻烦了。一次性工具,直接在输入侧转码是最省事的。

3.4 时间字符串解析:统一换算成秒

观看时长统计需要把“进入时间”和“离开时间”转成可计算的数值。最稳的格式是Unix时间戳,也就是自某个基准时间以来的秒数。C语言里可以用mktime把struct tm转成时间戳。

time_t parseTimeStr(const char *s) { struct tm t; memset(&t, 0, sizeof(t)); int y, mo, d, h, mi, sec; if (sscanf(s, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &sec) == 6) { t.tm_year = y - 1900; t.tm_mon = mo - 1; t.tm_mday = d; t.tm_hour = h; t.tm_min = mi; t.tm_sec = sec; return mktime(&t); } if (sscanf(s, "%d/%d/%d %d:%d:%d", &y, &mo, &d, &h, &mi, &sec) == 6) { t.tm_year = y - 1900; t.tm_mon = mo - 1; t.tm_mday = d; t.tm_hour = h; t.tm_min = mi; t.tm_sec = sec; return mktime(&t); } return -1; }

mktime会自己处理月份溢出、闰年之类的问题,所以日期合法性校验不用自己写。如果解析失败返回-1,统计时跳过这一行并计数。

我遇到过一次导入的数据里“进入时间”是白天,“离开时间”是第二天凌晨,直接相减是负数。用mktime把两个时间都转成秒再减,结果就是正确的跨天时长。这是把时间统一成秒最大的好处。

4. 统计引擎:结构体设计、聚合逻辑与时长换算

4.1 统计口径:明细行到底怎么算

明细表的每一行代表一次观看会话。聚合时以“姓名”为键,把该人的所有观看时长累加。这里有个决策点:到底用后台字段“观看时长”,还是用进出时间相减。

我主张优先用“观看时长”字段。因为后台给的观看时长是服务端计算好的,通常扣掉了他切后台、网络中断等无效时间。进出时间相减只代表“挂机时间”,不代表“实际观看时间”。当然,如果对平台信任度不高,可以两种口径都算,输出两个字段:挂机时长和后台时长。不过写成报告时,我一般只展示“观看时长”,把所有秒数求和。

如果明细表里既没有观看时长,也没有进出时间,那就只能按人头计数,统计不出真正时长。这种情况直接放弃时长统计,只出在在线人数,这就另说了。

4.2 数据结构:动态数组比链表更好用

参与人数不确定,可能几百也可能两千。在C语言里两种选择:链表或动态数组。我选动态数组,因为:

  • 最终要排序输出,数组可以用库函数qsort,链表还得手写归并。
  • 数组内存连续,访问速度快。
  • 动态扩容用realloc,代码量不大。

结构体定义:

typedef struct { char name[128]; long long totalSeconds; long long watchSeconds; // 仅统计有效观看(过滤后) int recordCount; } Person;

数组是Person *persons,初始容量100,满了就翻倍扩容。这里有一个常被忽略的点:realloc之后要及时更新容量变量,并且不要直接realloc(persons, newCap * sizeof(Person)),先赋值给临时指针,判断成功后再更新原指针,避免内存分配失败导致原指针丢失。

Person *tmp = realloc(persons, newCap * sizeof(Person)); if (tmp == NULL) { // 处理错误 } else { persons = tmp; cap = newCap; }

4.3 聚合逻辑:线性查找在这个规模够用

每次来一行数据,先在线性查找中找同名的人。找到了就累加,找不到就新增。

int findPerson(const char *name) { for (int i = 0; i < count; i++) { if (strcmp(persons[i].name, name) == 0) return i; } return -1; }

如果人数是几千,线性查找的复杂度是O(n^2),但实际数据只有几千行,每次查找几十次就能命中,总体耗时仍然在毫秒级。不需要引入哈希表。这是C语言工具“够用就好”的典型场景:先跑起来,再谈优化。

不过姓名比较要注意空格和全角空格。有的导出数据会在姓名后面带一个空格,或者用全角空格做对齐。我建议在查找前统一trim一遍,全角空格转普通空格。否则同一人可能出现两条记录:一条“张三”,一条“张三 ”。

4.4 精度与溢出:用long long存秒数

观看时长字段如果单位是秒,一个人看10小时就是36000秒,一万个人加起来几亿秒。普通int能存约21亿,其实也够,但为了稳妥,聚合秒数用long long。输出时再做格式化,换算成HH:MM:SS。

另一个容易踩的坑是,有的导出CSV里观看时长是“1:23:45”这种格式,而不是数字秒。解析时要识别出来:

long long parseDurationStr(const char *s) { int h = 0, m = 0, sec = 0; if (sscanf(s, "%d:%d:%d", &h, &m, &sec) == 3) { return h * 3600LL + m * 60LL + sec; } if (sscanf(s, "%d:%d", &m, &sec) == 2) { return m * 60LL + sec; } return atoll(s); }

如果字段本身是整数秒,第三个分支直接转成long long。这个小函数能兼容多种导出格式,避免不同机器上导出的时间格式不同导致程序崩溃。

5. 排序、阈值过滤与报告输出:让结果可以直接交付

5.1 qsort快速排序:按总时长降序排列

统计完所有记录后,排序用标准库qsort。比较函数里返回负数表示第一个元素排在前面。

int cmpByWatchSeconds(const void *a, const void *b) { const Person *pa = (const Person *)a; const Person *pb = (const Person *)b; if (pa->watchSeconds > pb->watchSeconds) return -1; if (pa->watchSeconds < pb->watchSeconds) return 1; return 0; }

如果想按姓名排,直接strcmp。但在中文环境下,按姓名排序其实是按拼音或编码顺序,不同平台结果不一致。一般输出报告用时长降序就够,姓名只是标识。

5.2 报告输出:控制台和文件双通道

输出用printf打一份到屏幕,同时用fprintf写一份到文件。文件推荐用CSV,方便同事再进表格软件加工。

void formatDuration(long long seconds, char *buf, size_t size) { long long h = seconds / 3600; long long m = (seconds % 3600) / 60; long long s = seconds % 60; snprintf(buf, size, "%02lld:%02lld:%02lld", h, m, s); }

输出格式示例:

排名,姓名,观看时长,记录次数 1,王同学,03:25:12,3 2,李同学,02:58:40,1 3,赵同学,02:30:00,2

记录次数这个字段很有用。如果某个人被记录了5次,说明他频繁进出直播间。这个信息在统计“是否全勤观看”时非常有价值,不能省略。

5.3 阈值过滤:把“看过一点”和“全程在线”分开

命令行里的-t参数在输出前做过滤。逻辑是:如果一条记录观看时长小于阈值,或者聚合后总时长小于阈值,就不出现在报告里。

我通常采用“行级过滤”和“人级过滤”两级:

  • 行级过滤:观看时长小于阈值的明细行直接不计入watchSeconds。
  • 人级过滤:聚合后总时长小于阈值的人,在最终报告中整体过滤。

比如一场直播要求看满30分钟,-t 1800:一个只看10分钟的人,他的观看次数也会被记为0次有效记录。这样的报告可以直接作为“有效观看名单”使用。只做一层过滤的话,会把“每人看了几分钟但总时长够”的人错误地算进去。

把两层过滤逻辑分开实现,代码上就是两个判断位置,不复杂但对结果影响很大。我踩过一次坑:只做了人级过滤,结果一个把直播挂后台挂满30分钟的人被算成有效,实际上他可能只看了开头几分钟。所以行级过滤不能省。

6. 实测踩坑记录:换行符、中文逗号与其他边界情况

6.1 文件末尾无换行导致最后一行被丢弃

CSV文件最后一行经常没有换行符。fgets在读取时,如果没有换行符,它依然会返回这一行,只是\n位置不同。我一开始用while (fgets(...) != NULL)就没问题,fgets能处理“读到最后没有换行”的文本。但如果是先统计行数再逐行处理,就可能在最后一行上出错。建议直接使用fgets循环,不要假设文件一定以换行符结尾。

6.2 CSV字段中的逗号和引号

部门字段或姓名里出现逗号,导出方会给字段加双引号。比如"研发,一部"。如果不处理引号,按逗号切割后这个字段会变成两个字段,后面所有列全部错位。

我在3.2节的nextField函数里已经处理了引号。实测下来,引号里还有引号的情况极少见,CSV规范里是连续两个双引号表示转义,但我没有遇到,所以先没实现。如果你要处理,可以在扫描引号内内容时,遇到""就把它当成一个"写回缓冲区。

6.3 中文名在终端显示正常但写入文件乱码

这个坑出现在Windows控制台代码页不是UTF-8的情况。用printf打印UTF-8字符串到控制台可能乱码,但写入文件用fprintf之后,用文本编辑器打开反而正常。因为文件里的字节本来就是UTF-8。

解决方法是:程序内所有输出统一按UTF-8写文件;控制台打印仅作调试用,乱码不影响结果。不要在程序里做代码页切换,Windows的SetConsoleOutputCP在不同版本上行为不一致,一次性工具不值得折腾。

6.4 重复导出和0秒记录的处理

如果统计时不小心导出了两份相同数据合并在一起,同一人的观看时长会被翻倍。我加了一个“按进入时间去重”的字段判断:如果同一姓名、同一进入时间出现两次,第二次跳过。不过这个逻辑只在处理统计时需要,如果要求“把两次导出数据累加”,反而不能去重。

0秒记录一般来自只点开直播间就退出的人。统计有效观看时直接过滤掉。但如果想看“多少人点击过直播”,0秒记录也有意义。所以我让-t阈值同时控制这个:阈值大于0时,0秒记录不统计;阈值等于0时,0秒记录照常计数。

6.5 性能观察

实测一万行明细,动态数组扩容几次,线性查找聚合,再加排序,整个过程在0.1秒内完成。完全不用考虑读取优化或索引。这个数据量级下,C语言和脚本语言的性能差距体现不出来。但如果数据是几十万行,C语言的编译型优势才会更明显。

写在最后的小经验

我自己做这类小工具,最深的体会是:统计逻辑本身不难,难的是搞清数据来源的“脾气”。钉钉直播导出的CSV,不同版本的字段顺序、编码方式都可能不一样,所以程序里一定要做字段名或列顺序的“自适应”。最稳妥的做法是:让程序先读第一行表头,通过表头找到“姓名”列和“观看时长”列的下标,再逐行解析。这样即使导出格式变了,程序不用重写。列顺序自适应这个功能,看起来只是几十行代码,但实际使用时非常救命。

如果后续要扩展,可以考虑加一个-c参数指定按哪个字段聚合(姓名、部门),或者支持多文件合并统计。这个工具我已经在几次月度培训统计里用得很顺手了,编译一次,后面每次改个文件名就能出报告,比打开表格软件手动筛选快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询