Sử dụng PowerShell để viết tệp trong UTF-8 mà không cần BOM


246

Out-File dường như buộc BOM khi sử dụng UTF-8:

$MyFile = Get-Content $MyPath
$MyFile | Out-File -Encoding "UTF8" $MyPath

Làm cách nào tôi có thể viết một tệp trong UTF-8 mà không có BOM bằng PowerShell?


23
BOM = Dấu hiệu đơn hàng. Ba ký tự được đặt ở đầu tệp (0xEF, 0xBB, 0xBF) trông giống như "ï» ¿"
Tín hiệu15

40
Điều này là vô cùng bực bội. Ngay cả các mô-đun bên thứ ba cũng bị ô nhiễm, như cố gắng tải lên một tệp qua SSH? BOM! "Vâng, chúng ta hãy làm hỏng từng tệp một, nghe có vẻ là một ý kiến ​​hay." -Microsoft.
MichaelGG

3
Mã hóa mặc định là UTF8NoBOM bắt đầu với phiên bản Powershell 6.0 docs.microsoft.com/en-us/powershell/module/ trộm
Paul Shiryaev

Nói về việc phá vỡ tính tương thích ngược ...
Dragas

Câu trả lời:


220

Sử dụng UTF8Encodinglớp của .NET và chuyển $Falseđến hàm tạo dường như hoạt động:

$MyRawString = Get-Content -Raw $MyPath
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($MyPath, $MyRawString, $Utf8NoBomEncoding)

42
Ugh, tôi hy vọng đó không phải là cách duy nhất.
Scott Muc

114
Một dòng [System.IO.File]::WriteAllLines($MyPath, $MyFile)là đủ. Quá WriteAllLinestải này ghi chính xác UTF8 mà không có BOM.
Roman Kuzmin

6
Tạo một yêu cầu tính năng MSDN tại đây: connect.microsoft.com/PowerShell/feedbackdetail/view/1137121/
mẹo

3
Lưu ý rằng WriteAllLinesdường như đòi hỏi $MyPathphải tuyệt đối.
sschuberth

9
@xdhmoore WriteAllLineslấy thư mục hiện tại từ [System.Environment]::CurrentDirectory. Nếu bạn mở PowerShell và sau đó thay đổi thư mục hiện tại của bạn (bằng cách sử dụng cdhoặc Set-Location), thì [System.Environment]::CurrentDirectorysẽ không bị thay đổi và tệp sẽ nằm trong thư mục sai. Bạn có thể làm việc xung quanh điều này bằng cách [System.Environment]::CurrentDirectory = (Get-Location).Path.
Shaya Toqraee

79

Cách thích hợp như bây giờ là sử dụng giải pháp được đề xuất bởi @Roman Kuzmin trong các nhận xét cho @M. Dudley trả lời :

[IO.File]::WriteAllLines($filename, $content)

(Tôi cũng đã rút ngắn nó một chút bằng cách tước bỏ việc Systemlàm rõ không gian tên không cần thiết - nó sẽ được thay thế tự động theo mặc định.)


2
Điều này (vì bất kỳ lý do gì) đã không xóa BOM cho tôi, nơi mà câu trả lời được chấp nhận đã làm
Liam

@Liam, có lẽ là một số phiên bản cũ của PowerShell hoặc .NET?
ForNeVeR

1
Tôi tin rằng các phiên bản cũ hơn của hàm .NET WriteAllLines đã viết BOM theo mặc định. Vì vậy, nó có thể là một vấn đề phiên bản.
Bender lớn nhất

2
Xác nhận bằng cách viết với BOM trong Powershell 3, nhưng không có BOM trong Powershell 4. Tôi đã phải sử dụng câu trả lời ban đầu của M. Dudley.
chazbot7

2
Vì vậy, nó hoạt động trên Windows 10, nơi nó được cài đặt theo mặc định. :) Ngoài ra, đề xuất cải tiến:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content)
Johny Skovdal

