Використовуючи PowerShell для запису файлу в UTF-8 без BOM


246

Out-File Здається, що змушує BOM використовувати UTF-8:

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

Як я можу записати файл в UTF-8 без BOM за допомогою PowerShell?


23
BOM = байт-порядок. Три символи, розміщені на початку файлу (0xEF, 0xBB, 0xBF), схожі на "ï» ¿"
Signal15

40
Це неймовірно засмучує. Навіть сторонні модулі забруднюються, як, наприклад, намагаються завантажити файл через SSH? БОМ! "Так, давайте пошкодимо кожен окремий файл; це звучить як гарна ідея." -Microsoft.
MichaelGG

3
Кодування за замовчуванням - UTF8NoBOM, починаючи з версії Powershell 6.0 docs.microsoft.com/en-us/powershell/module/…
Павло Ширяєв

Розмова про порушення сумісності назад ...
Драгас

Відповіді:


220

Використання UTF8Encodingкласу .NET і перехід $Falseдо конструктора, здається, працює:

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

42
Фу, я сподіваюся, що це не єдиний спосіб.
Скотт Мук

114
Одного рядка [System.IO.File]::WriteAllLines($MyPath, $MyFile)достатньо. Це WriteAllLinesперевантаження пише саме UTF8 без BOM.
Роман Кузьмін

6
Створено тут запит на функцію MSDN: connect.microsoft.com/PowerShell/feedbackdetail/view/1137121/…
Groostav

3
Зауважте, що, WriteAllLinesздається, потрібно $MyPathабсолютне значення.
sschuberth

10
@xdhmoore WriteAllLinesотримує поточний каталог від [System.Environment]::CurrentDirectory. Якщо ви відкриєте PowerShell, а потім зміните поточний каталог (використовуючи cdабо Set-Location), він [System.Environment]::CurrentDirectoryне буде змінено, і файл в кінцевому підсумку опиниться в неправильному каталозі. Ви можете обійти це за допомогою [System.Environment]::CurrentDirectory = (Get-Location).Path.
Шаян Токрае

79

Правильний шлях як зараз полягає в використанні рішення, @Roman Кузьмін рекомендований в коментарі до @M. Відповідь Дадлі :

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

(Я також трохи скоротив її, знімаючи непотрібне Systemроз'яснення в просторі імен - воно буде замінено автоматично за замовчуванням.)


2
Це (з якихось причин) не видалило для мене BOM, де, як це було прийнято,
Ліам

@Liam, напевно, якась стара версія PowerShell або .NET?
ForNeVeR

1
Я вважаю, що старіші версії функції .NET WriteAllLines писали BOM за замовчуванням. Тож це може бути проблема з версією.
Бендер найбільший

2
Підтверджений записом з BOM в Powershell 3, але без BOM в Powershell 4. Мені довелося скористатися оригінальною відповіддю М. Дадлі.
chazbot7

2
Так він працює в Windows 10, де він встановлений за замовчуванням. :) Також запропонував покращення:[IO.File]::WriteAllLines(($filename | Resolve-Path), $content)
Джонні Сковдал

50

Я подумав, що це не буде UTF, але я просто знайшов досить просте рішення, яке, здається, працює ...

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

Для мене це призводить до отримання utf-8 без файлу bom, незалежно від формату джерела.


8
Це працювало для мене, за винятком того, що я використовував -encoding utf8свою вимогу.
Чім Чімз

1
Велике спасибі. Я працюю з дамп-журналами інструменту, який мав вкладки всередині нього. UTF-8 не працював. ASCII вирішив проблему. Дякую.
користувач1529294

44
Так, -Encoding ASCIIуникнути проблеми BOM, але ви, очевидно, отримуєте лише 7-бітові символи ASCII . Зважаючи на те, що ASCII є підмножиною UTF-8, отриманий файл технічно також є дійсним файлом UTF-8, але всі символи, що не є ASCII у вашому введенні, будуть перетворені в буквальні ?символи .
mklement0

4
@ChimChimz Я випадково підкреслив ваш коментар, але -encoding utf8все одно виводить UTF-8 з BOM. :(
TheDudeAbides

33

Примітка. Ця відповідь стосується Windows PowerShell ; на відміну від цього, у міжплатформенній версії PowerShell Core (v6 +) UTF-8 без BOM є кодуванням за замовчуванням для всіх командлетів.
Іншими словами: Якщо ви використовуєте PowerShell [Core] версії 6 або вище , ви отримуєте BOM-менш UTF-8 файлів за замовчуванням (які ви також можете явно запросити з -Encoding utf8/ -Encoding utf8NoBOM, в той час як ви отримуєте з -BOM кодування з -utf8BOM).


Щоб доповнити просту і прагматичну відповідь М. Дадлібільш стисле переформулювання ForNeVeR ):

Для зручності ось вдосконалена функція Out-FileUtf8NoBom, альтернатива на основі конвеєра, яка імітуєOut-File , а це означає:

  • Ви можете використовувати його так само, як Out-Fileу конвеєрі.
  • об'єкти введення, які не є рядками, форматуються так, як вони були б, якби ви надіслали їх на консоль, як і в Out-File.

Приклад:

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

Зауважте, як (Get-Content $MyPath)додається (...), що забезпечує відкриття всього файлу, читання повністю та закриття перед надсиланням результату по конвеєру. Це необхідно для того, щоб можна було записати назад у той самий файл (оновити його на місці ).
Однак, як правило, ця методика недоцільна з 2 причин: (а) весь файл повинен вміститися в пам'яті та (б) якщо команда буде перервана, дані втрачаються.

Примітка про використання пам'яті :

  • Власна відповідь М. Дадлі вимагає, щоб спочатку весь вміст файлу був накопичений у пам'яті, що може бути проблематично для великих файлів.
  • Наведена нижче функція покращується лише з цього приводу: всі вхідні об'єкти все ще спочатку буферуються, але їх рядкові зображення потім генеруються та записуються у вихідний файл по черзі.

Вихідний кодOut-FileUtf8NoBom (також доступний як ліцензований MIT ):

<#
.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

Починаючи з версії 6 повноваження підтримує UTF8NoBOMкодування як для встановленого контенту, так і для файлу, і навіть використовує це як кодування за замовчуванням.

Тож у наведеному вище прикладі це має бути просто так:

$MyFile | Out-File -Encoding UTF8NoBOM $MyPath

@ RaúlSalinas-Monteagudo, на якій версії ти працюєш?
Джон Бентлі

Приємно. Перевірка версії FYI$PSVersionTable.PSVersion
KCD

14

Використовуючи Set-Contentзамість Out-File, ви можете вказати кодування Byte, яке можна використовувати для запису байтового масиву у файл. Це в поєднанні зі спеціальним кодуванням UTF8, яке не випромінює BOM, дає бажаний результат:

# 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

Відмінність від використання [IO.File]::WriteAllLines()або подібного полягає в тому, що він повинен добре працювати з будь-яким типом елемента та контуру, а не лише з фактичними шляхами файлів.


5

Цей скрипт перетворить у UTF-8 без BOM всі файли .txt у DIRECTORY1 та виведе їх у DIRECTORY2

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

Цей виходить з ладу без попередження. Яку версію повноважень я повинен використовувати для його запуску?
darksoulsong

3
Рішення WriteAllLines відмінно підходить для невеликих файлів. Однак мені потрібно рішення для великих файлів. Кожен раз, коли я намагаюся використовувати це з більшим файлом, я отримую помилку 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" 
    }  

Джерело Як видалити позначку замовлення байтів UTF8 (BOM) з файлу за допомогою PowerShell


2

Якщо ви хочете використовувати [System.IO.File]::WriteAllLines(), вам слід надати другий параметр String[](якщо тип $MyFileє Object[]), а також вказати абсолютний шлях $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath), наприклад:

$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)

Якщо ви хочете використовувати [System.IO.File]::WriteAllText(), іноді вам слід | Out-String |вставити другий параметр, щоб додати CRLF в кінці кожного рядка експліцитно (Особливо, коли ви їх використовуєте 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)

Або ви можете використовувати [Text.Encoding]::UTF8.GetBytes()з 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"

див.: Як записати результат ConvertTo-Csv у файл у UTF-8 без BOM


Хороші вказівники; пропозиції /: простішою альтернативою $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($MyPath)є Convert-Path $MyPath; якщо ви хочете забезпечити затримку CRLF, просто використовуйте [System.IO.File]::WriteAllLines()навіть один вхідний рядок (не потрібно Out-String).
mklement0

0

Один із методів, який я використовую, - це перенаправлення виводу у файл ASCII за допомогою командлета Out-File .

Наприклад, я часто запускаю сценарії SQL, які створюють інший сценарій SQL для виконання в Oracle. При простому перенаправлення (">") вихід буде в UTF-16, який не розпізнається SQLPlus. Щоб обійти це:

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

Згенерований сценарій може бути виконаний через інший сеанс SQLPlus без будь-яких турбот Unicode:

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

4
Так, -Encoding ASCIIуникає проблеми BOM, але ти, очевидно, отримуєш підтримку лише для 7-бітових символів ASCII . Зважаючи на те, що ASCII є підмножиною UTF-8, отриманий файл технічно також є дійсним файлом UTF-8, але всі символи, що не є ASCII у вашому введенні, будуть перетворені в буквальні ?символи .
mklement0

Ця відповідь потребує більше голосів. Несумісність sqlplus з BOM є причиною багатьох головних болів .
Аміт Найду

0

Змініть декілька файлів шляхом розширення на UTF-8 без 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

З будь-якої причини, WriteAllLinesдзвінки все ще виробляли BOM для мене, з UTF8Encodingаргументом BOMless і без цього. Але мені працювало наступне:

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

Я повинен був зробити шлях до файлу абсолютним, щоб він працював. Інакше він записав файл на мій робочий стіл. Крім того, я вважаю, що це працює лише в тому випадку, якщо ви знаєте, що ваш BOM має 3 байти. Я поняття не маю, наскільки надійно очікувати заданого формату / довжини BOM на основі кодування.

Крім того, як написано, це, ймовірно, працює лише в тому випадку, якщо ваш файл вписується в масив повноцінної оболонки, який, здається, має обмеження довжини на якесь значення нижче, ніж [int32]::MaxValueна моїй машині.


1
WriteAllLinesбез кодування аргумент ніколи не пише BOM сам , але це можливо , що ваша рядок трапилася почати з BOM символом ( U+FEFF), який з написання ефективно створив BOM UTF-8; наприклад: $s = [char] 0xfeff + 'hi'; [io.file]::WriteAllText((Convert-Path t.txt), $s)(пропустіть, [char] 0xfeff + щоб побачити, що жодна BOM не написана).
mklement0

1
Що стосується несподіваного запису в інше місце: проблема полягає в тому, що рамка .NET зазвичай має інший поточний каталог, ніж PowerShell; ви можете або синхронізувати їх спочатку з [Environment]::CurrentDirectory = $PWD.ProviderPath, або, як більш загальну альтернативу вашому "$(pwd)\..."підходу (краще:, "$pwd\..."ще краще: "$($pwd.ProviderPath)\..."або (Join-Path $pwd.ProviderPath ...)), використовувати(Convert-Path BOMthetorpedoes.txt)
mklement0

Дякую, я не розумів, що може бути єдиний символ BOM для перетворення BOM у UTF-8.
xdhmoore

1
Усі послідовності байтів BOM (підписи Unicode) насправді є відповідним байтовим представленням кодування абстрактного єдиного символу UnicodeU+FEFF .
mklement0

Ну гаразд. Це, мабуть, спрощує справи.
xdhmoore

-2

Не вдалося скористатися нижче, щоб отримати UTF8 без BOM

$MyFile | Out-File -Encoding ASCII

4
Ні, він перетворить вихід у поточну кодову сторінку ANSI (наприклад, cp1251 або cp1252). Це зовсім не UTF-8!
ForNeVeR

1
Дякую Робін. Можливо, це не спрацювало для запису файлу UTF-8 без BOM, але опція -Encoding ASCII видалила BOM. Таким чином я міг генерувати bat-файл для gvim. Файл .bat спрацьовував на BOM.
Грег

3
@ForNeVeR: Ви правильні, що кодування ASCII- це не UTF-8, але це не поточна кодова сторінка ANSI - ви думаєте про це Default; ASCIIДійсно, це 7-бітове кодування ASCII, з кодовими точками> = 128, що перетворюються на буквальні ?екземпляри.
mklement0

1
@ForNeVeR: Ви, мабуть, думаєте про "ANSI" або " розширений ASCII". Спробуйте це переконатися, що -Encoding ASCIIдійсно є лише 7-бітний ASCII: 'äb' | out-file ($f = [IO.Path]::GetTempFilename()) -encoding ASCII; '?b' -eq $(Get-Content $f; Remove-Item $f)- äтранслітеровано в a ?. Навпаки, -Encoding Default("ANSI") правильно збереже його.
mklement0

3
@rob Це ідеальна відповідь для всіх, хто просто не потребує utf-8 або що-небудь інше, що відрізняється від ASCII і не зацікавлений у розумінні кодування та мети Unicode. Ви можете використовувати його як utf-8, оскільки еквівалентні символи utf-8 для всіх символів ASCII є ідентичними (означає перетворення ASCII-файлу в файл utf-8, що призводить до ідентичного файлу (якщо в ньому немає BOM)). Для всіх, хто має у своєму тексті символи, що не належать до ASCII, ця відповідь є просто хибною та оманливою.
ТНТ

-3

Цей для мене працює (використовуйте "За замовчуванням" замість "UTF8"):

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

Результат - ASCII без BOM.


1
Відповідно до документації Out-File із зазначенням Defaultкодування буде використано поточну кодову сторінку ANSI системи, яка не є UTF-8, як мені потрібно.
М. Дадлі

Здається, це працює для мене, принаймні, для Export-CSV. Якщо ви відкриєте отриманий файл у відповідному редакторі, кодування файлів - це UTF-8 без BOM, а не західна латинська ISO 9, як я б очікував з ASCII
eythort

Багато редакторів відкривають файл як UTF-8, якщо вони не можуть виявити кодування.
виправданий
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.