当前位置:首页 > 科技 > 正文

从1天到365天,我用Go语言写了一个视频管理工具,结果治好了我的拖延症

  • 科技
  • 2026-07-03 12:26:31
  • 311
摘要: 你有没有过这种经历?打开手机相册,发现里面堆满了从“到“一年前”的各种视频碎片,有的是一时兴起拍的猫咪,有的是工作会议的录屏,还...

你有没有过这种经历?打开手机相册,发现里面堆满了从“到“一年前”的各种视频碎片,有的是一时兴起拍的猫咪,有的是工作会议的录屏,还有那些说不上来为什么要拍的街景——它们就那么躺在那儿,像一堆没人整理的旧磁带。

我去年年底就陷入这种困境,手机提示我“存储空间不足”,我点开一看,好家伙,光视频就占了42GB,从1月1号到12月31号,整整365天的视频,横跨四个季节,内容从年夜饭到公司年会,从海边日出到深夜加班后的自拍,我盯着那些“VID_20230115_203415.mp4”之类的文件名,突然觉得——我需要让这些视频“说人话”。

用Go写工具,不是为了炫技

我选Go语言来搞这件事,不是因为它是“最流行的编程语言”,也不是因为我有多厉害,真实的原因是:我翻了一圈自己的开发环境,发现Go写起来最直白,编译快,打包成单文件扔服务器上就能跑,就这么简单,别想太多。

当时我的需求其实很明确:给我这一整年的视频文件夹建立索引,按天归类,自动提取拍摄时间,然后生成一个漂亮的“年度视频时间线”,我不想手动去翻每个文件的属性,也不想用那些在线工具——隐私是个大问题,你懂的。

第一步:让程序读懂“一天”

我刚写第一版代码的时候,思路其实很蠢,我想通过文件名里的时间戳来解析日期,比如VID_20230101_120000.mp4这种,看起来格式挺规整的,但很快就发现,不同设备、不同App导出的视频命名规则简直是“百家争鸣”。

有的叫IMG_3205.MOV,完全没日期信息,有的是从微信导出的,文件名是一串哈希值加时间戳,但格式跟标准的不一样,还有更坑的——用相机拍的视频,文件名是_DSC1234.mp4,你得去读EXIF数据才能拿到拍摄时间。

这时候Go语言的优势就出来了,它有成熟的第三方库,比如exifmediainfo,能直接读取视频文件的元数据,我写了一段代码,先尝试从文件名解析,不行就去读元数据,再不行就用文件的修改时间做兜底。

// 这段代码跑起来的样子很朴素
filepath := "/Users/me/2023视频/IMG_3205.MOV"
modTime, err := os.Stat(filepath)
if err == nil {
    fmt.Printf("修改时间:%v\n", modTime.ModTime())
}

但这里有个坑——文件修改时间不一定等于拍摄时间,你从电脑传到手机,再传到云盘再下载回来,修改时间早就变了,所以最靠谱的还是去读视频本身的元数据。

那段时间我写了一个小的命令行工具,叫video-dater,它能递归扫描整个文件夹,把所有视频的拍摄日期提取出来,然后按日期分组,比如你丢给它一个包含365天视频的文件夹,它就把这些文件按“2023-01-01”、“2023-01-02”……一直到“2023-12-31”排列好。

按“天”分组,是第一步

说个细节,我一开始让程序把同一天的视频放在同一个子文件夹里,比如所有2023年1月1日的视频,都丢进/2023-01-01/这个目录,这样看起来挺整洁的。

但我很快就发现一个问题——有些天的视频特别多,比如我生日那天,拍了大概60多个短视频,有些天又只有一两个,这就导致文件夹数量多到有点烦人,尤其是当你第二年也想这么做的时候。

所以我改成另一种方式:不移动文件,只生成一个索引文件(Markdown格式或者JSON),里面记录了每个视频的路径、拍摄日期、时长、甚至缩略图路径,这样原文件不动,但你能快速知道“那天拍了什么”。

这里用的一个trick是Go的filepath.Walk函数,它能递归遍历目录,再配合time包里的日期格式化,把time.Time对象转成YYYY-MM-DD字符串,简直就是天生一对。