50

Tôi đoán đây không phải là UTF, nhưng tôi chỉ tìm thấy một giải pháp khá đơn giản có vẻ hiệu quả ...

Get-Content path/to/file.ext | out-file -encoding ASCII targetFile.ext

Đối với tôi điều này dẫn đến một utf-8 không có tệp bom bất kể định dạng nguồn.


8
Điều này làm việc cho tôi, ngoại trừ tôi sử dụng -encoding utf8cho yêu cầu của tôi.
Chim Chimz

1
Cảm ơn rât nhiều. Tôi đang làm việc với các bản ghi kết xuất của một công cụ - có các tab bên trong nó. UTF-8 không hoạt động. ASCII đã giải quyết vấn đề. Cảm ơn.
dùng1529294

44
Có, -Encoding ASCIItránh vấn đề BOM, nhưng rõ ràng bạn chỉ nhận được các ký tự ASCII 7 bit . Cho rằng ASCII là tập con của UTF-8, tệp kết quả về mặt kỹ thuật cũng là tệp UTF-8 hợp lệ, nhưng tất cả các ký tự không phải ASCII trong đầu vào của bạn sẽ được chuyển đổi thành ?ký tự bằng chữ .
mkuity0

4
@ChimChimz Tôi vô tình bình chọn bình luận của bạn, nhưng -encoding utf8vẫn đưa ra UTF-8 với BOM. :(
TheDudeAdides

33

Lưu ý: Câu trả lời này áp dụng cho Windows PowerShell ; ngược lại, trong phiên bản PowerShell Core đa nền tảng (v6 +), UTF-8 không có BOMmã hóa mặc định , trên tất cả các lệnh ghép ngắn.
Nói cách khác: Nếu bạn đang sử dụng PowerShell [Core] phiên bản 6 trở lên , bạn sẽ nhận được các tệp UTF-8 không BOM theo mặc định (bạn cũng có thể yêu cầu rõ ràng với -Encoding utf8/ -Encoding utf8NoBOM, trong khi bạn nhận được bằng mã hóa -BOM với -utf8BOM).


Để bổ sung cho câu trả lời đơn giản và thực dụng của M. Dudley (và cải cách ngắn gọn hơn của ForNeVeR ):

Để thuận tiện, đây là chức năng nâng cao Out-FileUtf8NoBom, một giải pháp thay thế dựa trên đường ống bắt chướcOut-File , có nghĩa là:

  • bạn có thể sử dụng nó giống như Out-Filetrong một đường ống dẫn.
  • các đối tượng đầu vào không phải là chuỗi được định dạng như chúng sẽ được gửi nếu bạn gửi chúng đến bàn điều khiển, giống như với Out-File.

Thí dụ:

(Get-Content $MyPath) | Out-FileUtf8NoBom $MyPath

Lưu ý cách (Get-Content $MyPath)được bao trong (...), đảm bảo rằng toàn bộ tệp được mở, đọc đầy đủ và đóng trước khi gửi kết quả qua đường ống. Điều này là cần thiết để có thể ghi lại vào cùng một tệp (cập nhật nó tại chỗ ).
Tuy nhiên, nói chung, kỹ thuật này không được khuyến khích vì 2 lý do: (a) toàn bộ tệp phải vừa với bộ nhớ và (b) nếu lệnh bị gián đoạn, dữ liệu sẽ bị mất.

Một lưu ý về việc sử dụng bộ nhớ :

  • Câu trả lời của chính M. Dudley yêu cầu toàn bộ nội dung tệp phải được xây dựng trong bộ nhớ trước, điều này có thể gây ra vấn đề với các tệp lớn.
  • Hàm bên dưới chỉ cải thiện điều này một chút: tất cả các đối tượng đầu vào vẫn được đệm trước, nhưng các biểu diễn chuỗi của chúng sau đó được tạo và ghi vào tệp đầu ra từng cái một.

Mã nguồn củaOut-FileUtf8NoBom (cũng có sẵn dưới dạng Gist được MIT cấp phép ):

<#
.SYNOPSIS
  Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).

