[{"data":1,"prerenderedAt":625},["ShallowReactive",2],{"content-/check-earnings-data-pipeline":3,"all-pages-for-dir":623,"og-image-/check-earnings-data-pipeline":624},{"id":4,"title":5,"body":6,"category":604,"description":605,"extension":606,"meta":607,"navigation":608,"ogImage":609,"path":610,"project_name":611,"published":612,"publishedAt":613,"seo":614,"stem":615,"tags":616,"todo":609,"unpublished":612,"updatedAt":609,"__hash__":622},"pages/2026-05/2026-05-19/check-earnings-data-pipeline.md","決算データ取得ルートを設計し直して、半導体3銘柄を Turso に流し込む /check-earnings を完成させた",{"type":7,"value":8,"toc":589},"minimark",[9,14,18,31,35,38,45,56,60,79,98,101,105,108,114,129,140,144,147,152,167,171,181,196,208,215,240,407,410,414,417,470,480,490,494,516,520,535,540,547,551,558,575,578,582,585],[10,11,13],"h2",{"id":12},"朝の出発点-拡張機能-dl-ベースだったはずの設計","朝の出発点: 拡張機能 + DL ベースだったはずの設計",[15,16,17],"p",{},"朝、机に座って昨日の積み残しメモを開いた。「決算データプラットフォーム A の Actuals & Consensus データを Turso に蓄積するパイプラインの最終段」が残っていた。前日までの計画では、自作の Chrome 拡張機能のコピー機能を流用して、プラットフォーム上の該当ページから TSV をダウンロードさせ、それを Python で Turso に流す構成だった。",[15,19,20,21,25,26,30],{},"設計を立て直す前に、ユーザーから「",[22,23,24],"code",{},"browser-to-api"," というスキルがオープンソースで公開されているけれど、丸ごとダウンロードしないで必要なロジックだけ分析してこっちで実装してほしい」と注文が入った。Codex に計画レビューを依頼すると、致命的指摘が 3 件返ってきた。そのうち 1 件が朝のうちに刺さることになる: 「",[27,28,29],"strong",{},"SNDK のプラットフォーム内部 ID は、他銘柄と同じ命名プレフィックスではない可能性がある","」。",[10,32,34],{"id":33},"phase-1-a-銘柄検索の通信経路を-chrome-devtools-mcp-で当てに行く","Phase 1-A: 銘柄検索の通信経路を Chrome DevTools MCP で当てに行く",[15,36,37],{},"最初の山はプラットフォーム側の銘柄識別子（以下 KID）の解決だった。NVDA は URL から拾えていたが、MU と SNDK は URL を踏まないと分からない。手作業で 1 銘柄ずつ URL を踏むのは運用に乗らないので、プラットフォームのフロントが叩いているはずの銘柄検索エンドポイントを当てに行くことにした。",[15,39,40,41,44],{},"Chrome DevTools MCP でログイン済みのプラットフォームタブを掴み、検索バーをクリックしてポップアップを出させ、\"MU\" と入力した瞬間のネットワークリクエストを ",[22,42,43],{},"list_network_requests"," で拾った。一発で見つかった。",[15,46,47,48,51,52,55],{},"リクエスト body と JSON レスポンスを ",[22,49,50],{},"get_network_request"," で取得して、3 銘柄分の KID を確定させた。",[22,53,54],{},"f6-"," で始まる SNDK の KID を見た瞬間、Codex の指摘 B が机の上で実証された。プランの「他銘柄と同じプレフィックスで URL を組み立てる」ロジックを採用していたら、SNDK だけ静かに 404 を返して気付かないところだった。",[10,57,59],{"id":58},"phase-1-b-2-拡張機能で-dl-するところまでは動いた","Phase 1-B / 2: 拡張機能で DL するところまでは動いた",[15,61,62,63,66,67,70,71,74,75,78],{},"午前中の前半は予定どおり、拡張機能の ",[22,64,65],{},"content.js"," 末尾に ",[22,68,69],{},"getTickerId"," と単一銘柄ダウンロードハンドラ、該当ページへのボタン inject を追加していった。Content Script Isolation の壁に当たって isolated world のオブジェクトが MCP の ",[22,72,73],{},"evaluate_script"," から見えない問題には ",[22,76,77],{},"postMessage"," ブリッジを噛ませて回避した。",[15,80,81,82,85,86,89,90,93,94,97],{},"ボタンを inject して ",[22,83,84],{},"NVDA_EAC_20260519.tsv"," をダウンロードさせる E2E まで通った。",[22,87,88],{},"chrome.downloads.onChanged"," の ",[22,91,92],{},"complete"," を待って resolve するところまで動いた。歓声をあげかけたところで Downloads フォルダを覗くと、ファイル名が ",[22,95,96],{},"ダウンロード"," という固定文字列になっていた。",[15,99,100],{},"Workspace 管理ブラウザの DLP ポリシーが、ファイル名を強制リネームしていた。中身はちゃんと NVDA のデータだったが、名前がぜんぶ「ダウンロード」になるので、複数銘柄を回したら上書きで死ぬ。",[10,102,104],{"id":103},"設計の捨て直し-1-claude-code-自身に-dl-ファイルを見せればいい","設計の捨て直し 1: 「Claude Code 自身に DL ファイルを見せればいい」",[15,106,107],{},"ここでユーザーから方向転換の一言が飛んできた。",[109,110,111],"blockquote",{},[15,112,113],{},"いや、これはスラッシュコマンドでやるわけじゃないですか。スクリプトでやるんじゃなくて、Claude Code 自身にそのダウンロードファイルを見てもらうフローに変えればいいんじゃないですか。",[15,115,116,117,120,121,124,125,128],{},"確かにファイル名さえどうにかなれば、Python は無改修で動く。Claude Code が DL 前後でディレクトリ差分を取り、新規ファイルの中身を読んでティッカーを判定し、正しい名前に置き直してから Python ランナーに渡す、という構成に書き換えた。",[22,118,119],{},"check_earnings.py"," に ",[22,122,123],{},"--mcp-fetched"," フラグを足し、",[22,126,127],{},"check-earnings.md"," も Claude Code 向けの手順書に直した。",[15,130,131,132,135,136,139],{},"E2E を回すと、パイプラインは通ったが取り込み件数が 0 件で返ってきた。原因は別の場所にあった。期間ラベルのパース関数が ",[22,133,134],{},"FY ..."," 表記しか想定しておらず、ページ表示が ",[22,137,138],{},"Calendar Years"," に切り替わっていたヘッダを全部スキップしていた。issue にメモして朝のセッションを区切った。",[10,141,143],{"id":142},"設計の捨て直し-2-欲しいのは-actuals-consensus-じゃなくて-estimates-overview-だった","設計の捨て直し 2: 欲しいのは Actuals & Consensus じゃなくて Estimates Overview だった",[15,145,146],{},"午前後半、ユーザーからプラットフォーム画面のスクリーンショットが 3 枚届いた。",[109,148,149],{},[15,150,151],{},"このアナリスト Estimate の中の、Estimates Overview の中の、Sales と EBITDA と EBIT と EPS と EPS GAAP、これを毎日取得したいわけなんですよ。",[15,153,154,155,158,159,162,163,166],{},"実装していたのは Actuals & Consensus ページだった。本当に欲しいのは ",[27,156,157],{},"Estimates Overview"," ページの ",[27,160,161],{},"Earnings Matrix - Reported & Estimates"," セクションで、5 メトリクス × FY 2026-2029 × (1Q-4Q + Year + Growth) のマトリクスだった。手で書いた DOM スクレイパーで Earnings Matrix を読むところまで作ってみたが、ふと「",[27,164,165],{},"さっき銘柄検索の通信経路を当てたのと同じ手で、Estimates 側のデータ取得経路も特定できるんじゃないか","」と気付いた。",[10,168,170],{"id":169},"設計の捨て直し-3-拡張機能を外してデータ取得経路を直接呼ぶ","設計の捨て直し 3: 拡張機能を外して、データ取得経路を直接呼ぶ",[15,172,173,174,176,177,180],{},"Chrome DevTools MCP で再びプラットフォームタブを掴み、Estimates Overview ページをリロードしながら ",[22,175,43],{}," を流した。データキー指定型の汎用 RPC エンドポイントが複数回叩かれていて、そのうち 1 リクエストのレスポンスが Earnings Matrix の数字とぴったり一致した。リクエスト body の ",[22,178,179],{},"keys"," で欲しいデータを宣言する形になっていた。",[15,182,183,184,187,188,191,192,195],{},"Sales / EPS / EPS GAAP の ",[22,185,186],{},"fy0-fy3"," + ",[22,189,190],{},"fq1-fq11"," を指定してみたら、Free アカウントで全件返ってきた。EBIT / EBITDA だけは Basic 必須で 403 が返ったが、優先度を Sales / EPS / EPS GAAP に絞ればこの 3 メトリクスは全部取れる。",[27,193,194],{},"拡張機能を外して、データ取得経路を直接呼ぶ設計に丸ごと差し替えた","。",[15,197,198,199,201,202,204,205,207],{},"朝に書いた ",[22,200,65],{}," の追加コードも、",[22,203,77],{}," ブリッジも、",[22,206,123],{}," ハンドラも、ここでぜんぶ役目を終えた。",[10,209,211,212,214],{"id":210},"認証の壁を-mcp-の-evaluate_script-でかわす","認証の壁を MCP の ",[22,213,73],{}," でかわす",[15,216,217,218,221,222,224,225,228,229,232,233,236,237,239],{},"Python から直接通信させる案は、",[22,219,220],{},"auth_token"," が Bash transcript に残るので許可を弾かれた。理にかなった判断だった。代わりに MCP の ",[22,223,73],{}," でログイン済みドメイン上から ",[22,226,227],{},"fetch"," を発火させ、JSON レスポンスを ",[22,230,231],{},"%TEMP%"," 配下のファイルに保存させて、そのファイルだけ Python に ",[22,234,235],{},"stdin"," で流し込む構成にした。",[22,238,220],{}," は Chrome 側で完結し、Python は JSON しか触らない。",[241,242,247],"pre",{"className":243,"code":244,"language":245,"meta":246,"style":246},"language-js shiki shiki-themes vitesse-light vitesse-light","// MCP evaluate_script 内で実行\nconst res = await fetch(ENDPOINT, {\n  method: 'POST',\n  headers: { 'content-type': 'application/json' },\n  credentials: 'include',\n  body: JSON.stringify({ keys: [...] })\n})\n// レスポンスを TEMP に保存\n","js","",[22,248,249,258,293,317,347,364,395,401],{"__ignoreMap":246},[250,251,254],"span",{"class":252,"line":253},"line",1,[250,255,257],{"class":256},"sxvE3","// MCP evaluate_script 内で実行\n",[250,259,261,265,269,273,277,281,284,287,290],{"class":252,"line":260},2,[250,262,264],{"class":263},"stQ0i","const",[250,266,268],{"class":267},"s4oTP"," res",[250,270,272],{"class":271},"shFtX"," =",[250,274,276],{"class":275},"sHkkW"," await",[250,278,280],{"class":279},"senZ8"," fetch",[250,282,283],{"class":271},"(",[250,285,286],{"class":267},"ENDPOINT",[250,288,289],{"class":271},",",[250,291,292],{"class":271}," {\n",[250,294,296,300,303,307,311,314],{"class":252,"line":295},3,[250,297,299],{"class":298},"sz8Xr","  method",[250,301,302],{"class":271},":",[250,304,306],{"class":305},"sMJiu"," '",[250,308,310],{"class":309},"sdGka","POST",[250,312,313],{"class":305},"'",[250,315,316],{"class":271},",\n",[250,318,320,323,325,328,330,333,335,337,339,342,344],{"class":252,"line":319},4,[250,321,322],{"class":298},"  headers",[250,324,302],{"class":271},[250,326,327],{"class":271}," {",[250,329,306],{"class":305},[250,331,332],{"class":309},"content-type",[250,334,313],{"class":305},[250,336,302],{"class":271},[250,338,306],{"class":305},[250,340,341],{"class":309},"application/json",[250,343,313],{"class":305},[250,345,346],{"class":271}," },\n",[250,348,350,353,355,357,360,362],{"class":252,"line":349},5,[250,351,352],{"class":298},"  credentials",[250,354,302],{"class":271},[250,356,306],{"class":305},[250,358,359],{"class":309},"include",[250,361,313],{"class":305},[250,363,316],{"class":271},[250,365,367,370,372,375,378,381,384,387,389,392],{"class":252,"line":366},6,[250,368,369],{"class":298},"  body",[250,371,302],{"class":271},[250,373,374],{"class":267}," JSON",[250,376,377],{"class":271},".",[250,379,380],{"class":279},"stringify",[250,382,383],{"class":271},"({",[250,385,386],{"class":298}," keys",[250,388,302],{"class":271},[250,390,391],{"class":271}," [...]",[250,393,394],{"class":271}," })\n",[250,396,398],{"class":252,"line":397},7,[250,399,400],{"class":271},"})\n",[250,402,404],{"class":252,"line":403},8,[250,405,406],{"class":256},"// レスポンスを TEMP に保存\n",[15,408,409],{},"NVDA の取り込みが通ったあと、内部スキーマのキー名が前方一致で別のメトリクスに誤判定されるバグが出た。長い prefix 優先に直して直った。",[10,411,413],{"id":412},"午後-mu-sndk-冪等性-コマンド書き換え","午後: MU / SNDK / 冪等性 / コマンド書き換え",[15,415,416],{},"午後は KID を差し替えて MU と SNDK を順に流した。両方とも 1 発で通った。",[418,419,420,436],"table",{},[421,422,423],"thead",{},[424,425,426,430,433],"tr",{},[427,428,429],"th",{},"銘柄",[427,431,432],{},"periods",[427,434,435],{},"estimates",[437,438,439,451,461],"tbody",{},[424,440,441,445,448],{},[442,443,444],"td",{},"NVDA",[442,446,447],{},"18",[442,449,450],{},"60",[424,452,453,456,458],{},[442,454,455],{},"MU",[442,457,447],{},[442,459,460],{},"47",[424,462,463,466,468],{},[442,464,465],{},"SNDK",[442,467,447],{},[442,469,460],{},[15,471,472,473,475,476,479],{},"念のため NVDA を 2 回流して、",[22,474,435],{}," テーブルの件数が 60 のままで増えないことを確認した。",[22,477,478],{},"INSERT ... ON CONFLICT"," の冪等性が効いている。",[15,481,482,483,486,487,489],{},"そのあと ",[22,484,485],{},"/check-earnings.md"," を新しい経路に書き換えた。朝の段階では「拡張機能のボタンを押す → DL を待つ → ファイルをリネーム → Python」の手順書だったのを、「タブを選んで ",[22,488,73],{}," で 1 銘柄ずつ JSON を取得 → Python に流す」の 3 ステップに圧縮した。",[10,491,493],{"id":492},"c-3-ガイダンスも同じ-turso-に押し込む","C-3: ガイダンスも同じ Turso に押し込む",[15,495,496,497,89,500,503,504,507,508,511,512,515],{},"別件で動いている ",[22,498,499],{},"nvidia-guidance-watcher",[22,501,502],{},"notify.py"," に Turso 書き込み機能を足した。SEC EDGAR から拾ったガイダンス（売上・粗利率レンジ・OpEx など）を ",[22,505,506],{},"consensus_estimates"," と同じ DB の ",[22,509,510],{},"guidance"," テーブルに UPSERT する形にして、",[22,513,514],{},"data/{TICKER}_latest.txt"," から 3 銘柄 × 5 行を入れた。これも冪等性を確認した。",[10,517,519],{"id":518},"c-5-c-6-worker-とフロントは作ったが本番デプロイは保留","C-5 / C-6: Worker とフロントは作ったが本番デプロイは保留",[15,521,522,523,526,527,530,531,534],{},"午後の後半、Cloudflare Worker API のスケルトンとフロントの書き換えまで進めた。Worker は ",[22,524,525],{},"wrangler"," の dry-run でコンパイルが通り、",[22,528,529],{},"/api/health"," と SPA 配信は 200 で返るところまで確認した。ただし ",[22,532,533],{},".dev.vars"," に Turso の認証情報を入れる工程はユーザー側の手作業が必要で、ここで方針判断が入った。",[109,536,537],{},[15,538,539],{},"Cloudflare のデプロイはとりあえず置いといて大丈夫です。スラッシュコマンドでその日のデータを取って、決算があれば見に行くフローができていればそれで十分です。",[15,541,542,543,546],{},"データパイプライン本体は ",[22,544,545],{},"/check-earnings"," 1 発で稼働するところまで来ているので、Worker のローカル動作検証と本番デプロイは保留にして締めることにした。Stop hook が「全機能完成」を要求して何度か再開を促してきたが、ユーザーの意思決定が明示されたあとは判断を曲げずに保留扱いを維持した。",[10,548,550],{"id":549},"振り返り-朝の設計を-3-回捨て直した","振り返り: 朝の設計を 3 回捨て直した",[15,552,553,554,557],{},"今日の本当の作業は実装ではなく、",[27,555,556],{},"朝に書いた設計を 3 回捨て直すこと","だった。",[559,560,561,565,568],"ol",{},[562,563,564],"li",{},"拡張機能 + DL を完成させた直後に、Workspace のファイル名強制リネームで足元が崩れた",[562,566,567],{},"「Claude Code に DL ファイルを見せる」案に乗り換えた直後に、見るべきページが Actuals & Consensus じゃなく Estimates Overview だと判明した",[562,569,570,571,574],{},"DOM スクレイパーを書き始めた瞬間に、銘柄検索の経路を特定したのと同じ手で ",[27,572,573],{},"データ取得側も同じパターンで掴める","と気付いて、拡張機能ごと捨てた",[15,576,577],{},"Codex の事前指摘「SNDK は他銘柄と同じプレフィックスじゃない可能性がある」が朝のうちに実証されたのも、設計を 1 回捨てて済んだ分が積み上がった結果だった。",[10,579,581],{"id":580},"税理士会計士視点での応用","税理士・会計士視点での応用",[15,583,584],{},"顧問先の財務データを毎日 Turso に貯める構成は、税務にもそのまま持ち込める。月次推移を国税庁の e-Stat や、顧問先のクラウド会計の公開 API から日次で吸い上げて 1 つの DB に並べれば、決算月前の数字の動きをスラッシュコマンド 1 発で横串に確認できる。「今月の試算表を出してほしい」と毎月電話で頼まれる作業のうち、データ取得の部分はこれで自動化の射程に入る。",[586,587,588],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":246,"searchDepth":260,"depth":260,"links":590},[591,592,593,594,595,596,597,599,600,601,602,603],{"id":12,"depth":260,"text":13},{"id":33,"depth":260,"text":34},{"id":58,"depth":260,"text":59},{"id":103,"depth":260,"text":104},{"id":142,"depth":260,"text":143},{"id":169,"depth":260,"text":170},{"id":210,"depth":260,"text":598},"認証の壁を MCP の evaluate_script でかわす",{"id":412,"depth":260,"text":413},{"id":492,"depth":260,"text":493},{"id":518,"depth":260,"text":519},{"id":549,"depth":260,"text":550},{"id":580,"depth":260,"text":581},"dev","拡張機能 + DL から JSON 取得経路の差し替えに設計を捨て直し、Chrome DevTools MCP でデータ取得経路を当てに行って NVDA/MU/SNDK の決算データを Turso に取り込むまでの転換点を記録した一日","md",{},true,null,"/check-earnings-data-pipeline","financial-data",false,"2026-05-19T00:00:00.000Z",{"title":5,"description":605},"2026-05/2026-05-19/check-earnings-data-pipeline",[617,618,619,620,621],"Turso","Chrome DevTools MCP","データ取得経路","決算データ","スラッシュコマンド","YOU1LFEEEFl2NgKlEAKFRPDoAVrbgxa7RiSls2cw_Qw",[],"https://log.eurekapu.com/og/blog/check-earnings-data-pipeline.png?v=2026-05-19T00%3A00%3A00.000Z&title=%E6%B1%BA%E7%AE%97%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%96%E5%BE%97%E3%83%AB%E3%83%BC%E3%83%88%E3%82%92%E8%A8%AD%E8%A8%88%E3%81%97%E7%9B%B4%E3%81%97%E3%81%A6%E3%80%81%E5%8D%8A%E5%B0%8E%E4%BD%933%E9%8A%98%E6%9F%84%E3%82%92%20Turso%20%E3%81%AB%E6%B5%81%E3%81%97%E8%BE%BC%E3%82%80%20%2Fcheck-earnings%20%E3%82%92%E5%AE%8C%E6%88%90%E3%81%95%E3%81%9B%E3%81%9F&author=Kei%20Komatsu&sig=440f36ee13714e7e",1782528839482]