【Python/openpyxl】Excelファイル上の画像のセル番地を取得する

プログラミング
この記事でやりたいこと

Excel ファイル上の画像が配置されているセル番地を openpyxl を使って取得します。
セル番地の情報をもとに、Excel ファイル上の画像をまとめて保存できるようになります。

背景

Excel ファイル上に配置された画像を一括で保存したいという場面があると思います。
一般的な方法として、Excel ファイルを ZIP 形式に変換して解凍し、
内部の画像ファイルを直接参照する方法が知られています。
しかし、この方法では各画像ファイルとExcel ファイル上でのセル番地を対応づけることができません。
挿入順と配置の関係がバラバラなファイルにおいては、実用上の大きな課題となります。

openpyxl のソースコード[1]を読み漁ったところ、
画像ファイルが配置されているセル番地を取り出せることが分かったので追加で紹介します。
ただし、公式には画像に対応していないようなので参考程度でお願いします。

プログラム

実行環境

動作を確認した環境は下記の通りです。

Python3.12.2
openpyxl3.1.2
pillow10.3.0

インストールされていない方は、先にインストールしてください。

$ pip install openpyxl
$ pip install Pillow
サンプルデータ

いらすとやからお借りした画像で適当なエクセルファイルを作成しました。
今回は、表形式で画像が並べられているものを例とします。

行番号 \ 列番号ABC
1種目女性男性
2短距離走短距離走_女性
3ハードル
4長距離走
5走り幅跳び
6三段跳び
7走り高跳び
8棒高跳び
excel_image_address_2/irasutoya_athletics.xlsx at main · chromch/excel_image_address_2 (github.com)
コード

Notebook やサンプルデータは GitHub に置いてあるのでご覧ください。
chromch/excel_image_address_2 (github.com)

最初に、ライブラリをインポートします。

import openpyxl
from PIL import Image
from io import BytesIO
import os

次に、エクセルファイルを読み込んでワークシートを取得します。

path = 'irasutoya_athletics.xlsx'
reader = openpyxl.load_workbook(path)   # workbook の読み込み
ws = reader.worksheets[0]               # worksheet の取得

ワークシート上の画像リストは、 Worksheet._images で取得できます。

ws._images

各画像が配置されているセル番地の情報は、Image.anchor 内に入っています。
Image.anchor._from で画像の左上、Image.anchor.to で右下のセル情報を取得でき、
行番号と列番号は次のように取得できます。

img = ws._images[0]
row, col = (img.anchor._from.row, img.anchor._from.col)

余談ですが、xml ファイル解析で登場する画像パスは img.path で取得できそうですができません。
内部でハードコーディングされているため、全て ‘/xl/media/image1.png’ になります。

img.path # 全て '/xl/media/image1.png' と返ってくる

内部では値を持っているので、どうしても必要な方はいじってみると良いかもしれません。
openpyxl.reader.excel.ExcelReader.read_worksheets() 内の rel.target という変数に入っています。

続いて、画像のデータを取り出します。
Image._data() でバイナリデータを取得できます。
それを BytesIO 経由で PIL.Image オブジェクトとして読み込むことができます。

img = ws._images[0]
binary_image = img._data()
image = Image.open(BytesIO(binary_image))

ここまでで、画像ファイルとセル番地を取得できるようになりました。

今回のサンプルデータは表形式で画像が並べてあるため、
行見出しや列見出しなどのシート情報を用いて整理を試みます。
B1:C1 が性別を表す列見出し、A2:A8 が種目を表す行見出しです。
セルの値は次のように取得できます。

ws.cell(row=1, column=2).value # '女性' と返ってくる

エクセル自体の行番号や列番号は 1 始まりですが、Image.anchor から取得できるセル番地は 0 始まりです。
それに注意して、画像を保存します。

output_dir = './images'
os.makedirs(output_dir, exist_ok=True)  # 出力フォルダの作成
for img in ws._images:
    # セル番地
    row, col = (img.anchor._from.row + 1, img.anchor._from.col + 1)
    # 画像データ
    binary_image = img._data()
    image = Image.open(BytesIO(binary_image))
    # 見出し情報
    gender = ws.cell(row=1, column=col).value
    event = ws.cell(row=row, column=1).value
    # 保存
    output_path = os.path.join(output_dir, f'{event}-{gender}.png')
    image.save(output_path)
実行結果

実行すると、下記のように画像が出力されます。
このように、エクセル上の画像整理を自動で行えるようになりました。

./images
├── ハードル-女性.png
├── ハードル-男性.png
├── ...
└── 棒高跳び-男性.png

公式には画像に対応していないと記載がありますが、画像ファイルの情報は内部でアクセス可能なようですね。

参考

[1] Eric Gazoni さん, Charlie Clark さん | openpyxl 3.1.3 documentation(2024/9/13 アクセス)
https://openpyxl.readthedocs.io/en/stable/index.html

コメント