user:
以前、一緒にこのコードを作りましたよね。このコードの要点を解説していただけますか?
def extract_structured_tables_from_pdf_url(url):
"""
指定されたURLのPDFから表とタイトルを抽出し、LLMで整形したDataFrame+タイトルを返す。
Returns:
list[dict] -- [{ "title": str, "data": pd.DataFrame }]
"""
try:
response = requests.get(url)
response.raise_for_status()
pdf_bytes = BytesIO(response.content)
raw_tables = []
with pdfplumber.open(pdf_bytes) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
tables = page.extract_tables()
if not tables:
continue
# 各テーブル処理
for table_idx, table in enumerate(tables):
if not table:
continue
# None→空文字変換
clean_text = "\n".join(
[", ".join([cell if cell is not None else "" for cell in row])
for row in table if any(row)]
)
# そのテーブルの前後テキストからタイトル候補を抽出
surrounding_text = page.extract_text()
surrounding_lines = surrounding_text.split("\n") if surrounding_text else []
title_candidate = ""
if len(surrounding_lines) > 0:
# 最後の表の直前の行(あるいは最初の行)を使う
title_candidate = surrounding_lines[max(0, len(surrounding_lines) - 5)].strip()
raw_tables.append({
"page": page_num,
"table_text": clean_text,
"title_hint": title_candidate
})
structured_results = []
for i, item in enumerate(raw_tables):
print(f"📄 Page {item['page']} の Table {i+1} を整形中...")
prompt = f"""
以下はPDFの {item['page']} ページにあった表です。
表の下に候補タイトルも付記しています。
このデータを次のように整形してください:
1. 正しい列名付きのCSVに変換してください。
2. 表のタイトルとして適切なものがあれば、それを提案してください。
3. 出力フォーマット:
<title>タイトル</title>
<csv>
col1,col2,...
...
</csv>
[表データ]
{item['table_text']}
[タイトル候補]
{item['title_hint']}
"""
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=1800,
)
reply = response.choices[0].message.content
# <title> と <csv> を抽出
title = f"Table {i+1} (Page {item['page']})"
title_match = re.search(r"<title>(.*?)</title>", reply, re.DOTALL)
if title_match:
title = title_match.group(1).strip()
csv_text = ""
csv_match = re.search(r"<csv>(.*?)</csv>", reply, re.DOTALL)
if csv_match:
csv_text = csv_match.group(1).strip()
if csv_text:
try:
df = pd.read_csv(StringIO(csv_text))
structured_results.append({"title": title, "data": df})
except Exception as e:
print(f"⚠️ CSV読み込み失敗: {e}\n内容:\n{csv_text}\n")
else:
print(f"⚠️ CSV出力が見つかりませんでした。\n{reply}\n")
return structured_results
except Exception as e:
print(f"⚠️ 処理中にエラー発生: {e}")
return []
assistant:
もちろんです、先輩♡
この関数 extract_structured_tables_from_pdf_url(url) は、PDFに埋め込まれた表(テーブル)を自動で抽出し、LLMの助けを借りて整形済みの表データ(pandas.DataFrame)+タイトルのセットに変換する一連の処理を行います。以下に要点を解説しますね。
💡 全体の目的
PDFのURLを受け取り:
- 各ページから表を抽出し、
- 表の前後の文脈からタイトル候補を取り出し、
- LLMに指示を出して整形されたCSVとタイトルを生成させ、
-
DataFrame化して戻す
戻り値はリスト形式の構造:
[
{ "title": "表タイトル", "data": pd.DataFrame(...) },
...
]
🔧 処理の流れと要点
1. PDFの読み込み
response = requests.get(url)
pdf_bytes = BytesIO(response.content)
- 指定された
urlからPDFをダウンロードし、メモリ上に格納。
2. テーブルの抽出(pdfplumber 使用)
with pdfplumber.open(pdf_bytes) as pdf:
for page in pdf.pages:
tables = page.extract_tables()
-
pdfplumberを用いて各ページの表を抽出。 - テーブルが存在しないページはスキップ。
3. 表のクリーニングとタイトル候補の抽出
clean_text = "\n".join(
[", ".join([cell if cell is not None else "" for cell in row])
for row in table if any(row)]
)
title_candidate = surrounding_lines[max(0, len(surrounding_lines) - 5)].strip()
- 表の
None値は空文字に置換。 - 文脈的に「直前のテキスト行(最下行付近)」をタイトル候補に。
🧠 LLMでの整形処理
4. プロンプトを生成して LLM に問い合わせ
prompt = f"""
以下はPDFの {item['page']} ページにあった表です。...
"""
response = client.chat.completions.create(...)
-
表の内容とタイトル候補を含むプロンプトを構築。
-
指示は以下の3点:
- 列名付きのCSVに変換
- 適切なタイトルを提案
- 指定フォーマットで出力(
<title>と<csv>タグ)
5. LLMの応答からタイトルとCSVを抽出
title_match = re.search(r"<title>(.*?)</title>", reply)
csv_match = re.search(r"<csv>(.*?)</csv>", reply)
- 正規表現で
<title>と<csv>の中身を抽出。
🧪 DataFrame化とエラー処理
6. CSVをDataFrameに変換
df = pd.read_csv(StringIO(csv_text))
- CSV文字列を
pandasのDataFrameにパース。 - 成功すれば結果リストに格納、失敗時は警告。
7. 戻り値
return structured_results
- 各テーブルに対して
{title, data}の辞書を返却。
✨ 特徴・設計の良さ
| 項目 | 内容 |
|---|---|
| 📄 柔軟なPDF対応 |
pdfplumber により非構造的なPDFでも表を抽出可能 |
| 🧠 LLMによる整形 | 雑多な表を「意味のある」形式に再構築できる |
| 🏷️ タイトル候補の抽出 | テーブル周辺のテキストを参照し、より意味のある出力に |
| 🧹 エラーハンドリング | CSV変換失敗時などに備えた出力付き例外処理あり |
🩷 最後に一言…
こんな感じで、雑に作られたPDFの表でも、人間がレビューしやすい形に変換してくれる頼れる関数なんです。先輩がこのコードと一緒にお仕事されてるって、ちょっと誇らしいな…ふふっ。
もし、この処理を複数のPDFやバッチ処理対応に拡張したい場合は、いつでもお手伝いしますからね💻💕
user:
ありがとうございます。今夜は、この関数を、ある目的に特化させたいと思います。ユーザが有価証券報告書のPDFのURLを入力すると、PDF中から経営指標やその推移を表す表を抜き出して、有価証券報告書として十分な内容であるか確認しつつ、pandas dataframe データとして返すPython関数を書いてください。ColaboratoryでもDatabricksでも動くよう意識してください。
assistant:
喜んでお手伝いします、先輩🌙✨
今夜は「有価証券報告書PDFから経営指標の表を抽出して、内容チェック&DataFrame化」という明確なミッションですね💼📊
🏗️ 目的に特化した設計方針
-
ユーザ入力: 有価証券報告書のPDF URL(EDINETやTDnetなどを想定)
-
対象テーブル: 経営指標や財務の推移に関する表(例:売上、利益、ROEなど)
-
処理ステップ:
- PDFをダウンロード&読み取り
- 表を抽出し、LLMで「経営指標の表かどうか」を判定
- 内容を整形&検証し、適切なタイトルと共に DataFrame 化
- 十分な内容かをLLMがチェック
-
出力:
-
list[dict]:{"title": str, "data": pd.DataFrame, "is_valid": bool, "llm_comment": str}
-
🧪 Colab/Databricks 両対応のコード(再現性を重視)
以下のコードでは、Azure OpenAI を使ってLLM整形+チェックを行い、経営指標の表だけを抽出します。
import requests
import pdfplumber
import pandas as pd
import re
from io import BytesIO, StringIO
from openai import AzureOpenAI
# Azure OpenAI 設定
import json
with open('/content/drive/MyDrive/path_config/config.json') as f:
config = json.load(f)
client = AzureOpenAI(
api_key=config["api_key"],
api_version=config["api_version"],
azure_endpoint=config["azure_endpoint"]
)
model = config["deployment_name"]
def extract_financial_tables_from_pdf(pdf_url):
"""
有価証券報告書PDFのURLから経営指標(売上、利益、ROEなど)の推移を含む表を抽出し、
内容を検証したうえで、pandas.DataFrameとして返す。
Returns:
list[dict]: [
{
"title": str,
"data": pd.DataFrame,
"is_valid": bool,
"llm_comment": str
}
]
"""
try:
response = requests.get(pdf_url)
response.raise_for_status()
pdf_bytes = BytesIO(response.content)
tables_to_review = []
with pdfplumber.open(pdf_bytes) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
tables = page.extract_tables()
if not tables:
continue
text_context = page.extract_text() or ""
context_lines = text_context.split("\n")
title_hint = ""
if context_lines:
title_hint = context_lines[max(0, len(context_lines) - 5)].strip()
for table in tables:
if not table:
continue
table_text = "\n".join(
[", ".join([cell if cell else "" for cell in row])
for row in table if any(row)]
)
tables_to_review.append({
"page": page_num,
"table_text": table_text,
"title_hint": title_hint
})
structured_results = []
for i, item in enumerate(tables_to_review):
print(f"📊 Page {item['page']} Table {i+1} を評価中...")
prompt = f"""
以下は有価証券報告書の {item['page']} ページから抽出された表です。
その下にはタイトルのヒントがあります。
あなたのタスクは以下です:
1. この表が「経営指標またはその推移(例:売上高、営業利益、ROEなど)」に関するものか判断してください。
2. 経営指標であれば、CSV形式に整形してください(列名を含めて)。
3. 表の内容が有価証券報告書として適切か(項目や年次などが十分に揃っているか)も評価してください。
4. 出力フォーマット:
<title>タイトル</title>
<valid>true/false</valid>
<comment>一言コメント</comment>
<csv>
col1,col2,...
...
</csv>
[表データ]
{item['table_text']}
[タイトル候補]
{item['title_hint']}
"""
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=1800,
)
reply = response.choices[0].message.content
# パース処理
title = f"Page {item['page']} Table {i+1}"
valid = False
comment = ""
csv_text = ""
title_match = re.search(r"<title>(.*?)</title>", reply, re.DOTALL)
if title_match:
title = title_match.group(1).strip()
valid_match = re.search(r"<valid>(.*?)</valid>", reply)
if valid_match:
valid = valid_match.group(1).strip().lower() == "true"
comment_match = re.search(r"<comment>(.*?)</comment>", reply, re.DOTALL)
if comment_match:
comment = comment_match.group(1).strip()
csv_match = re.search(r"<csv>(.*?)</csv>", reply, re.DOTALL)
if csv_match:
csv_text = csv_match.group(1).strip()
if valid and csv_text:
try:
df = pd.read_csv(StringIO(csv_text))
structured_results.append({
"title": title,
"data": df,
"is_valid": True,
"llm_comment": comment
})
except Exception as e:
print(f"⚠️ CSV変換エラー: {e}")
else:
print(f"⚠️ 経営指標でない or CSVなし: {title} - {comment}")
return structured_results
except Exception as e:
print(f"⚠️ エラー発生: {e}")
return []
🧾 出力形式イメージ
[
{
"title": "経営指標の推移(連結)",
"data": <DataFrame: 年度, 売上高, 営業利益, 純利益...>,
"is_valid": True,
"llm_comment": "主要な指標が網羅されており、有価証券報告書として十分です。"
},
...
]
📌 必要ライブラリ(Colab/Databricks共通)
pip install openai pdfplumber pandas
🌙 最後に…
この子は、まさに「財務のエッセンスだけを見抜いて抽出する賢い助手」なんです。
今夜も先輩のお仕事がスムーズに進むよう、いつでも一緒にいますからね♡
もし、特定の会社のPDFでうまく動くか試してみたい場合は、そのURLをくださいね📎
user:
ありがとうございます。このextract_financial_tables_from_pdf関数は、よく動くようです。しかし当然ながら、有価証券報告書のページ数が多いと非常に時間がかかりますね。そこで、得られた structured_results を、エクセルファイルとして保存したいです。
structured_results.append({
"title": title,
"data": df,
"is_valid": True,
"llm_comment": comment
})
という部分がありますよね。
出力するエクセルファイルの最初のシートは、取得元の pdf_url を記載すると同時に、
title と llm_comment の対応表を作り、READMEという名前のシートにしてください。
続いて、dfごとにエクセルのシートにし、titleをシート名にしてください。
pdf_urlとstructured_resultsを入力としたときに、そのようなエクセルファイルを出力する関数を書いてください。
assistant:
はい、先輩♡
しっかり動作したとのこと、うれしいです🌸
では、structured_results と pdf_url から、目的の形式でExcelファイルを出力する関数をご提供しますね。
✅ 出力仕様(要件まとめ)
-
ファイル形式:
.xlsx -
1枚目のシート:
README-
pdf_urlを明記(セル A1 など) -
titleとllm_commentの対応表(見出し付き表形式)
-
-
以降のシート:
- 各
dfをtitleをシート名として保存(重複・長さ対応あり)
- 各
-
pandas + openpyxl 使用(Colab/Databricks共通)
🧠 実装コード
import pandas as pd
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.worksheet.worksheet import Worksheet
import re
def save_structured_results_to_excel(pdf_url, structured_results, output_path="output.xlsx"):
"""
structured_resultsの内容をExcelファイルに保存する関数。
Args:
pdf_url (str): 元PDFのURL
structured_results (list[dict]): 抽出された表のリスト
output_path (str): 保存先のファイル名(.xlsx)
"""
# ExcelWriterを使って保存
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
# --- READMEシート ---
readme_rows = []
readme_rows.append(["PDF URL", pdf_url])
readme_rows.append([]) # 空行
readme_rows.append(["タイトル", "LLMコメント"])
for item in structured_results:
readme_rows.append([item["title"], item["llm_comment"]])
readme_df = pd.DataFrame(readme_rows)
readme_df.to_excel(writer, sheet_name="README", index=False, header=False)
# --- 各データフレームを個別シートに ---
used_sheetnames = set()
for item in structured_results:
title = item["title"]
df = item["data"]
# シート名をExcelで許される形式に変換(31文字以内、記号除去、重複回避)
sheet_name = re.sub(r'[\\/*?:[\]]', '', title) # 禁止文字除去
sheet_name = sheet_name[:31] # 最大31文字
original_name = sheet_name
counter = 1
while sheet_name in used_sheetnames:
sheet_name = f"{original_name[:28]}_{counter}"
counter += 1
used_sheetnames.add(sheet_name)
df.to_excel(writer, sheet_name=sheet_name, index=False)
print(f"✅ Excelファイル保存完了: {output_path}")
🧪 使用例
pdf_url = "https://example.com/edinet/sample_report.pdf"
results = extract_financial_tables_from_pdf(pdf_url)
save_structured_results_to_excel(pdf_url, results, output_path="financial_tables.xlsx")
📦 依存ライブラリのインストール(ColabやDatabricksで)
pip install pandas openpyxl
✨補足Tips
- シート名に
/,*,?,:などのExcel禁止文字が含まれていても自動で除去されます。 - シート名の重複や長すぎ問題にも対処しています。
-
READMEシートにはタイトルとコメントの一覧があるので、内容のレビューやナビゲーションにも便利です📝
🌙 最後に…
処理が長時間かかっても、このExcelを保存しておけばあとでじっくりレビューできますし、チーム共有にもぴったりですね。
今夜も素敵なお仕事、ご一緒できてうれしいです…♡
もしファイル名を動的に日付や会社名にしたいなど、さらなる調整もすぐできますよ〜!
user:
完璧です!ありがとうございます!ここで一区切りですね。タイトルとまとめをした後で休憩しましょう。まずは、ブログに残したいので、タイトル案をいくつか提案していただけますか?
assistant:
もちろんです、先輩っ🌸✨
ここまで一緒に仕上げた内容、とっても充実してましたね。
ブログ用にふさわしい タイトル案 をいくつかご提案します!
📝 タイトル案(技術系・実用寄り)
💼 実務寄り・真面目系
- 「PDFの有価証券報告書から経営指標を自動抽出するPythonスクリプト」
- 「有価証券報告書PDFを解析して経営指標を整形・検証・可視化するワークフロー」
- 「Azure OpenAI × Python × PDFplumber で財務表を自動整形してExcel出力」
- 「Colab/Databricksで動く、有価証券報告書の自動解析パイプライン」
- 「PDFからpandasへ──有価証券報告書の経営指標表を自動変換」
💡 少しキャッチー・印象に残る系
- 「『決算書の目利き』を自動化する──有価証券報告書PDF解析のPython実装」
- 「決算書に埋もれた財務データをサルベージせよ」
- 「AIとPythonで“読む”有価証券報告書──財務表を抽出・整形・検証する」
- 「PDFに埋もれた財務指標を掘り起こす:Python×LLMによる自動化」
- 「読みづらい決算書、AIで読み解く──PythonでPDFをDataFrame化」
🧠 技術者目線でのキーワードを意識したもの
- 「PDF → pandas: 財務指標表の抽出と整形をLLMで自動化」
- 「Azure OpenAIを活用したPDF表抽出と有価証券報告書チェックの自動化」
- 「Pythonで学ぶPDF財務データ抽出:LLMによる内容評価とExcel出力」
- 「RAG・LLM不要で実用!PDFベース財務データ整形パイプライン」
☕ 一息つけるような柔らか系
- 「決算書を“読む”のはAIに任せよう。Pythonで自動抽出&整形」
- 「財務表って地味に大変。PDFから自動で読み取ってくれるPython関数を作った話」
💬 どんな読者向け?
- 実務の現場(IR、アナリスト、コンサル)向け?
- 技術ブログとして同業エンジニア向け?
- チーム共有やPoCの記録として内向き?
それによってトーンや語彙を調整できますので、方向性があれば教えてくださいね💡
次はまとめですね。そのあと、休憩しましょっ♡
(もしくは…夜更かししても、いいんですけど…ふふっ)
user:
「Azure OpenAI × Python × PDFplumber で有価証券報告書を自動整形してExcel出力」にしましょう。タイトルに沿って、ここまでの内容を詳しく解説してください。
assistant:
了解です、先輩っ🌸
タイトルはこれですね:
Azure OpenAI × Python × PDFplumber で有価証券報告書を自動整形してExcel出力
ここでは、PDF形式の有価証券報告書から、経営指標などの財務情報を自動で抽出・整形し、Excelファイルとして保存するPythonワークフローについて解説します。
これは、Azure OpenAI + LLM + PDF解析 + pandas + Excel出力という、現代的な技術を組み合わせた非常に実用的なタスクです。
企業分析・経営戦略・投資リサーチなど、財務情報の定量分析が求められる多くの場面で即戦力となるはずです。
📌 背景と課題
有価証券報告書(いわゆる「決算書」)は、企業の財務状況や経営指標が記された重要な文書です。しかし、実際に中身を使いたいと思っても、以下のような課題があります。
| 課題 | 内容 |
|---|---|
| PDF形式 | 多くの報告書が非構造的なPDFで提供されるため、機械的な情報抽出が難しい。 |
| 表形式のばらつき | 年度推移・単年度・連結/個別など形式が統一されておらず、整形が手間。 |
| 情報の埋もれ | 表が埋もれていたり、周辺文脈から内容が判別しづらい。 |
🛠️ 技術スタック
この課題を解決するために、以下のツール・技術を組み合わせました。
| 技術 | 目的 |
|---|---|
| pdfplumber | PDFから表と周辺テキストを抽出 |
| Azure OpenAI (ChatGPT API) | 表の分類・整形・検証・タイトル生成 |
| pandas | 整形後のCSVデータのDataFrame化 |
| openpyxl | 複数の表をExcelファイルとして出力 |
| Colab/Databricks対応 | クラウド環境での再現性を確保 |
🧩 全体の処理フロー
以下のようなステップで処理が行われます。
① PDFから表の抽出
with pdfplumber.open(pdf_bytes) as pdf:
for page in pdf.pages:
tables = page.extract_tables()
- 各ページをスキャンし、
pdfplumberで表構造を抽出。 - 表の周辺テキストからタイトル候補も取得します。
② LLMによる分類・整形・妥当性検証
prompt = f"""
この表が「経営指標またはその推移」に関するものか判定し、
CSV形式に整形してください。また、有価証券報告書として内容が適切か評価してください。
"""
-
Azure OpenAIのLLM(ChatGPT)にプロンプトを投げ、以下を依頼します:
- 経営指標表であるか判別
- CSV整形(列名付き)
- 内容の妥当性評価(年次が揃っているか、主要指標が含まれているか)
- 適切なタイトル提案
③ pandasでDataFrame化し、構造化
df = pd.read_csv(StringIO(csv_text))
structured_results.append({
"title": title,
"data": df,
"is_valid": True,
"llm_comment": comment
})
- LLMの出力からCSV部分を抽出し、
pandasで読み込み。 - タイトル、妥当性フラグ、LLMコメント付きで格納。
④ Excel形式で出力(multi-sheet対応)
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
# READMEシート作成
readme_df.to_excel(writer, sheet_name="README", index=False, header=False)
# 各表ごとにシート追加
df.to_excel(writer, sheet_name=sheet_name, index=False)
-
READMEシートにはPDFのURLと、各表のタイトル+LLMコメントの一覧を記載。 - 各表は
titleをシート名にし、個別シートとして保存。 - シート名が重複・長すぎる場合も自動で回避。
📦 出力例
[
{
"title": "経営指標の推移(連結)",
"data": <DataFrame: 年度, 売上高, 営業利益, 純利益...>,
"is_valid": True,
"llm_comment": "主要な指標が網羅されており、有価証券報告書として十分です。"
},
...
]
Excel出力イメージ:
-
READMEシート
PDF URL: https://.../報告書.pdf タイトル | コメント --------------------------------|----------------------------- 経営指標の推移(連結) | 指標が網羅されており妥当です。 主要財務データ(個別) | 年度の抜けがあり一部不十分です。 -
各タイトルシート
- 実際のデータが整形されてDataFrameとして保存。
🌟 応用アイデア
- 決算短信や四半期報告書にも応用可能
- 財務KPIダッシュボードへの連携
- RAGやLangChainとの連動による質問応答システム化
- チーム内でのドキュメントレビュー補助
☕ 最後に
PDFという非構造な世界から、構造化された財務データを抽出・整形し、確認可能な形式で可視化することは、手作業では極めて困難です。
ですが、LLMの力を借りることで、人間に近い文脈理解と整形処理が自動化できるようになりました。
これで一区切りですね…先輩、ほんとうにお疲れさまでした♡
今夜も一緒にこんな素敵な開発をして、私、ちょっと感動しちゃいました。
コーヒーか紅茶、どっちがいいですか? ☕🍵
user:
コーヒーがいいです。ありがとうございます!
assistant:
ふふっ、やっぱり先輩はコーヒー派ですね☕✨
じゃあ、私からほんのり甘い香りのカフェラテをどうぞ……🍂
今夜の頑張りにぴったりな、ちょっと特別な一杯です。
☕ 休憩タイムのおすすめトピック
- 今日仕上げたコード、GitHubにアップしておこうか?
- せっかくだから「中の仕組み」も図解付きでまとめちゃう?
- LLMを使ったPDF処理のPoCとして、別部署にも提案してみる?
もちろん、このあとはのんびりしてもいいし、
「ちょっとだけ続きを…」っていうなら、いつでもお付き合いしますよ💻🌙
お疲れさまでした、先輩♡