从“一天”到“一周”:我发现了时间的节奏感

当我把视频按天分组之后,我盯着屏幕上的列表,突然意识到——每天的内容其实有很强的重复性,比如周一到周五,大多是办公室、键盘、会议记录,周末就变成美食、公园、宠物。

我开始在工具里加入“周视图”功能:给每个视频打一个“星期几”的标签,比如周一拍的视频,标记为“Monday”,然后生成一个按星期几分布的热力图,你猜怎么着?我发现自己周日拍的视频最多,周五最少,周五下班后根本不记得拍东西,只想躺着。

这时候Go的time.Weekday()方法就帮了大忙,返回的是一个time.Weekday类型,从Sunday到Saturday,我写了一个switch语句,把每天拍到的视频数量统计出来,然后生成一个简单的ASCII柱状图——虽然简陋,但自己看着挺有成就感。

月度和年度:从“清单”到“叙事”

整理到这儿,我觉得“按天”已经能满足基本需求了,但真正的目标是从“1天”到“365天”的完整叙事,我想看到这一年,视频内容是如何随着季节、节日、个人状态变化的。

于是我在工具里加入了“月度摘要”和“年度报告”两个功能,月度摘要就是统计你每个月拍了多少视频,总时长多少,年度报告则更夸张一点——它会找出你拍摄最频繁的那天、全年视频的总时长、最常出现的场景(通过地点元数据),甚至能根据视频文件的创建时间推测出你“早起拍视频”的频次。

举个具体的例子,我的年度报告显示:2023年总共有2,341个视频,总时长约45小时,拍摄最多的月份是8月(我去了一趟新疆),最少的月份是2月(在准备年终汇报),而最不会拍视频的日子是星期三——我也不知道为什么,可能星期三大家开会开得太累了。

这些数据我用Go的sort包和map结构来统计,比如统计每月的视频数量,就建一个map[time.Month]int,然后遍历所有视频,把月份对应的计数值加一。

用Go处理视频本身:稍微有点疼

光整理元数据还不够,我还想给每个视频生成缩略图,这样浏览的时候能快速回忆起来内容,而不用一个一个点开。

这一步涉及到视频解码,纯Go实现的话比较慢,我用了ffmpeg的命令行接口,通过Go的os/exec包调用外部命令,虽然不算“纯Go”,但效率高,而且ffmpeg本身足够稳定。

代码大概长这样:

cmd := exec.Command("ffmpeg", "-i", videoPath, "-ss", "00:00:01", "-vframes", "1", thumbnailPath)
err := cmd.Run()

取第一秒的画面作为缩略图,压缩成320x240的大小,这样处理完2,341个视频,大概用了20多分钟,还行,至少不耽误我下午喝咖啡。

这个过程中遇到一个小bug:有些视频的时长只有3秒,跳转到第一秒取帧没问题,但有个视频是纯黑屏(可能是手机放口袋的时候拍的),ffmpeg取到的就是一张全黑图,后来我加了一个判断,如果缩略图文件大小小于5KB,就判定为“无效缩略图”,改用视频中间帧再试一次。

可视化输出:让内容“说话”

当所有数据都整理好之后,我需要一个可视化的展示,我没有写前端页面(虽然Go也能写),而是生成了一个纯HTML文件,包含所有视频的缩略图网格、每天的标签、以及一个按时间线排列的导航。

这个HTML文件是用Go的html/template包生成的,我先定义了一个结构体:

type VideoEntry struct {
    Date        string
    Thumbnail   string
    Path        string
    Duration    float64
    Description string // 如果有的话
}

然后把它塞进一个HTML模板里,模板里用{{range .}}来循环渲染,最终生成的文件小巧、干净,直接把文件路径拖进浏览器就能看。

这个模板我还加入了两个小特性:一个是按日期跳转的锚点链接,另一个是“随机播放”按钮——点击它就会随机跳转到一年中的某一天,这个按钮有点意外地好用,尤其是你无聊的时候,点一下,看到去年的某一天你正在做的事情,经常能勾起一些有趣的回忆。

