文章总结: 本文分析了Python脚本输出字符缓慢的问题,定位到colorama0.4.6版本init()方法缺乏幂等保护,在循环中多次调用导致终端控制序列处理开销增大。解决方案是仅在main中调用一次init(autoreset=True)或使用justfixwindows_console(),并提供了检测代码和修复前后对比。
综合评分: 75
文章分类: 其他
命令行打印输出字符缓慢之谜
原创
微慑网
微慑网
微慑网
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("\n 【%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("\n" + "=" * 62 + "\n") raw.write(" 真实输出演示:每\"篇\"调用一次 init(autoreset=True),再输出一行\n") raw.write(" 单次耗时超过 %.1f 秒自动停止,也可随时按 Ctrl+C 中断\n" % limit_sec) raw.write("=" * 62 + "\n\n") raw.flush() sys.stdout.write("下面的彩色行走的是被嵌套后的 sys.stdout:\n") 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 篇] 模拟一行带颜色的输出\n" % k) el = time.perf_counter() - t raw.write(" 层数=%2d 单次耗时=%10.1f ms\n" % (k, el * 1000)) raw.flush() if el > limit_sec: stopped = k break if stopped: raw.write("\n 单次耗时已超过 %.1f 秒 —— 演示停止。\n" % limit_sec) raw.write(" 再往上加层数,终端就会明显一个字符一个字符往外蹦。\n") else: raw.write("\n 已到达设定的最大层数 %d。\n" % max_layers) # ---- 对照:还原后只 init 一次,即修复后的写法 ---- raw.write("\n" + "-" * 62 + "\n") raw.write(" 对照:还原 sys.stdout 后只 init() 一次(修复后的写法)\n") raw.write("-" * 62 + "\n") sys.stdout = sys.__stdout__ init(autoreset=True) raw.write(" 当前嵌套层数 = %d\n" % 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 篇] 模拟一行带颜色的输出\n" % k) el = time.perf_counter() - t raw.write(" 连续输出 %d 篇总耗时 = %.1f ms(平均 %.2f ms/篇)\n" % (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("\n[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("\n[2] 计数模式(不刷屏,精确统计底层写入次数)") print_table("指数路径 · 台式机 Cmder 属于此类", convert=False, strip=False) print_table("短路路径 · 非 tty / 重定向属于此类", convert=False, strip=True) print("\n 怎么读这两张表:") print(" · 指数路径:修复前单篇峰值 = 2^N,20 篇累计 209 万次写入,") print(" 修复后只有 40 次 —— 差 5 万倍,这就是卡顿的来源。") print(" · 短路路径:修复前后完全持平,每篇恒定 1 次写入。") print(" 层数照样在涨,却多不出任何一次写入 —— 这就是该环境下") print(" bug 不显形的直接证据。") if args.real: real_demo(args.max, args.limit) print("\n演示结束。若中间卡住过久,关闭窗口重开即可。") else: print("\n[3] 真实输出演示已跳过") print(" 想肉眼感受卡顿,加 --real 重跑:") print(" python %s --real" % __file__.split("\\")[-1].split("/")[-1]) print(" 建议先 --max 10,确认能承受再往上加。")if __name__ == "__main__": main()
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:微慑网 微慑网
微慑网《命令行打印输出字符缓慢之谜》