VPS Notes
セルフホスト

有価証券報告書を AI (Claude Code) で分析する: EDINET API で本文を取り、前年からの変化を拾う

有報の「事業等のリスク」と「経営者による分析 (MD&A)」を 2 期分、Claude Code に読ませて、前年から変わった点を抜き出しました。SUBARU で試すと 82 秒で 10 項目、引用 10 文はすべて原文に実在。本文を XBRL の TextBlock で切り出す方法と、つまずいた点まで。

edinet有価証券報告書claude-codeai株式投資

本記事にはプロモーション (アフィリエイト広告) が含まれます。

「有価証券報告書を AI で分析したい」と考えている人向けに、先に結論を置きます。

有報の「事業等のリスク」と「経営者による分析 (MD&A)」を前期と当期の 2 期分並べて Claude に渡し、「前年から変わった点を、当期の原文を引用しながら出して」と頼むと、人が読み比べると時間のかかる変化が 1〜2 分で出てきます。 SUBARU (2026 年 3 月期) で試すと、4.6 万字を 82 秒で読み、10 項目を挙げました。引用した原文 10 文は、照合するとすべて実在しました。

数字は XBRL から取れます (前回の記事)。AI に読ませる価値があるのは、数字に出ない変化の方です。リスクの項目が消えた、言い回しが「さらに強化」から「大きく変更」に変わった、株主還元の目標が消えた。こうした変化は、決算の数字をいくら見ても出てきません。

この記事は、有報の読み方を AI で効率化する方法の紹介です。特定の銘柄の売買を勧めるものではありません。例に出す会社の記述は、EDINET で公開されている有報からの引用です。

この記事で使う環境

XServer VPS クラウド
EDINET の取得と AI の分析を毎晩回すなら、常時動いている Linux が要ります。自宅にサーバーを置けない人向け。4GB (4 コア) で月 2,480 円〜。
公式サイトを見る

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 つです。

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 行です。

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 項目の見出しです。

  1. 米国関税は実績として記述され、独立項目から既存項目へ移った
  2. 減益理由が入れ替わり、「環境規制クレジット」が新たに加わった
  3. 電動化は自社開発 BEV を延期し、リソースを ICE に回す
  4. 環境規制の前提が「さらに強化」から「大きく変更」へ変わった
  5. 中東情勢が新たに加わり、すでに業績に影響している
  6. 台数減の理由は需要側から供給側へ移り、増収理由は「価格構成の改善」になった
  7. 株主還元から総還元性向 40% の目標が消え、キャッシュ保有の上限を新設
  8. 金融市場リスクの焦点が「調達できない」から「金利上昇」へ移った
  9. サイバーセキュリティに生成 AI のリスクが加わった
  10. サプライチェーンは不確実性の認識を強め、影響度の表現は弱めた

特に良かった 3 つを、出力のまま載せます。

1. 米国関税は実績として記述され、独立項目から既存項目へ移った

前期は冒頭に「米国の関税政策」という独立項目がありました。当期はこの項目が消えています。中身は⑤「特定の事業および市場への集中」と⑰に移り、⑰では影響が出たことを過去形で認めています。対応策の列挙からは「売上台数の増加」が落ちました。(中略) 独立項目がなくなったのは、軽く見るようになったからではないと読みます。一時的な論点から事業構造上のリスクに扱いを変えたのだと推測します。

「特に米国の関税政策について、前事業年度の政策変更の影響を受け、米国販売子会社が日本から輸入する完成車や、米国生産拠点において一部の国から輸入する部品に追加関税が課されるなど、当社グループの収益に一定の影響を及ぼしました。」

リスクの項目が「消えた」ことを、「軽視した」ではなく「構造的なリスクに格上げした」と読んでいます。そのうえで、それが推測であることを明記しています。項目の数を数えるだけのツールでは出てこない読みです。

4. 環境規制の前提が「さらに強化」から「大きく変更」へ変わった

前期は、規制が強まる一方向の前提で書かれていました。当期は方向を決めない「変更」に書き換えています。(中略) 規制が緩む方向に動き、クレジットの価値が下がって 2. の損失につながったと推測します。ただし、原文はこの因果を説明していません。

「また、環境などに関する主な法的規制は、自動車の燃費、排出ガス、省エネルギーの推進、騒音、リサイクル、製造工場からの汚染物質排出レベルに関するものであり、これらの規制は今後大きく変更される可能性があります。」

たった一語の違いです。これを人が 2 万字の中から見つけるのは大変です。

7. 株主還元から総還元性向 40% の目標が消え、キャッシュ保有の上限を新設

前期の目標「総還元性向40%以上」は、当期の記載にありません。DOE3.5% は維持しました。そのうえで「増配や機動的な自己株式取得」を掲げています。新たにネットキャッシュ 2.5 月商を保有上限とし、1,500 億円の自己株式取得も決議しました。(中略) 利益が大きく落ちると、性向目標のままでは還元額が縮みます。それを避けるため、キャッシュ残高を基準に還元する形へ移したと推測します。

「財務健全性と安定性の実現に関しては、不確実性の高い事業環境下においても中長期的な資本効率向上を念頭に、キャッシュ保有の上限目安をネットキャッシュ2.5月商※1と定めコントロールします。」

株主にとって一番影響の大きい変化かもしれません。目標が「消えた」ことは、差分を取らないと気づきません。

答えは正しかったか

AI の答えは、そのまま信じずに確かめます。確かめたのは 2 つです。

語 前期 当期
総還元性向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 社) に全部を回すのは現実的ではありません。保有している銘柄や、気にしている銘柄に絞ることになります。

つまずいた点

EDINET から有報を取り込む仕組みができていれば、本文の切り出しから Claude に比べさせるまでは 1 時間かかりませんでした。

次にやること

まとめ

XServer VPS クラウド
EDINET の取得と Claude の分析を毎晩回す場所として。自宅にサーバーを置けない人向けです。
公式サイトを見る