2026年に東京証券取引所へ新規上場した38社について、「事業等のリスク」章に20の語がそれぞれ何社に書かれているかを数えました。この記事は、その取得 → 章の抽出 → 語の集計を、他人が同じ数字を再現できる形で残すためのものです。
先に結論を書くと、1社で通った手順は、全数で3か所崩れました。 崩れた場所と直し方を順に書きます。
やったこと
- 対象=2026年に新規上場した38社(基準日 2026年9月29日・日本取引所グループ「新規上場銘柄一覧(株式)」)。うちテクニカル上場7社を除いた31社を集計の母集団にした
- 取得=EDINET API v2。書類一覧(
documents.json)で該当書類を探し、書類取得で本文を落とす - 認証=
Subscription-Keyをクエリパラメータで渡す。キーの値は手元の設定ファイルから読む(記事には書かない) - 結果=章が取れたのは 37/38社(実質の新規上場 31/31・テクニカル上場 6/7)
以下のコードは、実際に回したスクリプトから要点だけを抜き出し、読みやすく簡略化したものです。
落とし穴1:上場前の会社は secCode で引けない
書類一覧には secCode(証券コード)の列があるので、試しに使った既上場の1社はこれで引けたので、同じやり方で回すつもりでした。上場前の会社では、これは全滅します。
有価証券届出書を出す時点では、その会社はまだ上場前です。書類一覧の secCode は未採番(00000) で入っています。secCode で回していたら、38社すべて0件で返っていました。
そこで、提出者名(filerName)で突き合わせました。社名から「株式会社」「(株)」「ホールディングス」などを落とした芯の名前で部分一致させます。
def core_name(name: str) -> str:
for w in ("株式会社", "(株)", "(株)", "ホールディングス"):
name = name.replace(w, "")
return name.strip()
hit = [d for d in docs if core_name(target) in core_name(d["filerName"])]
落とし穴2:1社目が例外だった(届出書と有報)
全数を回し始めたとき、母集団の最初の1社の書類は docTypeCode が 120(有価証券報告書)でした。ここで私たちは「上場会社は承認日に有報を出す。届出書の解析は要らない」と書きかけました。
全数では逆でした。030(有価証券届出書)が36社、120 が1社。 31社の母集団で見ると、届出書30社・有報1社です。
1社目が、たまたま例外の側だったのです。「1社で通った」は「手順が決まった」ではありませんでした。書類の種類は決め打ちせず、030 と 120 の両方を候補に入れ、どちらが取れたかを社ごとに記録する形に変えました。
落とし穴3:「取れなかった」は「無い」ではない
書類一覧は日付で引くので、探す窓が要ります。最初は上場承認日の前後3日、次に前後14日で探しました。この窓では、テクニカル上場7社は 0/7 でした。
ここで「テクニカル上場は届出書が無い」と書きかけました。
探す範囲を承認日の150日前まで広げたら、6/7社が取れました。 提出日は承認日の48〜118日前でした。理由はつきます。持株会社化は株主総会の前に届出書を出すので、上場承認よりずっと早いのです。
実際には、窓を広げて回し直すことを手で繰り返しました。次に同じことをするなら、最初からこの形にします。
# 窓は決め打ちせず、段階的に広げて「どの窓で取れたか」を残す(手で繰り返した手順を整理した形)
for days_back in (3, 14, 150):
found = search_documents(filer=target, start=approval - days_back, end=approval + 3)
if found:
record(target, window=days_back, docs=found)
break
この件から1つ規則にしました。「取れなかった」と書くときは、どの範囲で探したかを一緒に書く。 窓を書かない「無い」は、再現できません。
章の抽出
書類取得(type=1)で落ちてくる ZIP の中から、本文 HTML(ファイル名に honbun を含む .htm)を順に開き、タグを落としてテキストにします。
章の切り出しは、見出しの文字列に頼っています。「事業等のリスク」の見出しの位置から、次の章の見出し(「経営者による財政状態」「経営上の重要な契約」「研究開発活動」「設備の状況」のうち最初に出たもの)の手前までを章としました。複数のファイルで見つかったときは、長い方を採っています。
START = "事業等のリスク"
ENDS = ("経営者による財政状態", "経営上の重要な契約", "研究開発活動", "設備の状況")
def cut_chapter(text: str) -> str:
s = text.find(START)
if s < 0: return ""
ends = [text.find(e, s + len(START)) for e in ENDS]
ends = [e for e in ends if e > 0]
return text[s:min(ends)] if ends else text[s:]
XBRL のタグなど構造は使っていません。見出し文字列への依存は弱点なので、取れた章の文字数を全社ぶん記録し、極端に短い社が無いかを見ました。抽出が途中で切れていれば、文字数に出ます。
語の集計と、部分一致の落とし穴
集計の前に、20語を固定しました。
セキュリティ/サイバー/個人情報/情報セキュリティ/不正アクセス/情報漏えい/情報漏洩/システム障害/API/暗号化/ランサム/マルウェア/ISMS/プライバシーマーク/内部統制/クラウド/BCP/災害/法令遵守/コンプライアンス
数え方は、章のテキストに対する単純な文字列の出現回数です。社数は「1回以上出た社」。
ここに落とし穴があります。部分一致なので、「セキュリティ」は「情報セキュリティ」の中の出現も数えます。「ランサム」は「ランサムウェア」を含みます。 公開したリリースでは「『ランサム』は『ランサムウェア』を含む語として数えています」と注記しました。語を単独で数えたいなら、前後の文字を見る処理が要りますが、今回はそうしていません。
表記ゆれも別の語として数えています。「情報漏洩」6社・「情報漏えい」3社で、合算はしていません。
もう1つ、数字の意味の線を引いておきます。人の解釈は入れていません。「リスクがある」と書いたのか「対策している」と書いたのかは区別していません。だから出てくる数字は**「その語が書かれている社数」**であって、「そのリスクを認識している社数」ではありません。書いていないことは、対策をしていないことを意味しません。
再現できる形で残す
公開したのは、抽出条件・基準日・20語・集計日です。同じ手順で同じ数字が出ます。素データ(社ごとの語の回数)は JSON で手元に残しています。
一番効いた数字の例を挙げると、31社で「API」は1社(3%)、「サイバー」または「セキュリティ」は27社(87%)、「ランサム」は0社でした。当社(SecuROI)はこの結果を調査リリースとして公開しましたが、この記事で残したいのは数字ではなく手順のほうです。
まとめ
1社で通った手順は、全数で3か所崩れました。
-
secCodeは上場前の会社では使えない →filerNameで突き合わせる - 1社目の書類種別(
120)は例外で、本線は030だった → 両方を候補にして記録する - 窓を狭く取ると「無い」に見える → 窓を広げ、どの窓で取れたかを残す
「通った」「取れなかった」は、どの範囲で確かめたかとセットで書く。それだけで、次に同じことをする人が同じ数字にたどり着けます。
出典:EDINET 閲覧(提出)サイト( https://disclosure2.edinet-fsa.go.jp/ ) /公共データ利用規約(PDL1.0)( https://www.digital.go.jp/resources/open_data/public_data_license_v1.0 )
上記をもとに筆者が「事業等のリスク」章を抽出し、語の出現を集計した。