.DESCRIPTION
  Mimics the most important aspects of Out-File:
  * Input objects are sent to Out-String first.
  * -Append allows you to append to an existing file, -NoClobber prevents
    overwriting of an existing file.
  * -Width allows you to specify the line width for the text representations
     of input objects that aren't strings.
  However, it is not a complete implementation of all Out-String parameters:
  * Only a literal output path is supported, and only as a parameter.
  * -Force is not supported.

  Caveat: *All* pipeline input is buffered before writing output starts,
          but the string representations are generated and written to the target
          file one by one.

.NOTES
  The raison d'être for this advanced function is that, as of PowerShell v5,
  Out-File still lacks the ability to write UTF-8 files without a BOM:
  using -Encoding UTF8 invariably prepends a BOM.

#>
function Out-FileUtf8NoBom {

  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)] [string] $LiteralPath,
    [switch] $Append,
    [switch] $NoClobber,
    [AllowNull()] [int] $Width,
    [Parameter(ValueFromPipeline)] $InputObject
  )

  #requires -version 3

  # Make sure that the .NET framework sees the same working dir. as PS
  # and resolve the input path to a full path.
  [System.IO.Directory]::SetCurrentDirectory($PWD.ProviderPath) # Caveat: Older .NET Core versions don't support [Environment]::CurrentDirectory
  $LiteralPath = [IO.Path]::GetFullPath($LiteralPath)

  # If -NoClobber was specified, throw an exception if the target file already
  # exists.
  if ($NoClobber -and (Test-Path $LiteralPath)) {
    Throw [IO.IOException] "The file '$LiteralPath' already exists."
  }

  # Create a StreamWriter object.
  # Note that we take advantage of the fact that the StreamWriter class by default:
  # - uses UTF-8 encoding
  # - without a BOM.
  $sw = New-Object IO.StreamWriter $LiteralPath, $Append

  $htOutStringArgs = @{}
  if ($Width) {
    $htOutStringArgs += @{ Width = $Width }
  }

  # Note: By not using begin / process / end blocks, we're effectively running
  #       in the end block, which means that all pipeline input has already
  #       been collected in automatic variable $Input.
  #       We must use this approach, because using | Out-String individually
  #       in each iteration of a process block would format each input object
  #       with an indvidual header.
  try {
    $Input | Out-String -Stream @htOutStringArgs | % { $sw.WriteLine($_) }
  } finally {
    $sw.Dispose()
  }

}

16

Bắt đầu từ phiên bản 6 hỗ trợ PowerShell sự UTF8NoBOMmã hóa cho cả set-nội dungout-tập tin và thậm chí sử dụng điều này như mã hóa mặc định.

Vì vậy, trong ví dụ trên, nó chỉ đơn giản là như thế này:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath

@ RaúlSalinas-Monteagudo bạn đang dùng phiên bản nào?
John Bentley

Đẹp. Phiên bản kiểm tra FYI với$PSVersionTable.PSVersion
KCD

14

Khi sử dụng Set-Contentthay vì Out-File, bạn có thể chỉ định mã hóa Byte, có thể được sử dụng để ghi một mảng byte vào một tệp. Điều này kết hợp với mã hóa UTF8 tùy chỉnh không phát ra BOM cho kết quả mong muốn:

# This variable can be reused
$utf8 = New-Object System.Text.UTF8Encoding $false

$MyFile = Get-Content $MyPath -Raw
Set-Content -Value $utf8.GetBytes($MyFile) -Encoding Byte -Path $MyPath

Sự khác biệt để sử dụng [IO.File]::WriteAllLines()hoặc tương tự là nó sẽ hoạt động tốt với bất kỳ loại mục và đường dẫn nào, không chỉ các đường dẫn tệp thực tế.