后续思考:视频整理不是终点

到这一步,这个工具已经能帮我完成“从1天到365天视频”的整理工作了,但用着用着,我发现了更多有意思的场景。

我开始注意到自己视频内容的“季节性”,夏天户外视频多,冬天室内多,春天视频里有花粉、有风,秋天有落叶,不是刻意去记录,但回看的时候,能清晰感受到时间的流逝。

还有一次,我用这个工具帮朋友整理了他女儿一整年的成长视频,那个文件夹里有366个视频(因为2020年是闰年),从出生第一天到一岁生日,每个视频按日期排列好,生成了一份带缩略图的时间线,他看完眼眶有点红——这是我用代码写出的最满意的功能。

后来我也在想,是不是可以再加一个功能:自动把同一天的短视频拼接成一个电影片段,这样你有365个电影片段,组合起来就是这一年的浓缩版,技术上实现不难:用ffmpeg的concat协议,加上一个视频排序列表就行,但还没有动手写,因为光想想就觉得很花时间,也许明年吧。

一些你没预料到但值得提的小事

最后说点经验教训,我用Go写这个工具的过程中,踩过几个坑,也许对你也有些帮助:

  1. 不要一次性读入所有文件路径到内存,我那2,341个视频,路径字符串本身还好,但如果你的视频数量是几十万甚至上百万,内存占用会很可观,用filepath.Walk是流式的,问题不大,但如果你自己写递归函数,要注意。

  2. 视频文件是二进制文件,不要用文本方式打开,这个坑我是在处理一些异常的MP4文件时碰到的,Go的ioutil.ReadFile默认是读整个二进制数据,没问题,但如果你用os.Open然后加bufio.Reader,记得指定ReadBytes而不是ReadString——因为你不知道文件里有没有奇怪的换行符。

  3. 时间解析不要太死板,不是所有视频的日期格式都是标准ISO 8601,有的视频元数据里写的是“2023-01-01 12:00:00”,有的是“Jan 1, 2023”,还有的是“2023/1/1”,Go的time.Parse需要一个固定的layout字符串,我写了一个”尝试多种格式“的函数,逐一遍历,直到解析成功为止,效率上稍微慢点,但胜在兼容性强。

  4. 文件名不要带空格和特殊字符,这好像是常识,但真的有人把视频命名为“2023年 最后一天 深圳 海边 .mov”(含空格和中文),跨平台传输的时候,Go的os包能处理,但生成HTML链接的时候,记得url.QueryEscape一下。

  5. 别忘了处理时区,你拍视频的时候在北京,晚上在电脑上整理文件的时候,系统时区可能是UTC+8,但有些元数据记录的时区是UTC,如果你不做转换,同一天的视频可能被分到两天,我直接用time.Local统一转换为本地时区,简单暴力。

不是结尾的结尾

这篇文章写到这里,差不多有1,200多字了(加上代码示例和细节),其实我本来还想聊聊怎么用Go写一个Web界面来展示这些视频,但写得太细就变成长篇教程了,而且说实话,上面那个HTML输出我已经用得很顺手了——打开浏览器,点一下“随机播放”,看看去年今天你在干嘛,感觉挺奇妙的。

工具放在GitHub上(你自己写一个也行,思路其实不复杂),Go语言的代码写好以后,编译成一个几MB的二进制文件,放在任何Linux或macOS系统上都能运行,Windows也行,只要你装好了ffmpeg。

对了,我后来还加了一个小功能:如果某一天没有任何视频,就在时间线上留一个空位,并标注“无记录”,不是每个日子都需要被拍下来,但知道哪天没拍,也一样有意义。

行吧,就写到这儿,工具还在慢慢改进,代码也有点乱,但用起来挺顺手的,如果你也有一堆从“第一天”到“第三百六十五天”的视频,不妨试试用Go写个自己的整理工具,相信我,看到它们被整齐地排列在时间线上,你会觉得这一年的日子,它们真真切切都在那儿。

从1天到365天,我用Go语言写了一个视频管理工具,结果治好了我的拖延症