意外と簡単!Pythonを使ってPDFを出力してみた

Pandas.DataFrame で to_excel( ) 、to_csv( ) メソッドを使えば簡単に Excel や CSVに出力できますが、同じようにPDF化して業務で使いたいという要望も意外に多いようです。

先日、お客様と打ち合わせしていたときにも、「ExcelとPDFを同時に出力できない?」というリクエストがありました。

そこで、今回はPythonでPDFを生成してみます。

まずは、PythonでPDF操作するライブラリをまとめました。

PDF操作をするライブラリ

==========================
コードからPDFに変換したいとき
==========================

  • ReportLab

PythonのPDF生成ライブラリの中でも最も一般的で、機能が豊富。X軸やY軸といった指定により、文字や画像などの配置を行うことができます。

==========================
HTMLからPDFに変換したいとき
==========================

  • django-wkhtmltopdf
  • PyPDF
  • PDFKit
  • WeasyPrint

HTML構造のWebページであればPDF化を行うことができます。
ライブラリによっては、JavaScriptを認識して表示した内容をPDF化することもできます。
別途ソフトウェアインストールが必要なものもあります。

今回は、このライブラリの中から、コードからPDFに変換したいときに使う ReportLab で実際にPDFを作成してみます!

動作環境

MacBook pro
Anaconda3(MiniForge3)
Python 3.9.10
reportlab 3.6.12(無料版)

reportlabのインストール

下記コマンドで、インストールします。

$ conda install -c conda-forge reportlab

今回は、気象庁のホームページからスクレイピングで取得した2023年2月26日の東京の気象データ(東京天気.csv)をサンプルにPDFを生成したいと思います。

東京天気.csv

サンプルコード

# -*- coding: utf-8 -*-
import pandas as pd
import datetime
import math 

from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.lib.pagesizes import A4, portrait #用紙の向き
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus.frames import Frame

t_delta = datetime.timedelta(hours=9)
JST = datetime.timezone(t_delta, ‘JST’)
now = datetime.datetime.now(JST)
d = now.strftime(‘%Y%m%d’)
ymd = now.strftime(‘%Y年%m月%d日’)

# 初期設定
def make(pdf_data):
    filename=”気象データ”#ファイル名
    pdf_canvas = set_info(filename) #キャンバス名
    print_string(pdf_canvas, pdf_data)
    pdf_canvas.save() #保存

def set_info(filename):
    pdf_canvas = canvas.Canvas(“./{0}_{1}.pdf”.format(d,filename)) #保存先
    pdf_canvas.setAuthor(“”) #作者
    pdf_canvas.setTitle(“”)  #表題
    pdf_canvas.setSubject(“”)  #件名
    return pdf_canvas