5

Tập lệnh này sẽ chuyển đổi, thành UTF-8 mà không có BOM, tất cả các tệp .txt trong DIRECTORY1 và xuất chúng thành DIRECTORY2

foreach ($i in ls -name DIRECTORY1\*.txt)
{
    $file_content = Get-Content "DIRECTORY1\$i";
    [System.IO.File]::WriteAllLines("DIRECTORY2\$i", $file_content);
}

Điều này thất bại mà không có bất kỳ cảnh báo. Tôi nên sử dụng phiên bản nào của powershell để chạy nó?
darksoulsong

3
Giải pháp WriteAllLines hoạt động tuyệt vời cho các tệp nhỏ. Tuy nhiên, tôi cần một giải pháp cho các tệp lớn hơn. Mỗi lần tôi cố gắng sử dụng điều này với một tệp lớn hơn, tôi sẽ gặp lỗi OutOfMemory.
BermudaLamb

2
    [System.IO.FileInfo] $file = Get-Item -Path $FilePath 
    $sequenceBOM = New-Object System.Byte[] 3 
    $reader = $file.OpenRead() 
    $bytesRead = $reader.Read($sequenceBOM, 0, 3) 
    $reader.Dispose() 
    #A UTF-8+BOM string will start with the three following bytes. Hex: 0xEF0xBB0xBF, Decimal: 239 187 191 
    if ($bytesRead -eq 3 -and $sequenceBOM[0] -eq 239 -and $sequenceBOM[1] -eq 187 -and $sequenceBOM[2] -eq 191) 
    { 
        $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False) 
        [System.IO.File]::WriteAllLines($FilePath, (Get-Content $FilePath), $utf8NoBomEncoding) 
        Write-Host "Remove UTF-8 BOM successfully" 
    } 
    Else 
    { 
        Write-Warning "Not UTF-8 BOM file" 
    }  

Nguồn Cách xóa Dấu hiệu đặt hàng Byte UTF8 (BOM) khỏi tệp bằng PowerShell


2

Nếu bạn muốn sử dụng [System.IO.File]::WriteAllLines(), bạn nên truyền tham số thứ hai thành String[](nếu loại $MyFileObject[]) và cũng chỉ định đường dẫn tuyệt đối với $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath), như:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Set-Variable MyFile
[System.IO.File]::WriteAllLines($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath), [String[]]$MyFile, $Utf8NoBomEncoding)

Nếu bạn muốn sử dụng [System.IO.File]::WriteAllText(), đôi khi bạn nên đưa tham số thứ hai vào | Out-String |để thêm CRLF vào cuối mỗi dòng một cách rõ ràng (Đặc biệt là khi bạn sử dụng chúng với ConvertTo-Csv):

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Out-String | Set-Variable tmp
[System.IO.File]::WriteAllText("/absolute/path/to/foobar.csv", $tmp, $Utf8NoBomEncoding)

Hoặc bạn có thể sử dụng [Text.Encoding]::UTF8.GetBytes()với Set-Content -Encoding Byte:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
Get-ChildItem | ConvertTo-Csv | Out-String | % { [Text.Encoding]::UTF8.GetBytes($_) } | Set-Content -Encoding Byte -Path "/absolute/path/to/foobar.csv"

xem: Cách ghi kết quả của ConvertTo-Csv vào một tệp trong UTF-8 mà không cần BOM


Con trỏ tốt; đề xuất /: thay thế đơn giản hơn $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath)Convert-Path $MyPath; nếu bạn muốn đảm bảo một CRLF dấu, chỉ cần sử dụng [System.IO.File]::WriteAllLines()ngay cả với một đơn chuỗi đầu vào (không có nhu cầu Out-String).
mkuity0

0

Một kỹ thuật tôi sử dụng là chuyển hướng đầu ra sang tệp ASCII bằng lệnh ghép ngắn Out-File .

