文字有三个来源:写在参数里、从文件读、从模板展开。 一旦文字里有冒号、空格或百分号,就该换来源,而不是硬加转义。
参数已经摸清了,接下来要解决「文字本身从哪儿来」。 这一节决定的是「怎么写才不出事」,不是「能写什么」。
一、写在参数里:最直接,也最容易出事
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 秒实验
- 换编码:把 txt 改成 GBK 写一遍,看倍数掉到多少。
- 加百分号:文字里放一个单独的
%,看 normal 与 none 的差别。 - 真跑一次:有 drawtext 的机器上,
textfile=note.txt:expansion=none对比两次。
常见误区
- 在 text= 里硬写冒号与空格。会被参数解析吃掉,剩下半截。
- 用字符数猜字节数。汉字 3 字节、字母 1 字节,混排时比例在 1 到 3 之间。
- 文字里有
%还留默认 expansion。解析不了可能把整段吞掉。
自测三问(点开对答案)
1. text= 最怕哪三种字符? —— 空格、冒号、单引号。它们都会被参数解析吃掉或提前闭合。 2. 为什么 58 字节只有 24 个字符? —— 汉字在 UTF-8 里占 3 字节,数字与换行占 1 字节,混排后比例不到 3。 3. expansion=none 解决什么问题? —— 让文字里的百分号原样输出,不被当模板去解析。- ☐ (本节完,下一节 6.3.5 drawtext 的主要参数)