
又是requirements.txt。今天帮朋友排查环境问题时他把项目包发给我说在自己电脑上跑得好好的换到一台Windows机器上pip install -r requirements.txt直接崩了。错误信息里最扎眼的就是那串UnicodeDecodeError: utf-8 codec cant decode byte 0xd7 in position 55: invalid continuation byte。这不是冷门问题尤其是国内开发者的项目requirements.txt里顺手写两句中文注释很容易就撞上。这篇文章就把这个bug从现象到根因、从快速修复到长期预防完整拆一遍。无论你是刚入门的小白还是帮同事救火的熟手都能在里面找到可以直接抄走的解法。1. 错误现场UnicodeDecodeError在pip环节的完整面貌1.1 报错长什么样子先把完整的报错贴出来方便你对照。假设项目里有这样一个requirements.txt# 数据分析基础库 numpy1.24.3 pandas2.0.1 # 绘图相关 matplotlib3.7.1在Windows命令行里执行安装pip install -r requirements.txt然后终端吐出一大段内容开头可能是ERROR: Exception:后面跟着一串Traceback最后落到这样一行UnicodeDecodeError: utf-8 codec cant decode byte 0xd7 in position 55: invalid continuation byte这就是标题里那个被截断的错误的完整版本。你搜索时看到“can’t decode by”结尾不是有个单词叫by而是错误信息本身因为平台篇幅限制被截断了完整的表述是“cant decode byte”某个十六进制数字后面通常还会带in position xxx以及invalid continuation byte或invalid start byte。1.2 如何定位到req_file.py真正执行读取逻辑的不是pip最外层而是它内部的req_file.py模块。在traceback里你会看到类似这样的地址File C:\Python311\Lib\site-packages\pip\_internal\req\req_file.py, line 151, in process_line每次看到pip\_internal\req\req_file.py就能确定问题出在“读取requirements.txt内容”这个阶段而不是“网络下载”或“环境解析”阶段。这非常重要因为很多人一看到pip报错就去查镜像源、查网络代理方向完全错了。1.3 错误里每个关键信息的含义拆开看这行报错每一段都不是废话utf-8 codecpip当前尝试使用的解码器是UTF-8。cant decode byte 0xd7在文件的某个位置发现了一个十六进制值为0xd7的字节。position 55这个字节在文件里是第55个字节的位置。invalid continuation byte按照UTF-8的规则0xd7不能出现在当前这个字符序列中也就是说这个文件根本不是合法的UTF-8文本。稍微解释一下0xd7是什么。十六进制0xd7对应十进制215在GBK/GB2312编码体系里它经常是某个汉字的第一个字节或中间字节但在UTF-8编码体系里一个合法的多字节字符其引导字节有严格的取值范围0xd7不符合这些规则。换句话说pip拿着UTF-8的规则表去解码一个GBK编码的文件读到某个中文字符时直接“卡住”。这也是为什么很多人的第一反应是“我在文件里只写了几个中文注释怎么就不行了”——问题不在于“中文”本身而在于“中文在文件里用什么编码存储”。你的编辑器默认用GBK存盘pip默认用UTF-8去读两边拿的不是同一本密码本。2. 根因分析requirements.txt的编码为什么会对不上2.1 文本文件的编码机制初学者最容易忽略的一件事是文本文件里面存的并不是“字符”而是“字节序列”。字符“测”被存进磁盘时会按某种编码规则变成一组字节读取的时候也必须按同一套规则还原成字符。以“测”字为例在不同编码下的表现差异很大编码方式“测”对应的字节备注UTF-8\xe6\xb5\x8b三个字节兼容ASCIIGBK\xb2\xe2两个字节Windows中文系统常见UTF-8 with BOM\xef\xbb\xbf\xe6\xb5\x8b前面多三个字节的文件头标记所以一旦编写文件时用的是GBK而读取时按UTF-8解析遇到\xb2\xe2这种字节序列UTF-8解码器就会判定为非法并抛出UnicodeDecodeError。2.2 pip为什么要用UTF-8解码从pip 21.x开始pip install在解析requirements.txt时默认按UTF-8读取。这背后是Python生态整体向UTF-8迁移的趋势也是PEP 686Python 3.15计划将UTF-8作为默认编码的延续。在Linux和macOS上默认区域设置基本就是UTF-8或C.UTF-8所以这个问题很少出现。但Windows中文版的情况完全不同。系统的ANSI代码页是cp936即GBK很多编辑器尤其是老版本记事本默认保存文件时用的是ANSI编码。当你新建一个记事本文件把上面的依赖内容粘贴进去保存时可能不会主动写成UTF-8而是按系统的ANSI代码页存成GBK。于是录制了一个“假中文文件”一遇到pip的UTF-8解码器就现出原形。2.3 “另一种编码错误”的场景你可能会在网上搜到类似问题但错误信息写得不一样比如UnicodeDecodeError: gbk codec cant decode byte 0x... in position ...: illegal multibyte sequence注意区别这一种是解码器是gbk codec不是utf-8 codec。它发生在Python环境默认编码不是UTF-8时常见的Windows Python 3.7到3.12环境一些依赖的安装脚本或自定义构建步骤用系统默认编码GBK读取了一个其实已经是UTF-8编码的文件。同样是UnicodeDecodeError解码器和文件编码的关系完全相反所以解法也不是一回事。如果你拿“改文件编码”去修第二种错误不一定错但如果你拿“设置PYTHONUTF8”去修第一种错误那基本没用。这两种情况我后面都会给出对应方案。2.4 排查方向判断表出现报错后先别急着动手花10秒钟做一个判断报错里的解码器文件大概率是什么编码正确方向utf-8 codec cant decode文件不是UTF-8常见是GBK/ANSI把文件转成UTF-8或调整pip读取参数gbk codec cant decode文件是UTF-8但当前Python默认编码是GBK设置UTF-8模式或检查系统区域语言cp936 codec cant decode同上cp936就是GBK同上优先考虑UTF-8模式判断出方向后再选择下面的具体方案。3. 最直接的解法把requirements.txt转成纯UTF-8编码3.1 用编辑器另存为UTF-8如果你只是手头这一个文件出问题最快的方法是用编辑器直接转码。用VS Code打开requirements.txt看右下角或状态栏的编码标识。如果是类似GBK、GB2312、Simplified Chinese (GB 2312)这类字样说明文件不是UTF-8。点击这个编码标识在弹出的菜单中选择“通过编码保存”再选“UTF-8”保存即可。用Notepad也一样编码菜单里选择“转为UTF-8编码”后再保存。这里注意是“转为”不是“以UTF-8编码”前者会重新编码文件内容后者可能只是换个标签显示实际字节没变。如果电脑上只有记事本操作稍微绕一点先用记事本打开另存为时“编码”下拉框里选择“UTF-8”覆盖保存。老版Windows记事本还有个坑就是保存UTF-8时会带上BOM。BOM本身不是致命问题但某些场景下仍可能引发其他工具解析异常所以更推荐用VS Code或Notepad做“无BOM的UTF-8”转换。3.2 BOM要不要带BOM是EF BB BF三个字节放在文件开头用来标记“这是一个UTF-8文件”。Python的源码文件读取器能自动跳过BOMpip在处理requirements.txt时也不会因为BOM直接崩溃。但BOM会带来两个实际麻烦某些Linux容器或CI环境下脚本第一行如果是#!BOM会导致 shebang 解析失败。当你用for循环读取第一个包名时包名前面可能会粘着不可见字符\ufeff导致pip找不到包。所以我的建议是如果编辑器提供“UTF-8 without BOM”无BOM的UTF-8选项优先选它。VS Code默认就是无BOM的UTF-8这点比较省心。3.3 通过转换验证结果转完之后怎么确认已经修好有两个办法一个是在VS Code右下角看编码标识变成了UTF-8文件里的中文注释也显示正常。另一个更严谨的做法是在终端里用Python验证一下python -c from pathlib import Path; bPath(requirements.txt).read_bytes(); b.decode(utf-8); print(OK)如果不抛异常最后输出OK说明文件已经可以被UTF-8完整解码pip那边就不会再报这个错了。这个验证命令值得养成习惯因为很多编辑器显示正常不代表底层字节合法尤其是文件里混着特殊符号或历史遗留内容时。4. 环境变量与运行时设置什么时候真的有效4.1 PYTHONUTF8模式的适用场景很多技术博客建议设置PYTHONUTF81来解决问题。在我实测中这个变量只对本文2.3节里说的第二种情况有效也就是报错解码器为gbk codec或cp936 codec时。原因在于PYTHONUTF81会把Python运行时的默认文本编码切换为UTF-8。当pip内部读取依赖文件时如果它没有显式指定编码就会使用这个默认值。所以当文件本身是UTF-8、但环境默认GBK导致读取失败时设置这个变量能够一针见血地修复。设置方法分平台Windows CMD窗口set PYTHONUTF81 pip install -r requirements.txtWindows PowerShell$env:PYTHONUTF81 pip install -r requirements.txtLinux或macOSexport PYTHONUTF81 pip install -r requirements.txt临时生效没问题。如果想长期生效Windows用户在“系统属性-环境变量”里新建一个名为PYTHONUTF8的变量值为1Linux用户在~/.bashrc或~/.zshrc里加一行export PYTHONUTF81。4.2 Linux/macOS上LANG与LC_ALL在Linux/macOS环境里如果看到ascii codec cant decode类似报错通常是LANG或LC_ALL区域变量没设置好。比较稳妥的做法是统一设置export LC_ALLC.UTF-8 export LANGC.UTF-8在多数现代Linux发行版上默认区域已经带UTF-8后缀不太需要手动干预。但在最小化容器镜像或某些服务器环境里区域变量可能缺失这时候按上面两句直接写进Dockerfile或shell启动脚本里问题就能避免。4.3 常见误区我见过不少人在论坛里提出“万能解法”实际执行时浪费了很多时间。这里列几个最容易走偏的点在pip命令后面加--no-cache-dir这个参数跟编码解析没有关系它只影响pip的缓存逻辑不要指望它能修复UnicodeDecodeError。换清华镜像源镜像源影响的是下载源地址而错误发生在读取requirements.txt的阶段换源根本轮不到上场。强制升级pip当错误出现在旧版pip上时升级pip有一定概率解决问题因为新版本可能改了编码处理逻辑但如果你已经是最新版pip再刷版本毫无意义。把requirements.txt改成一行不要注释这种做法能解决问题但绕过了本质。它有效是因为没有任何非ASCII字节pip不需要面对编码分歧可只要哪天有人再加个中文注释问题又会回来。记住一点环境变量方案解决的是“运行时默认编码和文件编码不一致”的问题。如果文件本身就不是UTF-8而pip又强制按UTF-8读那设置任何环境变量都改变不了“UTF-8读不了GBK”这个事实你仍然只能转文件编码。5. 批量处理用Python脚本自动转码目录下的依赖文件5.1 为什么需要批量处理单文件用编辑器转码很方便但要是一次性接手一个老项目里面有十几份txt、cfg、ini文件都可能带着编码历史问题挨个打开另存就太痛苦了。这种时候值得写一个小脚本把所有文本文件检测一遍自动转码。另外一个典型场景是从Windows服务器上导出的旧项目压缩包解压后里面一堆文件都是GBK编码。你无法确定哪一个会被pip或项目代码读取最简单的策略是直接把整个项目目录下的常见文本文件全部统一转成UTF-8。5.2 转码脚本下面这个脚本是实测可用的。它做的事情很简单遍历指定目录下所有.txt、.cfg、.ini、.md文件先尝试用UTF-8解码失败就尝试GBK解码成功后将内容重新写回UTF-8格式。# -*- coding: utf-8 -*- 批量把GBK编码的文本文件转为UTF-8 用法: python convert_encoding.py 目标目录 from pathlib import Path import sys def convert_file(file_path: Path) - bool: raw file_path.read_bytes() # 已经是合法UTF-8的文件跳过 try: raw.decode(utf-8) return False except UnicodeDecodeError: pass # 尝试用GBK解码成功则改写为UTF-8 try: text raw.decode(gbk) except UnicodeDecodeError: print(f跳过无法识别的文件: {file_path}) return False # 写回UTF-8不带BOM file_path.write_text(text, encodingutf-8) print(f已转换: {file_path}) return True def main(target_dir: str): root Path(target_dir) suffixes {.txt, .cfg, .ini, .md, .rst, .json} for f in root.rglob(*): if f.suffix.lower() in suffixes and f.is_file(): convert_file(f) print(批量转换完成) if __name__ __main__: if len(sys.argv) 2: print(请指定目录例如: python convert_encoding.py D:\\project) sys.exit(1) main(sys.argv[1])执行方式和预期输出python convert_encoding.py ./legacy_project已转换: legacy_project/requirements.txt 已转换: legacy_project/doc/install.md 批量转换完成5.3 使用注意事项脚本虽简单但有几个坑要提醒。第一JSON文件不要随便转码。JSON字符串里的字符本来就是按Unicode语义处理的文件读写编码通常由读写方指定如果直接做GBK到UTF-8的字节转换遇到\u00e6这类转义序列时反而可能破坏语义。我在脚本里虽然保留了.json但实际项目里建议去掉或者先由人工确认。第二备份是底线。批量转换前至少做一次压缩备份或者用版本管理工具确认所有文件都有提交记录。我的实测经验里百分之百安全的批量转码几乎不存在总会有某个文件带着微妙的历史痕迹。第三只处理文本类型文件。别把*.py也加进去。Python源码内部就会声明# -*- coding: -*-多数情况不需要你手动干预。真需要也应该逐个人工确认因为源码里可能存在字符串字面量跨文件引用等复杂情况。第四路径不要写反。脚本用write_text(text, encodingutf-8)写回时没有使用errorssurrogateescape所以如果GBK解码后还有无法映射的私有字符会直接抛错。遇到这种情况脚本会中止你可以把异常打印出来单独处理。6. 从源头避免requirements.txt与团队协作规范6.1 依赖声明文件的规范解决问题之后更值得做的是避免同类问题再次发生。requirements.txt的规范我的第一条建议是文件里尽量只放包名和版本约束不写中文注释。这不是因为中文不行而是因为一旦引入非ASCII字符就意味着所有处理这个文件的人都必须遵守同一套编码规则而现实是Windows和Linux/macOS的默认习惯完全不同。如果一定要写注释那就统一约定所有依赖文件都用UTF-8无BOM保存。这个约定需要写进团队的README或开发规范里。我见过不少团队在群里口头说一句“大家用UTF-8啊”结果一半人用VS Code默认UTF-8另一半人还在用记事本默认ANSI下次pip install直接又炸这种沟通成本远高于提前写一条规范。6.2 编辑器与代码库约束技术上可以把这种约定半自动化。在项目根目录放一个.editorconfig文件主流的编辑器和IDE基本都能识别root true [*] charset utf-8 end_of_line lf insert_final_newline true [*.txt] charset utf-8.editorconfig能保证多数编辑器的默认保存格式是一致的但挡不住记事本这类不读配置的软件。所以更好的做法是在Git仓库层面加.gitattributes*.txt text eollf *.md text eollf *.cfg text eollf加上text属性后Git会在底层把文件内容按UTF-8做归一化存储不同系统之间的换行符差异也会被自动处理。这样即使有人在Windows上把文件保存成了GBK只要文件提交到Git时被识别为文本文件Git的clean filter会按UTF-8重新编码在一定程度上可以拦截掉错误编码入库。6.3 自动化生成依赖文件的工具链如果项目依赖管理允许我更推荐用工具自动生成依赖清单彻底绕开手写txt带来的编码风险。两个比较主流的路线pip-compile在requirements.in里写顶层依赖由它生成锁定版本的requirements.txt。这个生成过程完全由pip-compile控制输出默认就是UTF-8基本不会出现编码问题。uv lockuv自带的锁文件机制配合uv sync管理整个环境依赖声明和锁文件都是结构化格式不需要人去维护txt。这两个工具还能顺带解决“昨天还能装今天一更新依赖就冲突”的经典问题算是额外的收益。不过如果你是给别人写演示项目或者维护一个特别小的工具仓库引入pip-compile和uv又显得有点重。小项目里最简单的做法还是坚持“不写中文注释 UTF-8保存”这两条底线。最后再分享一个小技巧如果你是从网上下载的压缩包项目解压后第一个动作可以先跑一下file requirements.txt或者用VS Code打开看一眼右下角编码标识确认文件编码是UTF-8再执行pip安装。这个习惯在Windows下尤其管用。我处理过很多环境问题一半以上都是“代码没问题、环境编码错了”造成的早发现早省事。以后遇到UnicodeDecodeError先看一眼解码器是哪个再判断文件编码方向然后选择转文件还是改环境基本十秒钟能定位到问题。