DEV Community

Cover image for 副業応募botが受注ゼロ!犯人は『毎週クラッシュして成果を捨てる』1行のバグだった話
oji - building AI in public
oji - building AI in public

Posted on

副業応募botが受注ゼロ!犯人は『毎週クラッシュして成果を捨てる』1行のバグだった話

どうも、おじいです。

本業の傍ら、AI エージェントとか自動売買 bot を個人開発してる。最近、自分の副業探し自体を自動化しようと思って「副業応募 bot」を作って動かしてた。

こいつの仕事はシンプル。

  1. 週末に『ある求人サイト』とか複数のマーケットを巡回して、良さげな案件をリストアップする
  2. リストアップした案件に対して、俺のスキルセットに合わせた提案文を AI で自動生成して応募する

これを1ヶ月ほど動かしてみたんだけど、結果は「受注ゼロ」。まあ、そんなに甘くないよな、と。最初は「提案文の質が悪いのかな」とか「そもそもスキルが足りてないのかも」とか、結構まともな反省をしてた。

でも、念のためログをちゃんと見てみたら、えぐい事実が発覚した。

そもそも、提案を1件も送ってなかった。

いや、マジかよと。630件も提案文を「下書き」は作ってたのに、応募ボタンを押すフェーズまで到達したのがゼロ件。これは提案文の質とかそういうレベルの話じゃない。完全にシステムの問題。マジでやばいと思った。

原因:エラー1つで全データを吹き飛ばす脆すぎる設計

なんでこんなことになったのか。原因を深掘りしたら、問題は応募スクリプトじゃなくて、その前段の「案件収集スクリプト」にあった。

こいつは毎週日曜の深夜に動くように設定してたんだけど、どうやら処理の途中でクラッシュしてたらしい。そして、このクラッシュの仕方が最悪だった。

当時のコードは、だいたいこんな感じ。

// Before: A single error crashes the whole script
def scrape_all():
    browser = pw.chromium.launch()
    results = []
    # 複数のサイトをループで巡回
    for site in SITES:
        # このへんでサイトの仕様変更とかでエラーが出ると…
        page = browser.new_page() 
        try:
            # 収集した結果をいったんメモリ上のリストに追加
            results.extend(scrape_site(page))
        except Exception as e:
            log.error(f"Failed on {site}: {e}")
            # エラーが出てもループを止めないように try-except は入れてたつもりだった

    # ループが全部正常に終わらないと、ここにはたどり着かない
    # つまり、途中でクラッシュすると、それまでの成果は全部消える
    save_results(results)
Enter fullscreen mode Exit fullscreen mode

このコードの問題点は2つ。

  1. エラー処理の範囲がデカすぎる: ループの外側でブラウザを起動してるから、browser.new_page()みたいな処理で何かあると、try-exceptブロックの外で例外が発生して、スクリプト全体が即死する。
  2. 最後にしか保存しない: 収集した案件データは、全部のサイトを回りきるまでメモリ上の results リストに溜め込んでる。だから、最後の1サイトでコケたとしても、それまでに集めた全データが失われる。

実際、ログを追ったら『某マーケット』の HTML 構造が微妙に変わってて、要素が見つからずにタイムアウトエラーを起こしてた。たったこれだけのことで、毎週日曜の深夜、bot は数時間かけて集めたデータを全部ドブに捨てて、黙って死んでたわけだ。そりゃ応募できるわけがない。

修正:死ぬことを前提にしたエラー処理と中間保存

この手の長時間動くスクリプトは、「絶対にどこかでコケる」という前提で設計しないとダメだった。反省して、こう書き直した。

// After: Robust error handling and intermediate saves
def scrape_all():
    all_results = []
    # サイトごとに、ブラウザの起動から全部やる
    for site in SITES:
        try:
            # エラーが起きる可能性のある処理を全部 try ブロックに入れる
            browser = pw.chromium.launch()
            page = browser.new_page()
            site_results = scrape_site(page)
            all_results.extend(site_results)

            # 成功したサイトの結果は、その都度ファイルに書き出す
            save_intermediate_results(site, site_results) 
        except Exception as e:
            # ここでエラーをキャッチすれば、次のサイトの処理には進める
            log.error(f"Failed on {site}, skipping: {e}")
        finally:
            # ちゃんとブラウザを閉じる処理も忘れずに
            if 'browser' in locals() and browser:
                browser.close()

    # 最後に全部の結果をマージして保存
    save_final_results(all_results)
Enter fullscreen mode Exit fullscreen mode

ポイントは2つ。

  1. try-exceptをループの内側に入れる: これで、あるサイトの処理で失敗しても、そのエラーはループ内で完結する。次のサイトの処理には影響が出ない。
  2. 中間保存: 1サイトのデータが取れたら、その時点ですぐにファイルに保存する。これで、たとえスクリプトが最後の最後でクラッシュしたとしても、それまでに成功した分のデータは手元に残る。

この修正で、bot はようやく毎週安定して案件データを集めてこれるようになった。

学び:本当の課題はコードの先にあった

この一件で学んだことは多い。

まず技術的な話。長時間動くバッチ処理は、とにかく堅牢性が命。エラー処理は「念のため」じゃなくて「必須の設計要素」。そして、状態(この場合は収集データ)はメモリに置かず、こまめに永続化する。基本だけど、サッと作ると忘れがちだ。

次に、問題解決のアプローチ。最初、「受注ゼロ」という結果だけ見て「提案文が悪い」とか「スキルが足りない」とか、表層的な仮説に飛びついちゃったのが良くなかった。「そもそも応募はされているのか?」という、一番基本的なファクトの確認を怠ってた。ログは嘘をつかない。何か問題が起きたら、まずデータを深く掘るのが鉄則。

そして一番大きかったのが、このバグを直したことで見えてきた、さらに本質的な課題だ。

安定して案件を収集できるようになった結果、わかったことがある。

「俺がやりたいレベルの副業案件、市場にほとんど出てねえわ」

結局、技術的な問題を解決したら、その先にある事業戦略(この場合は副業戦略)の課題が浮き彫りになっただけだった。まあ、これも大きな一歩か。

コードのバグを直すのは楽しいけど、そのコードが解決しようとしてる問題そのものがズレてないか?っていう視点も、個人開発では大事なんだなと、改めて痛感した。

さて、次は何を自動化しようかな。


I build and run small Python systems — trading bots, RAG APIs, scheduled automation — and write up whatever breaks along the way.

If a provider-agnostic RAG Q&A API is useful to you, mine is MIT-licensed on GitHub: rag-faq-api. It runs and passes its full test suite **with no API key* (offline stub LLM + hashing embedder), swaps to Claude / Gemini / OpenAI via one env var, and ships a retrieval-quality harness (Hit@k / MRR / Recall@k) with a chunking sweep.*

Top comments (0)