🎬 第六章 FFmpeg命令行实现图片水印及文字跑马灯
📄 一句话概括

文字有三个来源:写在参数里、从文件读、从模板展开。 一旦文字里有冒号、空格或百分号,就该换来源,而不是硬加转义。

📝 本节和前面几节的关系

参数已经摸清了,接下来要解决「文字本身从哪儿来」。 这一节决定的是「怎么写才不出事」,不是「能写什么」。

一、写在参数里:最直接,也最容易出事

text='...' 只适合短、且不含特殊字符的文字:

想写的内容 直接用 text= 会怎样


hello world 空格后被当成下一个参数 10:30 冒号被当分隔符,只剩 10 it's 单引号提前闭合

这三行覆盖了九成以上的翻车现场。 它们共同的根子是:文字与滤镜参数共用同一层语法,谁也躲不开谁。

二、从文件读:最稳的一条路

写一个两行的小文件,量它的字节与字符:

项 值


文件字节 58 字符数 24 行数 2 每个汉字占字节 3

58 个字节对应 24 个字符,字节数是字符数的 2.4 倍。 不到 3 倍,因为里面还有数字与换行,它们各占 1 个字节。

所以判断编码的尺子是「明显大于 1」,不是「正好等于 3」。 换成 GBK 存同一段话,倍数会掉到 2 以内——两把尺子一比就能定案。

文件还有个额外好处:它可以被程序生成。 批量给几百个视频加水印时,把文字写进 txt 再调用,是唯一可行的做法。

三、从模板展开:expansion 的两档

%{localtime} 这类写法叫模板,默认会展开:

expansion 输出 长度


normal(默认) 2026-10-21 07:57:55 监控画面 24 none %{localtime} 监控画面 17

设成 none 之后,%{localtime} 会原样写在画面上,一个字符都不展开。 这个开关的意义是:当文字里本来就有百分号时,别让它去解析。

默认的 normal 遇到解析不了的百分号会怎么办? 轻则报警告,重则把整段文字吞掉——这也是「明明写了字却什么都没出现」的常见原因。

四、三种来源怎么选

来源 写法 什么时候用


参数 text='...' 短、无特殊字符 文件 textfile=note.txt 长文本、含标点 模板 %{localtime} 时间、帧号、文件名

第二条最容易被忽略:文字里一出现冒号或空格, 不要硬着头皮在命令行里加引号转义,直接写进 txt 最省事。

第三条里的模板还能组合,比如 %{n} 是帧号,%{filename} 是文件名。 做「每帧显示帧号」这种调试水印时,一行模板就够了。

五、动手 Demo:三种来源各试一遍

先写一个两行的 txt,量字节与字符的比例。 再把同一个字符串按三种编码解读,看长度怎么变。

import io, time

body = u"第一行:拍摄于 2026 年\n第二行:请勿外传\n"
io.open(u"t103.txt", u"w", encoding=u"utf-8").write(body)
raw = io.open(u"t103.txt", u"rb").read()
print(u"字节 %d  字符 %d  行 %d  倍数 %.1f"
      % (len(raw), len(body), len(body.rstrip(u"\n").split(u"\n")),
         len(raw) / float(len(body))))

s = u"水印"
b = s.encode(u"utf-8")
print(u"UTF-8 字节 %s  长度 %d" % (b.hex(), len(b)))
print(u"按 GBK 解  %s  长度 %d" % (b.decode(u"gbk", u"replace"),
                                    len(b.decode(u"gbk", u"replace"))))
print(u"按 latin1 解 %s  长度 %d" % (b.decode(u"latin1"),
                                      len(b.decode(u"latin1"))))

tpl = u"%{localtime} 监控画面"
real = time.strftime(u"%Y-%m-%d %H:%M:%S") + u" 监控画面"
print(u"normal %s  长度 %d" % (real, len(real)))
print(u"none   %s  长度 %d" % (tpl, len(tpl)))

实跑输出:

--------------------------------------------------------------------
6.3.4 drawtext 的文字内容来源
--------------------------------------------------------------------

**文字可以从三个地方来:写在参数里、从文件读、从环境或时间展开。**

**① 写在参数里最直接,也最容易出事**

**文字里有空格、冒号、单引号,都会被滤镜的参数解析吃掉。**

  想写的内容      直接用 text= 会怎样
  ---------------------------------------------
  hello world     空格后被当成下一个参数
  10:30           冒号被当成分隔符,只剩 10
  it's            单引号提前闭合

**所以文字简单时用 `text='...'`,复杂时一律走文件。**

**② 从文件读:textfile 的实测**

  项                 值
  -------------------------
  文件字节           58
  字符数             24
  行数               2
  每个汉字占字节     3

**58 个字节对应 24 个字符,字节数是字符数的 2.4 倍。**
**倍数不到 3,因为里面还有数字和换行,它们各占 1 个字节。**
**所以判断编码的尺子是「明显大于 1」,不是「正好等于 3」。**
**换成 GBK 存同一段话,倍数会掉到 2 以内——两把尺子一比就能定案。**

**③ 从模板展开:expansion 的两档**

  expansion          输出                             长度
  ------------------------------------------------------------
  normal(默认)     2026-10-21 08:02:55 监控画面     24
  none               %{localtime} 监控画面            17

**设成 none 之后,`%{localtime}` 会原样写在画面上,一个字符都不展开。**
**这个开关的意义是:当文字里本来就有百分号时,**
**默认的 normal 会去解析它,解析不了就报警告,甚至把整段文字吞掉。**

**④ 三种来源怎么选**

  来源     写法                  什么时候用
  -----------------------------------------------------
  参数     text='...'            短、无特殊字符
  文件     textfile=note.txt     长文本、含标点
  模板     %{localtime}          时间、帧号、文件名

**第二条最容易被忽略:一旦文字里出现冒号或空格,**
**不要硬着头皮在命令行里加引号转义,直接写进 txt 文件最省事。**
**而且文件还能被程序生成——批量为几百个视频加水印时,这条路是唯一可行的。**

io.open(..., encoding=u"utf-8") 一定要写编码。 Windows 上不写会按 GBK 打开,写进去的就是 GBK 字节,后面全对不上。

b.decode(u"gbk", u"replace") 里的 replace 很重要: 不加的话遇到解不出的字节直接抛异常,程序就断了,没法把乱码打出来看。

len(raw) 是字节数,len(body) 是字符数,两者不能混着比。 Python 3 的 str 存的是字符,编码之后才是字节。

30 秒实验

  1. 换编码:把 txt 改成 GBK 写一遍,看倍数掉到多少。
  2. 加百分号:文字里放一个单独的 %,看 normal 与 none 的差别。
  3. 真跑一次:有 drawtext 的机器上,textfile=note.txt:expansion=none 对比两次。

常见误区

  1. 在 text= 里硬写冒号与空格。会被参数解析吃掉,剩下半截。
  2. 用字符数猜字节数。汉字 3 字节、字母 1 字节,混排时比例在 1 到 3 之间。
  3. 文字里有 % 还留默认 expansion。解析不了可能把整段吞掉。
自测三问(点开对答案) 1. text= 最怕哪三种字符? —— 空格、冒号、单引号。它们都会被参数解析吃掉或提前闭合。 2. 为什么 58 字节只有 24 个字符? —— 汉字在 UTF-8 里占 3 字节,数字与换行占 1 字节,混排后比例不到 3。 3. expansion=none 解决什么问题? —— 让文字里的百分号原样输出,不被当模板去解析。

相关笔记