取引先や競合の資本関係を調べるとき、有価証券報告書の「大株主の状況」を1社ずつ開いていないでしょうか。5社分を並べても、同じ株主がどの会社に出るのか、比べる会社どうしが互いの株主に入っているのかは、表を見比べないと分かりません。しかも上位には「日本マスタートラスト信託銀行(信託口)」のような名義が並びます。
前回のディープ解析①では、有報の本文を見出しで並べ、各社のリスクの区分の立て方を比べました。今回は同じ「開示リーダー」の「株主」の画面で、大株主の上位10・所有者別の構成・大量保有報告を、自社と競合の数社で並べて読みます。
画面の内容は、2026年10月31日にZENITH Codeの実データを取得して保存したデモです。トヨタ自動車(7203)・デンソー(6902)・本田技研工業(7267)・マツダ(7261)・テイン(7217)の、2026年3月期の有価証券報告書と、大量保有報告書を並べています。①は10月30日の取得でしたが、大量保有報告のデータが報告書ごとに返る形に変わったのに合わせて、取り直しました。
私たち株式会社Crabgrassは、企業の財務データなどをAPIで提供するZENITH Codeを開発しています。本記事は自社サービスのデータを使うためのテンプレートを題材にした実装例です。
この記事のコードは、公開サンプル集のdisclosure-readerテンプレート(執筆時点の版)に対応しています。画面の仕組みの基本(ローカルのPythonサーバーとブラウザの役割分担)は、実践編で詳しく扱いました。
1. この回で読む画面と操作
| 画面の部分 | 分かること |
|---|---|
| 比べる会社で共通する大株主 | 2社以上の上位10に出る名前と、各社での順位・割合 |
| 比べる会社が大株主に入っている | 比べる会社どうしの株の持ち合い(上位10の範囲) |
| 所有者別の構成 | 金融機関・事業法人等・外国法人等・個人その他などの割合 |
| 会社ごとのカード | 大株主の上位10と、直近の大量保有報告 |
2. 登録不要で試す
必要なものはPython 3.9以上とブラウザです。追加のパッケージはありません。
git clone https://github.com/Crab-grass/zenith-code-examples.git
cd zenith-code-examples
python3 templates/disclosure-reader/server.py
ブラウザで http://127.0.0.1:8790 を開き、左の「株主」(上から2番目)を押します。上の「比べる会社」で2〜5社を選べます。デモではAPIを呼ばないので、会員登録もAPIキーも要りません。終了するときは端末で Ctrl+C を押します。
3. 株主のデータは3つのAPIから受け取る
株主の画面は、1社につき3つのAPIを使います。/stable/major-shareholders が有報の「大株主の状況」、/stable/shareholder-categories が「所有者別状況」、/stable/large-holders が大量保有報告書です。
ローカルのサーバーは、大株主の行を次の関数で整えてからブラウザに渡します。
def clean_major(rows, expected):
out = []
for row in _list(rows or [], 40):
_check_symbol(row, expected)
item = _pick(row, MAJOR)
item["region"] = region(row.get("address"))
item.update(_document(row, ()))
out.append(item)
return out
住所は region で**都道府県(海外は「海外」)**に置き換え、番地は渡しません。項目は許可リスト(MAJOR)のものだけで、画面に出るのは株数と割合だけです。
所有者別状況は、種類株式を発行している会社でも普通株式の表だけを渡します。書類へのリンクは、EDINETの閲覧ページだけを残します。
4. 共通する大株主を名前でそろえる
有報では、同じ会社が「日本生命保険(相)」とも「日本生命保険相互会社」とも書かれます。model.mjs は、次の2つの関数で名前をそろえます。
// Names are compared after NFKC and without corporate suffixes and spaces:
// "トヨタ自動車㈱" = "トヨタ自動車株式会社" = "トヨタ自動車", "日本生命保険(相)" = "日本生命保険相互会社".
export function companyKey(name) {
return String(name ?? "").normalize("NFKC")
.replace(/\((?:注|※)[^)]*\)/g, "")
.replace(/株式会社|有限会社|合同会社|相互会社|\((?:株|有|相)\)/g, "")
.replace(/[\s・,.、、]/g, "")
.toUpperCase();
}
// Holder names also drop the trust-account suffix: "(信託口)" "(信託口4)" "(投信口)".
export const holderKey = (name) => companyKey(String(name ?? "").normalize("NFKC").replace(/\((?:信託口|投信口)[0-9]*\)\s*$/, ""));
commonHolders はこの名前で各社の上位10を集め、2社以上に出る名前を、出る会社の多い順に並べます。デモの5社では、冒頭の画面のとおり9つの名前が出ます。5社すべてに出るのは日本カストディ銀行(トヨタ自動車 3位 6.10%〜テイン 10位 1.46%)、4社に出るのは日本マスタートラスト信託銀行と日本生命保険です。割合は各社の発行済株式(自己株式を除く)に対する割合で、分母が会社ごとに違うので、行の中で足しません。
信託銀行の名前は「名義」で、実際の持ち主ではない
日本マスタートラスト信託銀行と日本カストディ銀行の「信託口」は、年金や資産運用会社などから預かった株式を、信託銀行の名義で持つ口座です。大株主の表は株主名簿の名義で書くので、実際の持ち主が多数いても信託銀行の名前が1行で出ます。海外の「STATE STREET BANK AND TRUST COMPANY」なども、預かった株式を管理する銀行の名義です。
資本関係を読むときは、名義の行とそれ以外を分けて見ます。デモでは、名義の行を除くと、トヨタ自動車・トヨタ不動産・豊田自動織機の3社と、日本生命保険・明治安田生命保険の2社が残ります。12節で、この切り替えをボタンにします。
名前の照合でそろわないもの
カタカナと英字の違い(本田技研工業の「ステートストリートバンクアンドトラストカンパニー505001」と、デンソー・マツダの「STATESTREETBANKANDTRUSTCOMPANY505001」)や、口座の番号の違いは、別の名前として扱います。
また、同じ会社の上位10に同じ名前が2回出るときは、上の順位の1行だけを使います。本田技研工業の日本カストディ銀行は「(信託口)」2位 7.08%と「(信託口4)」10位 0.97%の2行ですが、表には7.08%が出ます(割合は足しません)。
5. 比べる会社どうしの持ち合いを見る
比べる会社が、別の比べる会社の上位10に入っているかは peerHolders で調べます。株主の名前(holderKey)を、比べる会社の社名(companyKey)と照合します。デモでは3行が出ます。
| 株主 | 発行会社 | 順位 | 割合 | 千株 |
|---|---|---|---|---|
| デンソー | トヨタ自動車 | 7 | 3.45% | 449,576 |
| トヨタ自動車 | デンソー | 1 | 22.24% | 598,927 |
| トヨタ自動車 | マツダ | 3 | 5.06% | 31,928 |
トヨタ自動車とデンソーは、互いの上位10に入っています。カードのこの行には「比べる会社」の印が付きます。
この一覧は上位10の範囲だけです。保有の全体は、有報の「株式の保有状況」(政策保有株式)で見ます。次回の③で扱います。
6. 所有者別の構成を100%の横棒にする
有報の「所有者別状況」は、株主の区分ごとの株主数・単元数・割合の表です。画面はこれを、model.mjs の categoryShares で1社1本の100%横棒にします。
const share = (r) => finite(r.ratio) ?? (finite(r.units) !== null && finite(total?.units) > 0 ? r.units / total.units : null);
const values = new Map();
for (const r of rows) {
if (r.category === "total") continue;
const key = /^foreign/.test(r.category) ? "foreign" : r.category;
const value = share(r);
if (value === null) continue;
values.set(key, (values.get(key) ?? 0) + value);
}
外国法人等は、有報の「個人以外」と「個人」の2行(/^foreign/)を足して1つの帯にします。割合は有報の値をそのまま使い、空のときだけ単元数を計の単元数で割ります。政府及び地方公共団体の欄が「—」の会社は、0とせず帯を出しません。
デモの5社の値です(2026年3月期、1桁に丸めた値)。
| 会社 | 金融機関 | 証券会社 | 事業法人等 | 外国法人等 | 個人その他 | 政府・地方公共団体 |
|---|---|---|---|---|---|---|
| トヨタ自動車 | 27.0% | 1.5% | 20.3% | 22.8% | 28.4% | 0.0% |
| デンソー | 25.8% | 2.0% | 31.7% | 22.0% | 18.6% | 0.0% |
| 本田技研工業 | 28.5% | 3.4% | 7.5% | 31.8% | 28.9% | — |
| マツダ | 32.5% | 3.7% | 10.8% | 36.6% | 16.5% | — |
| テイン | 4.8% | 0.7% | 37.3% | 4.6% | 52.5% | — |
画面の「証券会社」は有報の「金融商品取引業者」、「事業法人等」は「その他の法人」です。自己株式は「個人その他」に入っています(有報の表のとおり)。また、表は単元の数で数えるので、単元未満の株は入りません。
表から読めるのは区分の構成の違いです。デンソーとテインは事業法人等が、本田技研工業とマツダは外国法人等が3割を超えています。所有者別の割合と大株主の割合は分母も単位も違うので、足し合わせて比べません。
7. 大量保有報告を1通1行にする
大量保有報告書は、上場会社の株式を発行済株式の5%を超えて持った者が提出する書類です。その後、割合が1%以上変わると変更報告書を出します。グループ会社などが一緒に持つ場合は、共同保有者としてまとめて1通で報告します。
APIは共同保有者を1者ずつ、同じ書類IDの別の行で返します。largeHolderRows は、これを1通1行にまとめます。割合は、報告書に書かれた共同保有者の合計(groupRatio)を使い、空のときだけ各者の割合を足します。株数は各者の和です。名前の欄には保有割合の大きい1者を出し、その下に内訳を3者まで並べます。「種類」の列は、新規の報告か変更の報告か(reportKind)です。
デモのマツダには、4通の報告が並びます。
| 提出日 | 名前(画面の表記) | 種類 | 割合 | 千株 |
|---|---|---|---|---|
| 2026/10/22 | 野村アセットマネジメント株式会社 | 変更 | 6.51% | 41,110 |
| 2026/03/30 | 三菱UFJアセットマネジメント株式会社 ほか3者 | 新規 | 5.06% | 31,949 |
| 2026/02/05 | 三井住友トラスト・アセットマネジメント株式会社 ほか2者 | 変更 | 6.66% | 42,063 |
| 2025/03/19 | ブラックロック・ジャパン株式会社 ほか10者 | 変更 | 6.17% | 38,958 |
どれも資産運用会社のグループの報告です。資産運用会社が運用する株式は株主名簿では信託銀行などの名義で載るのが一般的で、大株主の表に出ない名前が大量保有報告に出ます。
トヨタ自動車のカードには「大量保有報告はありません」と出ます。APIは提出者ごとに最新の報告だけを返すので、最新の報告で5%を下回ると一覧から外れます。大株主の表に5%を超える名前があっても、一覧が空になることがあります。
8. 大株主の表と大量保有報告は、数字が合わなくて当然
同じ株主でも、大株主の表と大量保有報告の数字は一致しません。テインで並べます。
| 大株主の表(有報) | 大量保有報告 | |
|---|---|---|
| 書類ID | S100YIDI | S100Z93C |
| 基準日 | 2026年3月末(事業年度末) | 2026/10/26(報告義務が生じた日) |
| 株式会社イチノホールディングス | 3,664千株・37.34% | 3,664,000株・37.34% |
| 市野 諮 | 1,375千株・14.02% | 1,376,100株・14.03% |
| 2者の合計 | (表には合計の行はない) | 5,040,100株・51.37%(共同保有) |
| 種類 | — | 変更 |
違いの理由は3つです。
- 基準日が違います。大株主の表は事業年度末の株主名簿、大量保有報告は報告義務が生じた日の保有です。
- 数え方が違います。大株主の表は名義ごとの1行、大量保有報告は共同保有者の合計です。
- 割合の分母が違います。大株主の表は自己株式を除いた発行済株式、大量保有報告は発行済株式の総数に潜在株式を加えた数で割ります。大株主の表は千株単位です。
大株主の表の2行を足すと51.36%で、大量保有報告の51.37%とは0.01ポイント違います。上の違いや丸めから生じる差です。2つの表の数字は足し引きせず、それぞれの基準日の値として読みます。
9. AIに頼むときは、出典と数字だけを渡す
株主の画面にはプロンプトのコピーのボタンはありません。AIにまとめてもらうときは、①と同じく出典と数字だけを渡し、推測させない指示を付けます。
次の数字は、有価証券報告書の「大株主の状況」「所有者別状況」から取り出したものです。出典は各社の書類IDです。
- 共通する株主と所有者別の構成の違いを表にしてください。信託銀行などの名義(信託口など)は分けてください。
- 大株主の表(事業年度末の名義)と大量保有報告(報告義務が生じた日の共同保有の合計)は、基準日も数え方も違います。数字を足し引きしないでください。
- ここにない事実は推測せず、「原文で確認が必要」と書いてください。
- 数値の予想や、株式の売買、経営の良し悪しについての意見は書かないでください。
## トヨタ自動車(7203) 2026-03期 書類ID S100Y8NY
大株主の上位10(割合は発行済株式(自己株式を除く)に対する割合)
1 日本マスタートラスト信託銀行株式会社 12.80%
2 株式会社豊田自動織機 9.15%
3 株式会社日本カストディ銀行 6.10%
4 日本生命保険(相) 4.86%
(10位まで同じ形)
所有者別:金融機関 27.0% / 証券会社 1.5% / 事業法人等 20.3% / 外国法人等 22.8% / 個人その他 28.4%(自己株式を含む) / 政府・地方公共団体 0.0%
## デンソー(6902) 2026-03期 書類ID S100Y9T1
(以下、同じ形で各社を続ける)
10. 自社のコードで動かす
会員登録をしてAPIキーを作成し、--live を付けて起動し直します。
python3 templates/disclosure-reader/server.py --live
キーは端末の非表示入力で渡し、ローカルのPythonプロセスだけが持ちます。画面で「ライブ取得」を選び、証券コードを2〜5社入れて「取得」を押します。株主の画面の分は、1社あたり3リクエスト(/major-shareholders・/shareholder-categories・/large-holders)です。ほかの画面の分も含めると1社あたり12〜13リクエスト、5社なら最大65リクエストです。
404(その開示がない)は「なし」として扱い、それ以外の失敗が1件でもあれば全体を中止します。画面の切り替えや12節で足すボタンでは、APIを呼びません。
11. 数字の検算
記事の数字はテンプレートの関数で計算したものです。自社や競合のデータでも、次の手順で確かめられます。筆者はデモで1と2を行いました。
1. 大株主の割合の和と、所有者別の表の和を確かめる。 テンプレートの関数を使わず、リポジトリの直下で実行します。
python3 - <<'EOF'
import json
d = json.load(open("templates/disclosure-reader/demo.json", encoding="utf-8"))
for c in d["companies"]:
major = sum(r["ratio"] for r in c["majorShareholders"])
rows = c["shareholderCategories"]["categories"]
total = next(r for r in rows if r["category"] == "total")
parts = [r for r in rows if r["category"] != "total"]
units = sum(r["units"] or 0 for r in parts)
ratio = sum(r["ratio"] or 0 for r in parts)
print(c["symbol"], f"{major:.4f}", units, total["units"], f"{ratio:.4f}")
EOF
7203 0.5109 157893678 157893678 1.0000
6902 0.5948 29102619 29102619 1.0000
7267 0.4283 45304754 45304754 1.0000
7261 0.4073 6312060 6312060 1.0000
7217 0.8052 99930 99930 0.9990
- 2列目は大株主の上位10の割合の和で、5社とも100%を超えません。
- 3列目と4列目は、区分ごとの単元数の和と計の行で、5社とも一致します。
- 5列目は、区分ごとの割合の和です。テインだけ99.9%で、表の割合が0.1%単位に丸めてあるためです。画面の横棒は割合の和で割って幅を決めるので、端まで埋まります。
2. 大量保有報告の共同保有者の割合を足す。 報告書(書類ID)ごとに、共同保有者の株数と割合を足し、groupRatio と比べます。SYMBOL を変えれば、ほかの会社でも確かめられます。
python3 - <<'EOF'
import json
SYMBOL = "7217"
d = json.load(open("templates/disclosure-reader/demo.json", encoding="utf-8"))
c = next(c for c in d["companies"] if c["symbol"] == SYMBOL)
reports = {}
for r in c["largeHolders"]:
reports.setdefault(r["docId"], []).append(r)
for doc_id, rows in reports.items():
print(doc_id, rows[0]["reportKind"], rows[0]["obligationDate"], len(rows),
sum(r["shares"] for r in rows), f"{sum(r['ratio'] for r in rows):.4f}", rows[0]["groupRatio"])
EOF
S100Z93C change 2026-10-26 2 5040100 0.5137 0.5137
テインの報告は共同保有者が2者で、割合の和(37.34%+14.03%=51.37%)が groupRatio と一致します。画面の「51.37%」「5,040千株」とも同じです。共同保有者の間で重なる株を差し引いて合計を出す報告書では、和が groupRatio より少し大きくなります。和が小さい、または共同保有の報告なのに1者しか返らないときは、3で原文の共同保有者の一覧と合計の欄を見比べます。
3. 原文と目で照合する。 「出典」の表の書類IDでEDINETの有報を開き、大株主の順位・株数・割合と所有者別の単元数を画面と見比べます。大量保有報告は、カードの「EDINET」から報告書を開き、共同保有者の一覧と合計を見ます。
12. 小さな変更:信託銀行などの名義を外す
4節で見たとおり、「共通する大株主」の表には信託銀行などの名義が並びます。資本関係を読むために、名義の行を外す切り替えを足します。
計算(model.mjs)とサーバー、APIの呼び出し回数は変えません。変えるのは app.mjs・i18n.mjs・style.css の3つです。
1. 状態を1つ足す(app.mjs の state)。
keyword: params.get("q") ?? "", active: null, prompt: null, hideNominees: false,
2. 名義を見分ける正規表現を足す(app.mjs の renderHolders の上)。
// Trust banks and custodians hold shares in their own name for the real owners (nominee names).
// Matched on holderKey(), so "(信託口)", spaces and full-/half-width differences are already removed.
const NOMINEE = /信託銀行|カストディ銀行|STATESTREET|ステートストリート|JPMORGANCHASE|ジェーピーモルガンチェース|BANKOFNEWYORK|バンクオブニューヨーク/;
commonHolders が返す key は holderKey でそろえた名前(英字は大文字)なので、空白や全角半角を気にせずに照合できます。
3. 表に出す行を選び、ボタンを置く(app.mjs の renderHolders())。 元の common は書き換えず、表に出す行だけを shown にします。
const shown = state.hideNominees ? common.filter((r) => !NOMINEE.test(r.key)) : common;
続く commonTable の common を shown に置き換え、ボタンを足します。
], shown) : node("p", t("commonNone"), { class: "muted" });
const hide = node("button", t("hideNominees"), { type: "button", class: "chip toggle", "aria-pressed": String(state.hideNominees) });
hide.addEventListener("click", () => { state.hideNominees = !state.hideNominees; render(); document.querySelector(".holder-bar .toggle")?.focus(); });
const bar = node("div", undefined, { class: "holder-bar" });
bar.append(hide);
if (state.hideNominees) bar.append(node("span", fill("nomineesHidden", { n: common.length - shown.length }), { class: "muted small" }));
const top = node("div", undefined, { class: "two-up" });
top.append(panel(t("commonTitle"), t("commonNote"), bar, commonTable));
押すと画面を描き直し、ボタンにフォーカスを戻します。会社ごとのカードの「共通4社」などの印は、外す前の数のままです。
4. 文言を足す(i18n.mjs の ja と en)。
moreHolders: "ほか{n}者", hideNominees: "信託銀行などの名義を外す", nomineesHidden: "名義の{n}件を外しています",
moreHolders: "{n} more", hideNominees: "Hide trust-bank nominees", nomineesHidden: "{n} nominee names hidden",
5. 見た目を整える(style.css)。 押した状態は塗りつぶしで示します。
.holder-bar { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; margin-bottom: 10px; }
.chip.toggle { flex: none; padding: 3px 12px; font-size: 12.5px; }
.chip.toggle[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--sheet); }
保存して再読み込みし、「株主」の画面で「信託銀行などの名義を外す」を押します。「名義の4件を外しています」と出て、日本生命保険・トヨタ自動車・トヨタ不動産・豊田自動織機・明治安田生命保険の5行が残れば完成です。もう一度押すと、9行に戻ります。
テストはそのまま通ります(node --test tests/disclosure-reader.test.mjs)。文言を日本語と英語の片方だけに足すと失敗します。NOMINEE はデモの5社に合わせた最小限の一覧なので、別の名義が出るときは足します。
13. 確認箇所
| 症状 | 確認箇所 |
|---|---|
| 同じ株主のはずが、共通する大株主に並ばない | カタカナと英字、口座の番号の違い。holderKey は表記そのものの違いまではそろえない |
| 大量保有報告が空 | 最新の報告で5%を下回った提出者は外れる。EDINETで提出者の最新の報告書を見る |
| 名前の横の注記(「*1」など)が見当たらない | APIでは notes に分かれていて、テンプレートは渡していない。注記の中身は原文で読む |
14. 次回は政策保有株から取引関係を読む
今回は、大株主の上位10を名前でそろえて共通する株主と持ち合いを見つけ、所有者別の構成と大量保有報告を並べました。画面は入口で、判断は原文で行います。
次回のディープ解析③では、同じ開示リーダーの「政策保有」の画面で、有報の「株式の保有状況」から、各社が持つ取引先の株式と、期中に売り切った保有先、比べる会社どうしの持ち合いを読みます。


