8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

F#(fsi)を使ったExcel操作の覚え書き(準備編)

Last updated at Posted at 2021-03-11

(2024.8.31 更新)
個人メモ程度の記事ですが、現在でもポツポツと反応をいただいています。
F# を取り巻く環境も変わってきたということで、現状に合わせて手を入れさせていただきました。

はじめに

高校で非常勤講師をしております。
当然、学校でもあらゆる場面で Excel が利用されていますが、定型作業が大半です。

個人的に Excel の定型作業で F# インタラクティブ(以下 fsi)を重用しているので記事にしてみました。
fsi の詳細はこちらです。

まずは、VBA でマクロを書いた経験のある方、そして、F# に関心のある方を対象に、fsi の導入から単独セルの読み書きまでを準備編としてまとめました。
(実例は実例編で)

本稿は「覚え書き」であり、個人的な試行錯誤の結果をまとめたものです。
不正確な表記や誤りもあるかと思います。
また、Office アプリケーションを外部から操作すると、「COM オブジェクトの解放漏れ」などの問題もつきまといます。
素人の実験記事として参考程度にお取り扱いください。

作業環境

  • Windows 11 Pro(Ver. 23H2)
  • F# 8.0 (.NET SDK 8.0.401 または Visual Studio 2022 Community 17.11.1)
  • Microsoft Excel for Microsoft 365 MSO (バージョン 2407 ビルド 16.0.17830.20166) 64bit
  • PowerShell 7.4.5
  • ThinkPad L15 Gen 1 (Core i5-10210U 8.00GB)

fsi の導入と起動

fsi は Visual Studio や .NET SDK に含まれていますので、どちらかのインストールが必要です。
.NET SDK の方が簡単にインストールできますし、保守も楽です。
どちらかをインストール済みとの前提で話を進めます。

(1) Visual Studio Community 2022 の場合

まずは、PowerShell で fsi.exe を探します。

PowerShell 7
ls "C:\Program Files\Microsoft Visual Studio\2022\" -s fsi.exe | % f*

C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools\fsi.exe
が fsi.exe のフルパスになります。

このフルパスで起動してみます。(起動が確認できたらショートカットやバッチファイルを作ります。)

PowerShell 7
& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools\fsi.exe"

起動メッセージとプロンプトが出たら、.NET のバージョンを確認してみます。

F# Interactive
System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;;
F# Interactive(結果)
val it: string = ".NET Framework 4.8.9261.0"

.NET Framework 4.8 環境であることがわかります。

なお、fsi ではセミコロン2つ「;;」が実行の指示です。
以下、「;;」の有無も意識してお読みください。

fsi の終了は

F# Interactive
#q;;  // #quit

です。一旦終了させておきます。

【画面1】 fsi.exe のフルパス確認~起動 → .NET のバージョン確認 → fsi の終了

(2) .NET 8 SDKをインストールした場合

コマンドラインからdotnet fsiで起動します。
こちらは .NET 8 環境になります。

【画面2】 dotnet fsi による起動 → .NET のバージョン確認 → fsi の終了

(1) と (2) の違いは最後に補足しておきますが、以下の作業ではどちらの環境でも問題ありません。本稿は両環境で動作確認しました。

fsi から Excel を起動する

初回投稿時から状況が変わり、参照設定できない場合がありました。
確実な方法として、アセンブリをフルパスで参照設定するように変更しました。

アセンブリの確認

Excel 起動前に必要なアセンブリ(次の2つ)を探します。

  • Microsoft.Office.Interop.Excel.dll
  • office.dll
PowerShell 7
ls C:\Windows\assembly\ -s M*Excel.dll | % f*
ls C:\Windows\assembly\ -s office.dll | % f*

私のPC環境では
C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Excel.dll

C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\OFFICE.DLL
が見つかりました。
(Office のバージョンやPCの環境によって異なる場合があります。)

fsi から Excel を起動

あらためて fsi を起動します。
以下のコードを fsi にコピペするとブックのない Excel が起動します。

コード先頭の#rで、先ほど見つけたアセンブリへの参照を設定するので、こちらは個々の環境に合わせてください。パス直前の@を忘れると\がエスケープ文字と解釈されて失敗します。

F# Interactive
#r @"C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Excel.dll"
#r @"C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\OFFICE.DLL"
open System.Runtime.InteropServices  // 後始末で利用
open Microsoft.Office.Interop.Excel
let ex: _Application = ApplicationClass(Visible = true)
;;

もし、手入力する場合は大文字・小文字の区別があることに注意してください。
また、入力時にはTABキー補完、上下カーソルキーでの入力履歴が使えます。

途中のopen System.Runtime.InteropServicesは後述する「後始末」で使います。

ブックとワークシートへのアクセス

Excel の起動に成功したら、ブックを作成(または開く)し、ブックやワークシートに名前(識別子)をつけます。(let 束縛)
起動させた Excel の識別子はexです。

let 束縛時、識別子にはex: _Applicationのように型を明記できます。
型を省略した場合は型推論されますが、トラブル回避のため明記した方がよいでしょう。

F# Interactive
let wb: _Workbook = ex.Workbooks.Add()  // 新規ブックを作成する場合
// 既存ファイルを開く場合の例
// let wb : _Workbook = ex.Workbooks.Open(@"C:\Users\user1\Desktop\sample.xlsx")
let ws: _Worksheet = downcast wb.Worksheets[1]
;;

コード下行のdowncastについて簡単に補足しておきます。

F# で Excel 内部のオブジェクトを取得するとobj型を返す場合があります。
そのときはdowncast演算子で本来の型にキャストする必要があります。

キャストに関しては後半でも触れます。

単独セルの操作

まず、セル操作の前に Sheet1 へ適当なデータを入力します。
下表の範囲をコピーして、fsi で起動した Excel のセル A1 に [形式を選択して貼り付け - テキスト] でデータを入れます。

番号 読み
1 阿部 アベ
2 伊藤 イトウ
3 植田 ウエダ
4 遠藤 エンドウ
5 大石 オオイシ

貼り付け後のイメージ
ex1.png

RangeCells

最初に VBA でおなじみのRangeCellsについて確認します。
セル B2 をそれぞれの方法で let 束縛してみますが、あえて、型を明記しないでおきます。

F# Interactive
let cell1 = ws.Range("B2")
let cell2 = ws.Cells[2, 2]
;;
F# Interactive(結果)
val cell1: Range
val cell2: obj

cell1の型はRangeです。一方、cell2objです。

cell2の右辺 ws.Cells[2, 2]ws.Cells.Item(2, 2)の省略形です。
Cellsプロパティは全セル範囲をRangeで返しますが、その先の Itemプロパティがobjを返します。

Cells[row, col]だけではセルの値を取得できません。
VBA でValueの省略に慣れている方はご注意ください。

それぞれについてValueプロパティで値を見てみます。
Valueプロパティは引数をとるので、引数は省略できても( )は省略できません。Value()とします。

F# Interactive
cell1.Value();;
F# Interactive(結果)
val it: obj = "阿部"

cell1から文字列を取得できましたが、型はobjということに注意してください。

let 束縛のない式を実行すると、暗黙的にitという識別子で束縛されます。
新たに書き換えられない限りitを利用することができます。

cell2Valueプロパティを確認します。

F# Interactive
cell2.Value();;
F# Interactive(結果)
error FS0039:  'Object' は、フィールド、コンストラクター、またはメンバー 'Value' を定義していません。

cell2objなので、Valueプロパティが使えません。
前述のようにRangeにダウンキャストする必要があります。

キャストを考慮した例です。

F# Interactive
let cell3: Range = downcast ws.Cells[2, 2]
let cell4: Range = ws.Cells.Resize(1, 1).Offset(1, 1)  // キャスト不要
;;
F# Interactive(結果)
val cell3: Range
val cell4: Range

cell3objからRangeにキャストされています。

cell4Resize, Offsetプロパティを使ってセル B2 を指しています。
これらのプロパティはRangeを返してくれるのでキャストが不要です。
単独セルに限らず、セル範囲を数値で指示したい場合に重宝します。

Cells(シート上の全セル範囲)に対して、リサイズ前にオフセットするとセル範囲がオーバーフローするのでResizeを先にします。

F# Interactive(エラーになります!)
let cell4 = ws.Cells.Offset(1, 1).Resize(1, 1);;  // Resize を前に置くこと!

値の取得

セルの値はValueValue2プロパティなどで取得します。
Value2が簡便ですが、日付データの取得にはValueプロパティが適切です。
(詳しくは実例編で)

ValueValue2objを返します。

F# Interactive
// キャストしない場合は obj を返す
let no1 = ws.Range("A2").Value()
let sei1 = ws.Range("B2").Value()
let yomi1 = ws.Range("C2").Value()
;;
F# Interactive(結果)
val no1: obj = 1.0
val sei1: obj = "阿部"
val yomi1: obj = "アベ"

obj型はfloatintは不可)やstringにキャストしないと数値や文字列としての処理ができません。

挿入文字列への挿入や、Object.ToStringで文字列を作る場合などはキャスト不要です。(後述)

通常はキャストしましょう。

F# Interactive
let no1: float = downcast ws.Range("A2").Value()
let sei1: string = downcast ws.Range("B2").Value()
let yomi1: string = downcast ws.Range("C2").Value()
;;

キャストには:?>演算子を使う方法もあります。こちらが少し簡潔です。

F# Interactive
// :?> 演算子を使う方法
let no1 = ws.Range("A2").Value() :?> float
let sei1 = ws.Range("B2").Value() :?> string
let yomi1 = ws.Range("C2").Value() :?> string
;;

どちらも結果は同じです・

F# Interactive(結果)
val no1: float = 1.0
val sei1: string = "阿部"
val yomi1: string = "アベ"

詳細は以下を参照してください。

おまけ

Textプロパティは数値も文字列として返します。(要キャスト)
他にObject.ToStringによる文字列の取得、'obj'型から .NET API で型変換もできます。

F# Interactive
let a3_text = ws.Range("A3").Text :?> string
let a3_tostr = ws.Range("A3").Text.ToString()
let a3_int = ws.Range("A3").Value2 |> System.Convert.ToInt32
;;
F# Interactive(結果)
val a3_text: string = "2"
val a3_tostr: string = "2"
val a3_int: int = 2

値の書き込み

値の用意

とりあえず、先ほど取得したデータを加工して、書き込み用に文字列を作ります。
ここでは、挿入文字列を使ってみましょう。

F# Interactive
let kojin1 = $"{no1,2} {sei1}({yomi1})";;
F# Interactive(結果)
val kojin1: string = " 1 阿部(アベ)"

結合した文字列kojin1ができました。

挿入文字列に埋め込む値はobjでも構いません。

値を書き込む

fsi で上書きされたセルは Excel で元に戻せません。
値の書き込みには細心の注意を払ってください。

セルへの書き込みはValue2プロパティの使用が簡潔です。
Value2プロパティを使って、文字列kojin1をセル E2 に書き込んでみます。

F# はプロパティのセットに<-を使います。

F# Interactive
ws.Range("E2").Value2 <- kojin1;;

Excel のセル E2 に文字列が表示されるはずです。

Value2でセットされた文字列や数値は Excel 側で解釈してくれます。
たとえば、文字列 "20240101" は数値、 "2024/01/01" は日付として解釈されます。

Valueプロパティを使う場合

Valueによる書き込みは引数を省略できません。
文字列kojin1をセル G2 に書き込んでみます。

F# Interactive(失敗します)
ws.Range("G2").Value() <- kojin1;;  // Value() は引数が必要
F# Interactive(エラー)
error FS0193: 内部エラー: 入力は負以外である必要があります。 (Parameter 'n')

引数を渡せば動作します。

F# Interactive
ws.Range("G2").Value(XlRangeValueDataType.xlRangeValueDefault) <- kojin1;;

または

F# Interactive
ws.Range("G2").Value(null) <- kojin1;;

Valueプロパティは文字列や数値の他にSystem.DateTime型なども渡すことができます。

Cells[,]プロパティも使えます

セル I2 (2行9列)に書き込んでみます。

F# Interactive
ws.Cells[2, 9] <- kojin1;;

ドキュメントを見るとItemプロパティはセット可能です。

単独セルの値をコピーするだけなら、VBA 的な書き方が可能です。

F# Interactive
ws.Cells[1, 5] <- ws.Cells[1, 1];;

for ループで連続コピーも可能です。

F# Interactive
for row in 2..6 do
    ws.Cells[row, 5] <- ws.Cells[row, 1]
;;

一旦、後始末しましょう。

お疲れさまでした。

ここまでは F# を使うメリットが見えませんが、実例編では VBA では得られない F# の強力さと爽快感を書きます。

では、重要な後始末です。
いくら注意していても「COMオブジェクトの解放漏れ」をやらかします。
この件に関してはネット上でも長年議論が続いてますし、説明もできません。
ということで、次の記事などを参考にしてアレンジしています。

これらの記事で指摘されている「中間オブジェクト」(暗黙的に作成されるオブジェクト)の件も承知していますが、本稿では見なかったことにしています。

