如何使用 Windows 命令行合并多个文件以删除 BOM?

How do I combine several files removing BOM using Windows command line?

我有几个非常大的 CSV(技术上是 TSV)文件需要附加在一起。我用过:

copy file1.txt + file2.txt + ... + fileN.txt combined.txt

但后来发现每个文件的开头都有一个 BOM (),然后在文件中间多次出现。

但是,这些文件非常大(每个文件有 30-4000 万行)所以我无法在 NotePad++ 中打开它们并重新保存它们以删除 BOM,因此需要一个命令行解决方案(cmd 或PowerShell),最好是不需要下载额外库的东西。

回顾一下:

(在我的例子中 N=4,所以我可以处理从单个文件中删除 BOM 的解决方案,因此 运行 在合并之前首先对每个文件执行此操作)

编辑:这可能是一个可能的解决方案: 但我对编码和 PowerShell/batch 的了解太少了,我什至不知道它是否适用!我不介意合并后的文件是 ANSI 还是 UTF-8(我将把它加载到一个可以处理其中任何一种的程序中),只要它内部一致且正确即可。

最后我放弃了,在Python中做了(坚持你知道的,嗯?):

import shutil

with open("combined.txt", "w", encoding="utf-8") as wfd:
    for f in my_file_iterator():
        with open(f, "r", encoding="utf-8-sig") as fd:
            shutil.copyfileobj(fd, wfd)

my_file_iterator() 替换为您选择的循环文件的方法或表达式,例如基于 pathlib.Path.glob()

的东西

在这些答案的帮助下:

  • shutil.copyfileobj()魔法)
  • (使用 utf-8utf-8-sig 编码的技巧)
  • 可能是最简单且 best-performing 的解决方案。

  • 如果你刚好安装了WSL,你可以试试下面的方法把所有的file*.txt合并成combined.text ] 同时从每个中剥离 UTF-8 BOM(语法用于从 PowerShell 调用 ):

    bash.exe -c 'for f in file*.txt; do tail -c +4 \"$f\"; done > combined.txt'
    
    • tail -c +4 从每个文件中剥离前 3 个字节并传递剩余的字节。请注意,可以通过将重定向 > 应用于整个 for 语句来捕获 for 循环的整个输出。

    • 注意:此处不需要使用 \ 转义 "$,但在撰写本文时是这样的:

      • -c 调用 bash.exe 意外地使命令字符串成为 up-front 字符串插值 ,需要转义;请注意,通过 wsl.exe -e 进行的以下看似等效的调用确实 而不是 表现出此问题(这就是为什么 $f 未转义为 $f 的原因) :

         wsl.exe -e bash -c 'for f in file*.txt; do tail -c +4 \"$f\"; done > combined.txt'
        
      • 独立地,至少在 PowerShell 7.2.2 之前,PowerShell 传递给外部程序的参数在空参数和 embedded 参数方面从根本上被打破了" 个字符,需要 手动 \-转义 - 参见 .


至于原生PowerShell解决方案

  • 您的文件的大小可能需要 一个 memory-efficient 流解决方案.

  • 但是,考虑到 PowerShell 管道基于 object 的性质,没有原始字节支持,这可能会非常慢,尤其是 byte-by-byte 处理,其中每个字节都必须与内存中的 .NET [byte] 对象相互转换。

    • 有关背景信息,请参阅

作为记录,这里是 PowerShell 解决方案,但对于大文件来说它可能太慢了:

# !! Unfortunately, the syntax for requesting byte-by-byte processing
# !! has changed between Windows PowerShell and PowerShell (Core) 7+,
$byteStreamParam = 
  if ($IsCoreClr) { @{ AsByteStream = $true } } 
  else            { @{ Encoding = 'Byte' } }

Get-ChildItem -Filter file*.txt |
  ForEach-Object {
    $_ | Get-Content @byteStreamParam | Select-Object -Skip 3
  } |
    Set-Content @byteStreamParam -LiteralPath combined.txt 

但是,您可以通过使用 Get-Content-ReadCount 参数 读取 块中的文件来显着提高性能(字节数组)。块大小越大 - 内存允许 - 运行时性能将提高更多:

$byteStreamParam = 
  if ($IsCoreClr) { @{ AsByteStream = $true } } 
  else            { @{ Encoding = 'Byte' } }

# How many bytes to read at a time.
$chunkSize = 256mb

Get-ChildItem -Filter file*.txt |
  ForEach-Object {
    $first = $true
    $_ | Get-Content @byteStreamParam -ReadCount $chunkSize | ForEach-Object {
      if ($first) { $_[3..($_.Count-1)]; $first = $false } 
      else        { $_ }
    }
  } |
    Set-Content @byteStreamParam -LiteralPath combined.txt 

基于文本的PowerShell解决方案:

Text-based 解决方案虽然速度较慢,但​​具有 启用 转码 的优点,即将文件从一种字符编码转换为另一个,使用 Get-ContentSet-Content-Encoding 参数。

在最简单的情况下 - 如果单个文件适合整个内存(这可能不适合你),你可以使用 Get-Content-Raw 开关 [1] 将文件内容作为 single, multi-line string, 即 fast.

虽然 line-by-line 流解决方案(省略 -Raw 是可行的,但它带有 警告

  • 它会很慢,因为PowerShell用元数据修饰读取的每一行,元数据既是time-consuming又是memory-intensive。

  • 关于输入文件的换行格式的信息(Windows-format CRLF vs. Unix-format LF)总是 lost,包括是否存在 trailing 换行符。

请注意,Get-Content 不需要下面的 -Encoding 参数,因为两个 PowerShell 版本都直接识别带有 BOM 的 UTF-8 文件。

Windows PowerShell 中,不幸的是,file-writing cmdlet 支持编写 UTF-8 文件 无 BOM - -Encoding utf8 总是创建文件 带有 BOM,因此来自 .NET API 的帮助是需要:

# Determine the output file, as a *full path*, because
# .NET's working dir. usually differs from PowerShell's.
$outFile = Join-Path ($PWD | Convert-Path) combined.text

# Create the output file, initially empty.
$null = New-Item -Path $outFile

Get-ChildItem -Filter file*.txt | 
  ForEach-Object {
    # BOM-less UTF-8 is the default.
    [IO.File]::AppendAllText($outFile, ($_ | Get-Content -Raw))
  }

PowerShell (Core) 7+中,现在不仅-Encoding utf8生成BOM-less UTF-8文件(你可以请求 with-BOM files with -Encoding utf8bom), BOM-less UTF-8 现在是 consistent默认,所以解决方案简化为:

Get-ChildItem -Filter file*.txt | 
  Get-Content -Raw |
   Set-Content -LiteralPath combined.txt # BOM-less UTF-8 implied.

[1] 尽管它的名称如此,但此开关 而不是 导致读取原始 字节 。相反,它绕过默认的 逐行 读取,转而将整个文件内容一次读取到一个字符串中。