[F# + Open XML SDK] Word文書（.docxファイル）からテキストを抽出

## はじめに
同じ様式で作成された複数のWord文書（.docxファイル）から、表の指定セルのテキストを抽出する案件に対応した際のメモとして残しておきます。

**Python + python-docx** を使った事例はよく見かけますが、ここでは、**F# + Open XML SDK for Office** を採用しました。
ここを読めば Office のファイルに関して大半のことはできそうです。

https://learn.microsoft.com/ja-jp/office/open-xml/open-xml-sdk

## 環境
* Windows 11 Pro 23H2
* F# 8.0 (.NET SDK 8.0.304)
* ThinkPad L15 Gen 1 (Core i5-10210U 8.00GB)

## とりあえずテキスト抽出（表などを無視）
次の内容が記録されたファイル <i>**example.docx**</i> を材料にします。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/5d702ec7-f737-34a4-0c2b-c8e3eb72b385.png width=480px>

表などを無視して、段落単位でテキスト抽出・保存する場合は簡潔に記述できます。
```fsharp:F#
#r "nuget: DocumentFormat.OpenXml"
open DocumentFormat.OpenXml.Packaging
open DocumentFormat.OpenXml.Wordprocessing

let docxPath = @"C:\Users\User\example.docx"
let txtPath = @"C:\Users\User\example.txt"
let doc = WordprocessingDocument.Open(docxPath, false)  // Read only
let txt =
    doc.MainDocumentPart.Document.Body.Elements<Paragraph>()
    |> Seq.map _.InnerText

doc.Dispose()
System.IO.File.WriteAllLines(txtPath, txt)
```
F# Interactive での実行結果です。
続けて`cat(Get-Content)`コマンドで、保存されたテキストの内容を確認しています。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/99bb9c3a-ed40-c70b-4ce7-20a4270b6734.png width=480px>
表以外の文字列がテキストファイルになっています。

::: note warn
処理の際、対象になるWord文書は閉じておく必要があります。
:::

## 表の内容も含めて抽出
こちらが参考になります。

https://learn.microsoft.com/ja-jp/office/open-xml/word/how-to-change-text-in-a-table-in-a-word-processing-document?tabs=cs-0%2Ccs-1%2Ccs-2%2Ccs-3%2Ccs

文書中の表も含めてテキスト抽出・保存してみます。

表は列をタブ区切りにします。
複数行を持つセルについてはセル内改行によるイメージの乱れを防ぐために、改行の代わりに文字列<b>`<LF>`</b>を挿入しています。

表をテキストにする関数も、F#のパイプラインによる記述で処理がつかみやすいと思います。
ここでは、理解しやすくするために型も可能な限り明記しています。
```fsharp:F#
#r "nuget: DocumentFormat.OpenXml"

open DocumentFormat.OpenXml
open DocumentFormat.OpenXml.Packaging
open DocumentFormat.OpenXml.Wordprocessing

let docxPath: string = @"C:\Users\User\example.docx"
let txtPath: string = @"C:\Users\User\example.txt"

// 表をテキストにする関数
let getTableText (tbl: Table) : string =
    tbl.Elements<TableRow>()
    |> Seq.map (fun (row: TableRow) ->
        row.Elements<TableCell>()
        |> Seq.map (fun (cell: TableCell) ->
            cell.Elements<Paragraph>()
            |> Seq.map _.InnerText
            |> String.concat "<LF>")
        |> String.concat "\t")
    |> String.concat "\n"

// 「通常の段落」と「表」に応じた処理をする関数
// 渡された要素に対して型テストパターンで分岐
let getText (element: OpenXmlElement) : string =
    match element with
    | :? Paragraph as para -> para.InnerText
    | :? Table as tbl -> getTableText tbl
    | _ -> ""

// docxファイルを開いて Body 内の要素順に処理を進める
let doc: WordprocessingDocument = WordprocessingDocument.Open(docxPath, false) // Read only

let txt: string seq =
    doc.MainDocumentPart.Document.Body.Elements()
    |> Seq.map getText

doc.Dispose()
System.IO.File.WriteAllLines(txtPath, txt)

```
F# Interactive での実行結果（上部省略）と`cat`コマンドによる確認結果ですが、想定どおりに保存されました。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/2e89fd23-b6e3-cab7-8301-55bba64157c4.png width=480px>
基本的な操作ができれば、指定した表の指定したセルからのテキスト抽出にも容易に対応できます。最後はスクリプトとして一括処理してミッション完了です。

**Open XML SDK for Office** は公式ドキュメントも充実していますし、F#  Interactive ですぐに確認できるので便利ですね。

## おまけ
調査中に発見したこちらの「力技」（下記記事後半のワンライナー：一部改変）も試した結果です。

https://engineer-milione.com/tips/word-text.html

これ、結構好きです。用途によってはこれで十分。（WSL2 + Ubuntu 22.04.4 で実行）
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/c40f8f8a-a9eb-5af6-e2df-da252156f143.png width=640px>