このプロジェクトについて
EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。
公開サイトはこちら →
https://stocks.happy-life-design.net
前回の記事はこちら →
https://qiita.com/rightcross-ae86/items/36cfd91c36621430498c
今回やったこと
情報収集処理のログが「成功」と記録していても、実際にはデータが消えていることがある、という指摘から調査を始めました。原因を追っていくと、ログの信頼性そのものの欠陥に加えて、実装以来一度も正常動作したことがなかった別の機能まで見つかりました。ログの表示を鵜呑みにせず実データで裏取りすることの大切さを、あらためて痛感した話です。
前提: 「ログは正常なのに、実際は消えていた」という指摘
決算データのバックフィル作業中、「ログには正常に取得できたと記録されているのに実際は消えていた、というのはログの吐き出し自体がおかしいのでは」という指摘を受けました。それまでログのstatus列は疑う対象にしていませんでした。
原因: statusが常に'success'固定でINSERTされていた
_write_log()の実装を確認したところ、status列は常に文字列'success'固定で書き込まれており、error_msg列も常に空欄でした。実際に2026-08-10のバックフィル作業(ログID6848)を確認すると、3件のエラーが発生していたにもかかわらず、記録はstatus='success'のままでした。このシステムのログは、エラーが起きても「成功」としか報告しない仕様になっていました。
修正: エラー内容に応じてstatusを出し分け
self.errorsの有無に応じてstatusを'success'/'partial_error'に出し分け、error_msg列に実際のエラー内容を記録するよう修正しました。まずKabutanFinancialsPipelineに実装しました。
同じ調査の過程で、別のパイプラインにも同型バグを発見
KabutanFinancialsPipelineを直した流れで他のパイプラインも確認したところ、株主優待データを扱うJpxBenefitsPipelineに、rollback巻き添えと偽装successという全く同じ2つの構造的バグが存在すると判明しました。
JpxBenefitsPipelineの過去被害を調べたら、テーブルが1件も無かった
過去の被害範囲を調べるためshareholder_benefitsテーブルとscrape_logsを確認したところ、テーブルは0件、収集ログも1件も存在していませんでした。Airflow DAGやcrontabにも登録がありませんでした。
実際に起動すると即クラッシュ: モジュールパスの設定ミス
試しにスパイダーを起動したところ、jpx_benefits.pyのITEM_PIPELINES設定が誤って"scrapers.pipelines.JpxBenefitsPipeline"(正しくは"pipelines.JpxBenefitsPipeline")となっており、起動直後にModuleNotFoundErrorでクラッシュしました。2026年6月21日の実装以来、一度も1件も正常動作したことがなかったと判明しました。モジュールパスを修正しました。
修正後に再度動かすと、今度は別の理由で404
モジュールパス修正後に再度クロールしたところ、今度は取得先URL(JPXの株主優待制度ページ)が404を返しました。JPXサイトの親ページ・robots.txt・Web検索のいずれからも、株主優待情報への導線は見当たりませんでした。検索でヒットするのは全てJPX自身の株主向け優待制度(2025年3月廃止)の話で無関係でした。JPX(取引所運営会社)が全上場企業の株主優待情報を集約公開しているという、このスパイダーの前提自体が実在しなかった可能性が高いと判断しています。
現状: 1つは対応予定、もう1つは判断待ち
同じ偽装successパターンは他の4つのパイプライン(kabutan_news_pipeline.py・minkab_news_pipeline.py・minkab_nse_prices_pipeline.py・minkab_pipeline.py)にも現在も残っています。こちらは横展開して直す対応予定です。もう1つ、株主優待データ機能そのものをどうするかは別の話で、別の情報源(民間サイト)で再設計するか保留にするかの判断はまだ出ていません。
技術メモ
ログのstatus出し分けロジック
# 修正前
status = "success" # 常に固定
error_msg = ""
# 修正後
status = "partial_error" if self.errors else "success"
error_msg = "; ".join(self.errors) if self.errors else ""
ITEM_PIPELINESのモジュールパス誤り
誤ったモジュールパス(実装以来ずっとこのまま): "scrapers.pipelines.JpxBenefitsPipeline"
正しいモジュールパス: "pipelines.JpxBenefitsPipeline"
先頭のscrapers.が余分に付いており、Pythonのモジュール解決でModuleNotFoundErrorになっていました。
まとめ
「ログが成功と言っているから大丈夫」という前提を疑ったところ、ログの信頼性そのものの欠陥と、実装以来一度も正常動作していなかった機能の両方が見つかりました。1つのバグを直した先に、もっと根深い問題が隠れていることがあります。気になる点・ご質問があればコメントでお知らせください。