用 PowerShell 自動化 Excel・CSV 業務處理 ── 彙總・比對・報表輸出的實務食譜

· · PowerShell, Windows, CSV, Excel, 自動化, 業務效率化, 腳本, 既有資產活用

「每個月,把從核心系統匯出的 CSV 用 Excel 打開,用樞紐分析表彙總,整理格式後寄出」「把兩套系統匯出的名冊互相比對,用肉眼找出差異」── 我經常收到這類例行工作的諮詢。單次作業就算只花 30 分鐘,只要每週、每月、加上多人重複執行,一年下來就會耗掉相當可觀的時間,而且手動複製貼上必定會混入失誤。

PowerShell 是與這個領域相性很好的工具。它是 Windows 內建的(Windows PowerShell 5.1),能把 CSV 讀入為「物件的表格」,並用管線把彙總、比對、輸出串連起來。再加上社群製作的 ImportExcel 模組,即使沒有 Excel 本體,也能做出 xlsx 報表。

另一方面,這個領域有著日文環境特有的地雷。因為字元編碼的預設值在 Windows PowerShell 5.1 和 PowerShell 7 之間完全不同,「在自己的電腦上能動,換了環境就亂碼」的狀況頻繁發生。本文將依序整理中小企業資訊部門・業務承辦人用 PowerShell 自動化 CSV・Excel 業務時的實務食譜,從字元編碼的陷阱開始談起。

前提知識:本文的對象是中小企業的資訊部門・業務承辦人,但這不是 PowerShell 的入門文章。變數與管線、foreachif 的寫法,以及建立腳本檔(.ps1)並執行的步驟,本文皆視為已知。若這部分還不熟悉,請先閱讀「PowerShell 指令基礎 ── 該先學會的操作與安全使用方式」。文中也會出現雜湊表(@{})、try/finally、COM 參照釋放等中級以上的寫法,但每次出現時都會附上「為什麼要這樣寫」的說明。適用的執行環境同時假設 Windows PowerShell 5.1 與 PowerShell 7,行為有差異的地方會逐一區分說明。

1. 先講結論

  • 讀寫 CSV 時,永遠明確指定 -Encoding。因為 Windows PowerShell 5.1 依指令而異,各自有不同的預設編碼,而 PowerShell 7 則統一為不含 BOM 的 UTF-8,兩者的預設值完全不同。1
  • 5.1 的 Export-Csv 預設是 ASCII。忘記加上 -Encoding,日文在儲存的當下就會遺失。此外 5.1 的 Import-Csv 會把不含 BOM 的檔案解讀為 UTF-8,因此直接讀取 Shift_JIS 的 CSV 會亂碼。1
  • 彙總的基本形是 Group-Object(分組)搭配 Measure-Object(加總・平均・最大最小值)。Excel 樞紐分析表每次做的彙總,很多都能用這兩者取代。23
  • 兩份 CSV 的比對,若只需要知道有無差異用 Compare-Object,若還需要引用欄位(合併)則用雜湊表。Compare-Object 會用 SideIndicator 顯示某一行只存在於哪一邊。4
  • 讀寫 xlsx 的第一選擇是 ImportExcel 模組(社群製作)。不需要安裝 Excel 本體,就能建立表格、設定格式,甚至做出樞紐分析表。5
  • 對 Excel 本體做 COM 操作是最後手段。參照(RCW)釋放不完全,容易造成 EXCEL.EXE 殘留67,而且 Microsoft 本來就不建議、也不支援在無人環境(服務或排程執行)中自動化 Office。8
  • 在放上工作排程器之前,先明確固定字元編碼、路徑、執行環境這三點。互動式執行時能動、排程執行時卻壞掉的原因,大多出在這三點上。18

2. Import-Csv/Export-Csv 的基本 ── 最大的地雷是字元編碼

Import-Csv 會把 CSV 讀入為「一行 = 一個物件、一欄 = 一個屬性」的表格。標題行會變成欄位名稱,之後的處理都能用屬性名稱來寫。9 Export-Csv 則反過來,把物件的各欄寫出成 CSV。10 到這裡都很簡單。問題出在字元編碼。

正如官方文件明確記載的,Windows PowerShell 5.1 的預設編碼在各個指令之間並不一致1 下表整理出會影響日文環境實務的部分。

操作 Windows PowerShell 5.1 的預設值 PowerShell 7 的預設值
Export-Csv ASCII(日文會遺失)1 不含 BOM 的 UTF-810
Import-Csv(不含 BOM 的檔案) 解讀為 UTF-81 不含 BOM 的 UTF-8
Get-Content(不含 BOM 的檔案) ANSI=日文環境下為 Shift_JIS1 不含 BOM 的 UTF-8
Out-File・重導向(>) UTF-16LE(含 BOM)1 不含 BOM 的 UTF-8

也就是說在 5.1 下,「用 Get-Content 讀取時能正確解讀為 Shift_JIS,換成 Import-Csv 卻亂碼」「用 Export-Csv 輸出後日文全部變成 ?」都會照著規格發生。PowerShell 7 統一以不含 BOM 的 UTF-8 保持一致1,但這次換成若原封不動讀取核心系統送來的 Shift_JIS CSV,就會亂碼。結論只有一個:讀寫時都要明確指定 -Encoding

# 讀取核心系統輸出的 Shift_JIS CSV
# Windows PowerShell 5.1:Default = 系統的 ANSI 字碼頁(日文環境下為 Shift_JIS)
$orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding Default

# PowerShell 7 可以用字碼頁編號指定(932 = Shift_JIS)
# $orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding 932

# 輸出設為含 BOM 的 UTF-8,即使雙擊用 Excel 開啟也比較不容易亂碼
# PowerShell 7:UTF8 會變成不含 BOM,含 BOM 需明確指定為 utf8BOM
$orders | Export-Csv -LiteralPath 'C:\data\orders_out.csv' -NoTypeInformation -Encoding utf8BOM
# Windows PowerShell 5.1:不存在 utf8BOM 這個值。指定 UTF8 就會變成含 BOM
# $orders | Export-Csv -LiteralPath 'C:\data\orders_out.csv' -NoTypeInformation -Encoding UTF8

PowerShell 6.2 以後的 -Encoding 也能用字碼頁編號(932)或註冊名稱指定,7.4 以後還能使用 ansi 這個值。1 另外 -NoTypeInformation 是為了抑制 5.1 開頭附加的 #TYPE 行,PowerShell 6 以後預設就不會附加,因此不需要指定(加了也不會出錯)。10 若腳本要同時在 5.1 和 7 上運作,加上這個參數比較保險。

CSV 這種格式本身的陷阱(用 Excel 開啟會讓開頭的零消失、含有逗號或換行的值、注入攻擊防範)在「CSV 不是「純文字」而已」中有詳細說明。字元編碼與換行符的基礎,請參考「整理 Windows 的字元編碼與換行符」。

為了讓讀者只看這篇文章就能判斷,先把委託給其他文章的重點濃縮成三行。確認用途時不要用 Excel 雙擊開啟 CSV(會發生開頭零消失、長數字變成指數表示法,讓人以為在確認,實際上看到的是被破壞的值。要看內容應使用文字編輯器或 Import-Csv)。含有逗號、換行、雙引號的值必須用引號括住(Export-Csv 會自動處理這件事,因此不要自行用字串串接來組出 CSV)。= 開頭的值,可能被試算表軟體解讀為公式(不要不經意就開啟從外部收到的 CSV)。至於換行符,從非 Windows 或舊型設備送來的檔案,有時會是 LF,Import-Csv 能正常讀取,但若要自行寫 -split 處理,請不要預設為 CRLF

3. 彙總 ── 用 Group-Object 與 Measure-Object 做出相當於樞紐分析表的結果

「各部門的件數與金額合計」這類彙總,基本形是先用 Group-Object 分組,再用 Measure-Object 對各組加總。23

先決定輸入內容。以下的食譜以這樣的 orders.csv(假設是核心系統的輸出,字元編碼為 Shift_JIS)為前提。

OrderNo,Date,Dept,Customer,Amount
1001,2026/07/01,営業1課,株式会社A,120000
1002,2026/07/01,営業2課,株式会社B,80000
1003,2026/07/02,営業1課,株式会社C,45000
1004,2026/07/03,管理部,株式会社A,15000
1005,2026/07/03,営業2課,株式会社D,230000
# 用 PowerShell 7 讀取 Shift_JIS 的 CSV,因此要明確指定字碼頁 932
# (7 的 -Encoding Default 意義是 UTF-8,會亂碼。若在 5.1 上執行則指定 Default)
$orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding 932

# 依部門統計件數與金額合計。
# Import-Csv 讀出的值全部都是字串,所以重點是先轉成 [decimal] 再加總
# (Measure-Object -Sum 的 Sum 會是 Double 型別,因此金額改為以 decimal 型別自行逐筆相加,
#  避免位數較大的合計或小數的精度損失)
$summary = $orders | Group-Object -Property Dept | ForEach-Object {
    $total = [decimal]0
    foreach ($row in $_.Group) { $total += [decimal]$row.Amount }
    [pscustomobject]@{
        Dept  = $_.Name    # 分組鍵的值
        Count = $_.Count   # 件數
        Total = $total     # 金額合計
    }
}

$summary | Sort-Object -Property Total -Descending |
    Export-Csv -LiteralPath 'C:\data\summary.csv' -NoTypeInformation -Encoding UTF8

對上面這 5 行輸入,$summary(排序後)所擁有的值如下。summary.csv 會連同標題輸出這 3 行。

Dept Count Total
営業2課 2 310000
営業1課 2 165000
管理部 1 15000

依樣操作時,請先確認是否得到這 3 行結果。如果金額或件數對不上,多半是部門名稱的寫法不一致(全形空格或「営業1課」這類差異),或是接下來要談的型別轉換問題。

容易踩坑的地方是CSV 的值全部都是字串。因為 Import-Csv 回傳的是字串屬性的集合9,把它當成數值傳給 Measure-Object -Sum 之前,要先明確轉換成 [decimal]。就算忘了轉換,在 5.1 上有時還是能勉強動作,容易變成金額位數跑掉之後才被發現的事故。

Measure-Object 除了 -Sum,也能同時取得 -Average-Maximum-Minimum3 另外使用 Group-Object -AsHashTable,可以直接得到「鍵值 → 該組的行陣列」的雜湊表,也能應用在後面的比對上。2 這類單一指令的使用時機,整理在「PowerShell 實用指令集錦」中。

4. 比對 ── Compare-Object 與雜湊表合併的使用時機

4.1. 只需要知道有無差異就用 Compare-Object

從昨天與今天的名冊 CSV 找出「增加的人・消失的人」,這種典型的比對,用 Compare-Object 最快。用 -Property 指定鍵值欄位,就只會用那個欄位的值來比較,結果的 SideIndicator 是 =>(只存在於差異方)或 <=(只存在於基準方),就能得知增減。4

# 用 @() 包起來,是因為在 0 筆或空檔案時 Import-Csv 的結果可能為 $null。
# 若 ReferenceObject/DifferenceObject 為 $null,Compare-Object 會以終止錯誤中止
$yesterday = @(Import-Csv -LiteralPath '.\users_0716.csv' -Encoding UTF8)
$today     = @(Import-Csv -LiteralPath '.\users_0717.csv' -Encoding UTF8)

# 只用員工編號比較。=> 是只存在於今天的行(新增),<= 是只存在於昨天的行(刪除)
Compare-Object -ReferenceObject $yesterday -DifferenceObject $today -Property EmpNo |
    Sort-Object -Property EmpNo |
    Format-Table -Property EmpNo, SideIndicator

要注意兩點。第一,指定 -Property 之後,結果只會留下該欄位與 SideIndicator,若還想看姓名等其他欄位,就需要用結果的鍵值再回去查原始資料。第二,若基準方或差異方任一個是 $null(而非 0 行),就會出錯中止。4 上面的程式碼把讀取結果用 @() 包起來,就是為了應對這個問題。因為 0 筆的 CSV 也會變成空陣列,即使遇到「今天全員都離職了(=所有行都以 <= 出現)」這種極端狀況,也能以差異結果的形式接收,而不是格式錯誤。

4.2. 若還需要引用欄位(合併)就用雜湊表

「把明細 CSV 的員工編號,對照主檔 CSV 引用姓名和部門」這種相當於 SQL JOIN 的處理,慣用做法是先把主檔那一方轉成鍵值 → 行的雜湊表,再逐行查找。雙層迴圈(明細 × 主檔的全比對)在數千筆 × 數千筆的規模下會明顯變慢,但用雜湊表的話,即使是數萬筆也能以實用的速度處理。

輸入設定為以下兩份檔案。

master.csv
EmpNo,Name,Dept
E001,山田 太郎,営業1課
E002,佐藤 花子,管理部

details.csv
EmpNo,Amount
E001,120000
E003,45000
# 把主檔轉成「員工編號 → 行」的雜湊表
# 若鍵值重複,會被後面的行覆蓋,若可能有重複,請事先檢查
$master = @{}
foreach ($row in (Import-Csv -LiteralPath '.\master.csv' -Encoding UTF8)) {
    $master[$row.EmpNo] = $row
}

# 逐行引用明細。找不到的行不悄悄丟棄,另存到別的檔案
$unmatched = New-Object System.Collections.Generic.List[object]
$joined = foreach ($row in (Import-Csv -LiteralPath '.\details.csv' -Encoding UTF8)) {
    $hit = $master[$row.EmpNo]
    if ($null -eq $hit) {
        $unmatched.Add($row)
        continue
    }
    [pscustomobject]@{
        EmpNo  = $row.EmpNo
        Name   = $hit.Name
        Dept   = $hit.Dept
        Amount = $row.Amount
    }
}

$joined    | Export-Csv -LiteralPath '.\joined.csv'    -NoTypeInformation -Encoding UTF8
$unmatched | Export-Csv -LiteralPath '.\unmatched.csv' -NoTypeInformation -Encoding UTF8

以這份輸入來說,joined.csv 會輸出主檔中存在的 E001 這一行(E001,山田 太郎,営業1課,120000),unmatched.csv 則會輸出主檔中不存在的 E003 這一行(E003,45000)。

這份食譜的關鍵在最後兩行。不要把找不到鍵值的行悄悄丟棄。比對作業的價值就在於「能讓人確認對不上的部分」,因此務必輸出 unmatched,並把件數記錄到日誌或標準輸出。以上面的例子來說,能不能注意到 unmatched.csv 裡出現了 E003,正是執行這項處理的意義所在。

4.3. 欄位名稱不一致與標題的處理

實務上的 CSV 常有「員工編號」「員工 No」「emp_no」這類欄位名稱不一致的情況。對策就是在讀取之後立刻正規化為內部名稱。之後的處理只用內部名稱來寫,即使格式改變,也只需要修改正規化那一處。

# 沒有標題行的 CSV,用 -Header 給定欄位名稱(從第 1 行開始就當成資料讀取)
# 字元編碼的思路與第 2 章相同:若是 Shift_JIS,7 用 932,5.1 用 Default 指定
$rows = Import-Csv -LiteralPath '.\no_header.csv' -Header 'EmpNo', 'Name', 'Dept' -Encoding 932

# 日文標題的 CSV,讀取後立刻正規化成英文的內部名稱
$normalized = Import-Csv -LiteralPath '.\jinji.csv' -Encoding 932 |
    Select-Object -Property @{ Name = 'EmpNo'; Expression = { $_.'社員番号' } },
                            @{ Name = 'Name';  Expression = { $_.'氏名' } },
                            @{ Name = 'Dept';  Expression = { $_.'所属部署' } }

-Header 是給沒有標題行的檔案用的,指定後連第 1 行也會被當成資料讀入,這點要注意。9 此外標題有空欄時,PowerShell 會自動配上像 H1 這樣的暫時欄位名稱9,若照原本預期的欄位名稱抓不到屬性,先懷疑標題行是否有問題。

5. 處理 xlsx ── ImportExcel 是第一選擇,COM 是最後手段

5.1. ImportExcel 模組 ── 不需要 Excel 本體就能讀寫 xlsx

「不要 CSV,要 Excel 檔案,還要幫表格上色」,這正是日本職場常見的要求。這時第一選擇,就是在 PowerShell Gallery 上發佈的社群製作 ImportExcel 模組。不需要安裝 Excel 本體就能讀寫 xlsx,還能建立表格、調整欄寬,甚至做出樞紐分析表。5

# 僅第一次執行。從 PowerShell Gallery 針對目前使用者安裝(不需要系統管理員權限)
# 若 Windows PowerShell 5.1 無法連線到 Gallery,請先啟用 TLS 1.2 再執行
# [Net.ServicePointManager]::SecurityProtocol =
#     [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Install-Module -Name ImportExcel -Scope CurrentUser

# 確認是否安裝成功以及版本
Get-InstalledModule -Name ImportExcel | Select-Object -Property Name, Version

# 讀取 xlsx(和 Import-Csv 一樣的感覺,工作表的表格會變成物件陣列)
$budget = Import-Excel -Path 'C:\data\budget.xlsx' -WorksheetName '予算'

# 把第 3 章的彙總結果,輸出成含表格、自動調整欄寬、附樞紐分析表的 xlsx
$summary | Export-Excel -Path 'C:\data\monthly-report.xlsx' `
    -WorksheetName '集計' -TableName 'Summary' -AutoSize `
    -IncludePivotTable -PivotRows Dept -PivotData @{ Total = 'Sum' }

導入時容易卡住的地方有兩點。一是連線到 PowerShell Gallery,因為使用 Gallery 需要 TLS 1.2 以上,在 Windows PowerShell 5.1 上若不像上面的註解那樣在工作階段中明確設定,Install-Module 有時會失敗(若覺得每次都寫很麻煩,可以把這一行放進設定檔腳本)。11 另一點是版本確認,這個模組支援哪個 PowerShell 版本,發佈頁面並沒有明確記載5 若在 5.1 與 7 混用的公司內使用,請先在實際要運作的那個 PowerShell 上跑一次 Export-Excel,確認檔案能打開之後再展開(若要透過工作排程器執行,請用啟動該工作時所用的相同執行帳戶來確認。用 -Scope CurrentUser 安裝的模組,只有該使用者才看得到)。

既然是社群製作,導入時請遵循貴公司的軟體導入規範來確認(此模組的取得來源為 PowerShell Gallery5)。即便如此,比起「在伺服器上安裝 Excel、用 COM 跑」的架構,無論從授權、穩定性還是維護的哪個角度來看,都是更合理的選擇。報表產生方式的比較(COM/Open XML/範本)整理在「Excel 報表輸出該怎麼做」中。

5.2. Excel COM 操作 ── 要用就寫到底做完釋放,不要無人執行

想踢動既有 xls 的巨集、需要 Excel 本身的功能(重新計算、列印、解析已定義名稱等)──只有在這種情況下,才對 Excel 本體做 COM 操作。從 PowerShell 可以用 New-Object -ComObject 啟動,但有個有名的問題就是EXCEL.EXE 的處理程序殘留。若對 COM 物件的參照(RCW)沒有被釋放,即使呼叫 Quit(),處理程序也會殘留。6 .NET 端的對策是用 Marshal.ReleaseComObject 明確釋放。7

# COM 是最後手段。用 finally 保證「開了一定關、參照一定釋放」
# Excel 啟動後的 COM 呼叫全部放在 try 裡面(讓啟動後那一行即使發生例外,善後也一定會執行)
$excel = New-Object -ComObject Excel.Application
$book = $null
try {
    $excel.Visible = $false
    $excel.DisplayAlerts = $false   # 避免因確認對話框而卡住
    $book  = $excel.Workbooks.Open('C:\data\template.xlsx')
    $sheet = $book.Worksheets.Item(1)
    $sheet.Range('B2').Value2 = 12345
    $book.SaveAs('C:\data\output.xlsx')
    $book.Close($false)
}
finally {
    # 即使 Quit 失敗(例如 Excel 沒有回應),釋放與 GC 也一定要執行
    try {
        $excel.Quit()
    }
    finally {
        # 碰過的 COM 物件若不明確釋放 RCW,EXCEL.EXE 容易殘留
        if ($sheet) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($sheet) }
        if ($book)  { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($book) }
        [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
        [GC]::Collect()
        [GC]::WaitForPendingFinalizers()
    }
}

麻煩的地方在於,像 $excel.Workbooks.Open(...) 這樣只是用點串接呼叫,就會產生中介物件(此例中是 Workbooks 集合)的參照,而它也是釋放對象。上面的程式碼嚴格來說也還留著 Workbooks 的參照,若要求萬無一失,中介物件也要接到變數再釋放。這種「要把參照管理寫到底」的難度本身,正是把 COM 定位為最後手段的理由。機制的細節與替換判斷,在「C# 操作 Excel 時 EXCEL.EXE 殘留的問題」中討論;從 PowerShell 呼叫 COM/.NET 的整體做法,則在同步發佈的「從 PowerShell 呼叫 COM 與 .NET 的實務」中處理。

還有另一個決定性的限制。Microsoft 並不建議、也不支援從無人・非互動的用戶端(服務、排程執行等)自動化 Office。Office 的設計前提是有互動使用者存在,官方明確記載無人環境下可能發生不穩定的行為或死結。8 SSIS 的文件中也建議,在無人執行的環境下不要使用 Excel 連線,改用 CSV 或 Open XML 系的方式取代。12「每晚用工作排程器開啟 Excel 做報表」這種架構,看起來能動,實際上卻是建在沙上的樓閣。想定期執行的處理,請改用 ImportExcel 或 Open XML 系的方式。

6. 放上工作排程器定期執行前的檢查清單

把手邊能動的腳本原封不動放上工作排程器,多半會因為下列其中一項而壞掉。

  • 字元編碼:互動式執行與排程執行的行為並不會改變,但「碰巧在手邊的 7 上能動」的腳本,因為工作那邊的啟動指令是 powershell.exe(=5.1),預設編碼因此改變而亂碼,這是常見的事故。只要照第 2 章那樣,讀寫兩端都明確指定 -Encoding,不論用哪個啟動,結果都會一樣。1
  • 路徑:不要使用依賴目前工作目錄的相對路徑,應以絕對路徑或 $PSScriptRoot 為基準來寫。網路磁碟(Z: 等)在排程執行的工作階段中不會被對應,因此請改用 UNC 路徑。
  • 執行環境:要用哪個 PowerShell 執行(powershell.exe 還是 pwsh.exe),請在啟動指令中明確指定。5.1 與 7 的差異與使用時機,請參考同步發佈的「Windows PowerShell 5.1 與 PowerShell 7 的差異」。
  • 不要放上含 Excel COM 的處理:如前一章所述,無人執行不受支援。8

工作排程器執行時如何留存日誌與紀錄,在「PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化」中有詳細說明。

7. 實務常規(判斷表)

議題 選項 判斷基準
CSV 的字元編碼 交給預設值 / 明確指定 -Encoding 永遠明確指定。因為 5.1 與 7 的預設值不同,這樣做能從結構上防止「換環境就亂碼」1
彙總 用 Excel 手動處理 / Group-Object + Measure-Object 若每週、每月都要重複,就寫成腳本。步驟會以程式碼的形式留存,變得可重現23
兩份 CSV 的比對 Compare-Object / 雜湊表合併 只需要知道有無差異用 Compare-Object。要引用其他欄位就用雜湊表。不一致的行務必另外輸出4
xlsx 輸出 用 CSV 應付 / ImportExcel / COM 需要格式或樞紐分析表就用 ImportExcel(不需要 Excel 本體)。COM 只用於需要執行巨集等 Excel 本身功能的情況5
Excel COM 的執行方式 用工作排程器無人執行 / 僅手動・互動式執行 無人自動化 Office 不受支援。想無人化的處理應改用不需要 Excel 本體的方式8
定期執行 每次手動執行 / 工作排程器 明確指定 -Encoding、絕對路徑、要啟動的 PowerShell 這三點,再排入工作排程

8. 總結

  • CSV 自動化最大的地雷是字元編碼。5.1 依指令而異(Export-Csv 是 ASCII),7 則統一為不含 BOM 的 UTF-8。讀寫時都明確指定 -Encoding,就能消除環境差異。
  • 彙總的基本形是 Group-Object + Measure-Object。CSV 的值全部都是字串,因此要先轉換成數值再彙總。
  • 比對方面,只需要知道有無差異用 Compare-Object,需要合併就用雜湊表。無法引用的行務必輸出到另一個檔案,讓人可以確認。
  • 欄位名稱不一致,靠讀取後立刻正規化來吸收,之後的處理只用內部名稱來寫。
  • xlsx 可以用 ImportExcel 模組在沒有 Excel 本體的情況下讀寫。COM 是能把釋放處理寫到底時的最後手段,不要用於無人執行。
  • 放上工作排程器之前,請明確固定字元編碼、路徑、要啟動的 PowerShell。

相關文章

相關諮詢領域

合同會社小村軟體處理透過 CSV・Excel 進行的例行業務自動化腳本開發、脫離對 Excel COM 依賴的報表處理(替換為 ImportExcel/Open XML),以及「只有在特定環境下才會亂碼」「EXCEL.EXE 殘留」這類問題的調查。

參考連結

  1. Microsoft Learn, about_Character_Encoding。關於 Windows PowerShell 5.1 的預設編碼依指令而異(Export-Csv 為 ASCII、Set-Content/Get-Content 為 ANSI 的 Default、Out-File 與重導向為 UTF-16LE、不含 BOM 檔案的 Import-Csv 解讀為 UTF-8)、PowerShell 6 以後統一以不含 BOM 的 UTF-8 為預設、6.2 以後可用字碼頁編號・註冊名稱指定 -Encoding,以及 7.4 的 ansi 值的說明。  2 3 4 5 6 7 8 9 10 11 12

  2. Microsoft Learn, Group-Object。關於 Group-Object 會依指定屬性的值對物件分組、回傳各組的件數與元素,以及用 -AsHashTable 可以取得鍵值 → 組的雜湊表的說明。  2 3 4

  3. Microsoft Learn, Measure-Object。關於 Measure-Object 除了件數,也能計算 Sum・Average・Maximum・Minimum(以及標準差)的說明。  2 3 4

  4. Microsoft Learn, Compare-Object。關於 Compare-Object 會比較兩組物件集合、用 SideIndicator(<= / => / ==)顯示只存在於哪一邊、可用 -Property 只比較指定欄位,以及參照方・差異方為 null 時會發生終止錯誤的說明。  2 3 4

  5. Microsoft Learn, An active Excel process continues to run after using a VBA macro to programmatically quit Excel。關於即使關閉活頁簿、呼叫 Quit、清除參照,只要對 Excel 或其成員的參照仍以全域方式被保留,EXCEL.EXE 處理程序就會持續殘留的說明。  2

  6. Microsoft Learn, Marshal.ReleaseComObject(Object) Method。關於 ReleaseComObject 會減少與 COM 物件關聯的 RCW(執行階段可呼叫包裝函式)的參照計數,用於明確控制 COM 物件的生命週期,以及存取已釋放的物件會擲回例外、使用時須留意的說明。  2

  7. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment。關於 Microsoft 並不建議、也不支援從無人・非互動的用戶端(ASP、DCOM、NT 服務等)自動化 Office 應用程式、無人環境下 Office 可能發生不穩定的行為或死結,以及建議改用 Open XML 等替代手段的說明。  2 3 4 5

  8. Microsoft Learn, Import-Csv。關於 Import-Csv 會從 CSV 建立表格式自訂物件、第一行會被解讀為標題、可用 -Header 為沒有標題的檔案指定欄位名稱、標題的空欄會配上以 H 開頭的暫時欄位名稱,以及值會以字串形式讀入的說明。  2 3 4

  9. Microsoft Learn, Export-Csv。關於 Export-Csv 會把物件的各屬性寫出成 CSV 的欄位、PowerShell 7 中預設編碼為 UTF8NoBOM,以及 PowerShell 6.0 以後預設不再輸出 #TYPE 行、NoTypeInformation 已成為隱含行為的說明。  2 3

  10. Microsoft Learn, Install a package manager for PowerShell。關於存取 PowerShell Gallery 需要傳輸層安全性(TLS)1.2 以上、在工作階段中啟用 TLS 1.2 的指令,以及將其寫入設定檔腳本的方法的說明。 

  11. Microsoft Learn, Import data from Excel or export data to Excel with SQL Server Integration Services (SSIS)。關於無人・非互動環境下不支援使用 Excel 元件,以及正式的自動化處理建議改用平面檔案(CSV)或 Open XML 系方式的說明。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

為什麼用 PowerShell 讀寫 CSV 會出現亂碼?
因為 Windows PowerShell 5.1 和 PowerShell 7 的預設字元編碼不同。5.1 依指令而異:Export-Csv 是 ASCII(日文字會遺失),讀取不含 BOM 檔案的 Import-Csv 會以 UTF-8 解讀,Get-Content 則是 ANSI(在日文環境下為 Shift_JIS)。7 則統一以不含 BOM 的 UTF-8 為預設。讀寫時都務必明確指定 -Encoding,是唯一安全的做法。
要如何比對(檢查差異)兩份 CSV?
如果只是想知道「有沒有差異」,用 Compare-Object 搭配 -Property 指定鍵值欄位最方便。透過 SideIndicator 可以分辨新增的行(=>)與消失的行(<=)。若要從另一份 CSV 引用姓名、部門等欄位並合併,慣用做法是把主檔那一方轉成「鍵值 → 該行」的雜湊表,再逐行查找,即使是數萬筆規模也能高速處理。找不到鍵值的行不要悄悄丟棄,應輸出到另一個檔案,讓人可以確認。
用 PowerShell 建立 Excel 檔案(xlsx)需要安裝 Excel 本體嗎?
不需要。使用社群製作的 ImportExcel 模組,即使機器上沒有安裝 Excel,也能讀寫 xlsx、建立表格、設定格式,甚至製作樞紐分析表。透過 PowerShell Gallery 的 Install-Module 即可導入。以 COM 操作 Excel 本體的方式,應定位為需要「Excel 本身功能」(例如執行巨集)時的最後手段,這樣才安全。
為什麼用 PowerShell 對 Excel 做 COM 操作後,EXCEL.EXE 會殘留?
因為對 COM 物件的參照(RCW)沒有被釋放,即使呼叫 Quit,Excel 的處理程序也不會結束。每次操作活頁簿或儲存格範圍,中介物件的參照就會增加,因此用完之後要用 Marshal.ReleaseComObject 明確釋放,並以 GC.Collect 促使回收。此外,由於 Microsoft 並不建議、也不支援在如工作排程器這類無人環境中自動化 Office,想定期執行的處理應改用 ImportExcel 等不需要 Excel 本體的方式。
CSV 的彙總該用 Excel 的樞紐分析表,還是用 PowerShell?
如果只是一次性的分析,用 Excel 就足夠了。如果每週、每月都要重複同樣的步驟,就值得用 Group-Object 和 Measure-Object 寫成腳本。因為步驟會以程式碼的形式留存,而非僅存在於文件中,即使承辦人異動也能重現相同的結果,也能串接到工作排程器的定期執行。若用 ImportExcel 把彙總結果輸出成 xlsx,收到的一方也能像平常一樣以 Excel 檔案處理。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