# 批次將所有 Java 原始碼檔案快速轉成 UTF-8 編碼的方法

最近公司接到一個老舊 Java 專案的升級改版案，由於原始碼全部都採用 `Big5` 編碼，導致在 Visual Studio Code 裡面無法成功編譯，雖然看了 [Troubleshooting Guide for Encoding Issues](https://github.com/microsoft/vscode-java-debug/blob/main/Troubleshooting_encoding.md) 文件，也成功[研究出維持 Big5 編碼也能持續開發的解決方案](https://youtu.be/bREVsyro5Zw)，但其實最好的解決方法，還是把所有 Java 原始碼變更為 `UTF-8` 才是王道。今天我就來分享幾個不同的方法，幫助你快速將專案的所有原始碼從 `Big5` 轉換成 `UTF-8` 字集編碼！

由於透過命令列工具會牽涉到不同環境的問題，以下我分別透過幾種不同的環境來說明我所研究出來的轉換方法。請務必看到最後，不然你可能會懊悔為什麼不看完！😅

### 使用 PowerShell 來快速轉換任意文字檔

由於 PowerShell 有兩個大的版本改變，兩者之間差異有點大，新版加入了許多語法與語言特性，所以寫法不太一樣！🔥

-   PowerShell 6 之後的版本
    
    這段程式其實很簡單，就是先取得所有檔案清單，然後用指定的字集編碼讀入文字檔案，最後指定 UTF-8 寫入同一個檔案：
    
    > 請注意: 以下程式執行到 PowerShell 5.1 之前的版本將會導致檔案內容整個亂掉，你一定要注意執行的 PowerShell 版本！你可以用 `$PSVersionTable.PSVersion` 查到版本資訊。
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '*.java'
    
    $DefaultEncoding = $PSDefaultParameterValues['*:Encoding']
    $PSDefaultParameterValues['*:Encoding'] = 'big5'
    Get-ChildItem -Path $sourceFolder -Include $filePatterns -File -Recurse | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      Write-Host "Converting from big5 to utf8: $filename ..." -NoNewline
      $filebody = Get-Content $filename -Raw
      Set-Content -Value $filebody -Path $filename -Encoding utf8
      Write-Host "Done"
    }
    $PSDefaultParameterValues['*:Encoding'] = $DefaultEncoding
    ```
    
    如果你的檔案非常多，可以考慮用以下這段程式，篩選檔案的速度快了 6 倍之多：
    
    > 請注意 `$filePatterns` 的格式跟上一段不太一樣！
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '.java'
    
    $DefaultEncoding = $PSDefaultParameterValues['*:Encoding']
    $PSDefaultParameterValues['*:Encoding'] = 'big5'
    Get-ChildItem -Path $sourceFolder -File -Recurse | where { $_.Extension -in $filePatterns } | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      Write-Host "Converting from big5 to utf8: $filename ..." -NoNewline
      $filebody = Get-Content $filename -Raw
      Set-Content -Value $filebody -Path $filename -Encoding utf8
      Write-Host "Done"
    }
    $PSDefaultParameterValues['*:Encoding'] = $DefaultEncoding
    ```
    
    > 詳見: [powershell performance: Get-ChildItem -Include vs. Get-ChildItem | Where-Object](https://stackoverflow.com/a/52294304/910074)
    
-   PowerShell 5.1 之前的版本
    
    由於 PowerShell 5.1 之前的版本完全不支援 `UTF-8` 編碼，僅支援 `UTF-8 with BOM` 編碼，重點是 Java 編譯器並不支援任何以 `UTF-8 with BOM` 編碼的 `*.java` 檔案，所以你不能使用 [PowerShell 5.1 之前提供的 Set-Content](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-content?view=powershell-5.1&WT.mc_id=DT-MVP-4015686) 來儲存檔案。我們要藉助 .NET Framework 提供的 `System.IO.File` 類別，才能成功儲存以 `UTF-8` 編碼的文字檔案。
    
    > 請注意: 以下程式執行到 PowerShell 6+ 版本將會導致檔案內容整個亂掉，你一定要注意執行的 PowerShell 版本！你可以用 `$PSVersionTable.PSVersion` 查到版本資訊。
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '*.java'
    
    $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
    Get-ChildItem -Path $sourceFolder -Include $filePatterns -File -Recurse | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      Write-Host "Converting from big5 to utf8: $filename ..."
      $filebody = Get-Content $filename -Encoding default
      [System.IO.File]::WriteAllLines($filename, $filebody, $Utf8NoBomEncoding)
      Write-Host "Done"
    }
    ```
    
    如果你的檔案非常多，可以考慮用以下這段程式，篩選檔案的速度快了 6 倍之多：
    
    > 請注意 `$filePatterns` 的格式跟上一段不太一樣！
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '.java'
    
    $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
    Get-ChildItem -Path $sourceFolder -File -Recurse | where { $_.Extension -in $filePatterns } | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      Write-Host "Converting from big5 to utf8: $filename ..."
      $filebody = Get-Content $filename -Encoding default
      [System.IO.File]::WriteAllLines($filename, $filebody, $Utf8NoBomEncoding)
      Write-Host "Done"
    }
    ```
    

其實上述寫法有個嚴重的問題，哪就是我們「假設」所有的 `*.java` 檔案都是 `Big5` 編碼，如果不是，哪就慘了，檔案內容會整個亂掉！🔥

### 使用 Linux 常用工具來轉換任意文字檔

我最愛 Linux 的一個理由，就是它有無窮無盡各種優異的命令列工具，許多自動化的工作都可以透過這些工具完成，雖然有時候要寫出一些複雜的腳本並不容易，但是至少都「一定」寫的出來，問題也都能夠被解決。

這裡我將說明幾個我研究與撰寫的步驟，如果你只想要現成可用的腳本，直接看最後一個步驟即可：

1.  取得指定檔案的字集編碼
    
    其實這個步驟非常重要，因為我們並不知道資料夾下所有的 `*.java` 原始檔是否「真的」是 `Big5` 編碼，要是有一些混雜 `UTF-8` 的原始檔怎麼辦？
    
    以下我用了 `find`, `file`, `cut`, `printf` 等工具，可以非常輕易的將每個檔案的編碼「猜」出來，雖然不一定會猜對，不過我想正確率十之八九不會錯的！
    
    ```bash
    sourceFolder='/mnt/c/Projects/ProjectA/src'
    
    for f in $(find $sourceFolder -type f -name '*.java' -o -name '*.properties' -o -name '*.txt'); do
      filemeta=`file -i $f` # Output: /src/Main.java: text/plain; charset=iso-8859-1
      filename=`echo $filemeta | cut -d ' ' -f 1 | cut -d ':' -f 1`
      filetype=`echo $filemeta | cut -d ' ' -f 2 | cut -d ';' -f 1`
      encoding=`echo $filemeta | cut -d ' ' -f 3 | cut -d '=' -f 2`
      printf "%s\t%-10s\t%s\n" "$filetype" "$encoding" "$filename"
    done
    ```
    
2.  正式利用 `iconv` 轉換檔案字集編碼
    
    先用「人眼」判斷一下檔案編碼，我們可以知道其實 `iso-8859-1` 其實就是 `big5` 編碼 (因為字集字碼範圍重疊的關係)，所以我們加個判斷式就可以把檔案快速的轉過去了！
    
    ```bash
    sourceFolder='/mnt/c/Projects/ProjectA/src'
    
    for f in $(find $sourceFolder -type f -name '*.java' -o -name '*.properties' -o -name '*.txt'); do
      filemeta=`file -i $f` # Output: /src/Main.java: text/plain; charset=iso-8859-1
      filename=`echo $filemeta | cut -d ' ' -f 1 | cut -d ':' -f 1`
      filetype=`echo $filemeta | cut -d ' ' -f 2 | cut -d ';' -f 1`
      encoding=`echo $filemeta | cut -d ' ' -f 3 | cut -d '=' -f 2`
      if [ "$encoding" == "iso-8859-1" ]
      then
          printf "Converting from big5 to utf8: %s ..." "$filename"
          iconv -f big5 -t utf8 "$filename" | sponge "$filename"
          printf "Done\n"
      fi
    done
    ```
    
    > 這裡我使用了 [sponge](https://linux.die.net/man/1/sponge) 工具，是個相當實用的小工具，大多 Linux 作業系統應該沒有內建。若是 Ubuntu 系統請用 `sudo apt install moreutils -y` 安裝。若有 Node.js 也可以用 `npm i -g sponge` 安裝 [sponge](https://www.npmjs.com/package/sponge) 全域工具套件。
    
    其實判斷 [CJK multibyte encodings](https://en.wikipedia.org/wiki/Variable-width_encoding#CJK_multibyte_encodings) (中日韓多位元編碼) 的字碼範圍是非常困難的，因為他們都只用兩個 Bytes 代表一個「字」，因此不同的「字集」之間都有大量的重疊的字元，你想用程式去分析一份文件，想要藉此判斷出該文件的確切編碼，基本上誤判率非常高。
    
    所以，我們在 Windows 作業系統的「控制台」中下才會有個 `Languages for non-Unicode programs` 的設定，專門用來給**當程式無法正確判斷檔案字集時預設使用的字集**！
    
    但是在 Linux 底下沒有這種設定，所以你必須人工判斷。但以我這次遇到的專案來說，專案中的檔案不可能出現「混和字集」的情況發生，因此我們只要判斷是否為 `utf8` 或 `non-utf8` 的字元即可，因此我會把程式改寫成以下這樣，只要檔案編碼「不是」用 `utf8` 編碼的檔案，全部都從 `big5` 轉成 `utf8` 即可！
    
    ```bash
    sourceFolder='/mnt/c/Projects/ProjectA/src'
    
    for f in $(find $sourceFolder -type f -name '*.java' -o -name '*.properties' -o -name '*.txt'); do
      filemeta=`file -i $f` # Output: /src/Main.java: text/plain; charset=iso-8859-1
      filename=`echo $filemeta | cut -d ' ' -f 1 | cut -d ':' -f 1`
      filetype=`echo $filemeta | cut -d ' ' -f 2 | cut -d ';' -f 1`
      encoding=`echo $filemeta | cut -d ' ' -f 3 | cut -d '=' -f 2`
      if [ "$encoding" != "utf-8" ]
      then
          printf "Converting from %s to utf8: %s ..." "$encoding (big5)" "$filename"
          iconv -f big5 -t utf8 "$filename" | sponge "$filename"
          printf "Done\n"
      fi
    done
    ```
    

### 使用更正確的方法解決 Windows 轉換文字檔編碼的問題

其實你在 Windows 作業系統中根本找不到一個像樣的命令列工具可以幫我們判斷檔案編碼，但是在 Win32 API 之中是有的，在 .NET Framework 裡面也有，但使用上就是沒有比用腳本語言整合來的方便。所以我決定在 Windows 安裝一個 [file](https://linux.die.net/man/1/file) 命令列工具，像 Linux 一樣方便的判斷檔案編碼！

你絕對沒想到的是，原來這個工具就被內建在 [Git for Windows](https://www.git-scm.com/download/win) 安裝目錄中，預設路徑在 `C:\Program Files\Git\usr\bin` 目錄下，你可以選擇要不要將此路徑加入到 `PATH` 環境變數中。

```
C:\Program Files\Git\usr\bin\file.exe
```

有了這個 `file` 神隊友，我們的 PowerShell 就不會再跛腳了，以下是最終版本：

-   PowerShell 6 之後的版本
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '*.java','*.txt','*.properties'
    
    Get-ChildItem -Path $sourceFolder -Include $filePatterns -File -Recurse | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      $encoding = $(file -i $filename)
        | Select-String -Pattern "charset=(.*)$" -Encoding default
        | foreach { $_.Matches[0].Groups[1].Value }
    
      if ($encoding -ne 'utf-8') {
        $DefaultEncoding = $PSDefaultParameterValues['*:Encoding']
        $PSDefaultParameterValues['*:Encoding'] = 'big5'
        $filebody = Get-Content $filename -Raw
        Write-Host "Converting from $encoding (big5) to utf8: $filename ..." -NoNewline
        Set-Content -Value $filebody -Path $filename -Encoding utf8
        Write-Host "Done"
        $PSDefaultParameterValues['*:Encoding'] = $DefaultEncoding
      }
    }
    ```
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/9b6977dbd2bbd9561a38-184500806-15563358-3266-4b42-86d2-ed82dad7273f.png)
    
    > 這裡我用了 [Select-String](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-string) 搭配 Regular Expression 來取得字集名稱，比較方便些。
    
-   PowerShell 5.1 之前的版本
    
    ```powershell
    $sourceFolder = 'C:\Projects\ProjectA\src'
    $filePatterns = '*.java','*.txt','*.properties'
    
    $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
    Get-ChildItem -Path $sourceFolder -Include $filePatterns -File -Recurse | ForEach-Object {
      $filename = $_.VersionInfo.FileName
      $encoding = $(file -i $filename) | Select-String -Pattern "charset=(.*)$" -Encoding default | foreach { $_.Matches[0].Groups[1].Value }
    
      if ($encoding -ne 'utf-8') {
        $filebody = Get-Content $filename -Encoding default
        Write-Host "Converting from $encoding (big5) to utf8: $filename ..."
        [System.IO.File]::WriteAllLines($filename, $filebody, $Utf8NoBomEncoding)
        Write-Host "Done"
        $PSDefaultParameterValues['*:Encoding'] = $DefaultEncoding
      }
    }
    ```
    

### 總結

判斷文字檔的編碼，其實沒有想像中簡單，雖然上述研究可以解決 `Big5` 轉 `UTF-8` 的問題，但專案中的檔案如果有用到超過不只一種 [DBCS](https://en.wikipedia.org/wiki/DBCS) ([雙位元組字元集](https://zh.wikipedia.org/zh-tw/%E9%9B%99%E4%BD%8D%E5%85%83%E7%B5%84%E5%AD%97%E5%85%83%E9%9B%86)) 編碼，例如大部分檔案是 `Big5` 編碼，但少部分檔案是 [GBK](https://en.wikipedia.org/wiki/GBK_\(character_encoding\)) 或 [GB2312](https://en.wikipedia.org/wiki/GB_2312) 編碼等，上述腳本一樣會有轉換失敗的問題，不過程式微調一下就可以轉換成功。

我這次遇到的狀況，主要是針對 Java 原始碼的**檔案編碼**問題做處理，解決之後，許多詭異的問題都可以迎刃而解，跨平台開發 Java 專案的問題也都消失的無影無蹤！ 👍

> 額外說個秘密，在 Git for Windows 之中，也有內建強大的 `iconv` 字碼轉換工具，你可以用 `"C:\Program Files\Git\usr\bin\iconv.exe" -l` 查詢所有支援的字集，會用的人應該會很開心！😊

### 相關連結

-   [分享幾個在 Windows 與 Linux 常見的編碼問題與解決方案 | The Will Will Web](/post/2021/08/05/Character-Encoding-Problems-for-Windows-and-Linux)
-   [vscode-java-debug/Troubleshooting\_encoding.md at main · microsoft/vscode-java-debug](https://github.com/microsoft/vscode-java-debug/blob/main/Troubleshooting_encoding.md)
-   [Powershell: Change / Save encoding How to convert several txt files UTF-8 to UTF-8-BOM](https://learn.microsoft.com/en-us/answers/questions/405610/powershell-change-save-encoding-how-to-convert-sev.html?WT.mc_id=DT-MVP-4015686)
-   [encoding - Using PowerShell to write a file in UTF-8 without the BOM - Stack Overflow](https://stackoverflow.com/a/5596984/910074)
-   [PowerShell 5.1 - Get-Content](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content?view=powershell-5.1&WT.mc_id=DT-MVP-4015686)
-   [PowerShell 5.1 - Set-Content](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-content?view=powershell-5.1&WT.mc_id=DT-MVP-4015686)
-   [Variable-width encoding - Wikipedia](https://en.wikipedia.org/wiki/Variable-width_encoding#CJK_multibyte_encodings)
-   [file(1): determine file type - Linux man page](https://linux.die.net/man/1/file)
-   [Linux File Command: How to Determine File Type in Linux {+10 Examples}](https://phoenixnap.com/kb/linux-file-command)
-   判斷字元編碼的工具 (大多都判斷的不夠準確)
    -   [encoding-checker - npm](https://www.npmjs.com/package/encoding-checker)
    -   [Get File Encoding PowerShell Script](https://jonlabelle.com/snippets/view/powershell/get-file-encoding-powershell-script)
    -   [Convert-FileEncoding and Get-FileEncoding](https://gist.github.com/jpoehls/2406504)