Ví dụ, tôi thường chạy các tập lệnh SQL tạo tập lệnh SQL khác để thực thi trong Oracle. Với chuyển hướng đơn giản (">"), đầu ra sẽ ở dạng UTF-16 không được SQLPlus nhận ra. Để giải quyết vấn đề này:

sqlplus -s / as sysdba "@create_sql_script.sql" |
Out-File -FilePath new_script.sql -Encoding ASCII -Force

Tập lệnh được tạo sau đó có thể được thực thi thông qua một phiên SQLPlus khác mà không phải lo lắng về Unicode:

sqlplus / as sysdba "@new_script.sql" |
tee new_script.log

4
Có, -Encoding ASCIItránh sự cố BOM, nhưng rõ ràng bạn chỉ nhận được hỗ trợ cho các ký tự ASCII 7 bit . Cho rằng ASCII là tập con của UTF-8, tệp kết quả về mặt kỹ thuật cũng là tệp UTF-8 hợp lệ, nhưng tất cả các ký tự không phải ASCII trong đầu vào của bạn sẽ được chuyển đổi thành ?ký tự bằng chữ .
mkuity0

Câu trả lời này cần nhiều phiếu hơn. Sự không tương thích của sqlplus với BOM là một nguyên nhân gây ra nhiều vấn đề đau đầu .
Amit N Nikol

0

Thay đổi nhiều tệp bằng cách mở rộng thành UTF-8 mà không cần BOM:

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)
foreach($i in ls -recurse -filter "*.java") {
    $MyFile = Get-Content $i.fullname 
    [System.IO.File]::WriteAllLines($i.fullname, $MyFile, $Utf8NoBomEncoding)
}

0

Vì bất kỳ lý do gì, các WriteAllLinescuộc gọi vẫn tạo ra BOM cho tôi, với UTF8Encodingđối số BOMless và không có nó. Nhưng những điều sau đây làm việc cho tôi:

$bytes = gc -Encoding byte BOMthetorpedoes.txt
[IO.File]::WriteAllBytes("$(pwd)\BOMthetorpedoes.txt", $bytes[3..($bytes.length-1)])

Tôi đã phải làm cho đường dẫn tập tin tuyệt đối để nó hoạt động. Nếu không, nó đã ghi các tập tin vào máy tính để bàn của tôi. Ngoài ra, tôi cho rằng điều này chỉ hoạt động nếu bạn biết BOM của bạn là 3 byte. Tôi không biết nó đáng tin cậy đến mức nào khi mong đợi một định dạng / độ dài BOM nhất định dựa trên mã hóa.

Ngoài ra, như đã viết, điều này có lẽ chỉ hoạt động nếu tệp của bạn phù hợp với một mảng powershell, dường như có giới hạn độ dài của một số giá trị thấp hơn [int32]::MaxValuetrên máy của tôi.


1
WriteAllLinesmà không cần mã hóa một cuộc tranh cãi không bao giờ viết một BOM bản thân , nhưng nó có thể tưởng tượng rằng bạn chuỗi xảy ra để bắt đầu với BOM nhân vật ( U+FEFF), mà trên bằng văn bản có hiệu quả tạo ra một BOM UTF-8; ví dụ: $s = [char] 0xfeff + 'hi'; [io.file]::WriteAllText((Convert-Path t.txt), $s)(bỏ qua [char] 0xfeff + để thấy rằng không có BOM nào được viết).
mkuity0

1
Đối với việc bất ngờ ghi vào một vị trí khác: vấn đề là .NET framework thường có một thư mục hiện tại khác với PowerShell; trước tiên [Environment]::CurrentDirectory = $PWD.ProviderPath, bạn có thể đồng bộ hóa chúng với , hoặc, như một cách thay thế chung hơn cho "$(pwd)\..."phương pháp của bạn (tốt hơn : "$pwd\...", thậm chí tốt hơn: "$($pwd.ProviderPath)\..."hoặc (Join-Path $pwd.ProviderPath ...)), sử dụng(Convert-Path BOMthetorpedoes.txt)
mkuity0

Cảm ơn, tôi đã không nhận ra rằng có thể có một ký tự BOM duy nhất để chuyển đổi BOM UTF-8 như thế.
xdhmoore

1
Tất cả các chuỗi byte BOM (chữ ký Unicode) trên thực tế là biểu diễn byte mã hóa tương ứng của ký tự Unicode đơnU+FEFF trừu tượng .
mkuity0

À được rồi Điều đó dường như làm cho mọi thứ đơn giản hơn.
xdhmoore

-2

Có thể sử dụng bên dưới để nhận UTF8 mà không cần BOM

$MyFile | Out-File -Encoding ASCII

4
Không, nó sẽ chuyển đổi đầu ra thành codepage ANSI hiện tại (ví dụ cp1251 hoặc cp1252). Nó hoàn toàn không phải là UTF-8!
ForNeVeR

1
Cảm ơn Robin. Điều này có thể không hoạt động để viết tệp UTF-8 mà không có BOM nhưng tùy chọn -Encoding ASCII đã xóa BOM. Bằng cách đó tôi có thể tạo một tập tin bat cho gvim. Tệp .bat đã bị vấp trên BOM.
Greg

3
@ForNeVeR: Bạn đã đúng rằng mã hóa ASCIIkhông phải là UTF-8, nhưng đó không phải là bảng mã ANSI hiện tại - bạn đang nghĩ đến Default; ASCIIthực sự là mã hóa ASCII 7 bit, với các điểm mã> = 128 được chuyển đổi thành các thể hiện bằng chữ ?.
mkuity0

1
@ForNeVeR: Có lẽ bạn đang nghĩ đến "ANSI" hoặc " ASCII mở rộng ". Hãy thử điều này để xác minh rằng đó -Encoding ASCIIthực sự chỉ là ASCII 7 bit: 'äb' | out-file ($f = [IO.Path]::GetTempFilename()) -encoding ASCII; '?b' -eq $(Get-Content $f; Remove-Item $f)- äđã được phiên âm thành a ?. Ngược lại, -Encoding Default("ANSI") sẽ bảo vệ chính xác nó.
mkuity0

3
@rob Đây là câu trả lời hoàn hảo cho tất cả những người không cần utf-8 hoặc bất cứ điều gì khác với ASCII và không quan tâm đến việc hiểu mã hóa và mục đích của unicode. Bạn có thể sử dụng nó dưới dạng utf-8 vì các ký tự utf-8 tương đương với tất cả các ký tự ASCII giống hệt nhau (có nghĩa là chuyển đổi tệp ASCII thành tệp utf-8 trong một tệp giống hệt nhau (nếu không có BOM)). Đối với tất cả những người có các ký tự không phải ASCII trong văn bản của họ, câu trả lời này chỉ là sai và gây hiểu nhầm.
TNT

-3

Cái này hoạt động với tôi (sử dụng "Mặc định" thay vì "UTF8"):

$MyFile = Get-Content $MyPath
$MyFile | Out-File -Encoding "Default" $MyPath

Kết quả là ASCII không có BOM.


1
Theo tài liệu Out-File chỉ định Defaultmã hóa sẽ sử dụng trang mã ANSI hiện tại của hệ thống, không phải là UTF-8, như tôi yêu cầu.
M. Dudley

Điều này dường như hoạt động với tôi, ít nhất là đối với Xuất-CSV. Nếu bạn mở tệp kết quả trong một trình chỉnh sửa phù hợp, mã hóa tệp là UTF-8 không có BOM và không phải là Western Latin ISO 9 như tôi mong đợi với ASCII
Eythort

Nhiều biên tập viên mở tệp dưới dạng UTF-8 nếu họ không thể phát hiện mã hóa.
trống
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.