出発点
企業や官公庁などが、ウェブサイトで公表する各種資料では、PDF形式が使われることが多い。
ただ、文章などはともかく、統計値など数値データは、PDFにされると、再利用に困る・・・
(同時にCSVなどで数値データ部分を公表してくれれば良いが、なかなか広まらない)
一応エクセルでは、データ→データの取得→ファイルから→PDFから、でPDFのテーブルデータを取り込めるが、なかなかに使いにくい(複数ファイルを一括処理するとさらに大変)
そこで、Pythonでうまいこと一括してテーブル部分のデータを取得できないかと考えた。
とりあえずやってみた(その1)
基本方針は、pdfplumberで読込、テーブル部分を検知、テーブル部分のみExcel書き出し
・1ページあたり1テーブルが前提
・1ファイルに複数ページある場合を考慮して、すべて連結
・2ページ目以降の先頭行をヘッダとみなして、除外可能
・列幅を自動制御
def export_table(file, output_xlsx, header = True):
tables = []
with pdfplumber.open(file) as pdf:
for n, page in enumerate(pdf.pages):
#テーブルと認識した部分のみ抽出
table = page.extract_table()
if table is not None:
#1ファイルに複数ページある場合、先頭行をヘッダとみなして除外
if not header and n > 0:
table.pop(0)
tables.extend(table)
if len(tables) == 0:
return None
#Pandasで見やすくなるように処理
df = pd.DataFrame(tables[1:], columns=tables[0])
# 空文字列やスペースだけをNaNに変換
df.replace(r'^\s*$', pd.NA, regex=True, inplace=True)
# 全列が空の行を削除
df.dropna(how='all', inplace=True)
# いったんメモリ上にexcelとして書き出し
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Sheet1')
output.seek(0)
excel_bytes = output.getvalue()
#改行と列幅調整
wb = load_workbook(filename=io.BytesIO(excel_bytes))
ws = wb.active
if ws is not None:
for row in ws.iter_rows():
for cell in row:
cell.alignment = Alignment(wrap_text=True)
# 改行考慮で列幅を調整
for col_cells in ws.columns:
max_length = 0
column = col_cells[0].column
if column is None:
continue
column_letter = get_column_letter(column)
for cell in col_cells:
if cell.value:
lines = str(cell.value).splitlines()
longest_line = max(lines, key=len)
max_length = max(max_length, len(longest_line))
adjusted_width = (max_length + 2) * 1.2 # 調整
ws.column_dimensions[column_letter].width = adjusted_width
try:
wb.save(output_xlsx)
except Exception as ex:
print(ex)
return False
else:
return True
問題点
pdfplumberは、ものすごく便利で、PDFファイル内の罫線を自動認識し、テーブル(二次元配列)として出力してくれる。
たいていの場合は、これだけで問題ないが、テストを続けていくと、日本語の壁にぶつかった。
一部環境では、日本語部分が空白文字になる問題が出現した。
(例: 2020年1月1日 → 2020 1 1となる)
おそらくCMap絡みと思われる事象が頻発し、残念ならpdfplumberは、この問題に対応していないようだった。
解決案
pdfplumberでは読めない漢字もPopplerでは読める!
Popplerのpdftotextでは、文字の位置も取得できるため、位置の基準の変換は必要であるが、うまく組み合わせると、いけるのでは・・・・と考えたが、かなり時間がかかりそうだったため断念・・・。
あれやこれや試行錯誤中、ふと思いついて、いったんpdftocairoでpdf出力してから、pdfplumberで読み込ませたら、見事に意図したとおり日本語が読み込めた!
とりえずやってみた(その2)
動きとしては、subprocessで(pdftocaito -pdf)を標準出力へ→pdfplumberで読込→以下おなじ
、という感じで
def cairo_pdf(file:Path):
#標準出力へ(-)
cmd = [
"pdftocairo",
"-pdf",
str(file),
"-",
]
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0:
output = result.stdout
if output:
# pdfplumberで読み込めるよう変換
return io.BytesIO(output)
else:
raise IOError
else:
print("Error:", result.stderr)
raise Exception
pdfplumberはio.BytesIOを引数にとれるため、そのまま指定すればOK
副産物として、pdfplumberで罫線認識がうまくいかないPDFファイルも、いったん pdftocairo -pdfを介すとうまく読み込める場合があった。
おまけ
よくあるPDFファイル(Excelファイルでテーブル作って、Microsfot print to pdfしたもの等)はほとんどpdfplumberだけで読めるが、一部業務用システムの出力したPDFファイルがかなりの曲者。
なかには縦書きのセル幅を縮めて無理やり横書きにしたり・・・
本件も、一部企業が出すPDFファイルへの対応のために試行錯誤した記録となります。
利用用途としてはデータ分析なため、テーブルレイアウトの保持は絶対条件、かつ、なるべく汎用性ありとなかなかに大変でした。