Skip to content

fix(extract): セクション抽出の bleed と same-ID 巻き戻しを解消#8

Merged
beatinaniwa merged 1 commit into
mainfrom
fix/section-extraction-bleed-and-same-id
May 12, 2026
Merged

fix(extract): セクション抽出の bleed と same-ID 巻き戻しを解消#8
beatinaniwa merged 1 commit into
mainfrom
fix/section-extraction-bleed-and-same-id

Conversation

@beatinaniwa

@beatinaniwa beatinaniwa commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

EDINET 有価証券報告書を edinet-cli doc text --section <id> で取り出すとき、 2 つの独立したバグで内容が壊れていたのを修正しました。

バグ 1: Bleed across unknown headings

walkForSections は KnownSections に含まれる heading 同士の境界でしか section を flush しない実装でした。 EDINET 有報は KnownSections 外の章 (「関係会社の状況」「経営方針」「サステナビリティ」「株式事務の概要」 等) を多数挟む構造のため、 一度 section が開くと 次の KnownSections マッチ heading まで全部 取り込んでしまい、 結果として隣接章のテキストが漏れていました。

実例: 日本マクドナルド HD 第55期 (S100XS22) の employees セクションは 12,737 chars で返り、 実際の従業員情報は約 1,500 chars。 残り約 11,000 chars は 第2【事業の状況】 → 経営方針 → サステナビリティ までの bleed でした。

バグ 2: Same-ID nested heading が content を吹き飛ばす

EDINET 有報には親 heading 「コーポレート・ガバナンスの状況等」 (h3) と直後に子 heading 「コーポレート・ガバナンスの概要」 (h4) が並ぶケースがあり、 両方とも KnownSections の governance に `strings.Contains` で一致します。 旧実装はマッチするたびに section を flush + `text.Reset()` していたので、 親の section が空のまま登録され、 子の section が次に登録される。 `cmd/doc.go` の selector は 最初に ID 一致した section を返す ので、 結果として governance がほぼ空 (raw=0) で返っていました。

Changes

`internal/extract/html.go`

  • `sectionWalkState` を導入し、 現在開いている section の 開始 heading の depth (h-level) を追跡
  • マッチする heading が 同じ ID の場合、 flush せず sub-heading として扱い text 蓄積を継続
  • マッチしない heading が 同じか浅い depth で出現したら、 section 境界として flush。 sub-heading (深い h-level) は section 内に保持
  • 最後に `mergeAdjacentSameIDSections` で同 ID 連続 section をマージ (edge case 用 safety net)

`cmd/doc.go`

  • `--section` の selector を 「最初に一致した section」 → 「最も text が長い一致 section」 に変更

`internal/extract/html_test.go`

  • `TestExtractSections_BleedAcrossUnknownHeadings`: 従業員 → 関係会社 → 第2章 → 経営方針 → サステナビリティ → 事業等のリスク のシーケンスで bleed が起きないこと
  • `TestExtractSections_SameIDNestedHeading`: コーポレート・ガバナンスの状況等 (h3) + コーポレート・ガバナンスの概要 (h4) の入れ子で content がすべて governance に集約されること
  • `TestMergeAdjacentSameIDSections`: merge safety net の動作確認

実 EDINET docID での検証

S100XS22 (日本マクドナルド HD 第55期):

  • governance: 0 chars (empty) → 30,201 chars
  • employees: 12,737 chars (bleed_truncated) → 1,573 chars (bleed なし) ✅

回帰確認:

  • S100XUBI (すかいらーく HD): governance 0 → 38,068、 employees 17,152(bleed) → 1,567
  • S100XTNW (楽天グループ): governance 0 → 8,813、 employees 27,184(bleed) → 2,337、 mda 7,859 → 14,985 (silent truncation も解消)

Test plan

  • `go test ./...` 全件パス (internal/extract に新規 3 テスト追加)
  • S100XS22 で governance / employees の手動確認 (bleed が消え、 隣接章テキストが含まれないこと)
  • S100XUBI / S100XTNW で回帰確認 (既存の正常 section が壊れないこと、 empty / bleed_truncated が解消すること)

影響範囲

  • `doc text --section ` の結果が EDINET 有報全般で改善 (特に governance / employees / mda)
  • 既存に raw=0 で empty 扱いしていた governance、 bleed_truncated していた employees が正しく取れる
  • 過去に取得済の section JSON も、 このバージョンで再 ingest すべき (silent truncation で短く取れていた可能性あり)

EDINET 有価証券報告書の HTML を `--section` フラグで取り出すとき、 2 種類のバグで内容が壊れていた:

## 1. Bleed across unknown headings

`walkForSections` は KnownSections に含まれる heading 同士の境界でしか section を flush しなかった。 EDINET 有報は KnownSections 外の章 (関係会社の状況 / 経営方針、 経営環境及び対処すべき課題等 / サステナビリティ / 株式事務の概要 等) を多数挟む構造のため、 一度 section が開くと「次の KnownSections マッチ heading まで」 すべて取り込んでしまい、 結果として隣接章のテキストまで漏れていた (raw_chars が極端に膨らむ bleed_truncated 現象)。

例: 日本マクドナルド HD 第55期 (S100XS22) の employees セクションは 12,737 chars で取得され、 実際の従業員の状況は約 1,500 chars にすぎないため約 11,000 chars が隣接章からの bleed だった。

## 2. Same-ID nested heading flushes content

EDINET 有報には親 heading 「コーポレート・ガバナンスの状況等」 (h3) と直後の子 heading 「コーポレート・ガバナンスの概要」 (h4) が並ぶケースがあり、 両方とも KnownSections の governance に `strings.Contains` で一致する。 旧実装はマッチするたびに section を flush + text.Reset() するため、 親の section が空のまま登録され、 子の section が次に登録される。 doc.go の selector は 「最初に ID 一致した section」 を返すため、 結果として governance がほぼ空 (raw=0) で返っていた。

