Python输入输出全解析:从input到文件、标准流与数据校验
说个我见过特别多的场景刚接触 Python 的人写完第一段交互脚本用的都是input()和print()但等脚本真的被丢到生产环境、被别的程序调用、或者需要处理几万行日志时才发现输入输出远不止“键盘敲进去、屏幕打出来”这么简单。Python 3 的输入输出体系从最简单的用户输入读取到格式化输出、文件持久化、标准流管理再到数据校验其实是一条完整的链路。这篇文章会把这条链路从头到尾拆开讲适合刚入门但想少踩坑的初学者也适合写了很多年脚本但一直靠复制粘贴处理 I/O 的老手。1. 从 input() 到 print()最基础的输入输出链路1.1 永远记住input() 返回的是字符串很多新手在写交互式程序时第一个坑就踩在这里。input()从标准输入读取一行内容无论用户输入的是数字、字母还是表情符号它都会以字符串形式返回。你可以试一下age input(请输入年龄) print(type(age)) # class str如果你直接拿这个值去做数值比较比如if age 18Python 会直接抛出TypeError因为字符串不能和整数比较。正确做法是先做类型转换age int(input(请输入年龄))但这里又引出了第二个坑如果用户输入的是abcint()会抛出ValueError程序直接崩溃。所以凡是正经一点的交互脚本都不会只写一行裸的input()而是会包一层校验逻辑。这个我在后面“输入数据校验”部分详细说。另外input()的提示参数是可选的比如input(请输入姓名)这个提示字符串会被直接打印到标准输出而且不会自动带换行。如果你在写一个长时间运行的脚本提示信息可能被缓冲区吞掉导致用户看不见这时候需要加上flushTrue或者在print()里处理。后面讲缓冲区的时候我会展开。1.2 print() 的隐藏参数sep、end、file、flushprint()在 Python 3 里是函数不是语句。它的完整签名是print(*objects, sep , end\n, filesys.stdout, flushFalse)这里有几个平时容易忽略但非常实用的点sep多个对象之间的分隔符默认是空格。比如print(2024, 05, 01, sep-)会输出2024-05-01。end输出末尾追加的字符串默认是换行符。print(进度, end)可以让光标停在当前行方便后续继续输出。file输出目标。默认是sys.stdout但你可以指定为文件对象比如print(error, filesys.stderr)这在日志分离时非常有用。flush是否立即刷新缓冲区。默认False表示可能不会立刻写入而是攒在缓冲区里。交互提示和进度条场景通常需要设为True。一个小例子打印一个不断增长的进度条for i in range(1, 101): print(f\r已完成 {i}%, end, flushTrue) time.sleep(0.05)这里的\r会让光标回到行首配合end和flushTrue就能在同一行刷新进度而不是刷屏。1.3 从用户输入到程序逻辑一个最小闭环把输入、处理、输出串起来最经典的例子就是“输入两个数求和”。但为了体现真实场景我会加一点容错def get_number(prompt): while True: raw input(prompt).strip() if not raw: print(输入为空请重新输入) continue try: return float(raw) except ValueError: print(无法解析为数字请重新输入) a get_number(请输入第一个数字) b get_number(请输入第二个数字) print(f{a:g} {b:g} {a b:g})这段代码虽然简单但包含了几个关键动作strip()去掉首尾空白、判空、异常捕获、循环重试。实际工作中用户输入几乎永远不会老老实实按你预期的格式来不做好这一层防御程序跑不了几次就会崩。2. 格式化输出从 % 到 f-string一次讲透2.1 三种格式化方式的演进与选择Python 3 里格式化字符串主要有三种方式老牌的%格式化、str.format()、以及 Python 3.6 引入的 f-string格式化字符串字面量。很多人只看过其中一两种但实际项目里三种都可能会碰到尤其是接手老代码时不认识%写法会非常痛苦。先看三种写法的对比方式示例适用场景%格式化%s 今年 %d 岁 % (name, age)老代码维护、日志模块格式化str.format(){} 今年 {} 岁.format(name, age)需要动态构造模板的场景f-stringf{name} 今年 {age} 岁日常开发首选简洁高效%格式化的语法来自 C 语言的printf用%s表示字符串、%d表示整数、%f表示浮点数。它的缺点是不够直观比如多个参数时要靠位置对应写长了容易错位。str.format()改进了一部分支持位置参数、关键字参数甚至支持用下标、属性访问。f-string 则在 3.6 之后成为我个人的默认选择因为它在运行时直接计算表达式的值性能和可读性都好。2.2 格式规格迷你语言对齐、精度、千位分隔f-string 和str.format()背后共用同一套格式规格语言语法是花括号里的冒号后面跟规格。掌握它你就能输出整齐的表格、精确的小数、带千位分隔符的数字而不需要手动拼空格。price 12345.6789 print(f默认输出{price}) print(f保留两位小数{price:.2f}) print(f千位分隔{price:,.2f}) print(f百分比{0.125:.1%}) print(f右对齐宽度10{price:10.2f}) print(f左对齐宽度10{price:10.2f}) print(f居中宽度10{price:^10.2f})输出结果默认输出12345.6789 保留两位小数12345.68 千位分隔12,345.68 百分比12.5% 右对齐宽度10 12345.68 左对齐宽度1012345.68 居中宽度10 12345.68这些看着不起眼但在输出报表对齐时是救命的。以前我写报表经常为了对齐列头手工数空格后来直接用f{col:20}指定宽度省了无数脑细胞。格式规格里还有几个高级用法0可以补零f{year:04}输出002025。科学计数法f{value:e}或f{value:.2e}。数字符号控制f{value:d}让正数也带号。f-string 的调试模式Python 3.8 之后可以直接写f{name}输出nameAlice调试时特别方便。嵌入更复杂的表达式f{2 * 3 1}也可以直接算。2.3 动态模板与性能取舍虽然 f-string 好用但它有一个限制在写代码时花括号里的内容必须是确定的。如果你在运行时才拿到一个格式字符串那就只能走str.format()或者%格式化比如从配置文件里读取日志格式。fmt {level}: {message} print(fmt.format(levelINFO, message启动成功))这种动态模板场景用 f-string 做不到因为 f-string 是编译期语法不能在运行时接受一个可变模板。性能上f-string 比str.format()快因为后者需要解析模板字符串而 f-string 在编译阶段就被处理成了字节码。但实际业务里差别通常微乎其微除非你在一个每秒调几十万次的循环里做格式化否则不用为了性能强迫自己不用str.format()。真正要注意的反而是可读性和维护性这个优先级最高。3. 文件读写与编码把数据稳稳落盘3.1 open() 的模式和编码输入输出不只是屏幕和键盘文件 I/O 才是绝大多数脚本的主战场。Python 内置的open()函数是文件读写的入口最常见的调用是with open(data.txt, r, encodingutf-8) as f: content f.read()模式参数r表示只读w表示写入并覆盖a表示追加。在模式后面加b就是二进制模式比如rb读二进制、wb写二进制。还有r、w、a这类带读写组合的模式但实际用得不多大多时候只用读、写、追加、二进制这几种。编码问题是我反复踩过的坑。如果不指定encodingPython 会依赖系统默认编码Linux 和 macOS 上通常是 UTF-8但 Windows 上可能是 GBK 或 CP936。这就导致同一个脚本在 Windows 上读 UTF-8 文件时中文直接乱码甚至抛出UnicodeDecodeError。所以凡是涉及文本文件读写强烈建议显式指定encodingutf-8并且最好在读取时加一个容错参数with open(data.txt, r, encodingutf-8, errorsreplace) as f: content f.read()errorsreplace的意思是遇到无法解码的字节用替换而不是直接崩溃。如果你在跑一个数据清洗脚本宁可用占位符替换坏字节也不想让整个任务中途挂掉。另一个和 Windows 相关的坑是换行符。Windows 文本用\r\nLinux 和 macOS 用\n。Python 的open()在文本模式默认做换行转换读的时候把\r\n统一转成\n写的时候把\n转回系统默认换行符。如果你不想让它自动转换需要传newline这在处理二进制文件或者写 CSV 时特别重要。写 CSV 时如果不指定newline很可能会在文件里混入空行这个问题在 Windows 上尤其明显。3.2 为什么你必须用 with 管理文件文件对象打开后操作系统会分配一个文件描述符。如果打开后没有关闭轻则占用资源重则在 Windows 上导致文件被锁住其他程序无法读写。新手最容易犯的错是只写f open(a.txt, w)用完忘记f.close()。正确做法的首选是with语句with open(a.txt, w, encodingutf-8) as f: f.write(hello)with会在代码块结束时自动调用f.close()即使中间抛了异常也会走清理逻辑。这等价于try/finally但更简洁、更不容易漏。如果你要同时读写两个文件可以这样并列写with open(src.txt, r, encodingutf-8) as src, \ open(dst.txt, w, encodingutf-8) as dst: dst.write(src.read())3.3 大文件读写别一次性读入内存很多人一开始图省事直接read()把整个文件读进内存。如果文件只有几十 KB完全没问题但如果文件有几个 GB内存直接爆炸。正确的做法是逐行处理或者分块读取。逐行处理最简单因为文件对象本身就是可迭代的with open(big.log, r, encodingutf-8) as f: for line in f: process(line)这里的for line in f底层用的是缓冲读取一次读一块按行 yield 出来不会把整个文件装进内存。如果是二进制文件图片、视频、压缩包可以用固定大小的块来读with open(img.jpg, rb) as f: while chunk : f.read(8192): process(chunk):是海象运算符在 Python 3.8 之后可用。这个循环的含义是每次读 8192 字节读到空串说明文件结束否则处理这一块。如果你需要在文件里跳着读可以用seek()和tell()。f.tell()返回当前文件指针位置f.seek(offset, whence)可以移动到指定位置。比如日志文件需要只读取最后几行时可以先用seek(0, 2)跳到文件末尾再往前读。不要小看这个技巧处理超大日志时它比把整个文件读进来快几个量级。3.4 结构化数据的读写从 CSV 到 JSON日常业务里纯文本文件只占一部分更多时候在跟 CSV、JSON 这类结构化数据打交道。Python 内置了csv和json模块不需要额外安装第三方库。先看 CSV 读写的标准姿势import csv with open(data.csv, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: print(row[姓名], row[分数]) with open(output.csv, w, encodingutf-8, newline) as f: writer csv.DictWriter(f, fieldnames[姓名, 分数]) writer.writeheader() writer.writerow({姓名: 张三, 分数: 90})注意上一节提到的newline在csv模块里基本是标配否则 Windows 下会在每行之间多出一个空行。JSON 的读写更直接import json data {name: Alice, age: 30, tags: [python, iot]} with open(config.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) with open(config.json, r, encodingutf-8) as f: loaded json.load(f)ensure_asciiFalse很关键。默认情况下json.dump会把所有非 ASCII 字符转成\uXXXX转义序列虽然结果是合法 JSON但人眼完全没法看。关掉它中文和表情符号就会以正常字符保存。4. 标准流、缓冲区与重定向看懂脚本和系统的交互4.1 stdin、stdout、stderr 三兄弟一个 Python 脚本启动之后操作系统会给它三个标准文件流标准输入sys.stdin、标准输出sys.stdout、标准错误sys.stderr。默认情况下input()从sys.stdin读print()往sys.stdout写而错误信息最好往sys.stderr写。为什么要分 stdout 和 stderr因为在真实环境里我们经常有这样的需求把正常输出重定向到日志文件错误信息单独输出到另一个窗口或文件。如果你把错误也打到 stdout程序崩了之后正常日志和错误日志混在一起排障时会非常痛苦。可以直接把 stdout 和 stderr 重定向到文件python script.py output.log 2 error.log这在 Shell 里是常见操作。如果你希望 stdout 和 stderr 合并成一个流用21。在 Python 内部也可以用print(..., filesys.stderr)来控制输出目标。比如调试信息打到 stdout只把异常栈打到 stderr这样在 CI 日志里就能一眼分离。4.2 缓冲区与 flush什么时候必须手动刷新很多人遇到过“明明 print 了但日志文件里就是没有”的情况。原因是 Python 的sys.stdout默认是有缓冲的它不会每打印一行就往终端写一次而是攒到一定大小、或者进程正常结束时才统一刷新。缓冲的行为还跟终端环境有关在交互式终端里stdout 默认是行缓冲换行符出现就会刷新所以你不会觉得异样。在管道重定向场景里stdout 默认是块缓冲缓冲区没满就不写。于是你发现python script.py | tee log.txt时输出经常延迟。异常崩溃时缓冲区里的内容可能还没落盘就丢了。解决方式有三种print(重要信息, flushTrue)或者创建 stdout 时设置行缓冲import sys sys.stdout.reconfigure(line_bufferingTrue)或者定期调用sys.stdout.flush()。在写日志、进度条、交互式提示时flushTrue几乎成了标配。如果你在写一个被其他程序调起的子进程且对方依赖你的输出内容做实时判断没加flushTrue很可能会导致两边程序互相等待形成死锁式的假象。4.3 在代码里重定向contextlib 的妙用除了在 Shell 层面重定向Python 也可以在脚本内部临时重定向。标准库contextlib提供了redirect_stdout和redirect_stderrimport contextlib with open(out.txt, w) as f: with contextlib.redirect_stdout(f): print(这行内容会写入 out.txt)这个功能在测试场景里很实用比如你调用了一个会直接print的第三方库又不想看到它的输出可以临时把 stdout 重定向到io.StringIO()里import io import contextlib buffer io.StringIO() with contextlib.redirect_stdout(buffer): # 调用一些会 print 的函数 noisy_function() output buffer.getvalue() # 拿到被吞掉的输出4.4 中文输出不乱码环境变量的作用回到热词里那个“CLion 运行中文输出乱码”的问题。Python 脚本输出中文乱码十有八九是 stdout 的编码和终端编码不一致。Windows 的 CMD 和 PowerShell 默认代码页可能是 GBK而 Python 3 在 Windows 上 stdout 编码有时会跟随控制台设置导致 UTF-8 中文变乱码。常用解法import sys sys.stdout.reconfigure(encodingutf-8)或者设置环境变量export PYTHONIOENCODINGutf-8PYTHONIOENCODING是 Python 专门用来指定标准流编码的环境变量。在切换到 Linux 服务器部署时强烈建议在启动脚本里显式加一下避免“本地好好的服务器上就乱码”。5. 输入数据校验与防坑指南实测翻车记录5.1 “只允许输入数字”的正确姿势网络上这个需求非常高频限制用户只能输入数字。但真正上手就会发现“数字”这个词本身就暧昧。input()拿到的是字符串要区分“看起来像数字的字符串”和“可以做数学计算的数字”。Python 自带三个判断方法str.isdecimal()、str.isdigit()、str.isnumeric()。它们之间有细微差别方法能识别阿拉伯数字能识别上标数字能识别中文大写数字能识别罗马数字isdecimal()是否否否isdigit()是是否否isnumeric()是是是否如果只是验证“只能输入数字 1-99”我会直接写import re def input_num_in_range(prompt, low1, high99): pattern re.compile(r^\d$) while True: s input(prompt).strip() if not pattern.match(s): print(只能输入正整数) continue value int(s) if low value high: return value print(f请输入 {low}-{high} 之间的数字)注意这里用了正则^\d$它可以避免用户输入12.5、-3、1_000这类带符号或小数点的内容。如果要允许小数和负数再用^[-]?\d(\.\d)?$这类正则去匹配。另外不要用float(s)后判断是否等于整数来实现“只能输入正整数”的约束因为float(1e3)也能成功但很多场景下“用户输入的是一串数字”和“用户输入了科学计数法表达式”不是一回事。5.2 空输入、EOF 与管道输入的陷阱input()在读取到 EOF文件结束符时会抛EOFError。这在两种场景下常见脚本被重定向到一个空文件python script.py empty.txt。在管道里前面没有数据echo -n | python script.py。如果不捕获程序会带着异常栈退出。健壮的脚本通常这样包一层def safe_input(prompt): try: return input(prompt) except EOFError: return 还有一个常见坑从管道读入的数据自带换行符如果你不处理逻辑判断if line quit永远不成立。所以用户输入的原始字符串必须strip()去掉首尾空白然后再做业务判断。如果你要从标准输入读多行不要傻傻地一行一行写input()直接用sys.stdin做迭代import sys for line in sys.stdin: line line.strip() if not line: continue print(f处理: {line})这种方式在管道数据处理里是最常用、最可靠的。5.3 密码输入与隐藏回显如果脚本需要让用户输入密码直接用input()会把密码显示在终端上非常不安全。标准库getpass模块提供了隐藏回显的输入方式import getpass password getpass.getpass(请输入密码)它会读取一行输入但不显示用户敲的内容并且在某些场景还会自动处理终端回显的问题。注意getpass在 IDE 里未必生效在真正的终端里更可靠。5.4 输入超时与自动化交互input()本身没有超时参数。如果你需要“用户 5 秒不输入就退出”这种交互标准库里没有直接方案只能借助信号或者第三方库。在 Linux/macOS 上可以用signal.alarm比如import signal def handler(signum, frame): raise TimeoutError(输入超时) signal.signal(signal.SIGALRM, handler) signal.alarm(5) try: value input(5 秒内输入) except TimeoutError: print(超时) finally: signal.alarm(0)但 Windows 对signal.alarm支持有限跨平台场景更推荐用selectors或干脆用第三方库pexpect来模拟交互。写自动化测试时我会用pexpect和subprocess组合来驱动命令行程序而不是直接跑input()。5.5 一个完整的健壮数字输入函数把上面的经验合并起来我会给出这样一个“实战版”输入函数import re _NUM_PATTERN re.compile(r^[-]?(\d\.?\d*|\.\d)$) def robust_number_input(prompt, allow_negativeTrue, allow_floatTrue): while True: try: raw input(prompt).strip() except EOFError: print(输入已结束) return None if not raw: print(输入不能为空) continue if not _NUM_PATTERN.match(raw): print(请输入合法数字) continue try: value float(raw) except ValueError: print(数字格式不正确) continue if not allow_negative and value 0: print(不能输入负数) continue if not allow_float and . in raw: print(不能输入小数) continue return value这个函数处理了空输入、EOF、非法字符、正负数、整数小数等各种情况基本能覆盖日常业务里对数字输入的全部验证要求。6. 实战场景从输入到输出的完整闭环6.1 需求描述与数据准备假设我们要写一个小工具从 CSV 文件读取一批销售记录统计每个品类的总销售额然后把结果格式化输出到屏幕同时生成一份 JSON 报告。这个任务很常见几乎涵盖了输入输出所有核心知识点。先准备一个简单的 CSV品类,销售额 手机,12999 笔记本,18999 手机,8999 耳机,799 笔记本,20999 耳机,12996.2 代码实现与解释第一步读取 CSVimport csv from collections import defaultdict sales defaultdict(float) with open(sales.csv, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: category row[品类].strip() amount float(row[销售额]) sales[category] amount注意newline和encodingutf-8这两个参数在上一节已经解释过这里直接按规范写。第二步格式化输出到屏幕print(f{品类:10}{销售额:12}) print(- * 24) for category, total in sorted(sales.items(), keylambda x: x[1], reverseTrue): print(f{category:10}{total:12,.2f})输出结果品类 销售额 ------------------------ 笔记本 39,998.00 手机 21,998.00 耳机 2,098.00这里的对齐技巧是品类左对齐宽度 10销售额右对齐宽度 12并带上千位分隔符和两位小数。这样输出的表格不需要手动数空格列对齐自动完成。第三步输出 JSON 报告import json report { total_sales: sum(sales.values()), by_category: { category: round(total, 2) for category, total in sorted(sales.items()) } } with open(report.json, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) print(报告已写入 report.json)6.3 结构化输出的另一种形态Dataclass JSON热词里有一个“structured output 结构化输出”这在 Python 里可以理解为不要把数据散落在字典和随意变量里而是用类型把结构固定下来。Python 3.7 的dataclasses能让这件事更优雅。from dataclasses import dataclass, asdict import json dataclass class CategoryReport: category: str total: float reports [ CategoryReport(category, total) for category, total in sorted(sales.items()) ] print(json.dumps([asdict(r) for r in reports], ensure_asciiFalse, indent2))asdict()把 dataclass 实例转成字典再用json.dumps输出。这样结构是明确写在类型定义里的后期别人接手代码一眼就能看出一条报表记录包含哪些字段。6.4 从文件输入到标准输出的完整流程最后把完整脚本串起来你会发现整个流程其实就是“输入 - 处理 - 输出”三件事的反复组合。文件是输入屏幕是输出数据库查询是输入JSON 是输出网络请求是输入日志是输出。理解了这一层抽象你就不会觉得输入输出只是input()和print()那两个函数了。7. 常见问题速查表症状可能原因解决办法input()报EOFError输入流提前结束用try/except EOFError包裹或改用sys.stdin迭代用户输入数字后int()报错输入含空白或非法字符先strip()再try/except ValueError打印中文在终端乱码stdout 编码与终端不一致设置PYTHONIOENCODINGutf-8或调用sys.stdout.reconfigure(encodingutf-8)输出被重定向后没有立即写入stdout 块缓冲print(..., flushTrue)或调用sys.stdout.flush()read()大文件内存爆掉一次加载整个文件用for line in f逐行处理或按块read(8192)写 CSV 后文件有空行没传newline打开文件时加newlineJSON 中文变成\uXXXXjson.dump默认转义加ensure_asciiFalse文件读完忘记close()被锁没用上下文管理器用with open(...) as f:这表里的每一项都是我或者身边同事在实际项目里踩过的坑。输入输出模块虽然基础但坑密度远高于其他标准库值得多花一点时间把这些细节记牢。最后分享一个小技巧调试输入输出问题时不要把目光只盯在代码上先确认运行环境再确认系统编码然后确认重定向方式。很多诡异现象根本不是代码逻辑问题而是缓冲区、编码或 Shell 环境导致的。先检查环境这套“外围”往往比逐行读代码更省时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →