はじめに
昨日、Jupyter上にExcel風のスプレッドシートを作成する記事を拝見しました。
そして思いました。
シームレスに本家Excelとデータを受け渡しできれば、それが一番最強なのでは
と。
つまり「Excelでインタラクティブにデータを触りたい場合にはExcelにデータを渡して、Excelで編集したデータをPythonで使用したい場合にはExcelからデータを受け取る。」この当たり前の動作を、ファイル入出力などの面倒な操作を介さずに即座に行えれば非常に捗りそうです。
どうしたか
Pythonで表形式のデータを扱う定番といえばPandasのDataFrameです。このクラスを継承して、Excelとの連携機能を追加します。
実装例
win32com
は日時データの扱いに癖があるため少しごちゃついてますが、Excelを開いてデータを読み書きしているだけですので、やっていることは至ってシンプルです。
df.push()
でExcelにデータを渡し、df.pull()
でExcelからデータを受けとれます。それ以外は通常のpd.DataFrameと同様に扱うことができます。
import pandas as pd
from datetime import datetime
import win32com.client
import pywintypes
import numpy as np
from openpyxl.utils import get_column_letter
class DataFrame(pd.DataFrame):
@property
def _constructor(self):
return DataFrame
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def push(self):
self._connect()
self.sheet.Cells.Clear()
row, col = self.shape
headers = [str(i) for i in self.columns]
self.sheet.Range("A1", f"{get_column_letter(col)}1").NumberFormatLocal = "@"
self.sheet.Range("A1", f"{get_column_letter(col)}1").Value = headers
values = self._to_pydatetime(self.values)
self.sheet.Range("A2", f"{get_column_letter(col)}{row+1}").Value = values
def pull(self):
self._connect()
col, row = self._get_range()
headers = self.sheet.Range("A1", f"{get_column_letter(col)}1").Value
headers = headers[0] if isinstance(headers, tuple) else [headers]
data = self._to_pydatetime(
self.sheet.Range("A2", f"{get_column_letter(col)}{row+1}").Value
)
return DataFrame(data, columns=headers)
def _get_range(self):
return (
self.sheet.Cells(1, self.sheet.Columns.Count).End(-4159).Column,
self.sheet.Cells(self.sheet.Rows.Count, 1).End(-4162).Row - 1,
)
def _connect(self):
try:
self.app = win32com.client.GetActiveObject("Excel.Application")
if not self.app.visible:
self.app.visible = True
self.workbook = self.app.Workbooks.Add()
else:
self.workbook = self.app.Workbooks[0]
self.sheet = self.workbook.Worksheets("Sheet1")
except Exception as e:
self.app = win32com.client.Dispatch("Excel.Application")
self.app.visible = True
self.workbook = self.app.Workbooks.Add()
self.sheet = self.workbook.Worksheets("Sheet1")
def _to_pydatetime(self, x):
def f(x):
return (
x.to_pydatetime()
if isinstance(x, pd._libs.tslibs.timestamps.Timestamp)
else datetime.fromisoformat(x.isoformat())
if isinstance(x, pywintypes.TimeType)
else x
)
return np.vectorize(f)(x)
おわりに
良いPythonライフを。