Python的输入输出看起来简单,实际写脚本时最容易翻车。程序的数据来源是输入,数据去向是输出。键盘、文件、网络报文都是输入;屏幕日志、磁盘文件、网络请求都是输出。Python内建的 input() 和 print() 只是冰山一角,底层支撑它们的是三个标准流:stdin、stdout、stderr。
在 sys 模块中对应 sys.stdin、sys.stdout、sys.stderr。print() 可以理解为 sys.stdout.write() 的便捷封装,input() 类似 sys.stdin.readline()。理解这层关系后,重定向输出、替换输入源时就不会只盯着 print 和 input。
- import sys
- sys.stdout.write('你好,世界\n')
- # data = sys.stdin.readline()
复制代码
一、print 的参数与格式化
print 除了打印内容,还有 sep、end、file 三个常用参数。sep 控制多个值之间的分隔符,默认空格;end 控制结尾字符,默认换行符;file 指定输出目标,默认 sys.stdout。把内容写进文件时可以传 file 参数:
- print('2025', '01', '18', sep='-')
- print('下载中', end='')
- print('...', end='')
- print('完成')
- with open('log.txt', 'a', encoding='utf-8') as f:
- print('这里是要记的日志', file=f)
复制代码
注意:print 写入的是字符串,如果文件以二进制模式打开,会直接报错。写日志建议用文本模式,或者直接用 f.write()。
格式化字符串有 % 占位、str.format()、f-string 三种。老教程常推 %,参数一多可读性差。f-string 从 Python 3.6 开始支持,可读性最高、性能最好,还能直接放表达式和函数调用。写爬虫拼接 URL、构造请求头、打印调试信息时尤其方便。
- name = '小明'
- score = 92.5
- print('姓名:%s,成绩:%.1f' % (name, score))
- print('姓名:{},成绩:{:.1f}'.format(name, score))
- print(f'姓名:{name},成绩:{score:.1f}')
复制代码
输出不刷新是常见坑。print 默认行缓冲,遇到换行符会刷新;但在循环里用 end='' 持续打印进度,或把输出重定向到管道时,缓冲区不一定立刻刷新。加 flush=True 可以强制刷新:
- import time
- for i in range(10):
- print(f'进度:{i}/10', end='\r', flush=True)
- time.sleep(0.5)
复制代码
\r 让光标回到行首,配合 flush=True 就能看到不断更新的进度条。也可以在运行脚本时加 python -u 强制无缓冲。
二、input 的细节与多行输入
input() 从 stdin 读一行,去掉末尾换行符,返回字符串。不管输入的是数字还是其他内容,返回值永远是字符串,直接做算术运算会报 TypeError。第一步永远是类型转换,整数用 int(),浮点数用 float()。用户输入不一定是合法数字,int('abc') 会抛 ValueError,实际项目必须做异常处理。
- age = input('请输入你的年龄:')
- print(age + 1) # TypeError
- # 正确做法
- try:
- age = int(input('请输入你的年龄:'))
- print(age + 1)
- except ValueError:
- print('请输入合法数字')
复制代码
多行输入有两种常见方式。行数未知时用 EOFError 判断结束:
- lines = []
- while True:
- try:
- line = input()
- lines.append(line)
- except EOFError:
- break
复制代码
数据量大或用管道重定向输入时,可以直接读整个标准输入:
- import sys
- data = sys.stdin.read().strip().split()
复制代码
例如 python sum.py < numbers.txt,< 会把文件内容作为标准输入喂给程序,sys.stdin.read() 一次性读取全部内容再拆分处理。处理日志文件、批量文本时很实用。
输入不可信。文件读取、网络请求都可能返回非预期格式。写命令行计算器时,用户输入的是表达式字符串,至少要做一次合法性检查,再考虑交给 eval(),能不用尽量不用。输入侧每加一层防御,后面逻辑就少一次连锁崩溃。
- expr = input('请输入一个数学表达式:')
- if not all(ch in '0123456789+-*/(). ' for ch in expr):
- print('表达式包含非法字符')
- else:
- print(f'结果:{eval(expr)}')
复制代码
三、文件读写:open 模式与编码
文件读写是更常见的输入输出。open() 接收文件路径和模式两个核心参数。模式选错后果可能很严重:w 会清空原有内容;a 在文件末尾追加,不会覆盖已有内容;x 排他创建,文件存在则报错;r+ 读写但不截断文件,要注意文件指针位置;w+ 读写会截断文件;a+ 是追加模式,读的位置需要额外处理。往已有文件写内容时,优先用 a 追加模式。
编码是另一个大坑。Linux 和 macOS 上 Python 默认使用 UTF-8;Windows 上 open() 默认使用系统本地编码,中文系统通常是 GBK。在 Windows 下用默认参数读 UTF-8 文件,会出现乱码或 UnicodeDecodeError。爬虫把 UTF-8 网页内容写入本地文件时,如果不指定编码,可能抛 UnicodeEncodeError: 'gbk' codec can't encode character。解决办法是打开文件时显式指定 encoding='utf-8':
- with open('result.txt', 'w', encoding='utf-8') as f:
- f.write(html_content)
复制代码
每次都写 encoding='utf-8' 有点啰嗦,但这是最稳妥的做法,代码移植到任何机器上行为都一致。
文件用完一定要关闭。忘记 f.close() 会导致句柄泄漏,程序跑久了报 Too many open files。with 语句在代码块执行完后自动关闭文件,即使内部抛异常也能正确关闭。同时读写多个文件可以用嵌套 with:
- with open('source.txt', 'r', encoding='utf-8') as src, \
- open('target.txt', 'w', encoding='utf-8') as dst:
- dst.write(src.read())
复制代码
爬虫结果落盘常用 JSON。json.dump 的 ensure_ascii=False 很重要,不加会把所有非 ASCII 字符转成 \uXXXX 转义序列,中文无法直接阅读:
- import json
- results = [
- {'title': '文章1', 'url': 'https://example.com/1'},
- {'title': '文章2', 'url': 'https://example.com/2'},
- ]
- with open('results.json', 'w', encoding='utf-8') as f:
- json.dump(results, f, ensure_ascii=False, indent=2)
复制代码
读取大文件不要用 f.read() 一次性读进内存,几个 GB 的文件会直接爆内存。文件对象本身是迭代器,逐行读取既快又省内存:
- with open('big_log.txt', 'r', encoding='utf-8') as f:
- for line in f:
- # 处理每一行
- pass
复制代码
四、常见 IO 问题排查
输出不显示:代码里写了 print('搞定了') 但屏幕没内容,通常是程序崩溃或缓冲区没刷新。正常退出时缓冲区会清空,被强杀或卡住时内容会丢失。临时解决可以 flush=True,或运行脚本时加 python -u。
编码报错 UnicodeEncodeError:三种解法。第一,文件读写时显式指定 encoding='utf-8';第二,修改标准输出流编码;第三,临时调试可设置环境变量 PYTHONIOENCODING=utf-8。修改标准输出编码的写法如下,但正式项目更建议在文件读写层把编码规范好:
- import sys
- import io
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
复制代码
文件找不到或权限拒绝:FileNotFoundError 先检查路径,用 os.path.exists() 确认,再尝试绝对路径。Linux 下 PermissionError 多半是文件属主或权限位不对,先看目录能不能写,不要先怀疑代码。
输入卡住不动:脚本里用了 input(),在定时任务等无人值守场景中会永久阻塞。自动化脚本尽量避免 input(),改成从命令行参数或配置文件读取:
- import sys
- if len(sys.argv) > 1:
- name = sys.argv[1]
- else:
- name = '默认名称'
复制代码
快速排查表:
打印不刷新:加 flush=True 或 python -u。
中文乱码:用 print(sys.getdefaultencoding()) 查看默认编码,检查文件编码是否匹配。
写入文件后内容为空:检查是否使用 with 或忘记关闭文件。
FileNotFoundError:用 os.path.exists('路径') 检查。
读取大文件卡顿:改用 for line in f 逐行读取。
五、IO 对性能的影响
频繁 print 会拖慢程序。终端输出速度远低于 CPU 处理速度,循环里大量 print,程序大部分时间都在等 IO。调试时可以随意打印,正式运行时减少不必要输出,或用 logging 统一管理日志级别:
- import logging
- logging.basicConfig(level=logging.INFO, format='%(message)s')
- logger = logging.getLogger(__name__)
- logger.debug('这里是调试信息')
- logger.info('这里是正常输出')
复制代码
写文件时逐条写入会产生大量小 IO,效率很低。攒一批再写,或使用缓冲写入。写一万条数据,不要循环 f.write() 一万次,可以先把内容存到列表,最后用 writelines 一次性写入:
- lines = []
- for i in range(10000):
- lines.append(f'第{i}行数据\n')
- with open('out.txt', 'w', encoding='utf-8') as f:
- f.writelines(lines)
复制代码
批量写入后,写盘时间可能缩短几个数量级。
Linux 和 macOS 下,管道和重定向能把输入输出的威力放大。一个程序的输出可以作为另一个程序的输入:
- python fetch_data.py | python analyze.py
复制代码
fetch_data.py 往标准输出写数据,analyze.py 从标准输入读数据,两个程序通过管道衔接。理解了 stdin/stdout,就能理解这种脚本协作模式。
输入输出不是简单的“打印”和“接收”。它决定了程序能否与人顺畅交互、能否在复杂环境稳定运行、能否高效处理海量数据。遇到 IO 问题时,先想清楚程序在从哪条管道读数据、往哪条管道写数据,大部分问题都能定位。 |