#フォーマット作成
def print_string(pdf_canvas, pdf_data):
    
    maxlow = 45 #行数
    pages = (len(pdf_data) + 1) / maxlow #ページ数
    num = math.ceil(pages)
        
    for i in range(0, len(pdf_data), (maxlow-1)):
        tmp_pdf_data = pdf_data[i: i+(maxlow-1)]
   
        pdfmetrics.registerFont(UnicodeCIDFont(‘HeiseiKakuGo-W5’)) #フォント
        width, height = A4 #用紙サイズ
        font_size = 24 #フォントサイズ

        # (1)タイトル
        pdf_canvas.setFont(‘HeiseiKakuGo-W5’, font_size)
        pdf_canvas.drawString(230, 770, ‘気象実績データ’) #書き出し(横位置, 縦位置, 文字)

        # (2)作成日
        font_size = 7 #フォントサイズ
        pdf_canvas.setFont(‘HeiseiKakuGo-W5’, font_size)
        pdf_canvas.drawString(465, 770,  f’作成日: {ymd}’)

        # (3) 気象データ取得地点
        target_date = pdf_data[0][0]
        prefecture = pdf_data[0][1]

        header_data = [
                [‘取得日 ‘,target_date],
                [‘都道府県 ‘,prefecture],  
            ]

        table = Table(header_data, colWidths=(40*mm,60*mm), rowHeights=5*mm)#tableの大きさ
        table.setStyle(TableStyle([#tableの装飾
                (‘FONT’, (0, 0), (-1, -1), ‘HeiseiKakuGo-W5’, font_size),#フォント
                (‘BOX’, (0, 0), (-1, -1), 1, colors.black),#罫線
                (‘INNERGRID’, (0, 0), (-1, -1), 1, colors.black),
                (‘VALIGN’, (0, 0), (-1, -1), ‘MIDDLE’),#フォント位置
            ]))
        table.wrapOn(pdf_canvas, 20*mm, 250*mm)#table位置
        table.drawOn(pdf_canvas, 20*mm, 250*mm)

        # (4)地点ごとの気象データ
        detail_data = [
                [‘地点’, ‘平均気温(℃)’, ‘最高気温(℃)’,’最低気温(℃)’,’降水量合計(mm)’,’降水量合計(mm)’,’天気(昼)’],
            ]

        temp_data = [row[2:9] for row in tmp_pdf_data]
        detail_data.extend(temp_data)

        if len(detail_data) <= maxlow:
            index_no = maxlow – len(detail_data)

            for idx in range(index_no):
                detail_data.append([” “, ” “, ” “, ” “, ” “, ” “, ” “])

        table = Table(detail_data, colWidths=(25*mm,25*mm,25*mm,25*mm,25*mm,25*mm,25*mm), rowHeights=5*mm)
        table.setStyle(TableStyle([
                (‘FONT’, (0, 0), (-1, -1), ‘HeiseiKakuGo-W5’, font_size),#フォント
                (‘BOX’, (0, 0), (-1, -(index_no + 1)), 1, colors.black),#罫線
                (‘INNERGRID’, (0, 0), (-1, -(index_no + 1)), 1, colors.black),#罫線
                (‘ALIGN’, (1, 0), (6, -1), ‘RIGHT’),#右揃え
                (‘VALIGN’, (0, 0), (-1, -1), ‘MIDDLE’),#フォント位置
            ]))
        table.wrapOn(pdf_canvas, 20*mm, 20*mm)
        table.drawOn(pdf_canvas, 20*mm, 20*mm)

        ## 1枚目終了(改ページ)
        pdf_canvas.showPage()


if __name__ == ‘__main__’:
    
    #気象データ読み込み
    input_fileName = “東京天気.csv”
    df_csv = pd.read_csv(filepath_or_buffer=input_fileName, encoding=’shift_jis’)
    pdf_data = df_csv.values.tolist()

 #PDF作成    
    make(pdf_data)

出力したPDFファイルがこちら↓

20230227_気象データ.pdf

Reportlab の豆知識

◆用紙サイズ

デフォルトでは、A4縦になります。
用紙サイズを指定するには、reportlab.lib.pagesizesで定義されている用紙サイズをCanvas作成時に指定します。

定義済みの用紙サイズは以下のとおり。

◆用紙の向き

用紙の向きを変更するには、reportlab.lib.pagesizesにあるlandscapeとportraitを使います。

ヨコ向き:landscape
タテ向き:portrait

◆改ページ

1ページ目を書き終えて、2ページ目に移る場合はshowPage()を使用します。

公式ドキュメントはこちら

Reportlab を使った感想

今回はシンプルなレイアウトでPDFを出力しましたが、ReportlabはPythonのコード上でスタイルを含めレイアウトを細かく指定できるので用途に応じて好みの帳票が作れそうです。
ただ、細かく指定できる分、コードは長くなりがち…

今回のようにデータ量も少なく画像がないPDFは、ちょっと見た目が寂しいですね。
今度は画像ありのPDF生成にチャレンジしてみようと思います!

まとめ

  • DataframeなどコードからPDFを生成する時は、ReportLab が便利
  • 画像がないと寂しいPDFが生成されてしまう