命令行打印输出字符缓慢之谜
原创
微慑网
微慑网
微慑网
2026年9月20日 13:46
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
问题描述:
之前写了一个脚本,但不知道从什么时候开始,使用python38/python39,在脚本运行一段时间后,输出字符很慢,一个个蹦出来似的,无论是cmd/cmder/powershell/Tabby都会这样;但是脚本在笔记本上运行又是正常的,台式机上却都有这个情况;
之前查过文章问过AI 都没结果,一直想着是电脑环境的问题;最近让AI直接检查了代码,发现是引用了colorama的锅,这个模块是后来加的,用来做终端彩色输出;在这之前使用的是termcolor,但不如colorama方便;
问题关键:
问题出在colorama的0.4.6的版本的init()没有幂等保护,存在多种问题,
参考:
https://data.safetycli.com/packages/pypi/colorama/changelog?page=1&
而我在for循环中,还多次调用了init();
另外,以下是AI给出的一个检测代码:
import sysfrom colorama import ansitowin32c = ansitowin32.AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=True)print(\"convert =\", c.convert, \" strip =\", c.strip, \" should_wrap =\", c.should_wrap())
台式机:
笔记本:
区别仅仅是strip的值,AI的解释是:
当 strip=False 时,你的输出里充满了类似 \\x1b[32m、\\x1b[0m 这样的控制序列。这些序列会被原样送到终端,终端必须逐个解析、渲染颜色和样式。在 Windows 控制台(即使是现代 Windows Terminal 的兼容层)或某些 IDE 内置终端中,处理这些序列的代价可能非常高。
而 strip=True 时,终端收到的几乎是纯文本。虽然 Python 多花了一点时间做正则匹配,但终端侧的负担被大幅卸掉了。两者相抵,你感知到的“输出变慢”就消失了。
解决问题:
(1)停止多次引用init(),直接在main使用一次init(autoreset=True)
(2)使用colorama.just\_fix\_windows\_console() ;可以多次引用,其内部有幂等保护,重复调用不会叠加层数
修复之前的代码:
sys.stdout.reconfigure(line_buffering=False, write_through=True)sys.stdout.reconfigure(line_buffering=True)
for ******:if match_content in List1 or checkTitle:
init(autoreset=True) print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) doChoice() elif match_content in List:
init(autoreset=True) print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) doChoice() else: init(autoreset=True) print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) print(Style.RESET_ALL) print ('\\t+-------------------------------------------------------------+\\r') doChoice()def main): *************************
修复之后
#sys.stdout.reconfigure(line_buffering=False, write_through=True)#sys.stdout.reconfigure(line_buffering=True)for ******:if match_content in List1 or checkTitle: print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) doChoice() elif match_content in List: print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) doChoice() else: print('\\t|-> ***:',Fore.GREEN+match_content+Style.RESET_ALL,Back.RED +alertWord+Style.RESET_ALL,flush=True) print(Style.RESET_ALL) print ('\\t+-------------------------------------------------------------+\\r') doChoice()def main): init(autoreset=True) *************************
脚本复现测试结果:
================================================================ colorama 输出卡顿:复现与验证工具 ================================================================ [1] 当前环境检测 isatty = True convert = False strip = False should_wrap = True 判定 : 指数爆炸:convert=False 且 strip=False → reset_all() 写文本并递归 → 会卡 当前嵌套层数 : 0 [2] 计数模式(不刷屏,精确统计底层写入次数) 【指数路径 · 台式机 Cmder 属于此类】convert=False strip=False 文章数 | 修复前(累计) | 修复前(单篇峰值) | 修复后(累计) -------+--------------+------------------+------------- 5 | 62 | 32 | 10 10 | 2046 | 1024 | 20 15 | 65534 | 32768 | 30 18 | 524286 | 262144 | 36 20 | 2097150 | 1048576 | 40 【短路路径 · 非 tty / 重定向属于此类】convert=False strip=True 文章数 | 修复前(累计) | 修复前(单篇峰值) | 修复后(累计) -------+--------------+------------------+------------- 5 | 5 | 1 | 5 10 | 10 | 1 | 10 15 | 15 | 1 | 15 18 | 18 | 1 | 18 20 | 20 | 1 | 20 怎么读这两张表: · 指数路径:修复前单篇峰值 = 2^N,20 篇累计 209 万次写入, 修复后只有 40 次 —— 差 5 万倍,这就是卡顿的来源。 · 短路路径:修复前后完全持平,每篇恒定 1 次写入。 层数照样在涨,却多不出任何一次写入 —— 这就是该环境下 bug 不显形的直接证据。 ============================================================== 真实输出演示:每\"篇\"调用一次 init(autoreset=True),再输出一行 单次耗时超过 3.0 秒自动停止,也可随时按 Ctrl+C 中断 ============================================================== 下面的彩色行走的是被嵌套后的 sys.stdout: [第 1 篇] 模拟一行带颜色的输出 层数= 1 单次耗时= 0.5 ms [第 2 篇] 模拟一行带颜色的输出 层数= 2 单次耗时= 1.1 ms [第 3 篇] 模拟一行带颜色的输出 层数= 3 单次耗时= 0.8 ms [第 4 篇] 模拟一行带颜色的输出 层数= 4 单次耗时= 1.5 ms [第 5 篇] 模拟一行带颜色的输出 层数= 5 单次耗时= 2.4 ms [第 6 篇] 模拟一行带颜色的输出 层数= 6 单次耗时= 4.6 ms [第 7 篇] 模拟一行带颜色的输出 层数= 7 单次耗时= 8.0 ms [第 8 篇] 模拟一行带颜色的输出 层数= 8 单次耗时= 23.5 ms [第 9 篇] 模拟一行带颜色的输出 层数= 9 单次耗时= 32.7 ms [第 10 篇] 模拟一行带颜色的输出 层数=10 单次耗时= 63.2 ms [第 11 篇] 模拟一行带颜色的输出 层数=11 单次耗时= 118.3 ms [第 12 篇] 模拟一行带颜色的输出 层数=12 单次耗时= 243.5 ms [第 13 篇] 模拟一行带颜色的输出 层数=13 单次耗时= 479.3 ms [第 14 篇] 模拟一行带颜色的输出 层数=14 单次耗时= 980.9 ms [第 15 篇] 模拟一行带颜色的输出 层数=15 单次耗时= 1896.4 ms [第 16 篇] 模拟一行带颜色的输出 层数=16 单次耗时= 3917.2 ms 单次耗时已超过 3.0 秒 —— 演示停止。 再往上加层数,终端就会明显一个字符一个字符往外蹦。 -------------------------------------------------------------- 对照:还原 sys.stdout 后只 init() 一次(修复后的写法) -------------------------------------------------------------- 当前嵌套层数 = 1 [第 1 篇] 模拟一行带颜色的输出 [第 2 篇] 模拟一行带颜色的输出 [第 3 篇] 模拟一行带颜色的输出 [第 4 篇] 模拟一行带颜色的输出 [第 5 篇] 模拟一行带颜色的输出 [第 6 篇] 模拟一行带颜色的输出 [第 7 篇] 模拟一行带颜色的输出 [第 8 篇] 模拟一行带颜色的输出 [第 9 篇] 模拟一行带颜色的输出 [第 10 篇] 模拟一行带颜色的输出 [第 11 篇] 模拟一行带颜色的输出 [第 12 篇] 模拟一行带颜色的输出 [第 13 篇] 模拟一行带颜色的输出 [第 14 篇] 模拟一行带颜色的输出 [第 15 篇] 模拟一行带颜色的输出 [第 16 篇] 模拟一行带颜色的输出 连续输出 16 篇总耗时 = 15.4 ms(平均 0.96 ms/篇) 演示结束。若中间卡住过久,关闭窗口重开即可。
检测脚本:
# -*- coding: utf-8 -*-\"\"\"colorama 输出卡顿:复现与验证工具=================================背景----当脚本在处理循环里反复调用 colorama 的 init(autoreset=True) 时,sys.stdout 会被一层层包裹 AnsiToWin32,层数等于已处理条数。在 convert=False 且 strip=False 的环境下,单次 print 的底层写入次数会变成 2^层数,表现为终端一个字符一个字符往外蹦。本工具提供三种验证方式----------------------1. 环境检测 —— 看当前终端下 colorama 会自动选哪条路径2. 计数模式 —— 精确统计底层写入次数,不刷屏(默认,安全)3. 真实输出 —— 真的往终端写,肉眼感受越跑越慢(需 --real,带自动熔断)用法----python test_output_slowdown.py 环境检测 + 计数模式(默认,安全)python test_output_slowdown.py --env 只做环境检测python test_output_slowdown.py --real 真实输出演示(会让终端变卡,可 Ctrl+C)python test_output_slowdown.py --real --max 14 --limit 5依赖:colorama\"\"\"import argparseimport sysimport time# ============================================================ 一、环境检测def probe_env(): \"\"\"检测当前终端环境下,colorama 会自动选择哪条路径\"\"\" from colorama import ansitowin32 c = ansitowin32.AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=True) return { \"isatty\": sys.stdout.isatty(), \"convert\": c.convert, \"strip\": c.strip, \"should_wrap\": c.should_wrap(), }def verdict(info): \"\"\"根据检测结果判断是否会踩指数爆炸\"\"\" if not info[\"should_wrap\"]: return \"不会包装 sys.stdout —— 此环境下 init() 不累积层数\" if info[\"strip\"]: return \"线性:strip=True 使 reset_all() 短路,递归被剪断 → 不会卡\" if info[\"convert\"]: return \"线性:convert=True,reset_all() 走 Win32 API 旁路 → 不会卡\" return \"指数爆炸:convert=False 且 strip=False → reset_all() 写文本并递归 → 会卡\"def count_layers(): \"\"\"sys.stdout 当前被 colorama 包了几层(正常应恒为 1)\"\"\" s, n = sys.stdout, 0 while True: w = getattr(s, '_StreamWrapper__wrapped', None) if w is None: break n += 1 s = w return n# ============================================================ 二、计数模式class CountingStream(object): \"\"\"只计数、不真写,对外伪装成 isatty=True 的真实终端\"\"\" def __init__(self): self.writes = 0 self.chars = 0 self.flushes = 0 def write(self, s): self.writes += 1 self.chars += len(s) return len(s) def flush(self): self.flushes += 1 def isatty(self): return True def fileno(self): return -1 @property def closed(self): return Falsedef build_chain(base, layers, convert, strip): \"\"\"手工套 layers 层 AnsiToWin32,绕开当前是否真 tty 的影响\"\"\" from colorama import ansitowin32 stream = base for _ in range(layers): w = ansitowin32.AnsiToWin32(stream, convert=convert, strip=strip, autoreset=True) w.call_win32 = lambda *a, **k: None # 屏蔽真实 Win32 调用,只统计写入 stream = w.stream return streamdef simulate(articles, convert, strip): \"\"\" 模拟处理 articles 篇文章。 返回 ((修复前累计, 修复前峰值), (修复后累计, 修复后峰值)) 修复前 = 每篇都 init() 一次;修复后 = 启动时只 init() 一次 \"\"\" TEXT = \"\\x1b[31mID: 12345 标题: 某安全漏洞通告\\x1b[0m\" def run(fixed): base = CountingStream() stream = build_chain(base, 1, convert, strip) if fixed else None total = peak = 0 for k in range(1, articles + 1): if not fixed: stream = build_chain(base, k, convert, strip) # 层数 = 第几篇 before = base.writes stream.write(TEXT) cost = base.writes - before total += cost peak = max(peak, cost) return total, peak return run(False), run(True)def print_table(label, convert, strip): print(\"\
【%s】convert=%s strip=%s\" % (label, convert, strip)) print(\" 文章数 | 修复前(累计) | 修复前(单篇峰值) | 修复后(累计)\") print(\" -------+--------------+------------------+-------------\") for n in (5, 10, 15, 18, 20): (total_old, peak_old), (total_new, _) = simulate(n, convert, strip) print(\" %6d | %13d | %15d | %12d\" % (n, total_old, peak_old, total_new))# ============================================================ 三、真实输出def real_demo(max_layers, limit_sec): \"\"\"真的往终端写,逐层累积,肉眼感受越跑越慢\"\"\" from colorama import init, Fore raw = sys.__stdout__ # 最原始 stdout,不受 colorama 包装影响 raw.write(\"\
\" + \"=\" * 62 + \"\
\") raw.write(\" 真实输出演示:每\\\"篇\\\"调用一次 init(autoreset=True),再输出一行\
\") raw.write(\" 单次耗时超过 %.1f 秒自动停止,也可随时按 Ctrl+C 中断\
\" % limit_sec) raw.write(\"=\" * 62 + \"\
\
\") raw.flush() sys.stdout.write(\"下面的彩色行走的是被嵌套后的 sys.stdout:\
\") sys.stdout.flush() stopped = None for k in range(1, max_layers + 1): init(autoreset=True) # ← 修复前的写法:每篇一次 t = time.perf_counter() sys.stdout.write(Fore.RED + \"[第 %d 篇] 模拟一行带颜色的输出\
\" % k) el = time.perf_counter() - t raw.write(\" 层数=%2d 单次耗时=%10.1f ms\
\" % (k, el * 1000)) raw.flush() if el > limit_sec: stopped = k break if stopped: raw.write(\"\
单次耗时已超过 %.1f 秒 —— 演示停止。\
\" % limit_sec) raw.write(\" 再往上加层数,终端就会明显一个字符一个字符往外蹦。\
\") else: raw.write(\"\
已到达设定的最大层数 %d。\
\" % max_layers) # ---- 对照:还原后只 init 一次,即修复后的写法 ---- raw.write(\"\
\" + \"-\" * 62 + \"\
\") raw.write(\" 对照:还原 sys.stdout 后只 init() 一次(修复后的写法)\
\") raw.write(\"-\" * 62 + \"\
\") sys.stdout = sys.__stdout__ init(autoreset=True) raw.write(\" 当前嵌套层数 = %d\
\" % count_layers()) raw.flush() n = stopped or max_layers t = time.perf_counter() for k in range(1, n + 1): sys.stdout.write(Fore.GREEN + \"[第 %d 篇] 模拟一行带颜色的输出\
\" % k) el = time.perf_counter() - t raw.write(\" 连续输出 %d 篇总耗时 = %.1f ms(平均 %.2f ms/篇)\
\" % (n, el * 1000, el * 1000 / n)) raw.flush()# ============================================================ 主流程def main(): ap = argparse.ArgumentParser(description=\"colorama 输出卡顿复现与验证工具\") ap.add_argument(\"--real\", action=\"store_true\", help=\"真实输出演示(会让终端变卡)\") ap.add_argument(\"--max\", type=int, default=12, help=\"真实模式最大层数,默认 12\") ap.add_argument(\"--limit\", type=float, default=3.0, help=\"真实模式单次熔断秒数,默认 3.0\") ap.add_argument(\"--env\", action=\"store_true\", help=\"只做环境检测\") args = ap.parse_args() print(\"=\" * 64) print(\" colorama 输出卡顿:复现与验证工具\") print(\"=\" * 64) info = probe_env() print(\"\
[1] 当前环境检测\") for key in (\"isatty\", \"convert\", \"strip\", \"should_wrap\"): print(\" %-13s = %s\" % (key, info[key])) print(\" 判定 : %s\" % verdict(info)) print(\" 当前嵌套层数 : %d\" % count_layers()) if args.env: return print(\"\
[2] 计数模式(不刷屏,精确统计底层写入次数)\") print_table(\"指数路径 · 台式机 Cmder 属于此类\", convert=False, strip=False) print_table(\"短路路径 · 非 tty / 重定向属于此类\", convert=False, strip=True) print(\"\
怎么读这两张表:\") print(\" · 指数路径:修复前单篇峰值 = 2^N,20 篇累计 209 万次写入,\") print(\" 修复后只有 40 次 —— 差 5 万倍,这就是卡顿的来源。\") print(\" · 短路路径:修复前后完全持平,每篇恒定 1 次写入。\") print(\" 层数照样在涨,却多不出任何一次写入 —— 这就是该环境下\") print(\" bug 不显形的直接证据。\") if args.real: real_demo(args.max, args.limit) print(\"\
演示结束。若中间卡住过久,关闭窗口重开即可。\") else: print(\"\
[3] 真实输出演示已跳过\") print(\" 想肉眼感受卡顿,加 --real 重跑:\") print(\" python %s --real\" % __file__.split(\"\\\\\")[-1].split(\"/\")[-1]) print(\" 建议先 --max 10,确认能承受再往上加。\")if __name__ == \"__main__\": main()
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论