Python textwrap.indent 的空白行:都加了前缀,为什么段落中间仍然缺两处标记

昨天 3阅读

给一段诊断文本逐行加上引用标记,首尾两行正常,段落中间却没有标记。检查发现,一行完全为空,另一行只含两个空格。textwrap.indent默认只为并非全由空白组成的行添加前缀,所以“把整段文字传进去”不意味着每一行都会获得同样处理。

本例在Linux、CPython 3.12.14实跑。保存为demo.py,运行python3 demo.py。程序只处理自建字符串,用repr把换行和空格保留在可观察的结果中,再用自定义predicate记录它实际收到了哪些行。

Python textwrap.indent 的空白行:都加了前缀,为什么段落中间仍然缺两处标记

AI生成的概念示意图:一边仅给有内容的行贴标记,另一边每行都有标记;浅色块代表空白位置,不是编辑器截图。

完整程序与本地结果

完整可运行程序

from textwrap import indent

source = "alpha\n\n  \nbeta\n"
normal = indent(source, "> ")
all_lines = indent(source, "> ", lambda line: True)
assert normal == "> alpha\n\n  \n> beta\n"
assert all_lines == "> alpha\n> \n>   \n> beta\n"
print("default:", repr(normal))
print("all lines:", repr(all_lines))

seen = []
def select(line):
    seen.append(line)
    return line.startswith("beta")

selected = indent(source, "! ", select)
assert selected == "alpha\n\n  \n! beta\n"
assert seen == ["alpha\n", "\n", "  \n", "beta\n"]
print("selected:", repr(selected))
print("predicate receives:", repr(seen))
assert source == "alpha\n\n  \nbeta\n"
assert indent("", "> ", lambda line: True) == ""
assert indent("last", "> ") == "> last"
assert indent("\t\n", "> ") == "\t\n"
print("empty, no-final-newline, tab checks: passed")

本次实际输出(以下为结果,不是程序)

default: '> alpha\n\n  \n> beta\n'
all lines: '> alpha\n> \n>   \n> beta\n'
selected: 'alpha\n\n  \n! beta\n'
predicate receives: ['alpha\n', '\n', '  \n', 'beta\n']
empty, no-final-newline, tab checks: passed

默认规则跳过的是纯空白,不只空字符串

source包含alpha、空行、两个空格组成的行以及beta。default输出只在alpha和beta前出现前缀,中间两行保持原样。两个空格没有被删掉,也没有获得标记;看起来“少处理了两行”,实际是选择条件把它们排除在外。

这个差别会影响引用文本、缩进日志与纯文本报告。例如要求每行都带来源标记时,默认行为可能让空白行脱离同一视觉区域。遇到这种要求,先确定空白行应保留原貌还是也要带标记,再选择参数,不要靠事后删除空行来掩盖差异。

始终返回True,才会选择每一条现有行

all_lines把predicate设成总返回True的函数,结果中第二行变成前缀加换行,第三行变成前缀、原有两空格再加换行。这里没有替换原内容,只是在被选中的行开头添加相同字符串,因此空格数量可以从repr输出直接核对。

前缀里的空格也属于结果的一部分。本例前缀含一个大于号和一个空格,所以原来只有两个空格的行,标记后会有三个空格。写对照断言时应按字符计算,避免凭编辑器显示宽度把原空白与新前缀混为一谈。

predicate看到的行仍带有原来的结束符

select将每次输入追加到seen,再检查是否以beta开头。输出的列表包含alpha加换行、单独换行、两空格加换行和beta加换行,四项顺序与原文一致。这是因为indent按splitlines(True)拆分,保留行结束符后再决定添加前缀。

如果判断函数需要匹配整行关键字,就要考虑换行仍在参数里;若只是startswith判断,则不受末尾换行影响。可以为判断创建去除结束符的临时视图,但不要因此把原始行内容一起改掉,让“选择哪一行”意外变成“清洗这一行”。

自定义选择可以只标记需要关注的行

selected只在beta前加感叹号标记,其他三行保持原样。这适合在已有文本里突出某类记录,例如只标记明确错误前缀的行。判断规则由应用给出,indent并不识别日志等级、结构字段或其它业务含义。

示例记录seen是为了证明调用边界,生产中的判断函数通常应尽量简单且没有外部副作用。若里面触发网络查询、修改全局状态或按时间返回不同结果,相同文本就可能生成不同展示,排查格式问题反而会引入新的执行条件。

空文本与末尾换行要单独验收

空字符串即使用总为True的predicate,结果仍为空,因为没有可以添加前缀的行。没有末尾换行的last会变成带前缀的last,但函数不会顺便补上换行。一个只含制表符与换行的文本,在默认规则下也原样保留。

这些边界说明,逐行加前缀不承担保证文件末尾换行、统一制表符或规范段落的责任。若输出协议需要这些变化,应把规则写成独立步骤,并明确执行顺序;先折叠空白再缩进与先缩进再折叠,可能产生完全不同的展示结果。

indent返回新字符串,source在本例结束时仍与原值相等。保存原文和展示版本,方便后续核对来源;若展示文字还要嵌入HTML、Markdown或终端,目标格式的转义也应另外处理。添加前缀只是字符串操作,并不会自动使内容适合所有展示渠道。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。