Yamagishi Complexity Torus Lab山岸勇輝ラボ

方法とスクリプト ── この調査は、誰でも再現できます

この調査は、誰でも再現できます。使ったスクリプトを、全文そのまま載せます。

データは有料でも非公開でもありません。JPX と EDINET が無料で公開しているものだけを使っています。必要なのは Python と、EDINET の API キー(無料)だけです。

手順

  1. JPX「東証上場銘柄一覧」をブラウザで1回ダウンロードする。(JPX はプログラムからの取得を弾くので、ここだけ手作業になります)
  2. get_growth_list.py を実行 → 市場区分ごとの CSV ができる
  3. EDINET で API キーを取得し、_データ/edinet_key.txt に1行書く
  4. edinet_fetch.py --list → 過去400日ぶんの有価証券報告書の一覧を集める
  5. edinet_fetch.py --download → 本体(ZIP)を落とす
  6. 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:
                    # ★ &#160; 等の実体参照をほどく(有報の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)

元のデータ

間違いを見つけたら、教えてください。判断が誤っていたと分かった場合、訂正を記録に残します。書き換えて消すことはしません。