有価証券報告書を AI (Claude Code) で分析する: EDINET API で本文を取り、前年からの変化を拾う
有報の「事業等のリスク」と「経営者による分析 (MD&A)」を 2 期分、Claude Code に読ませて、前年から変わった点を抜き出しました。SUBARU で試すと 82 秒で 10 項目、引用 10 文はすべて原文に実在。本文を XBRL の TextBlock で切り出す方法と、つまずいた点まで。
本記事にはプロモーション (アフィリエイト広告) が含まれます。
「有価証券報告書を AI で分析したい」と考えている人向けに、先に結論を置きます。
有報の「事業等のリスク」と「経営者による分析 (MD&A)」を前期と当期の 2 期分並べて Claude に渡し、「前年から変わった点を、当期の原文を引用しながら出して」と頼むと、人が読み比べると時間のかかる変化が 1〜2 分で出てきます。 SUBARU (2026 年 3 月期) で試すと、4.6 万字を 82 秒で読み、10 項目を挙げました。引用した原文 10 文は、照合するとすべて実在しました。
数字は XBRL から取れます (前回の記事)。AI に読ませる価値があるのは、数字に出ない変化の方です。リスクの項目が消えた、言い回しが「さらに強化」から「大きく変更」に変わった、株主還元の目標が消えた。こうした変化は、決算の数字をいくら見ても出てきません。
この記事は、有報の読み方を AI で効率化する方法の紹介です。特定の銘柄の売買を勧めるものではありません。例に出す会社の記述は、EDINET で公開されている有報からの引用です。
この記事で使う環境
- 有報の取得: EDINET API (無料、API キーが要る)。取り方は 前回の記事
- 読ませる AI: Claude Code 2.1.280、モデルは Claude Opus 5.5。
claude -p(非対話モード) で動かす - 本文の切り出し: Python と
lxml - 実行した場所: 自宅のサーバーと Mac。毎晩の取り込みは自宅のラズパイで動かしている
1. 有報のどこを読ませるか
有報の本文は長いです。プライム市場の有報 50 本を無作為に選んで数えると、本文全体の文字数 (空白を除く) は中央値で 23.7 万字、最大 43.7 万字ありました。全部を 2 期分読ませるのは現実的ではないので、変化が出やすい節に絞ります。
有報の ZIP の中で、読むべきは XBRL/PublicDoc/0102010_honbun_*.htm、「第 2 事業の状況」です。ここに、経営方針、サステナビリティ、事業等のリスク、MD&A が入っています。
| 節 | 文字数の中央値 | 最小 | 最大 |
|---|---|---|---|
| 「事業の状況」全体 (0102010) | 31,004 | 11,392 | 98,092 |
| 経営方針・対処すべき課題 | 3,280 | 664 | 10,932 |
| 事業等のリスク | 4,284 | 1,379 | 19,847 |
| MD&A | 6,543 | 3,502 | 17,138 |
| サステナビリティ | 9,093 | 1,143 | 58,928 |
(2026-06-01〜07-15 に提出されたプライム市場の有報から無作為に 50 本。空白と改行を除いた文字数)
私が選んだのは 事業等のリスク と MD&A の 2 つです。
- 事業等のリスク: 会社が「何を恐れているか」。項目の追加・削除、言い回しの強弱に、経営の認識の変化が出ます
- MD&A: 会社が「業績の増減をどう説明しているか」。減益の理由が入れ替わると、事業の中で起きていることが変わったと分かります
2 節を 2 期分合わせても、中央値で 2 万字台です。日本語の有報はほぼ 1 文字 1 トークンだったので、1 回で読ませられます。
2. 本文を節ごとに切り出す
有報の本文には、節ごとに XBRL のタグ (TextBlock) が付いています。見出しの文字列ではなく、タグの名前で節を切り出せます。
| 節 | TextBlock の要素名 |
|---|---|
| 事業等のリスク | jpcrp_cor:BusinessRisksTextBlock |
| MD&A | jpcrp_cor:ManagementAnalysisOfFinancialPositionOperatingResultsAndCashFlowsTextBlock |
| 経営方針 | jpcrp_cor:BusinessPolicyBusinessEnvironmentIssuesToAddressEtcTextBlock |
| サステナビリティ | jpcrp_cor:DisclosureOfSustainabilityRelatedFinancialInformationTextBlock |
上の 50 本では、4 つの TextBlock がすべての書類で取れました。見出し (「3 【事業等のリスク】」) で切る方法もありますが、全角数字や空白を含む文字列に頼るので、書き方の揺れに弱いです。
次のコードは、EDINET の書類 ID を 2 つ受け取り、それぞれの ZIP から 2 節を切り出して、Claude に渡すプロンプトを組み立てます。API キーの扱いと ZIP の取り方は、前回の記事と同じです。
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "lxml"]
# ///
"""有報 2 期分の「事業等のリスク」と「MD&A」を切り出して、Claude に渡すプロンプトを作る."""
import io
import os
import re
import sys
import zipfile
import httpx
from lxml import etree
BASE = "https://api.edinet-fsa.go.jp/api/v2"
HEADERS = {"Ocp-Apim-Subscription-Key": os.environ["EDINET_API_KEY"]}
SECTIONS = {
"BusinessRisksTextBlock": "事業等のリスク",
"ManagementAnalysisOfFinancialPositionOperatingResultsAndCashFlowsTextBlock": "MD&A",
}
PROMPT = """あなたは株式投資のアナリストです。以下は同じ会社の有価証券報告書のうち、「事業等のリスク」と「経営者による財政状態、経営成績及びキャッシュ・フローの状況の分析」を、前期と当期の 2 期分並べたものです。
前期から当期で変わった点を抜き出してください。
- 新しく加わったリスク、消えたリスク、書きぶりが強まった・弱まったリスク
- 業績の説明で、会社が挙げる増減の理由がどう変わったか
- 数字に出にくい変化 (方針、前提、言い回しの変化) を優先する
- 各項目に、根拠になる当期の原文を 1 文だけ「」で引用する
- 原文に無いことは書かない。推測するときは「推測」と明記する
- 重要なものから順に、最大 10 項目
"""
def download(doc_id: str) -> bytes:
r = httpx.get(f"{BASE}/documents/{doc_id}", params={"type": 1},
headers=HEADERS, timeout=120)
r.raise_for_status()
return r.content
def sections(zip_bytes: bytes) -> dict[str, str]:
"""0102010 (事業の状況) の iXBRL から、TextBlock ごとに本文のテキストを取る."""
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as z:
name = next(n for n in z.namelist()
if n.startswith("XBRL/PublicDoc/0102010_honbun_") and n.endswith(".htm"))
root = etree.parse(z.open(name), etree.HTMLParser(encoding="utf-8")).getroot()
out = {}
for el in root.iter():
# iXBRL の ix:nonNumeric の name 属性に TextBlock の要素名が入っている
local = (el.get("name") or "").split(":")[-1]
if local in SECTIONS:
out[SECTIONS[local]] = block_text(el)
return out
def block_text(el: etree._Element) -> str:
"""段落と表の行ごとに 1 行にする.
元の HTML はタグの間や文の途中に改行を含むので、itertext() を素直に繋ぐと文や表の
行が割れる。空白をいったん潰してから、段落・見出し・表の行の後ろに改行を、セルの
後ろに空白を足して繋ぐ。セルの中にも <p> があるので、そこでは改行を足さない。
"""
for node in el.iter():
if node.text:
node.text = re.sub(r"\s+", " ", node.text)
if node is not el and node.tail:
node.tail = re.sub(r"\s+", " ", node.tail)
for b in el.iter("p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "br"):
if b.tag != "tr" and b.xpath("ancestor::td or ancestor::th"):
continue
b.tail = "\n" + (b.tail or "")
for c in el.iter("td", "th"):
c.tail = " " + (c.tail or "")
lines = (" ".join(line.split()) for line in "".join(el.itertext()).split("\n"))
return "\n".join(line for line in lines if line)
def main() -> None:
prev_id, curr_id = sys.argv[1], sys.argv[2]
parts = [PROMPT]
for label, doc_id in (("前期", prev_id), ("当期", curr_id)):
secs = sections(download(doc_id))
parts.append(f"===== {label} ({doc_id}) =====")
for title, text in secs.items():
parts.append(f"## {title}\n{text}")
print(f"{label} {doc_id}: " + ", ".join(f"{t} {len(x):,} 字" for t, x in secs.items()),
file=sys.stderr)
print("\n\n".join(parts))
if __name__ == "__main__":
main()
SUBARU の 2 期分で実行すると、こう出ます。
EDINET_API_KEY=... uv run yuho_prompt.py S100W1AE S100YFKZ > prompt.txt
前期 S100W1AE: 事業等のリスク 12,129 字, MD&A 9,012 字
当期 S100YFKZ: 事業等のリスク 14,946 字, MD&A 8,901 字
prompt.txt は 535 行で、段落と表の行が 1 行ずつになります。表は「国内合計 9.9 10.4 0.5 5.4」のように 1 行にまとまります。
後半の block_text() が要るのは、有報の HTML が、文の途中やタグの間に改行を含んでいるからです。そのままテキストを繋ぐと、「(3) 当社グループの」と「リスクマネジメントの取り組み」のように、見出しが 2 行に割れます。最初の版ではこの割れが 83 か所ありました。空白をいったん潰してから、段落と表の行の終わりにだけ改行を入れ直しています。
次の節の実験結果 (出力と 82 秒) は、このコードではなく、macOS の textutil でテキスト化し、見出しで切り出した本文で取ったものです。このコードは、同じ 2 節を取り出せることまで確かめています。このコードの出力をそのまま Claude に渡して、答えが変わるかは試していません。
書類 ID は、前回の記事の書類一覧 (/documents.json) で探します。同じ会社の前期と当期の有報 (docTypeCode が 120) を、periodEnd (期末日) で選んでください。理由は「つまずいた点」に書きます。
3. Claude Code に読ませる
組み立てたプロンプトを、claude -p に標準入力で渡します。
EDINET_API_KEY=... uv run yuho_prompt.py S100W1AE S100YFKZ > prompt.txt
claude -p --output-format json --tools "" < prompt.txt > result.json
--tools "" で、ファイルの読み書きやコマンドの実行といったツールをすべて無効にしています。今回は渡した本文を読んで答えるだけなので、ツールは要りません。無効にしておけば、本文の中に紛れた指示で Claude が何かを実行する心配もありません。--output-format json にすると、答えのほかにトークン数と所要時間も記録されます。
プロンプトで効いているのは、次の 3 行です。
- 「数字に出にくい変化を優先する」。これがないと、売上や利益の増減の説明が並びます。それは XBRL から取れます
- 「当期の原文を 1 文だけ引用する」。答えの根拠を、後から自分で確かめられるようにします
- 「原文に無いことは書かない。推測するときは明記する」。AI の解釈と、有報に書いてある事実を分けます
4. SUBARU で試した結果
題材に選んだのは SUBARU (7270) の 2025 年 3 月期と 2026 年 3 月期の有報です。営業利益が 4,053 億円から 401 億円へ 90% 減っていて、その理由が米国の関税、環境規制、EV 計画といった「数字の外」にありそうだったためです。
| 項目 | 値 |
|---|---|
| 読ませた文字数 | 46,191 字 (前期 21,508 字、当期 24,257 字、プロンプト 372 字、区切りの行。空白と改行を含む) |
| 入力トークン | 46,104 |
| 出力トークン | 9,066 (うち思考 6,105) |
| 所要時間 | 82 秒 |
| 回数 | 1 回 (分割なし) |
Claude が挙げた 10 項目の見出しです。
- 米国関税は実績として記述され、独立項目から既存項目へ移った
- 減益理由が入れ替わり、「環境規制クレジット」が新たに加わった
- 電動化は自社開発 BEV を延期し、リソースを ICE に回す
- 環境規制の前提が「さらに強化」から「大きく変更」へ変わった
- 中東情勢が新たに加わり、すでに業績に影響している
- 台数減の理由は需要側から供給側へ移り、増収理由は「価格構成の改善」になった
- 株主還元から総還元性向 40% の目標が消え、キャッシュ保有の上限を新設
- 金融市場リスクの焦点が「調達できない」から「金利上昇」へ移った
- サイバーセキュリティに生成 AI のリスクが加わった
- サプライチェーンは不確実性の認識を強め、影響度の表現は弱めた
特に良かった 3 つを、出力のまま載せます。
1. 米国関税は実績として記述され、独立項目から既存項目へ移った
前期は冒頭に「米国の関税政策」という独立項目がありました。当期はこの項目が消えています。中身は⑤「特定の事業および市場への集中」と⑰に移り、⑰では影響が出たことを過去形で認めています。対応策の列挙からは「売上台数の増加」が落ちました。(中略) 独立項目がなくなったのは、軽く見るようになったからではないと読みます。一時的な論点から事業構造上のリスクに扱いを変えたのだと推測します。
「特に米国の関税政策について、前事業年度の政策変更の影響を受け、米国販売子会社が日本から輸入する完成車や、米国生産拠点において一部の国から輸入する部品に追加関税が課されるなど、当社グループの収益に一定の影響を及ぼしました。」
リスクの項目が「消えた」ことを、「軽視した」ではなく「構造的なリスクに格上げした」と読んでいます。そのうえで、それが推測であることを明記しています。項目の数を数えるだけのツールでは出てこない読みです。
4. 環境規制の前提が「さらに強化」から「大きく変更」へ変わった
前期は、規制が強まる一方向の前提で書かれていました。当期は方向を決めない「変更」に書き換えています。(中略) 規制が緩む方向に動き、クレジットの価値が下がって 2. の損失につながったと推測します。ただし、原文はこの因果を説明していません。
「また、環境などに関する主な法的規制は、自動車の燃費、排出ガス、省エネルギーの推進、騒音、リサイクル、製造工場からの汚染物質排出レベルに関するものであり、これらの規制は今後大きく変更される可能性があります。」
たった一語の違いです。これを人が 2 万字の中から見つけるのは大変です。
7. 株主還元から総還元性向 40% の目標が消え、キャッシュ保有の上限を新設
前期の目標「総還元性向40%以上」は、当期の記載にありません。DOE3.5% は維持しました。そのうえで「増配や機動的な自己株式取得」を掲げています。新たにネットキャッシュ 2.5 月商を保有上限とし、1,500 億円の自己株式取得も決議しました。(中略) 利益が大きく落ちると、性向目標のままでは還元額が縮みます。それを避けるため、キャッシュ残高を基準に還元する形へ移したと推測します。
「財務健全性と安定性の実現に関しては、不確実性の高い事業環境下においても中長期的な資本効率向上を念頭に、キャッシュ保有の上限目安をネットキャッシュ2.5月商※1と定めコントロールします。」
株主にとって一番影響の大きい変化かもしれません。目標が「消えた」ことは、差分を取らないと気づきません。
答えは正しかったか
AI の答えは、そのまま信じずに確かめます。確かめたのは 2 つです。
- 引用した当期の原文 10 文: 空白を除いて照合すると、10 文とも有報に実在しました
- 「前期にあって当期に消えた」「新しく加わった」と言った語: 前期と当期の本文で出現回数を数えると、どれも主張と合っていました
| 語 | 前期 | 当期 |
|---|---|---|
| 総還元性向40% | 2 | 0 |
| 32か月 | 2 | 0 |
| さらに強化 | 1 | 0 |
| 中東 | 0 | 5 |
| 生成AI | 0 | 8 |
ただし、「推測」とした解釈 (関税リスクを格上げした、規制が緩んだ、など) が正しいかは確かめていません。ここは読む側が判断するところです。AI の役割は「どこが変わったか」を見つけて根拠の原文を示すところまで、と割り切るのが安全です。
5. 費用と、毎晩回すなら
--output-format json の出力には、total_cost_usd が 0.555 ドルと出ました。これは API の料金に換算した表示値です。月額プランで動かしているなら、この額が請求されるわけではありません。ただ、月額プランにも使用量の上限があり、Opus は枠の減りが速いモデルです。
私は Claude Pro で動かしました。1 回の実行で枠を使い切ることはありませんでした。実行の前後で claude.ai の「設定 → 使用量」(Claude Code なら /usage) を見れば、1 回で枠の何 % を使ったかが分かります。
毎晩の取り込みの後に、その日に出た有報を自動で比べる形はまだ作っていません。作るなら、1 社 80 秒前後なので、6 月下旬の提出ピーク (1 日 400 社) に全部を回すのは現実的ではありません。保有している銘柄や、気にしている銘柄に絞ることになります。
つまずいた点
- 同じ会社の「最新 2 本」を取ると、同じ期を比べてしまう。SUBARU は当期の有報と同じ日に、前期の訂正有報を出していました。訂正有報は本文を丸ごと出し直すので、ZIP の中身は有報そのものです。提出日の新しい順に 2 本取ると、前期の原本と前期の訂正版を比べることになります。期末日 (
periodEnd) で前期と当期を選んでください - 見出しで切るのは脆い。最初は「3 【事業等のリスク】」から「5 【重要な契約等】」の手前までを見出しで切っていました。SUBARU では動きましたが、見出しは全角数字と空白を含む文字列で、書き方の揺れに弱い方法です。TextBlock の要素名で切る方が確実です
- テキストにすると文が途中で割れる。有報の HTML は文の途中やタグの間に改行を含みます。lxml の
itertext()を素直に繋ぐと、見出しや文が 2 行に割れました。空白を潰してから、段落の終わりにだけ改行を入れる (上のコードのblock_text()) と直ります - 表が 1 セル 1 行に崩れる。本文をテキストにすると、生産台数の表が「普通自動車 / (万台) / 88.0 / △7.0」のように縦に並びます。「事業の状況」のうち表の中の文字は、中央値で 3,408 字、多い会社で 2.7 万字あります。前年比較では数値の表はノイズになりやすいので、数値は XBRL から取り、本文は文章だけにする方が良いはずです (表を除いた版はまだ試していません)
- 節の中の小見出しは会社ごとに違う。SUBARU のリスクは「①主要市場の経済動向」の下に「<リスク><対応策>」が並ぶ形でした。この形は会社によって違うので、項目の対応付けは Claude に任せる方が早いです。実際、「米国の関税政策」という独立項目が別の項目に移ったことも、Claude が拾いました
- △ は負の数。有報では減少を △ で書きます (△7.0 = -7.0)。Claude は正しく読んでいましたが、答えを後で数値として扱うなら注意が要ります
EDINET から有報を取り込む仕組みができていれば、本文の切り出しから Claude に比べさせるまでは 1 時間かかりませんでした。
次にやること
- 保有・監視している銘柄に絞って、有報が出た夜に自動で前年比較を回す。結果は Telegram に送る
- 表を除いて文章だけを渡し、答えの質が変わるかを比べる
- 「経営方針・対処すべき課題」も比較に加える。中期経営計画の書き換えが拾えるはず
まとめ
- 有報の「事業等のリスク」と「MD&A」を 2 期分並べて Claude に渡すと、前年から変わった点を原文の引用つきで出せる。SUBARU で 4.6 万字、82 秒、10 項目
- 節の切り出しは、見出しではなく XBRL の TextBlock の要素名で。
BusinessRisksTextBlockとManagementAnalysis...TextBlock - プロンプトは「数字に出にくい変化を優先」「当期の原文を 1 文引用」「推測は明記」の 3 つが効く
claude -p --tools ""でツールを無効にして、読むだけにする- 引用が実在するか、消えた語が本当に消えたかは、機械的に確かめられる。解釈 (推測) の正しさは読む側が判断する
- 前期と当期は期末日で選ぶ。訂正有報を取り違えない