## 修正内容

### internal/extract/html.go

- `sectionWalkState` を導入し、 現在開いている section の **開始 heading の depth (h-level)** を追跡。
- マッチする heading が **同じ ID** の場合、 flush せず sub-heading として扱い text 蓄積を継続。
- マッチしない heading が **同じか浅い depth** で出現したら、 section 境界として flush。 sub-heading (深い h-level) は section 内に保持。
- 最後に `mergeAdjacentSameIDSections` で念のため同 ID 連続 section をマージ (edge case 用 safety net)。

### cmd/doc.go

- `--section` の selector を 「最初に一致した section」 → 「最も text が長い一致 section」 に変更。 walker が改善された後でも、 複数 doc にまたがる edge case で同 ID が複数登録されたときに最も内容のあるものを返す。

### internal/extract/html_test.go

- `TestExtractSections_BleedAcrossUnknownHeadings`: 従業員 → 関係会社 → 第2章 → 経営方針 → サステナビリティ → 事業等のリスク のシーケンスで bleed が起きないこと
- `TestExtractSections_SameIDNestedHeading`: コーポレート・ガバナンスの状況等 (h3) + コーポレート・ガバナンスの概要 (h4) の入れ子で content がすべて governance に集約されること
- `TestMergeAdjacentSameIDSections`: merge safety net の動作確認

## 実 EDINET docID での確認 (S100XS22 = 日本マクドナルド HD 第55期)

- governance: 0 chars → 30,201 chars
- employees: 12,737 chars (bleed_truncated) → 1,573 chars (bleed なし)

回帰確認 (S100XUBI すかいらーく / S100XTNW 楽天) でも:
- すかいらーく governance 0 → 38,068、 employees 17,152(bleed) → 1,567
- 楽天 governance 0 → 8,813、 employees 27,184(bleed) → 2,337、 mda 7,859 → 14,985 (silent truncation も解消)

## 影響範囲

- `doc text --section <id>` の結果が EDINET 有報全般で改善 (特に governance / employees / mda)
- 既存に raw=0 で empty 扱いしていた governance、 bleed_truncated していた employees が正しく取れる
- 既存のテスト全件パス (`go test ./...`)
@beatinaniwa beatinaniwa merged commit f2f9886 into main May 12, 2026
2 checks passed
@beatinaniwa beatinaniwa deleted the fix/section-extraction-bleed-and-same-id branch May 12, 2026 02:35
beatinaniwa added a commit that referenced this pull request May 17, 2026
PR #8 で導入した depth-aware section flush は physical h-level (h1〜h6 タグ) のみを見ていたが、 EDINET 提出書類によっては 章見出しと sub-heading に同じ <h3> タグを使っているため、 sub-heading で section が誤 flush される。 具体例:

セコム 第64期 (S100W3TS) の risk 章:
- `<h3>3【事業等のリスク】` ← 章 marker
- `<h3>(1)事業環境に起因するリスク` ← sub-heading だが同じ <h3>
- `<h3>(2)経営戦略に関するリスク` ← 同上
- `<h3>4【経営者による財政状態...】` ← 次の章 marker (mda)

PR #8 の depth-only flush は (1)事業環境に起因するリスク で risk を flush してしまい、 risk セクションが 4,050 chars → 286 chars (intro のみ) に劇的に短縮されていた (= silent data loss)。

## 修正

### chapter vs sub-numbering を識別

`chapterHeadingPrefixRe` で 「N【...】」 「第N【...】」 形式の **章 marker** のみを検出。 「(1)」 「①」 「a.」 などの sub-numbering はマッチしない。

`walkForSections` の非マッチ heading の flush 判定を:

旧: `level > 0 && level <= state.depth` (depth のみ)
新: `level > 0 && level <= state.depth && isChapterHeading(headingText)` (depth + 章 marker)

これで、 sub-heading は同 depth でも章を flush せず sub-section として継続。 章 marker が出てきたら flush する。

### EDINET フッターアーティファクトを除去

各 `_honbun_*.htm` の末尾には `有価証券報告書(通常方式)_<14桁timestamp>` という EDINET メタ情報が書かれている。 walker はこれを最後の section の末尾に取り込んでしまうため、 governance / financial 等の末尾に metadata text が混入していた。

`filingFooterRe` で末尾の `有価証券報告書(通常方式)_<digits>` を section text から除去。 各 section の Text に `stripFilingFooter` を適用。

## 検証

| docID | section | before (PR #8+#9) | after (本 PR) |
|---|---|---|---|
| S100W3TS (セコム) | risk | **286 (regression)** | **4,050 (restored)** |
| S100XTNW (楽天) | governance | 8,813 | 38,947 (footer/章 marker 整理) |
| S100XS22 (マクドナルド) | governance | 30,201 | 30,170 (footer -31 chars) |
| S100VT7P (セブン&アイ) | governance | 59,998 | 59,967 (footer -31 chars) |

全 section の text 末尾 100 chars に `有価証券報告書(通常方式)_` を検出しなくなった。

## Tests

- `TestExtractSections_SubHeadingSameHTag`: セコムパターン (同 h3 タグで章 + sub) で section が正しく分割されること
- `TestIsChapterHeading`: 章 marker と sub-numbering の判定が正しいこと
- `TestStripFilingFooter`: footer 除去ロジック単体
- `TestExtractSections_FilingFooterStripped`: section 抽出時に footer が除去されること

`go test ./...` 全件パス。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant