🌐

PowerShellで“Markdownテーブル”と“Excelファイル”を相互変換する方法

2024/06/20に公開

概要

「Excelファイル → Markdown形式のテーブル の変換」はExcel のアドオンなど仕事効率化ツールを導入することで実現できましたが、
「Markdown形式のテーブル → Excelファイル の変換」は調べたかぎり対応可能な手段が少なく感じました。
(おそらく、Markdownテーブルをプレビューして対象の表をコピーしExcelに貼り付けることで対応可能なためだと思われます。)

2024年6月現在、MarkdownテーブルからExcelファイルをツールで実現したい場合は、オンラインWebツールやPythonスクリプトが必要そうでした。

オンラインWebツールを使用すると、機密情報を含むデータが取り扱えません。
また、Pythonスクリプトを実行する場合は、難しくはありませんが実行環境を構築する必要があります。

そこで今回、Windows環境において標準搭載されているPowerShellで相互の変換を実現する方法を検証してみました。

この記事のターゲット

  • Windows OS ユーザーの方
  • Markdown形式のテーブル から Excelファイル に変換したい方
  • Excelファイル から Markdown形式のテーブル に変換したい方

対応方法

Windows OSでは、すぐにPowerShellの使用が可能です。

今回はそれぞれ変換するPowerShellのコードでExcelファイルを扱うため、事前にExcelが制御可能となるモジュール「ImportExcel」の導入が必要。

また、そのモジュールは、管理者権限でインストールする必要があります。

事前準備

事前にモジュール「ImportExcel」をインストールします。

モジュール「ImportExcel」のインストール

下記のコードを実行することでモジュール「ImportExcel」を導入できます。
繰り返しにはなりますが、このコードを実行するPowerShellウィンドウは“管理者として実行”で起動(管理者権限)して実行が必要です。

PowerShellでExcelファイルが使えるようになるモジュール「ImportExcel」
#################################################################################
# 処理名  | Test-ModuleInstalled
# 機能   | モジュールの導入有無を確認
#          | 参考情報:https://zenn.dev/haretokidoki/articles/0a94c0f83bd428
#--------------------------------------------------------------------------------
# 戻り値  | Boolean(True: モジュール導入済み, False: モジュール未導入)
# 引数   | MoudleName: チェックするモジュール名
#################################################################################
function Test-ModuleInstalled {
    param (
        [System.String]$ModuleName
    )

    $moduleInstalled = $false
    # モジュール情報を取得
    $module = (Get-Module -ListAvailable -Name $ModuleName)
    if ($null -ne $module) {
        $moduleInstalled = $true
    }
    
    return $moduleInstalled
}
#################################################################################
# 処理名  | Test-IsAdmin
# 機能   | PowerShellが管理者として実行しているか確認
#          | 参考情報:https://zenn.dev/haretokidoki/articles/67788ca9b47b27
#--------------------------------------------------------------------------------
# 戻り値  | Boolean(True: 管理者権限あり, False: 管理者権限なし)
# 引数   | -
#################################################################################
function Test-IsAdmin {
    $win_id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $win_principal = new-object System.Security.Principal.WindowsPrincipal($win_id)
    $admin_permission = [System.Security.Principal.WindowsBuiltInRole]::Administrator
    return $win_principal.IsInRole($admin_permission)
}

# ImportExcel モジュールのインストール
$moduleName = 'ImportExcel'
if (-Not(Test-ModuleInstalled $moduleName)) {
    if (-Not(Test-IsAdmin)) {
        Write-Warning '管理者権限がない為、モジュールのインストールができません。処理を中断します。'
        return
    }
    # モジュールインストールのため、セキュリティプロトコルを“TLS1.2”に設定
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    # モジュールのインストール
    Install-Module -Name $moduleName -Scope CurrentUser -Force
    Write-Host "$($moduleName) モジュールをインストールしました。"
} else {
    Write-Host "$($moduleName) モジュールは既にインストールされています。"
}

(切り戻し手順)モジュール「ImportExcel」のアンインストール

モジュール「ImportExcel」が不要となった場合、下記のコマンドレットでモジュールのアンインストールが可能。
なお、インストール時と同様に 管理者権限が必要 です。

コピー用
Uninstall-Module -Name ImportExcel -AllVersions
実際の実行結果(管理者権限で実行)
# アンインストール前:モジュールが存在すること
PS C:\WINDOWS\system32> Get-Module -ListAvailable -Name ImportExcel


    ディレクトリ: D:\Documents\WindowsPowerShell\Modules


ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     7.8.9      ImportExcel                         {Add-ConditionalFormatting, Add-ExcelChart, Add-ExcelDataV...


PS C:\WINDOWS\system32>
# アンインストール実行
PS C:\WINDOWS\system32> Uninstall-Module -Name ImportExcel -AllVersions
PS C:\WINDOWS\system32>
# アンインストール後:モジュールがアンインストールされたこと
PS C:\WINDOWS\system32> Get-Module -ListAvailable -Name ImportExcel
PS C:\WINDOWS\system32>
参考情報:モジュール「ImportExcel」のアンインストールでエラーが発生する場合

今回紹介したモジュール「ImportExcel」を使用したコードの直後にアンインストールしようとすると、エラーが発生します。
実際に発生したエラーは下記のとおりです。

発生したエラー
PS C:\WINDOWS\system32> Uninstall-Module ImportExcel
警告: バージョン '7.8.9' のモジュール 'ImportExcel'
は現在使用中です。アプリケーションを終了した後で、操作をやり直してください。
PackageManagement\Uninstall-Package : モジュール 'ImportExcel' は現在使用中か、必要なアクセス許可がありません。
発生場所 C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.0.0.1\PSModule.psm1:2194 文字:21
+ ...        $null = PackageManagement\Uninstall-Package @PSBoundParameters
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Microsoft.Power...ninstallPackage:UninstallPackage) [Uninstall-Packag
   e]、Exception
    + FullyQualifiedErrorId : ModuleIsInUse,Uninstall-Package,Microsoft.PowerShell.PackageManagement.Cmdlets.Uninstall
   Package

PS C:\WINDOWS\system32>

エラーが発生するタイミングは、モジュール「ImportExcel」を使用した時のみだと複数回検証し確認済み。
おそらくモジュールの解放処理で問題があるようで、このような事象が発生していると思われる。

明示的にモジュールを開放する方法を調べましたが、見つかりませんでした。

同じ事象が発生した場合、モジュールを読み込み実行したコンソール(PowerShellウィンドウ や Windowsターミナルなど)を閉じて、
再度コンソールを立ち上げた後に Uninstall-Module コマンドレットを実行してください。

Markdown-Table から Excelファイル に変換できるPowerShellのコード

関連するFunctionを含めたコード。メインのFunctionは「Convert-MarkdownTableToExcel」です。
PowerShellの制約上、メインのFunctionは後方にコーディングしています。

Markdown形式 から Excelファイル に変換するコード
#################################################################################
# 処理名  | Test-FileLocked
# 機能   | ファイルが開かれているか(ロック状態を)確認
#          | 参考情報:https://zenn.dev/haretokidoki/articles/b4f4399570000a
#--------------------------------------------------------------------------------
# 戻り値  | Boolean
#        |  True: ファイルを開いている状態(ロック状態), False: 開いていない状態
# 引数   | -
#################################################################################
function Test-FileLocked {
    param (
        [Parameter(Mandatory=$true)][System.String]$Path
    )

    if (-Not(Test-Path $Path)) {
        Write-Error '対象ファイルが存在しません。' -ErrorAction Stop
    }

    # 相対パスだとOpenメソッドが正常動作しない為、絶対パスに変換
    $fullPath = (Resolve-Path -Path $Path -ErrorAction SilentlyContinue).Path

    $fileLocked = $false
    try {
        # 読み取り専用でファイルを開く処理を実行
        $fileStream = [System.IO.File]::Open($fullPath, 'Open', 'ReadWrite', 'None')
    }
    catch {
        # ファイルが開けない場合、ロック状態と判断
        $fileLocked = $true
    }
    finally {
        if ($null -ne $fileStream) {
            $fileStream.Close()
        }
    }

    return $fileLocked
}

# コンテンツを変換する処理
function Get-ExcelTable {
    param (
        [Parameter(Mandatory=$true)]
        [string]$FromMarkdownPath
    )

    # 内容を読み取る
    $inputContents = (Get-Content $FromMarkdownPath -Raw)

    # 表の開始と終了を検出する正規表現
    $tableRegex = '(?smi)^\|.*(?<!\\)\|.*$'
    $headerSplitRegex = '^\|?(.*?)(?<!\\)\|$'
    $rowSplitRegex = '^\|(.+?)(?<!\\)\|$'

    # 表の内容を抽出します
    $excelTables = @()
    [regex]::Matches($inputContents, $tableRegex) | ForEach-Object {
        $tableText = $_.Value

        # ヘッダーを解析します
        $rowCount = 0
        $rows = @()

        $tableText -split "`n" | ForEach-Object {
            if ($rowCount -eq 0) {
                $headers = [regex]::Split($_, '(?<!\\)\|')
                $headers = $headers[1..($headers.Length-2)] # 最初と最後の空の要素を削除
                $rowCount += 1
            }
            elseif ($rowCount -eq 1) {
                # ヘッダーとコンテンツの区切り行をスキップ
                $rowCount += 1
            }
            else {
                $row = [regex]::Split($_, '(?<!\\)\|')
                $row = $row[1..($row.Length-2)] # 最初と最後の空の要素を削除

                # ヘッダー項目数 と データ項目数を比較
                if ($row.Count -eq $headers.Count) {
                    $rows += ,$row
                }
                else {
                    # 空行以外
                    if (-Not([System.String]::IsNullOrEmpty($_.Trim()))) {
                        Write-Host "ヘッダーの項目数と合わなかった為、下記のデータをスキップしました。[ヘッダー項目数: $($headers.Count), データ項目数: $($row.Count)]"
                        Write-Host '```:markdown'
                        Write-Host "$_"
                        Write-Host '```'
                    }
                }
            }
        }

        # 各行をオブジェクトに変換します
        $rows | ForEach-Object {
            $rowObject = New-Object psobject
            for ($i = 0; $i -lt $headers.Count; $i++) {
                $rowObject | Add-Member -MemberType NoteProperty -Name $headers[$i].Trim() -Value $_[$i].Trim()
            }
            $excelTables += ,$rowObject
        }
    }

    return $excelTables
}

# Markdown形式のテーブル から Excelファイル に変換するFunciton
function Convert-MarkdownTableToExcel {
    param (
        [Parameter(Mandatory=$true)]
        [System.String]$FromMarkdownPath,
        [Parameter(Mandatory=$true)]
        [System.String]$ToExcelPath

    )

    # Markdown形式のテーブル を Excel
    $excelTables = (Get-ExcelTable -FromMarkdownPath $FromMarkdownPath)

    if ($null -eq $excelTables) {
        Write-Error 'ファイルを読み取りましたが、データを読み取れませんでした。'
        return
    }

    if (Test-Path -Path $ToExcelPath) {
        # ファイルがロックされているかテストします
        if (Test-FileLocked -Path $ToExcelPath) {
            Write-Warning 'Excelファイルが開かれています。ファイルを閉じてから再試行してください。'
            return
        }

        # 入力メッセージ
        $userInput = Read-Host 'ファイルを上書きしますか? (Yes/No)'
        $yesPatterns = '[Yy][Ee][Ss]|[Yy]'
        $noPatterns = '[Nn][Oo]|[Nn]'
        $isYes = $false
        # 試行回数: 3回
        $maxCount = 2
        for ($i = 0; $i -lt $maxCount; $i++) {
            if ($userInput -match $yesPatterns) {
                $isYes = $true
                break
            }
            else {
                Write-Host '上書き保存をキャンセルしました。'
                return
            }
        }
        if (-Not ($isYes)) {
            Write-Warning '試行回数を超過しました。処理を中断します。'
            return
        }

        # ユーザーが「はい」を選択した場合、ファイルを上書き保存します
        try {
            $excelTables | Export-Excel -Path $toExcelPath -Show
            Write-Host '上書き保存しました。'
        }
        catch {
            Write-Error '上書き保存でエラーが発生しました。'
        }
    }
    else {
        # 新規保存
        try {
            $excelTables | Export-Excel -Path $toExcelPath -Show
            Write-Host "新規保存しました。"
        }
        catch {
            Write-Error '新規保存でエラーが発生しました。'
        }
    }
}

下記でメインのFunction「Convert-MarkdownTableToExcel」を実行。

Function「Convert-MarkdownTableToExcel」を実行
# 実行
$markdownPath = 'D:\Downloads\inputMarkdownTable.md'
$excelPath = 'D:\Downloads\outputExcel.xlsx'
Convert-MarkdownTableToExcel $markdownPath $excelPath
補足情報:MarkdownテーブルからExcelファイルへの変換時の入出力データ一式

Excelファイル から Markdown-Table に変換できるPowerShellのコード

関連するFunctionを含めたコード。メインのFunctionは「Convert-ExcelToMarkdownTable」です。
PowerShellの制約上、メインのFunctionは後方にコーディングしています。

Excelファイル から Markdown形式 に変換するコード
#################################################################################
# 処理名  | Test-FileLocked
# 機能   | ファイルが開かれているか(ロック状態を)確認
#          | 参考情報:https://zenn.dev/haretokidoki/articles/b4f4399570000a
#--------------------------------------------------------------------------------
# 戻り値  | Boolean
#        |  True: ファイルを開いている状態(ロック状態), False: 開いていない状態
# 引数   | -
#################################################################################
function Test-FileLocked {
    param (
        [Parameter(Mandatory=$true)][System.String]$Path
    )

    if (-Not(Test-Path $Path)) {
        Write-Error '対象ファイルが存在しません。' -ErrorAction Stop
    }

    # 相対パスだとOpenメソッドが正常動作しない為、絶対パスに変換
    $fullPath = (Resolve-Path -Path $Path -ErrorAction SilentlyContinue).Path

    $fileLocked = $false
    try {
        # 読み取り専用でファイルを開く処理を実行
        $fileStream = [System.IO.File]::Open($fullPath, 'Open', 'ReadWrite', 'None')
    }
    catch {
        # ファイルが開けない場合、ロック状態と判断
        $fileLocked = $true
    }
    finally {
        if ($null -ne $fileStream) {
            $fileStream.Close()
        }
    }

    return $fileLocked
}

# コンテンツを変換する処理
function Get-MarkdownTable {
    param (
        [Parameter(Mandatory=$true)]
        [string]$FromExcelPath
    )

    # 内容を読み取る
    $inputContents = (Import-Excel -Path $FromExcelPath)

    # Markdown形式のテーブルを構築します
    $markdownTables = ''
    $headers = $inputContents[0].PSObject.Properties.Name

    # ヘッダー行を追加します
    $markdownTables += '| ' + ($headers -join ' | ') + ' |' + "`r`n"

    # 区切り行を追加します
    $markdownTables += '| ' + (($headers | ForEach-Object { '---' }) -join ' | ') + ' |' + "`r`n"

    # データ行を追加します
    for ($i = 0; $i -lt $inputContents.Count; $i++) {
        $rowData = @()
        foreach ($header in $headers) {
            $cellValue = $inputContents[$i]."$header"
            $rowData += $cellValue
        }
        $markdownTables += '| ' + ($rowData -join ' | ') + ' |'
        # 最後の行以外は改行を追加
        if ($i -lt $inputContents.Count - 1) {
            $markdownTables += "`r`n"
        }
    }
    return $markdownTables
}

# Exelファイル から Markdown形式のテーブル に変換するFunction
function Convert-ExcelToMarkdownTable {
    param (
        [Parameter(Mandatory=$true)]
        [System.String]$FromExcelPath,
        [Parameter(Mandatory=$true)]
        [System.String]$ToMarkdownPath
    )

    $markdownTables = (Get-MarkdownTable -FromExcelPath $FromExcelPath)

    if ($null -eq $markdownTables) {
        Write-Error 'ファイルを読み取りましたが、データを読み取れませんでした。'
        return
    }

    if (Test-Path -Path $ToMarkdownPath) {
        if (Test-FileLocked -Path $ToMarkdownPath) {
            Write-Warning 'Markdownファイルが開かれています。ファイルを閉じてから再試行してください。'
            return
        }

        # 入力メッセージ
        $userInput = Read-Host 'ファイルを上書きしますか? (Yes/No)'
        $yesPatterns = '[Yy][Ee][Ss]|[Yy]'
        $noPatterns = '[Nn][Oo]|[Nn]'
        $isYes = $false
        # 試行回数: 3回
        $maxCount = 2
        for ($i = 0; $i -lt $maxCount; $i++) {
            if ($userInput -match $yesPatterns) {
                $isYes = $true
                break
            }
            else {
                Write-Host '上書き保存をキャンセルしました。'
                return
            }
        }
        if (-Not ($isYes)) {
            Write-Warning '試行回数を超過しました。処理を中断します。'
            return
        }

        # ユーザーが「はい」を選択した場合、ファイルを上書き保存します
        try {
            $markdownTables | Out-File -FilePath $ToMarkdownPath -Encoding utf8
            Write-Host '上書き保存しました。'
        }
        catch {
            Write-Error '上書き保存でエラーが発生しました。'
        }
    }
    else {
        # 新規保存
        try {
            $markdownTables | Out-File -FilePath $ToMarkdownPath -Encoding utf8
            Write-Host "新規保存しました。"
        }
        catch {
            Write-Error '新規保存でエラーが発生しました。'
        }
    }
}

下記でメインのFunctionは「Convert-ExcelToMarkdownTable」を実行。

Function「Convert-ExcelToMarkdownTable」を実行
# 実行
$excelPath = 'D:\Downloads\inputExcel.xlsx'
$markdownPath = 'D:\Downloads\outputMarkdownTable.md'
Convert-ExcelToMarkdownTable $excelPath $markdownPath
補足情報:MarkdownテーブルからExcelファイルへの変換時の入出力データ一式

https://github.com/akiGAMEBOY/haretokidoki-zn_articles_35f534e04e731c/tree/main/ExcelToMarkdown

  • 入力ファイル:セル位置「A1」にヘッダー、「A2」以降からコンテンツがあるExcelファイル

https://github.com/akiGAMEBOY/haretokidoki-zn_articles_35f534e04e731c/blob/main/ExcelToMarkdown/inputExcel.xlsx

  • 出力ファイル:入力ファイルのExcelファイルを元に変換したMarkdownファイル

https://github.com/akiGAMEBOY/haretokidoki-zn_articles_35f534e04e731c/blob/main/ExcelToMarkdown/outputMarkdownTable.md#L1-L47

まとめ

  • 下記のFunctionを作成することができた
    • Markdown形式のテーブル を元に Excelファイル を出力するFunction
    • Excelファイル を元に Markdown形式のテーブル を出力するFunction
  • 入出力の要件が細かくある場合はカスタマイズが必要
    とくにExcelファイルが入力データとなる場合はヘッダーやコンテンツの書き出し位置など必要に応じてコードを修正してください。
  • 今後、ここZennの記事を作成する際にこのコードを活用しようと思います。

関連記事

https://zenn.dev/haretokidoki/articles/67788ca9b47b27
https://zenn.dev/haretokidoki/articles/b4f4399570000a
https://haretokidoki-blog.com/pasocon_powershell-startup/
https://zenn.dev/haretokidoki/articles/7e6924ff0cc960
https://zenn.dev/haretokidoki/articles/fb6830f9155de5

GitHubで編集を提案

Discussion