PowerShell批量转码实战:ANSI/GBK一键转UTF-8告别乱码
接手旧项目时最烦什么我最烦编码乱码。前几天在Win11上处理一套老管理系统导出的文本文件打开一看满屏的锟斤拷和问号典型的ANSI编码文件被按UTF-8解码了。单独改一个文件不难可文件夹里躺着几百个要一个个用编辑器另存为UTF-8手都能废掉。所以我把这事做成了命令行批量转换一条命令扫完指定文件夹顺便把文件全部转成UTF-8。今天把这套思路和脚本完整分享出来遇到同类问题的朋友可以直接抄作业。这篇文章适合谁开发、运维、数据处理或者手里积了一堆旧配置文件、日志、导出文本的普通用户。只要你的痛点是把一批文件从ANSI编码批量转成UTF-8又不愿意装额外软件下面的内容就是为你准备的。1. 为什么要做这件事ANSI乱码的根源与批量转码的应用场景1.1 一个真实场景日志文件全变乱码我接手的那个老系统导出的日志文件默认是ANSI编码。新服务器上的日志分析工具只认UTF-8两边一对接中文全部变成乱码。刚开始我还天真地想用记事本打开、另存为UTF-8不就行了等我把文件夹一统计好家伙txt、log、cfg、csv加起来四百多个文件有些文件还在子目录里。一个个另存为哪怕每个文件只用十秒也要一个多小时中间还不能走神。这种场景在生活中其实挺常见老网站的静态页面是GBK编码迁服务器时想统一转成UTF-8旧软件的导出报表中文在Excel里正常拿到Linux下全是乱码上一任同事写的bat脚本里带中文注释换台电脑运行就崩。凡是涉及跨系统、跨软件传文本文件编码不统一就是最阴间的坑。1.2 ANSI与UTF-8一字之差差在哪计算机只认识二进制文本文件里存的是字节而编码就是“字节序列”和“字符”之间的映射表。ANSI并不是一种具体编码它是“操作系统默认代码页”的统称。在简体中文版的Windows上ANSI就是GBK/GB2312在日文版Windows上ANSI又是Shift_JIS。同样是中文“编码”两个字存成GBK和存成UTF-8底层的字节完全不一样。UTF-8是国际通用的可变长编码能表示几乎所有语言的字符而且完全兼容ASCII。现在的主流系统和应用从Linux到Web服务器从数据库到代码编辑器默认都是UTF-8。把GBK文本直接丢给UTF-8的环境解码就会错位于是出现“锟斤拷”“烫烫烫”这类经典乱码。反过来坚持用GBK文件在别的语言环境的电脑上打开也必出问题。所以跨平台、跨团队协作时UTF-8基本是唯一的共识。1.3 为什么偏偏要用命令行来做批量转换有人可能会问“记事本和Notepad不都能转吗为什么非要用命令行”能用但痛点很明显。Notepad确实有批量转码功能可你得先安装还要会操作插件和批量宏。如果文件在两个不同环境中流转每次都要人肉去点就没法自动化了。命令行方案的优势在于Win11自带PowerShell不需要安装任何第三方软件脚本可以一次处理整个目录树包括所有子文件夹可以参数化想转txt就转txt想转log就转log能嵌入定时任务、CI/CD流程数据到了就能自动处理执行过程有日志哪些成功、哪些失败一目了然。对比一下主流方案你就明白我为什么这么选方案优点缺点记事本/编辑器另存为简单直观批量能力弱需要手动处理Notepad批量转换功能强大适合小批量依赖安装和插件不易自动化PowerShell脚本Win11自带可批量、可递归、可日志有上手门槛需要理解编码细节Python脚本灵活功能全Win11默认没有Python环境iconv工具Linux下非常方便Windows不自带需要额外移植对我这个场景来说PowerShell是最短路径系统自带不用装东西一次写好后反复用。2. 动手前必须先想清楚的三件事编码识别、BOM与脚本安全2.1 先确认你的文件到底是什么编码转换编码前最大的风险不是转完变慢而是把源编码判断错转完之后内容直接报废。所以第一步不是写脚本而是先确认文件夹里的文件到底是什么编码。判断方法有三个层次。第一用记事本打开如果显示乱码就手动切换编码打开看哪个正常。第二用PowerShell的Format-Hex看文件头几个字节。第三用专门的命令或脚本做“字节指纹”判断。Format-Hex是最直观的方式打开PowerShell执行Format-Hex -Path D:\data\a.txt -Count 16输出会直接显示文件开头的十六进制字节。这里只需要记几个关键值EF BB BFUTF-8带BOMFF FEUTF-16 LE也就是Windows下常见的UnicodeFE FFUTF-16 BE如果开头是中文汉字且第一个字节不是EF或FF那大概率就是ANSI/GBK。举个例子GBK编码的汉字“你”对应的字节是C4 E3UTF-8编码的“你”是E4 BD A0。看到C4 E3开头基本就能判断是GBK无疑。这个方法比猜稳多了。2.2 BOM到底要不要保留BOM全称Byte Order Mark是文件最开头的几个特殊字节。UTF-8的BOM是EF BB BF。它的作用是告诉阅读者“我是UTF-8编码”。Windows记事本认这个老派的Windows程序也认这个但Linux、macOS下的很多工具不认BOM反而会把BOM当成可见字符处理导致网页顶部多出空白行、bash脚本第一行报错、JSON解析失败。这就引出了一个关键选择转成UTF-8时到底带不带BOM我的建议很明确如果文件只在Windows下用或者要交给记事本、老软件读取带BOM最稳妥如果文件要放到Linux服务器、Web环境、代码仓库里或者要被脚本、程序解析无BOM更安全如果文件同时要被两边的工具读取优先无BOM因为大多数现代Windows程序也能正常识别无BOM的UTF-8。为了应对这两种需求下面的脚本里我会把是否带BOM做成一个开关你按场景自己切。2.3 脚本被系统拦截怎么办Win11对PowerShell脚本默认开了一重限制本地脚本文件可能因为执行策略直接报“无法加载因为在此系统上禁止运行脚本”。这不是脚本有问题是系统默认的安全策略。绕过方式很简单运行脚本时加上-ExecutionPolicy Bypasspowershell -ExecutionPolicy Bypass -File .\ConvertToUtf8.ps1如果你是在Windows Terminal或PowerShell窗口里直接粘贴代码执行没有通过.ps1文件那就完全不受这个限制影响。这是我最常用的方式把代码复制进去回车就完事省得管理执行策略。3. 干货实操两套可靠的批量转码脚本3.1 方案一已知源编码直接批量转换如果你能确定文件夹里的文件就是ANSI/GBK那就用这种最直接的方式。把下面的内容保存为ConvertToUtf8.ps1param( [string]$Folder D:\data, [string]$SourceEncoding GBK, [string]$Extensions txt,log,ini,cfg,csv,xml,html,bat, [switch]$NoBom ) if ($PSVersionTable.PSVersion.Major -ge 6) { try { [System.Text.Encoding]::RegisterProvider([System.Text.CodePagesEncodingProvider]::Instance) } catch {} } $encFrom [System.Text.Encoding]::GetEncoding($SourceEncoding) $encTo New-Object System.Text.UTF8Encoding($(-not $NoBom.IsPresent)) $extList $Extensions -split , | ForEach-Object { $_.Trim() } | Where-Object { $_ } $list Get-ChildItem -Path $Folder -Recurse -File | Where-Object { $extList -contains $_.Extension.TrimStart(.).ToLower() } $total $list.Count $ok 0 $fail () foreach ($file in $list) { try { $content [System.IO.File]::ReadAllText($file.FullName, $encFrom) [System.IO.File]::WriteAllText($file.FullName, $content, $encTo) Write-Host [OK] $($file.FullName) $ok } catch { Write-Warning [FAIL] $($file.FullName) : $($_.Exception.Message) $fail $file.FullName } } Write-Host Write-Host 处理完成总共 $total 个文件成功 $ok 个失败 $($fail.Count) 个。调用方式很简单把两个参数改成你自己的实际路径powershell -ExecutionPolicy Bypass -File .\ConvertToUtf8.ps1 -Folder D:\data -SourceEncoding GBK -Extensions txt,log,cfg -NoBom这段脚本有几个值得注意的细节Folder是你要扫描的根目录脚本会递归处理所有子文件夹SourceEncoding默认填GBK中文环境下稳定可靠。为什么不直接填ANSI因为ANSI在不同语言系统下含义不同中文Windows上是GBK英文Windows上是CP1252填GBK等于把语境彻底锁死Extensions是你要处理的扩展名白名单避免把图片和可执行文件也读进去弄坏-NoBom是可选参数加上它输出就是无BOM的UTF-8不加输出就是带BOM的UTF-8适合给Windows老软件用。脚本最后还会打印成功和失败的统计数免得你转完还要自己数文件。3.2 方案二自动识别混合编码文件并转码现实往往比想象复杂。有时候文件夹里既有UTF-8文件又有GBK文件你一视同仁全部按GBK来读那些本来就是UTF-8的文件反而会被读乱写回去就废了。针对这种情况我写了一个增强版脚本它会在转换前先识别文件编码只转换能识别的ANSI/GBK文件已经是UTF-8或纯ASCII的文件直接跳过。param( [string]$Folder D:\data, [string]$Extensions txt,log,ini,cfg,csv,xml,html, [switch]$NoBom ) if ($PSVersionTable.PSVersion.Major -ge 6) { try { [System.Text.Encoding]::RegisterProvider([System.Text.CodePagesEncodingProvider]::Instance) } catch {} } function Test-FileEncoding { param([string]$Path) $bytes [System.IO.File]::ReadAllBytes($Path) if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { return UTF8BOM } if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { return UTF16LE } if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { return UTF16BE } try { $strictUtf8 New-Object System.Text.UTF8Encoding($false, $true) [void]$strictUtf8.GetString($bytes) return UTF8NoBOM } catch { return ANSI } } $utf8Enc New-Object System.Text.UTF8Encoding($(-not $NoBom.IsPresent)) $gbkEnc [System.Text.Encoding]::GetEncoding(GBK) $extList $Extensions -split , | ForEach-Object { $_.Trim() } | Where-Object { $_ } $list Get-ChildItem -Path $Folder -Recurse -File | Where-Object { $extList -contains $_.Extension.TrimStart(.).ToLower() } $ok 0 $skip 0 $fail 0 foreach ($file in $list) { try { $kind Test-FileEncoding -Path $file.FullName if ($kind -eq UTF16LE -or $kind -eq UTF16BE) { Write-Host [SKIP] UTF-16 文件跳过: $($file.FullName) $skip continue } if ($kind -eq UTF8BOM -or $kind -eq UTF8NoBOM) { Write-Host [SKIP] 已是 UTF-8: $($file.FullName) $skip continue } $content [System.IO.File]::ReadAllText($file.FullName, $gbkEnc) [System.IO.File]::WriteAllText($file.FullName, $content, $utf8Enc) Write-Host [OK] $($file.FullName) (ANSI - UTF-8) $ok } catch { Write-Warning [FAIL] $($file.FullName) : $($_.Exception.Message) $fail } } Write-Host Write-Host 处理完成成功 $ok 个跳过 $skip 个失败 $fail 个。这个脚本的核心在Test-FileEncoding函数。它的判断逻辑是有BOM就按BOM识别的编码处理没有BOM就先用严格模式尝试按UTF-8解码。严格模式的意思是一旦遇到不合法的UTF-8字节序列就直接抛异常这样能过滤掉大部分GBK文件。如果严格UTF-8解码能成功说明文件要么是纯ASCII要么无BOM UTF-8这两种情况都没必要再动只有解码失败的文件才是真正要转的ANSI/GBK。这里我要提醒一句这个识别逻辑不是100%完美。极小概率下一段GBK中文的字节序列也能恰好通过UTF-8严格解码虽然实战中很少遇到但如果有极其重要的文件我建议转完之后抽查几个确认中文正常。3.3 关键参数说明与大文件效率问题两个脚本都用到了[System.IO.File]::ReadAllText和[System.IO.File]::WriteAllText而不是PowerShell里常用的Get-Content和Set-Content。为什么因为Get-Content是按行读取字符串对象的它会自动处理换行符你拿到的是“去掉了换行符的字符串数组”。如果你用Set-Content写回去默认行尾会变成Windows的\r\n原本Linux风格的\n会被改写。一次批量转换把换行符全部改了这种副作用很讨厌。而[System.IO.File]::ReadAllText是整体读入原始内容换行符原样保留写回时也原样写回不会多改一个字节。还有一点Get-Content的-Encoding参数不直接支持GBK你只能绕路去转码远不如直接给静态方法指定Encoding来得干净。大文件是另一个要考虑的问题。ReadAllText是一次性把整个文件读进内存对几十MB的日志文件没问题但如果你的文件有GB级别内存可能会吃紧。这种场景我一般不推荐用PowerShell要么用流式读取要么在转换前把大文件单独拎出来处理。日常处理配置、日志、文本数据这个脚本足够了。脚本里的参数可以按需调整汇总如下参数含义示例-Folder要扫描的根目录D:\data-SourceEncoding源编码名GBK-Extensions处理的扩展名白名单txt,log,cfg-NoBom输出无BOM的UTF-8加这个开关即可4. 踩坑实录与排查速查表4.1 最常见问题转完变乱码如果你的文件转完之后乱码更严重了第一反应不要怀疑脚本先怀疑“源编码认错了”。我遇到过三种典型情况第一种文件本身是无BOM的UTF-8但被方案一按GBK读入转完之后原来正常的UTF-8中文变成双重乱码。方案二通过严格UTF-8解码测试能避开这个问题但如果你用方案一就必须先确认源文件确实是GBK。第二种文件明明是UTF-16也就是Windows老式“Unicode”编码你当成GBK去读。这种情况下文件头是FF FE脚本会读到大量空字节。方案二直接跳过UTF-16不会动它们方案一最好加一个前置判断再批量转。第三种源文件在生成时做了双重编码本来就已经乱了一轮你拿到手的是一个“受害者”再转一次等于在伤口上撒盐。这种情况只能找原始文件没有别的办法。实操中判断方法还是先看文件头的十六进制字节再取样测试一个小文件确认转换后中文正常再对全量文件跑脚本。4.2 权限、占用、执行策略等运行问题脚本运行失败的报错五花八门但高频的就那几个。报错信息是“无法加载因为在此系统上禁止运行脚本”那就是执行策略问题。用powershell -ExecutionPolicy Bypass -File绕过就行。如果连这句话都看不懂最简单的方式是把代码直接粘贴进PowerShell窗口里运行不走.ps1文件。报错“对路径的访问被拒绝”通常是文件被另一个程序占用比如Excel、记事本正开着这个文件。关闭占用程序再跑一次就好。报错“文件由其他进程占用”也一样。还有种隐藏情况杀毒软件扫描正在读那个文件文件被临时锁住。这种偶发失败脚本里已经有失败统计你可以等一会儿重新执行一次。另一个容易忽略的问题是文件只读。如果目标文件属性是只读WriteAllText会直接抛异常。建议脚本运行时用Get-ChildItem列出的文件对象先清一下只读属性或者在批量转换前统一去属性Get-ChildItem -Path $Folder -Recurse -File | Where-Object { $_.IsReadOnly } | ForEach-Object { $_.IsReadOnly $false }这一行可以放在脚本最前面。4.3 常见问题速查表问题可能原因解决办法转完中文变乱码源编码判断错误先看文件头字节用小文件抽样测试文件变成双重乱码原文件其实是UTF-8用方案二自动识别输出文件开头有空字符或显示?BOM没有正确处理确认是否选对带/不带BOM参数脚本禁止运行系统执行策略限制加-ExecutionPolicy Bypass文件访问被拒绝文件被占用或只读关闭程序清除只读属性页面顶部多出空白行带BOM的UTF-8被传到Web环境转换时加-NoBom转换后文件体积变大正常现象GBK中文2字节UTF-8中文3字节体积增大属正常日志文件太大内存吃紧一次性读入全部内容改用流式读取或拆分处理最后分享一个小经验这套脚本我前后用了两年每次处理完一批文件我都会让脚本额外输出一份“转换前后文件大小对比”。虽然UTF-8转换后文件变大是正常现象但如果某个文件的大小异常缩减那多半是编码识别有问题内容可能已被读坏。现在我的习惯是批量转换前一定先备份一份到_backup文件夹再小范围跑两个测试文件确认无误后对全量执行。少想一步“应该没事”就能少走几次恢复数据的弯路。如果你手里也有成堆的ANSI文本等着收拾建议先拷一小批文件出来跑一次脚本满意了再上真数据。命令行批量转码这件事熟练之后就是三分钟的事难的是第一次下决心把方案配稳。