本稿中に隠れている「中間オブジェクト」の例
ex.Workbooks.Add() --- 途中にWorkbooksができる
wb.Worksheets[1] --- 途中にSheetsができる
ws.Cells[2, 2] --- 途中にRangeができる
ws.Range("A2").Value() --- 途中にRangeができる
大半のコードで「中間オブジェクト」が生じています。

以下のコードで終了時の処理を行います。

F# Interactive
;;
// ↑ 終了処理の前に式や関数を評価しておくこと
Marshal.ReleaseComObject(cell4)
Marshal.ReleaseComObject(cell3)
Marshal.ReleaseComObject(cell2)
Marshal.ReleaseComObject(cell1)
Marshal.ReleaseComObject(ws)
wb.Close()  // ファイル保存確認あり
// wb.Close(true)  // 上書き保存して閉じる
// wb.Close(false) // 保存しないで閉じる
Marshal.ReleaseComObject(wb)
ex.Quit()
Marshal.ReleaseComObject(ex)
System.GC.Collect()
System.GC.WaitForPendingFinalizers()
System.GC.Collect()
;;

これまでの経験で注意してきた点を記します。

終了処理の前に;;を置く
詳しくは実例編に書きますが、fsi を使った処理に慣れてくると、終了処理までのコードを一気に流してしまうようケースも出てきます。
上の例のように終了処理前に;;を置き、式や関数の評価を済ませておかないと、環境にもよりますが、隠れ Excel プロセスが発生します。

● fsi で起動した Excel は、fsi で終了させる
もし、自分で Excel を閉じてしまった場合は、下記コードからwb.Close(), ex.Quit()を抜くことになりますが、プロセスが残る可能性は大きくなります。

● let 束縛した Excel のオブジェクトをこまめに解放(Marshal.ReleaseComObject)する。
できれば、作業の途中でも COM オブジェクトを使い終わった段階で解放してください。

未解放の識別子を使い回し(上書き)しないでください
COM オブジェクトが生き埋めになります。
特にループ内は要注意です。

fsi の場合、COM オブジェクトがitで返ってきた場合は、すぐにitを解放してください。
例えば、セル A1 の内容を確認しようとして、うっかり.Value2を忘れると・・・

F# Interactive
ws.Range("A1");;

長いシーケンスが返ってきます...

F# Interactive(結果)
val it : Range =
  seq
    [seq
       [seq
          [seq
     (以下省略)

画面が埋め尽くされてしまいますが、スクロールアップして確認するとitRangeオブジェクトです。

すかさず

F# Interactive
Marshal.ReleaseComObject(it);;

で、解放してください。
未解放のまま新たなitが作成されると「生き埋め」状態になります。

さて、先ほど実行したcell1.Value()で返されたitの場合は?

F# Interactive(結果)(再掲)
val it : obj = "阿部"

このitobjですが、中身は COM オブジェクトではなくstringなので解放不要です。

先ほど作成したcell2objですが、こちらは中身が COM オブジェクトRangeなので解放しました。

後始末を忘れて fsi を閉じたり、オブジェクト解放を怠ると Excel を終了してもプロセスだけが残り、メモリを消費します。

プロセスが残っていないか確認しましょう

Excel が全て閉じられていても次の手順で確認しましょう。

PowerShell 7
ps excel
PowerShell 7 <成功>(プロセス "excel" が見つからない)
Get-Process: Cannot find a process with the name "excel". Verify the process name and call the cmdlet again.
PowerShell 7 <失敗>(見えないところで Excel が生存)
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     48    65.55     115.77       2.25    5880   7 EXCEL

失敗した場合は以下のコマンドで Excel のゾンビを退治します。

PowerShell 7
kill -name excel

単にkillするだけでは対策が不十分との意見もありますが、個人利用の範囲ではこの程度で十分かと思います。
あとは自己責任でお願いします。

おわりに

fsi.exe と dotnet fsi において F# の言語機能は全く同じです。
対象のフレームワークが .NET Framework か .NET 8 かの違いになります。

フレームワークの違いが影響する例として .NET Framework では、GetActiveObjectメソッドが使えます。
System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application")
により、既に起動している Excel も操作可能です。

一方で、.NET 8 にはGetActiveObjectがありません。
互換性が必要な場合は API のドキュメントを確認しておきましょう。

以下、「学校業務でありそうな作業」を例にした「実例編」に続きます。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?