この調査は、誰でも再現できます。使ったスクリプトを、全文そのまま載せます。
データは有料でも非公開でもありません。JPX と EDINET が無料で公開しているものだけを使っています。必要なのは Python と、EDINET の API キー(無料)だけです。
手順
- JPX「東証上場銘柄一覧」をブラウザで1回ダウンロードする。(JPX はプログラムからの取得を弾くので、ここだけ手作業になります)
get_growth_list.pyを実行 → 市場区分ごとの CSV ができる- EDINET で API キーを取得し、
_データ/edinet_key.txtに1行書く edinet_fetch.py --list→ 過去400日ぶんの有価証券報告書の一覧を集めるedinet_fetch.py --download→ 本体(ZIP)を落とすmarket_compare.py→ 3市場を同じ方法で測る
断っておくこと
- 分類の型は、私が定めたものです。査読を受けていません。型を変えれば、結果は変わります。型もスクリプトの中に全部書いてあります。
- 「事業等のリスク」の章の切り出しに、取りこぼしがありえます。実際、最初は範囲が広すぎて、後続の章まで拾っていました(訂正済み・記事に記録あり)。
- 社数・市場区分は毎月変わります。本調査は 2026年8月31日基準です。
- これは事業構造の記述であって、投資判断の推奨ではありません。
スクリプト(全文)
get_growth_list.py
JPX の「東証上場銘柄一覧」を、市場区分ごとの CSV に分ける
# -*- coding: utf-8 -*-
"""JPX「東証上場銘柄一覧」を落として、市場区分ごとに CSV に分ける。
出典: https://www.jpx.co.jp/markets/statistics-equities/misc/01.html
ファイル: https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls
※ 毎月末に更新される。URL は変わることがある。上のページで必ず確認すること。
出力:
_データ/data_j.xls (原本)
_データ/上場一覧_全体.csv
_データ/上場一覧_プライム.csv
_データ/上場一覧_スタンダード.csv
_データ/上場一覧_グロース.csv ← これが目的のもの
"""
import io, os, sys, ssl, urllib.request
URL = "https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls"
OUT = "_データ"
XLS = os.path.join(OUT, "data_j.xls")
os.makedirs(OUT, exist_ok=True)
def find_local():
"""_データ/ に手で置いたファイルを探す(.xls / .xlsx / .csv のどれでも)"""
import glob
for pat in ["data_j.xls", "data_j.xlsx", "data_j.csv", "*.xls", "*.xlsx"]:
hits = sorted(glob.glob(os.path.join(OUT, pat)))
if hits:
return hits[0]
return None
def fetch():
"""JPX はボット経由のダウンロードを弾く。だから、手で1回だけ落とす。"""
f = find_local()
if f:
print(" 見つけた: %s (%.1f MB)" % (f, os.path.getsize(f) / 1e6))
return f
print(" ★ ファイルがまだ無い。ブラウザで1回だけ落とすこと:")
print()
print(" 1. 下のページをブラウザで開く")
print(" https://www.jpx.co.jp/markets/statistics-equities/misc/01.html")
print()
print(" 2. 「東証上場銘柄一覧」の Excel ファイルをクリックして保存")
print()
print(" 3. 保存したファイルを、このフォルダに置く:")
print(" %s" % os.path.abspath(OUT))
print()
print(" 4. もう一度このスクリプトを実行する")
print()
print(" ★ JPX はプログラムからの直接取得を弾く。だから、ここだけ手作業になる。")
print(" ★ 月1回だけの作業である。")
return None
def split(path):
try:
import pandas as pd
except ImportError:
print(" ★ pandas が無い。 pip install pandas xlrd を実行すること")
return
try:
if path.lower().endswith('.csv'):
df = pd.read_csv(path, encoding='cp932')
else:
df = pd.read_excel(path)
except Exception as e:
print(" ★ 読めなかった: %s" % e)
print(" ★ pip install xlrd が要る場合がある")
return
print("\n 列: %s" % list(df.columns))
print(" 行数: %d" % len(df))
# 市場区分の列名を探す(年によって微妙に違うため)
col = None
for c in df.columns:
if "市場" in str(c) and "区分" in str(c):
col = c
break
if col is None:
print(" ★ 市場区分の列が見つからない。列名を見て、手で指定すること")
return
print("\n === 市場区分ごとの社数 ===")
vc = df[col].value_counts()
for k, v in vc.items():
print(" %-40s %5d" % (k, v))
df.to_csv(os.path.join(OUT, "上場一覧_全体.csv"), index=False, encoding="utf-8-sig")
# 「グロース(内国株式)」のように括弧つきなので、部分一致で拾う
for name, key in [("プライム", "プライム"), ("スタンダード", "スタンダード"), ("グロース", "グロース")]:
sub = df[df[col].astype(str).str.contains(key, na=False)]
p = os.path.join(OUT, "上場一覧_%s.csv" % name)
sub.to_csv(p, index=False, encoding="utf-8-sig")
print("\n %s: %d社 -> %s" % (name, len(sub), p))
if name == "グロース" and len(sub):
print(" 先頭5件:")
for _, r in sub.head(5).iterrows():
print(" %s %s" % (r.iloc[1], r.iloc[2]))
print("=== JPX 東証上場銘柄一覧の取得 ===\n")
_f = fetch()
if _f:
split(_f)
print("\n※ このファイルは毎月更新される。分析の日付を必ず記録すること。")
edinet_fetch.py
EDINET の API で、有価証券報告書の一覧と本体を取得する
# -*- coding: utf-8 -*-
"""EDINET から、グロース全社の有価証券報告書を集める(パイプライン第2段)。
★★★★★ 目的(§139-3):グロース企業の有報は、依存先を名指しで書かざるを得ない。
だから「事業等のリスク」を読めば、問5(自社のものか、借り物か)が機械的に答えられる。
------------------------------------------------------------------
準備(1回だけ)
------------------------------------------------------------------
1. EDINET のサイトで、API のアカウントを作り、キーを取得する(無料)
※ 登録方法・APIの仕様は変わる。必ず EDINET の公式案内で確認すること。
2. キーを環境変数に入れる(PowerShell):
$env:EDINET_KEY = "取得したキー"
3. 実行:
python _ツール/edinet_fetch.py --list … 書類一覧を集める(日数ぶんAPIを叩く)
python _ツール/edinet_fetch.py --download … 一覧から、グロース社の有報を落とす
------------------------------------------------------------------
出力(すべて _データ/ 配下。★★★★★ 非公開フォルダ)
------------------------------------------------------------------
_データ/edinet/一覧.csv … 提出書類の一覧(日付・docID・提出者・書類種別)
_データ/edinet/zip/{docID}.zip … 有報の原本
------------------------------------------------------------------
"""
import io, os, sys, csv, json, time, zipfile, datetime
import urllib.request, urllib.parse
KEYFILE = os.path.join("_データ", "edinet_key.txt")
def load_key():
"""① 環境変数 EDINET_KEY ② _データ/edinet_key.txt の順に読む。"""
k = os.environ.get("EDINET_KEY", "").strip()
if k:
return k
if os.path.exists(KEYFILE):
for line in io.open(KEYFILE, encoding="utf-8"):
line = line.strip()
if line and not line.startswith("#"):
return line
return ""
KEY = load_key()
BASE = "https://api.edinet-fsa.go.jp/api/v2"
OUT = os.path.join("_データ", "edinet")
LIST = os.path.join(OUT, "一覧.csv")
ZIPD = os.path.join(OUT, "zip")
import sys as _s
MKT = [a for a in _s.argv if a.startswith("--市場=")]
_m = MKT[0].split("=")[1] if MKT else "グロース"
GROWTH = os.path.join("_データ", "上場一覧_%s.csv" % _m)
# 有価証券報告書を指す組み合わせ(※ 仕様は要確認)
ORD, FORM = "010", "030000"
os.makedirs(ZIPD, exist_ok=True)
def need_key():
if KEY:
return True
print("★☆☆☆☆ APIキーが見つからない。次のどちらかで入れること。")
print()
print(" 【方法1・おすすめ】ファイルに書く(一度書けば、ずっと使える)")
print()
print(" %s" % os.path.abspath(KEYFILE))
print(" ↑ このファイルを作って、キーだけを1行書いて保存する")
print()
print(" 【方法2】そのときだけ使う(PowerShell を閉じると消える)")
print()
print(' $env:EDINET_KEY = "取得したキー"')
print()
print("★★★★★ キーは秘密である。_データ/ は非公開フォルダなので、そこに置くこと。")
print("★★★★★ サイトにも note にも上げないこと。GitHub 等に置くなら除外設定を入れること。")
return False
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=60) as r:
return r.read()
def cmd_list(days=400):
"""過去 days 日ぶんの提出書類一覧を集める。有報は年1回なので、1年強を見る。"""
if not need_key():
return
rows, today = [], datetime.date.today()
print("=== 書類一覧の取得(過去%d日)===" % days)
print("★★★☆☆ API を %d 回叩く。数分〜十数分かかる。\n" % days)
for i in range(days):
d = today - datetime.timedelta(days=i)
if d.weekday() >= 5: # 土日は提出が無い(疎通テストで確認済)
continue
u = "%s/documents.json?%s" % (BASE, urllib.parse.urlencode(
{"date": d.isoformat(), "type": "2", "Subscription-Key": KEY}))
try:
j = json.loads(get(u).decode("utf-8"))
except Exception as e:
print(" ★☆☆☆☆ %s : %s" % (d, e))
continue
n = 0
for r in (j.get("results") or []):
if r.get("ordinanceCode") == ORD and r.get("formCode") == FORM:
rows.append({
"日付": d.isoformat(),
"docID": r.get("docID", ""),
"証券コード": (r.get("secCode") or "")[:4],
"提出者": r.get("filerName", ""),
"書類": r.get("docDescription", ""),
})
n += 1
if n:
print(" %s : 有報 %d件" % (d, n))
time.sleep(0.15)
with io.open(LIST, "w", encoding="utf-8-sig", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=["日付", "docID", "証券コード", "提出者", "書類"])
w.writeheader()
for r in rows:
w.writerow(r)
print("\n有報 %d件 -> %s" % (len(rows), LIST))
def cmd_download():
"""一覧のうち、グロース市場の会社のものだけ落とす。"""
if not need_key():
return
if not os.path.exists(LIST):
print("★☆☆☆☆ 先に --list を実行すること")
return
codes = set()
if os.path.exists(GROWTH):
with io.open(GROWTH, encoding="utf-8-sig") as fh:
for r in csv.DictReader(fh):
# ★ 列名が「コード」ちょうどのものだけ(33業種コード等を拾わないため)
v = r.get("コード")
if v:
codes.add(str(v).strip()[:4])
print("グロース %d社 を対象にする" % len(codes))
else:
print("★☆☆☆☆ %s が無い。全件を落とす" % GROWTH)
with io.open(LIST, encoding="utf-8-sig") as fh:
rows = list(csv.DictReader(fh))
tgt = [r for r in rows if not codes or r["証券コード"] in codes]
print("対象書類: %d件\n" % len(tgt))
ok = 0
for i, r in enumerate(tgt, 1):
p = os.path.join(ZIPD, r["docID"] + ".zip")
if os.path.exists(p):
continue
u = "%s/documents/%s?%s" % (BASE, r["docID"], urllib.parse.urlencode(
{"type": "1", "Subscription-Key": KEY}))
try:
io.open(p, "wb").write(get(u))
ok += 1
print(" %4d/%d %s %s" % (i, len(tgt), r["証券コード"], r["提出者"][:24]))
except Exception as e:
print(" ★☆☆☆☆ %s : %s" % (r["docID"], e))
time.sleep(0.25)
print("\n取得: %d件 -> %s" % (ok, ZIPD))
if "--list" in sys.argv:
cmd_list()
elif "--download" in sys.argv:
cmd_download()
else:
print(__doc__)
print("★★★☆☆ --list か --download を指定すること")
edinet_extract.py
「事業等のリスク」から小見出しを抜き、依存先を分類する
# -*- coding: utf-8 -*-
"""有報の「事業等のリスク」から、依存を述べた文をそのまま抜き、そのあとで分類する(第3段)。
★★★★★ 設計の要点(§131):
カテゴリを先に当てにいかない。まず、会社自身が「依存している」と書いた文を、そのまま抜く。
分類は、そのあとで人間が決める。基底の選択は、機械にはできない。
★★★★★ §139-3:グロース企業は、依存が集中しているので、依存先を名指しで書かざるを得ない。
★★★★★ §134-5:有報の「事業等のリスク」は、経営者による自己診断である。
★★★★★ §134-6:経営者が書いたリスク(症状)と、7問の診断(原因)のズレが、記事になる。
出力(すべて ★★★★★ 非公開):
_データ/edinet/依存文.csv … 会社ごとの、依存を述べた文(生のまま)
_データ/edinet/分類.csv … その文をカテゴリに割り当てた結果
_データ/edinet/分布.md … ★★★★★ 依存先の分布(これは公開してよい)
"""
import io, os, re, csv, glob, zipfile, collections, html
Z = os.path.join("_データ", "edinet", "zip")
LIST = os.path.join("_データ", "edinet", "一覧.csv")
GROWTH = os.path.join("_データ", "上場一覧_グロース.csv")
O_SENT = os.path.join("_データ", "edinet", "依存文.csv")
O_CLS = os.path.join("_データ", "edinet", "分類.csv")
O_DIST = os.path.join("_データ", "edinet", "分布.md")
# ---- 依存を述べている文を拾うための語(会社自身の言葉)----
DEP = re.compile(u"依存|集中して|特定の|限られた|限定され|不可欠|停止した場合|"
u"中止された場合|変更された場合|継続できな|大きく影響")
# ---- 分類(§135-3 の型。既に書いた10本から作った)----
KEYS = collections.OrderedDict([
(u"検索エンジン", [u"検索エンジン", u"Google", u"グーグル", u"検索結果", u"検索順位", u"SEO"]),
(u"アプリストア", [u"App Store", u"AppStore", u"Google Play", u"アプリストア",
u"Apple", u"アップル", u"プラットフォーム事業者", u"アプリ配信"]),
(u"SNS・動画", [u"SNS", u"ソーシャル", u"YouTube", u"Twitter", u"Instagram",
u"TikTok", u"動画配信", u"配信プラットフォーム"]),
(u"特定の大口顧客", [u"特定の顧客", u"主要な顧客", u"特定の取引先", u"取引先の集中",
u"上位数社", u"大口顧客", u"顧客の集中"]),
(u"制度・法令", [u"法令", u"規制", u"許認可", u"制度変更", u"法改正", u"監督官庁", u"ガイドライン"]),
(u"補助金・公共", [u"補助金", u"助成金", u"交付金", u"公共事業", u"入札", u"官公庁"]),
(u"クラウド基盤", [u"AWS", u"Amazon Web", u"Microsoft Azure", u"クラウドサービス",
u"データセンター", u"サーバー", u"通信回線"]),
(u"決済・金融機関", [u"決済代行", u"カード会社", u"金融機関", u"クレジットカード", u"収納代行"]),
(u"人材", [u"人材の確保", u"優秀な人材", u"採用活動", u"離職", u"人材の流出", u"技術者の確保"]),
(u"特定の仕入先", [u"仕入先", u"外注先", u"委託先", u"サプライヤー", u"調達先", u"生産委託"]),
(u"知的財産・ライセンス", [u"ライセンス", u"知的財産", u"特許", u"使用許諾", u"商標"]),
(u"創業者・特定の個人", [u"代表取締役", u"創業者", u"特定の人物", u"経営陣に依存"]),
])
# ★★★★★ 小見出しの型(§145-x):有報のリスクは「(1) ○○について」の形で並ぶ。
# この見出しこそが、経営者が名指しした依存先である。
HEAD = re.compile(
u"[(((]?\s*(?:[0-90-9]{1,2}|[①-⑳]|[ア-ン])\s*[)))]?\s*"
u"([^。\s]{4,44}?(?:について|に関するリスク|のリスク|リスク|に係るリスク))\s")
# 決意表明・一般論を除くための語(これらを含む文は、依存の記述ではない)
NOISE = re.compile(u"認識しております|努めております|努めてまいります|考えており|方針で|"
u"取り組んでおり|周知徹底|ビジョン|理念")
TAG = re.compile(u"<[^>]+>")
SEC = re.compile(u"事業等のリスク")
def zip_text(zp):
"""ZIP内の本文HTMLを、タグを落として連結する。"""
out = []
try:
with zipfile.ZipFile(zp) as z:
names = [n for n in z.namelist()
if n.lower().endswith((".htm", ".html")) and "PublicDoc" in n]
if not names: # PublicDoc が無い形式もありうる
names = [n for n in z.namelist() if n.lower().endswith((".htm", ".html"))]
for n in sorted(names):
try:
raw = z.read(n)
except Exception:
continue
for enc in ("utf-8", "cp932"):
try:
t = raw.decode(enc)
break
except Exception:
t = None
if t:
# ★   等の実体参照をほどく(有報のHTMLに多い)
out.append(html.unescape(TAG.sub(u" ", t)))
except Exception:
return u""
return u"\n".join(out)
# リスク章の終わりを示す見出し(ここで切らないと、経理の状況まで拾ってしまう)
END = re.compile(u"経営者による財政状態|"
u"経営上の重要な契約等|"
u"研究開発活動|設備の状況|"
u"提出会社の状況|経理の状況|"
u"監査証明について")
def risk_part(t):
"""「事業等のリスク」の章だけを切り出す。
※ 後続の章(経理の状況等)まで拾うと、「監査証明について」が
リスク見出しとして数えられてしまう。"""
ms = list(SEC.finditer(t))
if not ms:
return u""
st = ms[-1].start() if len(ms) > 1 else ms[0].start()
seg = re.sub(r"\s+", u" ", t[st: st + 60000])
m = END.search(seg, 200)
return seg[:m.start()] if m else seg
# ---- docID → 証券コード・社名 ----
meta = {}
if os.path.exists(LIST):
with io.open(LIST, encoding="utf-8-sig") as fh:
for r in csv.DictReader(fh):
meta[r["docID"]] = (r.get("証券コード", ""), r.get("提出者", ""))
files = sorted(glob.glob(os.path.join(Z, "*.zip")))
if not files:
print("★☆☆☆☆ %s にZIPが無い。先に python _ツール/edinet_fetch.py --download を実行すること" % Z)
raise SystemExit(0)
print("=== 事業等のリスクの抽出 ===")
print("対象: %d件\n" % len(files))
sents, cls, headrows, no_sec = [], [], [], 0
for i, p in enumerate(files, 1):
doc = os.path.basename(p)[:-4]
code, name = meta.get(doc, ("", ""))
t = zip_text(p)
seg = risk_part(t)
if not seg:
no_sec += 1
continue
# 句点で切って、依存を述べている文だけ拾う
hits = [s.strip() for s in re.split(u"。", seg)
if DEP.search(s) and 20 < len(s) < 400 and not NOISE.search(s)]
hits = hits[:40]
# ★★★★★ 小見出し(経営者が名指しした項目そのもの)
# ★★★★★ 見出しは「依存がある」ことを言い、直後の本文が「何に」を言う。
# だから、見出しの位置から次の見出しまでを本文として切り出し、そちらで分類する。
ms, seen = [], set()
for m in HEAD.finditer(seg):
h = m.group(1).strip()
if h in seen or NOISE.search(h):
continue
seen.add(h)
ms.append((m.end(), h))
for j, (pos, h) in enumerate(ms[:40]):
end = ms[j + 1][0] if j + 1 < len(ms) else pos + 1500
body = seg[pos: min(end, pos + 2500)]
ks = [k for k, ws in KEYS.items() if any(w in (h + body) for w in ws)]
headrows.append({"証券コード": code, "社名": name, "docID": doc,
"分類": " / ".join(ks), "見出し": h,
"本文冒頭": re.sub(r"\s+", u" ", body)[:300]})
found = set()
for s in hits:
ks = [k for k, ws in KEYS.items() if any(w in s for w in ws)]
found |= set(ks)
sents.append({"証券コード": code, "社名": name, "docID": doc,
"分類": " / ".join(ks), "文": s})
cls.append({"証券コード": code, "社名": name, "docID": doc,
"依存文の数": len(hits), "分類の数": len(found),
"分類": " / ".join(k for k in KEYS if k in found)})
if i % 50 == 0:
print(" %d/%d ..." % (i, len(files)))
O_HEAD = os.path.join("_データ", "edinet", "リスク見出し.csv")
for path, rows, cols in [(O_HEAD, headrows, ["証券コード", "社名", "docID", "分類", "見出し", "本文冒頭"]),
(O_SENT, sents, ["証券コード", "社名", "docID", "分類", "文"]),
(O_CLS, cls, ["証券コード", "社名", "docID", "依存文の数", "分類の数", "分類"])]:
with io.open(path, "w", encoding="utf-8-sig", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=cols)
w.writeheader()
for r in rows:
w.writerow(r)
N = max(1, len(cls))
# ★★★★★ 分布は「見出し」で数える。文より、経営者が名指しした単位だから。
by_co = collections.defaultdict(set)
for r in headrows:
for k in r["分類"].split(" / "):
if k:
by_co[r["docID"]].add(k)
cnt = collections.Counter()
for d, ks in by_co.items():
for k in ks:
cnt[k] += 1
dep_co = sorted(set(r["docID"] for r in headrows if u"依存" in r["見出し"]))
dep_heads = [r for r in headrows if u"依存" in r["見出し"]]
print("\n=== 依存先の分布(公開する成果物)===\n")
md = [u"# グロース市場:会社は、何に依存していると書いているか", u"",
u"**有価証券報告書の「事業等のリスク」の小見出しを、そのまま集めて数えた。**", u"",
u"| | |", u"|---|---|",
u"| 対象 | %d社 |" % len(cls),
u"| リスクの小見出し | %d件(1社あたり %.1f件)|" % (len(headrows), float(len(headrows))/N),
u"| ★★★★★ **見出しに「依存」と書いた会社** | ★★★★★ **%d社(%.0f%%)** |" % (len(dep_co), 100.0*len(dep_co)/N),
u"| 出典 | EDINET(金融庁)|", u"",
u"> ★★★★★ **これは事実の集計であって、銘柄の優劣ではない(§143-4)。**",
u"> ★★★★★ **分類したのではない。経営者が自分で見出しに書いたものを、数えた。**", u"",
u"---", u"", u"## 依存先の分布", u"",
u"| 依存先 | 社数 | 割合 |", u"|---|---|---|"]
for k in KEYS:
n = cnt.get(k, 0)
print(" %-18s %4d社 %5.1f%% %s" % (k, n, 100.0*n/N, u"█"*int(34.0*n/N)))
md.append(u"| %s | %d社 | %.1f%% |" % (k, n, 100.0*n/N))
md += [u"", u"---", u"",
u"## ★★★★★ 経営者が、自分で「依存」と書いた見出し", u"",
u"**★★★★★ §134-5:有報の「事業等のリスク」は、経営者による自己診断である。**", u"",
u"**★★★★★ だから、問5(その経路は自社のものか、借り物か)は、機械的に答えられる。**", u"",
u"| 会社 | 見出し |", u"|---|---|"]
for r in dep_heads:
md.append(u"| %s | **%s** |" % (r["社名"][:20], r["見出し"]))
md += [u"", u"---", u"",
u"★★★★★ **【1社の記事は競合が多いが、この分布は誰も持っていない(§139-6)】**", u""]
print("\n 見出しに「依存」と書いた会社: %d社 / %d社 (%.0f%%)" % (len(dep_co), N, 100.0*len(dep_co)/N))
io.open(O_DIST, "w", encoding="utf-8").write(u"\n".join(md))
print("\n抽出: %d社 / %d文" % (len(cls), len(sents)))
if no_sec:
print("★☆☆☆☆ 「事業等のリスク」が取れなかった: %d件" % no_sec)
print("\n 依存文 -> %s ★★★★★ 非公開" % O_SENT)
print(" 分類 -> %s ★★★★★ 非公開" % O_CLS)
print(" 分布 -> %s ★★★★★ これは公開してよい" % O_DIST)
market_compare.py
3市場を同じ方法で測り、結果を突き合わせる
# -*- coding: utf-8 -*-
"""3市場を同じ方法で測り、§135-1 を検証する。
★★★★★ 検証する命題(§135-1):
市場区分は、経路の段階の分類である。
★★★★★ 先に置いた予測(§157/外れたら訂正を記録に残す):
・プライムは、依存見出しが少ないだろう(経路が固定しているから)
・グロースは多い(実測済み:15%が「依存」見出しあり)
・スタンダードは中間だろう
出力: _データ/edinet/3市場比較.md(★★★★★ これは公開してよい ── 事実の集計)
"""
import io, os, re, csv, glob, zipfile, collections, html, sys
Z = os.path.join(u"_データ", u"edinet", u"zip")
LIST = os.path.join(u"_データ", u"edinet", u"一覧.csv")
OUT = os.path.join(u"_データ", u"edinet", u"3市場比較.md")
TAG = re.compile(u"<[^>]+>")
SEC = re.compile(u"事業等のリスク")
END = re.compile(u"経営者による財政状態|経営上の重要な契約等|研究開発活動|設備の状況|"
u"提出会社の状況|経理の状況|監査証明について")
HEAD = re.compile(
u"[(((]?\\s*(?:[0-90-9]{1,2}|[①-⑳]|[ア-ン])\\s*[)))]?\\s*"
u"([^。\\s]{4,44}?(?:について|に関するリスク|のリスク|リスク|に係るリスク))\\s")
NOISE = re.compile(u"認識しております|努めております|努めてまいります|考えており|方針で|"
u"取り組んでおり|周知徹底|ビジョン|理念")
# 依存の型(見出しの語で分ける)
DEPTYPE = [
(u"人・経営陣", [u"人物", u"代表", u"経営陣", u"役員", u"創業"]),
(u"取引先・顧客", [u"取引先", u"顧客", u"販売先", u"得意先"]),
(u"事業・製品", [u"事業", u"製品", u"サービスへの依存", u"商品", u"コンテンツ"]),
(u"仕入・外注", [u"仕入", u"外注", u"委託", u"調達", u"供給", u"原材料"]),
(u"経路・基盤", [u"プラットフォーム", u"外部サービス", u"第三者", u"AWS", u"クラウド",
u"通信", u"モール", u"検索", u"媒体", u"システム", u"回線", u"インフラ"]),
(u"資金", [u"有利子負債", u"借入", u"金融機関", u"資金"]),
]
def zip_text(zp):
out = []
try:
with zipfile.ZipFile(zp) as z:
names = [n for n in z.namelist()
if n.lower().endswith((".htm", ".html")) and "PublicDoc" in n]
if not names:
names = [n for n in z.namelist() if n.lower().endswith((".htm", ".html"))]
for n in sorted(names):
try:
raw = z.read(n)
except Exception:
continue
t = None
for enc in ("utf-8", "cp932"):
try:
t = raw.decode(enc)
break
except Exception:
pass
if t:
out.append(html.unescape(TAG.sub(u" ", t)))
except Exception:
return u""
return u"\n".join(out)
def risk_part(t):
ms = list(SEC.finditer(t))
if not ms:
return u""
st = ms[-1].start() if len(ms) > 1 else ms[0].start()
seg = re.sub(r"\s+", u" ", t[st: st + 60000])
m = END.search(seg, 200)
return seg[:m.start()] if m else seg
def codes_of(name):
f = os.path.join(u"_データ", u"上場一覧_%s.csv" % name)
s = set()
if os.path.exists(f):
for r in csv.DictReader(io.open(f, encoding="utf-8-sig")):
if r.get(u"コード"):
s.add(r[u"コード"].strip()[:4])
return s
MK = [(u"プライム", codes_of(u"プライム")),
(u"スタンダード", codes_of(u"スタンダード")),
(u"グロース", codes_of(u"グロース"))]
meta = {}
for r in csv.DictReader(io.open(LIST, encoding="utf-8-sig")):
meta[r["docID"]] = r[u"証券コード"]
files = sorted(glob.glob(os.path.join(Z, "*.zip")))
print(u"解析対象: %d件" % len(files))
res = {m: {"n": 0, "heads": 0, "dep": 0, "dep_co": 0,
"type": collections.Counter()} for m, _ in MK}
for i, p in enumerate(files, 1):
doc = os.path.basename(p)[:-4]
code = meta.get(doc, "")
mkt = next((m for m, cs in MK if code in cs), None)
if not mkt:
continue
seg = risk_part(zip_text(p))
if not seg:
continue
seen, heads = set(), []
for m in HEAD.finditer(seg):
h = m.group(1).strip()
if h in seen or NOISE.search(h):
continue
seen.add(h)
heads.append(h)
heads = heads[:40]
dep = [h for h in heads if u"依存" in h]
r = res[mkt]
r["n"] += 1
r["heads"] += len(heads)
r["dep"] += len(dep)
if dep:
r["dep_co"] += 1
for h in dep:
for k, ws in DEPTYPE:
if any(w in h for w in ws):
r["type"][k] += 1
break
else:
r["type"][u"その他"] += 1
if i % 400 == 0:
print(u" %d/%d ..." % (i, len(files)))
sys.stdout.flush()
# ---------------------------------------------------------------- 出力
L = [u"# 3市場を、同じ方法で測った", u"",
u"**プライム・スタンダード・グロースの有価証券報告書「事業等のリスク」を、同じ手順で数えた。**", u"",
u"> **出典:EDINET(金融庁)/JPX 東証上場銘柄一覧(2026年8月31日基準)**",
u"> ★★★★★ **これは事実の集計であって、銘柄の優劣ではない。投資判断の推奨でもない。**", u"",
u"---", u"", u"## 1. 結果", u"",
u"| 市場 | 解析社数 | 1社あたり見出し | 「依存」見出し | ★★★★★ **依存を書いた会社の割合** |",
u"|---|---|---|---|---|"]
print(u"\n=== 結果 ===\n")
print(u" %-12s %8s %10s %10s %12s" % (u"市場", u"社数", u"見出し/社", u"依存見出し", u"依存を書いた%"))
rows = []
for m, _ in MK:
r = res[m]
n = max(1, r["n"])
hp = float(r["heads"]) / n
pct = 100.0 * r["dep_co"] / n
rows.append((m, r["n"], hp, r["dep"], pct))
print(u" %-12s %7d社 %9.1f件 %9d件 %11.1f%%" % (m, r["n"], hp, r["dep"], pct))
L.append(u"| **%s** | %d社 | %.1f件 | %d件 | **%.1f%%** |" % (m, r["n"], hp, r["dep"], pct))
L += [u"", u"---", u"", u"## 2. 「依存」と書いた中身(市場別)", u"",
u"| 何への依存か | " + u" | ".join(m for m, _ in MK) + u" |",
u"|---|" + u"---|" * len(MK)]
print(u"\n=== 依存の中身(%%)===\n")
print(u" %-16s %s" % (u"", u" ".join(u"%12s" % m for m, _ in MK)))
keys = [k for k, _ in DEPTYPE] + [u"その他"]
for k in keys:
cells = []
for m, _ in MK:
t = res[m]["type"]
tot = max(1, sum(t.values()))
cells.append(u"%.1f%%(%d件)" % (100.0 * t.get(k, 0) / tot, t.get(k, 0)))
print(u" %-16s %s" % (k, u" ".join(u"%12s" % c for c in cells)))
L.append(u"| %s | " % k + u" | ".join(cells) + u" |")
io.open(OUT, "w", encoding="utf-8").write(u"\n".join(L) + u"\n")
print(u"\n-> %s" % OUT)
元のデータ
- JPX 東証上場銘柄一覧:https://www.jpx.co.jp/markets/statistics-equities/misc/01.html
- EDINET(金融庁):有価証券報告書の本体と、API
間違いを見つけたら、教えてください。判断が誤っていたと分かった場合、訂正を記録に残します。書き換えて消すことはしません。