【設計思想】BOMデータ抽出の罠を回避せよ:堅牢なCSVエクスポート・アーキテクチャ
エンジニア諸君、業務自動化の現場で「とりあえず動くコード」を書いて満足していないか?
図面内の部品表(BOM)からデータを抽出する。一見、単純なルーチンに見えるだろう。しかし、現場の図面は、設計者の好みやテンプレートの変更によって、列の順番が入れ替わり、列が追加され、時には意図しないテーブルが混入する。
もし君が「3列目は材質、5列目は重量」といったマジックナンバー(ハードコード)に基づいたコードを書いているとしたら、それは自動化ツールではなく、単なる「時限爆弾」だ。運用が始まれば、必ずどこかでデータがズレ、ERP(基幹システム)にゴミデータを流し込み、会社に甚大な損害を与えることになる。
本稿では、プロフェッショナルとして、「列の構成が変わっても壊れない」「例外的な図面構造でも死なない」、真にプロダクションレベルに耐えうるBOM抽出ロジックの設計思想と実装を伝授する。
1. 避けるべきアンチパターン:インデックス依存の設計
多くの初級者が陥る失敗は、以下のようなアプローチだ。
' --- 典型的な失敗例 (Anti-Pattern) ---
For i = 1 To rowCount
material = swTable.GetText(i, 2) ' 2列目が材質だと決め打ち
weight = swTable.GetText(i, 5) ' 5列目が重量だと決め打ち
' ... CSV出力
Next i
この設計の欠陥は明白だ。設計者が「備考」列を左端に挿入した瞬間、このプログラムは「材質」の代わりに「備考」を抽出し、システムに誤った情報を送り込む。「列のインデックス(番号)」ではなく、「ヘッダー名(文字列)」でデータを特定する。これが鉄則だ。
2. アーキテクチャの要:ヘッダー・マッピング戦略
堅牢なツールを作るためには、以下の3フェーズで処理を分離すべきである。
1. Discovery(探索): 図面内のすべてのテーブルを走査し、対象となるBOMテーブルを特定する。
2. Mapping(写像): BOMのヘッダー行をスキャンし、「どの列名が、何番目のインデックスに対応するか」をメモリ上の辞書(Dictionary)に格納する。
3. Extraction(抽出): 辞書を参照して、指定したプロパティ名に基づきデータを正確に抜き出す。
これにより、列の順番がどう変わろうとも、プログラムはヘッダーの名前を探し出し、動的に対応できる。
3. プロダクション・コードの実装例
以下に、実務でそのまま使えるレベルの設計思想を組み込んだコードを示す。
※ `Microsoft Scripting Runtime` の参照設定は不要なよう、`CreateObject` によるレイトバインディングを採用している。これにより、配布時の環境依存トラブルを最小限に抑えている。
Option Explicit
''' <summary>
''' 図面内のBOMから指定したカラムを抽出し、CSV出力するメインルーチン
''' </summary>
Public Sub ExportBOMToCSV()
Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swDraw As SldWorks.DrawingDoc
Dim swSheet As SldWorks.Sheet
Dim swView As SldWorks.View
Dim swTableAnn As SldWorks.TableAnnotation
Dim csvPath As String
On Error GoTo ErrorHandler
Set swApp = Application.SldWorks
Set swModel = swApp.ActiveDoc
' 1. 文脈チェック:図面ファイルが開かれているか?
If swModel Is Nothing Then Exit Sub
If swModel.GetType <> swDocDRAWING Then
MsgBox "図面ファイルを開いた状態で実行してください。", vbCritical
Exit Sub
End If
Set swDraw = swModel
' 保存先パスの設定(実務ではダイアログで選択させるのが望ましい)
csvPath = swModel.GetPathName
If csvPath = "" Then
MsgBox "ファイルを保存してから実行してください。", vbExclamation
Exit Sub
End If
csvPath = Left(csvPath, InStrRev(csvPath, ".")) & "csv"
' 2. BOMテーブルの探索と抽出
' 図面の全ビューを走査してテーブルを探す
Set swView = swDraw.GetFirstView ' Sheet自体が最初のView
Do While Not swView Is Nothing
' ビュー内のテーブルを取得
Dim swViewDoc As SldWorks.View
Set swViewDoc = swView
' ビューに紐づくAnnotationを確認
Dim swAnn As SldWorks.Annotation
Set swAnn = swViewDoc.GetFirstAnnotation
Do While Not swAnn Is Nothing
If swAnn.GetType = swAnnotationTypeTable Then
Set swTableAnn = swAnn.GetSpecificAnnotation
' テーブルがBOM(部品表)かどうかを判定
' 注: TableTypeによって判定。機種により異なる場合があるため注意
If swTableAnn.Type = swTableTypeBom Then
' 抽出実行
ProcessBOMTable swTableAnn, csvPath
GoTo SuccessExit ' 最初のBOMを見つけたら終了(複数ある場合はループ設計を変更)
End If
End If
Set swAnn = swAnn.GetNext
Loop
Set swView = swView.GetNext
Loop
MsgBox "対象のBOMテーブルが見つかりませんでした。", vbInformation
Exit Sub
SuccessExit:
MsgBox "エクスポートが完了しました:" & vbCrLf & csvPath, vbInformation
Exit Sub
ErrorHandler:
MsgBox "致命的なエラーが発生しました。" & vbCrLf & Err.Description, vbCritical
End Sub
''' <summary>
''' 特定のテーブルからデータを抽出し、CSVに書き出す
''' </summary>
Private Sub ProcessBOMTable(swTable As SldWorks.TableAnnotation, filePath As String)
Dim dictCols As Object
Set dictCols = CreateObject("Scripting.Dictionary")
Dim rowCount As Long, colCount As Long
rowCount = swTable.RowCount
colCount = swTable.ColumnCount
' --- Phase 1: Mapping (ヘッダーのスキャン) ---
Dim c As Long
Dim headerText As String
For c = 0 To colCount - 1
headerText = Trim(swTable.GetText(0, c)) ' 0行目がヘッダー
If headerText <> "" Then
' 重複ヘッダー対策として、既存のキーがある場合はインデックスを保持
If Not dictCols.Exists(headerText) Then
dictCols.Add headerText, c
End If
End If
Next c
' --- Phase 2: Extraction (データの走査) ---
' 抽出したいターゲットカラムの定義(ここを変更するだけでカスタマイズ可能)
Dim targets As Variant
targets = Array("部品番号", "材質", "重量", "仕入先") ' 実際のヘッダー名に合わせる
Dim fileNum As Integer
fileNum = FreeFile
Open filePath For Output As #fileNum
' CSVヘッダーの書き出し
Dim targetHeader As String
targetHeader = Join(targets, ",")
Print #fileNum, targetHeader
' データ行の書き出し
Dim r As Long, t As Variant
Dim cellValue As String
Dim lineData As String
For r = 1 To rowCount - 1 ' 1行目(データ開始行)から
lineData = ""
For Each t In targets
cellValue = ""
' 辞書にターゲットが存在するか確認
If dictCols.Exists(CStr(t)) Then
cellValue = swTable.GetText(r, dictCols(CStr(t)))
End If
' CSVのカンマ混入対策(ダブルクォーテーション囲い)
lineData = lineData & """" & Replace(cellValue, """", """""") & """"
If t <> targets(UBound(targets)) Then lineData = lineData & ","
Next t
Print #fileNum, lineData
Next r
Close #fileNum
End Sub
4. チーフアーキテクトの視点:運用における3つの注意点
このコードを実務に投入する際、君が考慮すべき「プロの作法」が3つある。
① CSVのエンコーディング問題
上記のコードは、標準的な `Print #` 文を使用しているため、システムのロケール(通常はShift-JIS)に依存する。もし、出力先のシステムが UTF-8 (BOM付き) を要求する場合は、`ADODB.Stream` オブジェクトを使用して書き出すロジックに差し替える必要がある。ここを疎かにすると、「文字化け」という名の低レベルなバグに泣かされることになる。
② エラーハンドリングの階層化
コード内に `On Error GoTo ErrorHandler` を配置したが、実務では「ファイルが書き込み禁止(Excelで開いている)場合」「BOMのヘッダー名が想定と微妙に違う場合(例:『材質』ではなく『Material』)」など、エラーのパターンを予測し、ユーザーに的確な指示を出せるようにすべきだ。
③ パフォーマンスの重み
`swTable.GetText` は、セルにアクセスするたびにAPIコールを発生させる。数千行のBOMを扱う場合、この呼び出しコストは無視できない。もし極限の速度を求めるなら、一度全てのデータを配列(Variant Array)に吸い上げ、メモリ上で処理を完結させる設計へ昇華させよ。
結論
自動化とは、単に「作業を楽にする」ことではない。「人間が介在することで発生するエラーを、設計の力で根絶すること」である。
列番号に依存したコードを書くのは、今日限りでやめろ。ヘッダーを動的に解釈し、構造の変化に耐えうる、真に「堅牢な」コードを書くのだ。幸運を祈る。
