技術メモを残していきます
EDINET API v2 で有価証券報告書を自動取得する(Node.js / TypeScript)
EDINET は金融庁が運営する電子開示システムで、上場企業が提出した有価証券報告書・四半期報告書などを無償で取得できる API を提供している。 個人開発の財務分析ツールを作るにあたって、この API を Node.js / TypeScript で叩いた際のポイントをまとめる。 エンドポイント概要 ベース URL:https://api.edinet-fsa.go.jp/api/v2 用途 エンドポイント 書類一覧 GET /documents.json?date=YYYY-MM-DD&type=2 PDF取得 GET /documents/{docID}?type=2 XBRL取得 GET /documents/{docID}?type=1(ZIP) API キーはクエリパラメータ Subscription-Key で渡す。EDINET のサイト からアカウント登録すると発行される。ハードコードせず process.env.EDINET_API_KEY から読むのはもはや最低限のマナーといえる。 レスポンスの型定義 まず API レスポンスに型をつける。docID(大文字)が EDINET 公式の表記: interface EdinetDocumentResponse { docID: string // EDINET が振る書類ID docTypeCode: string | null secCode: string | null // 証券コード。上場企業以外は null edinetCode: string filerName: string docDescription: string | null submitDateTime: string } クライアントクラスで型を明示しておくと、後続のフィルタリングや保存ロジックで補完が効いて安全になる。 書類一覧の取得とフィルタリング /documents.json は指定日に提出されたすべての書類を返す。有価証券報告書だけを絞り込むには docTypeCode を見る: 120:有価証券報告書 130:訂正有価証券報告書 140:四半期報告書 また secCode が null の書類は上場企業以外なのでスキップする。 const filteredResults = results.filter((r: EdinetDocumentResponse) => { const typeCode = (r.docTypeCode ?? '').replace(/['"]/g, '') return ( r.secCode != null && (typeCode === '120' || typeCode === '130' || typeCode === '140') ) }) docTypeCode に余分なクォートが混入することがある("120" のように入ってくる)ので replace で除去している。実際にハマった。 ...
Gemini API で財務書類を「怪しさ判定」する:スコア付き出力の設計
個人開発の EDINET 分析ツールでは、取得した有価証券報告書の PDF を Gemini に渡して「怪しさ判定」をさせている。 単なる要約ではなく、3段階のスコア(normal / caution / danger) を返させる設計にしたので、その仕組みをまとめる。 なぜスコアが必要か 毎日数十〜数百件の書類が提出される。全部読むのは無理なので、AI に「これは要注意」かどうかを仕分けさせたい。 スコアが danger の書類だけ Discord 通知を飛ばす、といった使い方ができる。 プロンプト設計 プロンプトの末尾に必ずスコアを出力させるよう指示する: 分析の最後に必ず以下の形式でスコアを出力してください: SCORE:normal # 特に問題なし SCORE:caution # 気になる点あり・要確認 SCORE:danger # 重大なリスクの可能性 Gemini はマークダウン形式で分析テキストを返した後、最終行に SCORE:danger のような文字列を出力する。 PDF を渡す方法 @google/generative-ai SDK では PDF を base64 で渡せる: const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }) const result = await model.generateContent([ { inlineData: { mimeType: 'application/pdf', data: pdfBuffer.toString('base64'), }, }, { text: prompt }, ]) 最大 50MB まで渡せるが、大きすぎるとトークン消費が跳ね上がるので注意。 スコアのパース 正規表現で SCORE: 以降を抽出: ...
Hono + TypeScript でクリーンアーキテクチャもどきを個人開発に持ち込む
個人開発に「クリーンアーキテクチャ」は過剰では?という気持ちはある。 ただ実際にやってみたら、テストが書きやすい・外部APIの差し替えが楽という恩恵がちゃんとあった。 Hono + TypeScript (ESM) でどう組んだかをメモしておく。 ディレクトリ構成 backend/src/ ├── domain/ # エンティティ・リポジトリ Interface │ ├── entity/ │ └── repository/ ├── usecase/ # ビジネスロジック ├── infrastructure/ # DB・外部API の実装 │ ├── postgres/ │ ├── edinet/ │ └── gemini/ ├── api/ # Hono ルーター └── job/ # JobRunner 依存の方向 api / job ↓ usecase ← domain (Interface) ↓ infrastructure → domain (Interface を実装) usecase は domain の Interface にしか依存しない。 infrastructure が Interface を実装する。これだけ守れば十分。 ...
ReactのuseStateでDate.now()を使うとlintエラーになる話
何が起きたか useState の初期値で Date.now() を直接呼んでいたら、こんなエラーが出た。 error Error: Cannot call impure function during render `Date.now` is an impure function. コード的にはこういうやつ。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) なぜエラーになるか React のルールとして、レンダー中はコンポーネントが pure でなければならない。 Date.now() や Math.random() はレンダーのたびに異なる値を返す「impure な関数」なので、直接渡すと React(特に Strict Mode)に怒られる。 Strict Mode ではレンダーを意図的に 2 回実行するため、こういった副作用が顕在化しやすい。 解決策:lazy initialization useState に関数を渡すと、初回マウント時に一度だけ実行される。これが lazy initialization パターン。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( () => new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) const [stockPriceTo, setStockPriceTo] = useState<string>( () => new Date(Date.now() - 84 * 86400000).toISOString().split('T')[0] ) () => で包むだけ。それだけ。 まとめ パターン 評価タイミング useState(Date.now()) レンダーごとに評価される useState(() => Date.now()) 初回マウント時のみ new Date() も同様なので、日付系の初期値を useState に渡すときはアロー関数で包む癖をつけておくと良い。 ...
モダンオペレーティングシステム 第3章中盤メモ
モダンオペレーティングシステム 第5版 上 第3章中盤(3.3後半〜3.5)。ページテーブルの実装とTLB、ページ置き換えアルゴリズム。 3.3後半 ページテーブルの実装 多段ページテーブル 仮想アドレス空間が64bitの場合、単純なページテーブルは現実的じゃない。 理論上の最大:2^64 = 18,446,744,073,709,551,616 バイト 1ページ = 4KB = 4096バイト ページ数 = 2^64 / 2^12 = 2^52 個 1エントリ = 8バイトとして ページテーブルのサイズ = 2^52 × 8 = 32ペタバイト プロセス1個のページテーブルだけで32PB。話にならない。 実際のx86_64は64bitフルを使わず48bitに妥協している(一部の最新CPUは57bit)。64bitフルを使うと6〜7段の階層が必要になりメモリアクセスのオーバーヘッドが耐えられなくなるから。「理論上の最大は64bitだけど、現実のCPUは48bitに妥協している」という設計判断。 Linuxはこれを多段ページテーブルで解決してる。x86_64では4段構成。 仮想アドレス(48bit有効) ┌──────┬──────┬──────┬──────┬────────────┐ │ PGD │ PUD │ PMD │ PTE │ offset │ │ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │ └──────┴──────┴──────┴──────┴────────────┘ 名前は覚えなくていい。住所の階層構造だと思えばいい。 東京都 → PGD(一番大きい区分) 渋谷区 → PUD 代々木1丁目 → PMD 1番地 → PTE 101号室 → offset(ページ内の位置) ポイントは使っていない部分のテーブルを作らないこと。 ...
モダンオペレーティングシステム 第3章後半メモ
モダンオペレーティングシステム 第5版 上 第3章後半(3.6〜3.8)。実装上の課題、セグメンテーション、そしてCPU脆弱性とOSの関係。 3.6 実装上の課題 OSがページングに関わるタイミング ページング処理はいつ動くか。 【プロセス生成時】 → 新しいページテーブルを作る → ディスクからプログラムをロードする準備 【プロセス実行時】 → TLBミス → ページテーブルをたどる → ページフォルト → ページを物理メモリに載せる 【プロセス終了時】 → ページテーブルを解放 → 物理フレームを解放 → ディスク上のスワップ領域を解放 【コンテキストスイッチ時】 → ページテーブルの切り替え → TLBフラッシュ or ASID切り替え k3sでPodが終了した時: コンテナプロセス終了 → カーネルがページテーブル解放 → 物理フレーム解放 → 次のPodがそのフレームを使える 命令の再実行問題 ページフォルトが起きた時、フォルトした命令を再実行する。でもここに厄介な問題がある。 メモリのコピー命令(100番地→200番地にコピー) 実行途中の150番地でページフォルト ↓ 100〜149はすでにコピー済み ↓ 命令を再実行 ↓ 100〜149を二重にコピー → 壊れる 対処法 方法1: 命令実行前に全ページが存在するか先読みチェック → なければ先にページフォルトを起こしておく → 全部揃ってから命令実行(x86寄りの方式) 方法2: CPUが内部状態を保存 → どこまで実行したか記録 → 再開時はその続きから(一部のRISCの方式) ページのロック(ピン留め) I/O処理中のページは絶対に追い出せない。 DMA転送中 → デバイスが直接メモリに書き込んでる → このページを追い出したら → デバイスが存在しないメモリに書き込む → 最悪システムクラッシュ だからI/O中のページには**ロック(ピン留め)**をかける。置き換えアルゴリズムの対象外になり、I/O完了後にロック解除。 ...
Raspberry Pi 2台構成のWiFi APでSSIDが起動時に出ない問題を解決した
構成 自宅NWの冗長化目的で Raspberry Pi を2台使い、WiFiアクセスポイントを組んでいる。 ホスト名 チャンネル SSID pi-1 1 your-ssid pi-2 11 your-ssid 同一SSIDで別チャンネルにする構成は ESS(Extended Service Set) と呼ばれ、クライアントが自動的に電波の強い方に接続する。WPA2-PSKで普通に成立する冗長化構成。 ブリッジ構成で bridge interface(br0)にWiFiインタフェース(wlan0)を参加させ、hostapdで電波を飛ばしている。 問題 OS再起動後、SSIDが片方または両方スキャンに出ない。 systemctl status hostapd は active (running) エラーログも一見出ていない hostapdを手動で再起動すると出る 調査 チャンネルの干渉を疑う 最初にhostapd.confのチャンネル設定を確認した。 2.4GHz帯で実際に干渉しないチャンネルは 1、6、11 の3つだけ。元の設定は6と7だったため、ほぼ完全に干渉していた。 ch6 --|-- ch7 --|-- ← 隣接していてほぼ被る チャンネルを1と11に変更した。 # pi-1 channel=1 # pi-2 channel=11 BSSIDレベルで電波を確認 sudo iw dev wlan0 scan | grep -E "BSS |SSID|freq|signal" BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2462 signal: -15.00 dBm SSID: your-ssid BSS xx:xx:xx:xx:xx:xx(on wlan0) freq: 2412 signal: -68.00 dBm SSID: your-ssid 両台ともビーコンは出ていた。ESSとして動作は成立している。 ...
モダンオペレーティングシステム 第3章前半メモ
モダンオペレーティングシステム 第5版 上 第3章前半(3.1〜3.3)。メモリ管理の基本。 第3章前半 メモリ管理の基礎 ベースレジスタとリミットレジスタ CPUのハードウェアレジスタを使ったシンプルなメモリ保護の仕組み。 プログラムがメモリにロードされる ↓ ベースレジスタ ← プログラムの開始物理アドレス リミットレジスタ ← プログラムの長さ ↓ プロセスがメモリにアクセスするたびに ↓ CPUがアクセスアドレス + ベース値を自動加算 ↓ リミットを超えてないか同時チェック ↓ 超えてたらフォールト(アクセス中断) これによってプロセスAはプロセスBのメモリに触れない。CPUレベルで物理的にブロックしてる。 ProxmoxのVM(KVM)との絡み KVMの場合はベース/リミットだけじゃなく、**EPT(Extended Page Table)**というIntel VT-xの機能でさらに一段上の分離をしてる。 ゲストの仮想アドレス ↓ ゲストのページテーブル ↓ ゲストの物理アドレス(実はまだ仮想) ↓ EPT(ここがKVMの追加レイヤー) ↓ 本当の物理アドレス これをネストされたページングと呼ぶ。ゲストOSごとに完全に別の物理アドレス空間にマッピングされるので、VM同士はお互いのメモリに絶対アクセスできない。 EPTはソフトウェアじゃなくてCPUがハードウェアで変換するので速い。 KVMに必要なCPU機能 KVMは以下のCPU機能が必須: CPU 仮想化支援機能 Intel VT-x AMD AMD-V(SVM) ARM(ラズパイ4等) ARMv8 Virtualization Extensions BIOSでVT-xが無効になってるとKVMが動かない。ProxmoxでVM作れないエラーの原因の大半がこれ。 MacのDockerが遅かった理由もここ Intel Mac時代 → HyperKitでソフトウェア仮想化 → 遅い・重い Apple Silicon(M1〜) → ARMのVirtualization Extensions使える → ハードウェア仮想化 → 速い M1でDockerが速くなったのはApple SiliconがARMの仮想化支援機能をちゃんと使えるようになったから。ただしARMなのでx86イメージは--platform=linux/amd64でエミュレーションになり遅い。 ...
モダンオペレーティングシステム 第1章・第2章メモ
モダンオペレーティングシステム 第5版 上 読んでる。 読み進めた記事というよりは範囲の中でわからんとこを生成AIと相談しながら文章課題とか出してもらいつつ進めたものをまとめた記事。 第1章 序論 OSの2つの役割 OSには2つの顔がある。 1. 拡張マシン(Extended Machine) ハードウェアの複雑さを隠蔽する。アプリ開発者はディスクの物理構造を知らなくてもファイルを読み書きできる。Docker APIがLinuxの複雑さを隠すのと同じ発想。 2. リソースマネージャー(Resource Manager) CPU・メモリ・ディスク・ネットワークを複数プロセスに割り当てる。k3s schedulerがNodeのリソースをPodに割り当てるのと同じ役割。 k3s scheduler → OSのリソースマネージャーと同じ役割 Docker API → OSの拡張マシンと同じ役割 OSがやってることをk3sやDockerが上位レイヤーで再現している。 ユーザー空間とカーネル空間 ┌─────────────────────────────┐ │ ユーザー空間 │ │ Docker、アプリ、k3s、etc... │ │ → 直接ハードウェアは触れない │ └──────────┬──────────────────┘ │ システムコール(唯一の通路) ┌──────────▼──────────────────┐ │ カーネル空間 │ │ スケジューラ │ │ メモリ管理 │ │ ファイルシステム │ │ デバイスドライバ │ │ → ハードウェアを直接触れる │ └─────────────────────────────┘ アプリが直接ハードウェアを触れたら危険。カーネルが仲介することで安全性を保証する。 システムコール docker runした時の実際の流れ: docker run ubuntu ↓ Dockerデーモン ↓ clone() ← プロセス生成 unshare() ← namespaceを作る mount() ← ファイルシステムをマウント ↓ Linuxカーネル Dockerはシステムコールの集合体。魔法じゃなくてLinuxのシステムコールをうまく組み合わせてるだけ。 ...
Emacs 30.2 で *-ts-mode が壊れる問題と対処
結論 Mike Olson - Fixing typescript-ts-mode in Emacs 30.2 上記のブログで解説と対応スクリプトが公開されてる。 Emacs 30.2 + libtree-sitter 0.26 の組み合わせで go-ts-mode や typescript-ts-mode などの *-ts-mode が動作しない場合、現時点(2026年4月)では以下のワークアラウンドで動作させることができる。 現時点ではこれが最適解と思う。おそらくEmacs 31 安定版がリリースされるまでこのスクリプトを使い続けることになる。 mkdir -p ~/.emacs.d/init curl -o ~/.emacs.d/init/treesit-predicate-rewrite.el \ https://raw.githubusercontent.com/mwolson/emacs-shared/master/init/treesit-predicate-rewrite.el init.el の早い段階(他の *-ts-mode の設定より前)に追加する。 (load "~/.emacs.d/init/treesit-predicate-rewrite" nil nil nil t) 経緯 go-ts-mode を開いたとき *Messages* バッファに treesit-query-error が出てシンタックスハイライトが死んだ。 最初はグラマーの .so ファイルが原因だと思い、treesit-install-language-grammar で入れ直したり、~/.emacs.d/tree-sitter/ 以下のファイルを削除して再インストールしたりと試行錯誤した。しかし何をやっても症状が変わらず、グラマー側の問題ではないと判断した。 調べたところ Emacs bug#79687 に行き着いた。グラマーの問題ではなく、Emacs 30.2 と libtree-sitter 0.26 の組み合わせ自体が壊れていた。 症状 Emacs 30.2 で Go や TypeScript などのファイルを開くと、*Messages* バッファに以下のようなエラーが表示され、シンタックスハイライトが一切効かなくなる。 Error during redisplay: (jit-lock-function 35) signaled (treesit-query-error "Syntax error at" 73 "(call_expression function: ((identifier) @font-lock-builtin-face (#match \"...\" @font-lock-builtin-face)))" "Debug the query with `treesit-query-validate'") go-ts-mode だけでなく typescript-ts-mode、python-ts-mode、rust-ts-mode など、tree-sitter ベースの全モードで同様の問題が発生する。 ...