はじめに — 時給数十円から「育てる」発想へ

副業プログラミングを始めて1〜2年経つと、必ずぶつかるのが 「単価の天井」 です。1件3,000〜5,000円のスクレイピング案件をこなしていても、時給換算で本業を超えるのは難しい。けれどここで「副業は副業」と諦めてしまうと、永遠に時間切り売りから抜けられない。

単価を上げるために必要なのは、 依頼を受けてから書くコードの質を上げる ことではありません。 「依頼が来る前から、再利用可能な土台を仕込んでおく」 という、副業全体を ストック型のビジネス として育てる発想です。

本記事は3部作の 単価アップ編。私が初案件3,000円・時給58円から脱却するためにやってきた 5つの技術改善軸 と、その投資をクライアントに価値として伝える パッケージ化&保守メニュー の組み立てを、コード例を交えながら書きます。継続案件化までの基本は前記事 継続案件化編 を先にどうぞ。


改善軸1: 再利用性を高める構成

サイト別モジュール化

最初に投資すべきは、 「次の案件で半分使い回せる構造」 を作ることです。1つの案件のコードを main.py 1ファイルに書き殴っていると、次の案件で全部書き直しになる。これを防ぐために、サイトごとの取得ロジックをモジュール単位に分離します。

project/
├── main.py            # 共通エントリポイント
├── core/
│   ├── fetcher.py     # HTTP / Selenium の共通ラッパー
│   ├── exporter.py    # Excel / CSV 出力の共通処理
│   └── logger.py      # ログ設定の共通化
├── sites/
│   ├── site_a.py      # サイト固有のパース処理
│   └── site_b.py      # 別サイトのパース処理
└── config.yaml        # サイト別設定(URL・セレクタ・出力列)

core/ を一度作っておけば、 新しい案件で書くのは sites/site_x.py だけ で済みます。新サイト対応の工数が、私の場合は2時間→30分くらいまで圧縮できました。

設定ファイル駆動

セレクタやURLをコード内にハードコードせず、YAMLやJSONに外出しします。クライアントから「列を1つ追加してほしい」と言われたとき、 コードを触らず設定ファイルだけで対応できる のが大きい。

# config.yaml
site_a:
  url: "https://example.com/products"
  selectors:
    name: "h2.product-title"
    price: "span.price"
    stock: "div.stock-status"
  output_columns: ["name", "price", "stock", "fetched_at", "url"]

クライアントからの「ちょっとした調整」依頼が、コード変更ではなく 設定ファイルの編集だけ で済む構造になっていると、保守作業の単価が一気に上げやすくなります(クライアントから見て「待ち時間が短い=価値が高い」)。


改善軸2: 例外とリトライ

ステータスコード別ハンドリング

スクレイピングで遭遇するエラーは、ステータスコードによって対応が違います。これを雑に「全部 try/except でキャッチしてリトライ」にすると、 本来止めるべきエラーまで握り潰してしまう 危険があります。

ステータス 意味 対応
200 成功 データを処理
403 アクセス拒否 リトライしても無駄。User-Agent見直し or 即停止
404 URL不正 該当URLをスキップしてログに記録
429 レート制限 指数バックオフで待機してからリトライ
5xx サーバーエラー 指数バックオフで2〜3回リトライ

指数バックオフの実装

tenacity ライブラリを使うと、リトライ処理を1〜2行のデコレータで宣言できます。

from tenacity import retry, stop_after_attempt, wait_exponential
import requests

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=2, max=30),
    reraise=True
)
def fetch_with_retry(url: str) -> requests.Response:
    response = requests.get(url, timeout=10)
    if response.status_code == 429:
        # レート制限は再試行対象として例外を上げる
        raise requests.exceptions.RetryError("Rate limited")
    response.raise_for_status()
    return response

これだけで「失敗しても自動で2秒→4秒→8秒と待ってリトライする」処理が実装できる。 クライアントに見せられる「堅牢性」が一気に上がる ので、この投資は早めにやるべき軸の1つです。

要素待機の明示化

Seleniumで動的サイトをスクレイピングする場合、 time.sleep(3) のような暗黙待機は捨てる のが鉄則。サイトの応答時間は日によって変わるので、固定秒数の待機は不安定です。

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# 「要素が出現するまで最大10秒待つ」明示的待機
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "div.product-list"))
)

明示的待機にすることで、 速いときは速く・遅いときも待てる という適応的な動作になり、ツール全体の安定性が上がります。

💡 2026年現在の補足: 例外パターンの洗い出しは、Claude Code等のAIエージェントに「考えられる例外を全部リストアップして」と頼むと網羅性が一気に上がります。AIに任せられるフェーズと自分で判断すべきフェーズの線引きについては、別記事【AI活用編】で詳しく扱う予定です。


改善軸3: ログと通知

ログ設計の3レベル

Python標準の logging モジュールを使い、3レベルで使い分けます。

  • INFO: 正常な動作の記録(取得開始・取得完了・件数)
  • WARNING: 想定外だが致命的ではない(取得件数が想定より少ない、リトライ発生)
  • ERROR: 致命的な失敗(取得不能・パース不能・出力失敗)
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler('scraping.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)
logger.info(f"取得開始: {url}")
logger.warning(f"件数が想定より少ない: 期待100件 / 実際 {count}件")
logger.error(f"パース失敗: {error}")

ログファイルが残っていると、 クライアントから「動かなかった」と言われたときに原因切り分けが10倍速くなる。これも保守単価を取りに行くための前提条件です。

終了コードでバッチ運用に対応

EXE化したツールをクライアント側でタスクスケジューラに乗せて自動実行する、というユースケースが必ず出てきます。このとき 終了コードを正しく返す ことで、失敗時にだけメール通知を出すような運用が可能になります。

import sys

if total_failures > 0 and total_success == 0:
    sys.exit(2)  # 致命的失敗
elif total_failures > 0:
    sys.exit(1)  # 部分失敗
else:
    sys.exit(0)  # 完全成功

タスクスケジューラの「失敗時に通知」設定と組み合わせれば、 クライアント側で「失敗したときだけメールが届く」運用 が即実現できる。

失敗時通知の最小実装

Slack Webhookやメール通知を組み込みたい場合、最小実装は10行程度で済みます。Slack Incoming Webhookなら以下。

import requests

def notify_slack(webhook_url: str, message: str) -> None:
    requests.post(webhook_url, json={"text": message})

# エラー時に呼び出す
if errors:
    notify_slack(webhook_url, f"⚠️ スクレイピング失敗: {errors}")

これを オプション機能としてクライアントに提案 すると、追加¥3,000〜5,000の見積もりがすんなり通ります。


改善軸4: 人が嬉しい出力

列名・並び順の固定

継続案件化編 でも触れましたが、 列名と並び順は絶対に固定 が鉄則。クライアント側のVLOOKUPやマクロは列位置に依存していることが多く、勝手に変えると全部壊れます。

新しい列を追加するときは、必ず 既存列の末尾 に追加。これを設定ファイルで宣言的に管理しておくと、運用ミスが起きません。

日付付きファイル名の自動生成

ファイル名に取得日時を入れる運用は、 クライアント側の履歴管理を自動化 する効果があります。

from datetime import datetime
filename = f"products_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"

これだけで「先週のデータと比較したい」というニーズに、追加実装ゼロで応えられる。 小さいけれど効く タイプの工夫です。

Excelテンプレへの流し込み

クライアントが既に運用しているExcelテンプレ(書式・関数・グラフが入ったファイル)を そのまま使い回せる形で納品 すると、満足度が一段上がります。

from openpyxl import load_workbook

# テンプレファイルを開いて、データシートだけ書き換える
wb = load_workbook("template.xlsx")
ws = wb["データ"]

# 既存のデータをクリア(ヘッダーは残す)
ws.delete_rows(2, ws.max_row)

# 新しいデータを追加
for item in scraped_items:
    ws.append([item["name"], item["price"], item["stock"]])

wb.save(f"report_{datetime.now().strftime('%Y%m%d')}.xlsx")

クライアント側のグラフや集計関数はテンプレ内で生きたまま、データだけが更新される——これは マニュアルExcel運用からの解放 という大きな価値で、単価交渉の根拠になります。

データ整形の落とし穴

実装で踏みやすいのが 全角半角・改行・前後空白 の混在問題です。スクレイピング元のサイトは表記が揺れていることが多く、そのまま出すとExcel側で集計ミスにつながります。

import re

def normalize(text: str) -> str:
    text = text.strip()                          # 前後空白を削除
    text = re.sub(r"\s+", " ", text)             # 連続する空白を1つに
    text = text.replace(" ", " ")           # 全角空白を半角に
    return text

これを取得値に一律かけるだけで、 Excel側の重複検知や集計が綺麗に通る 状態になります。


改善軸5: 変更耐性

CSSセレクタとXPATHの併用設計

サイトのHTML構造は予告なく変わります。1つのセレクタに全依存していると、構造変更で即崩壊する。私のスタンダードは CSSセレクタを第1候補・XPATHを第2候補 として両方持っておくこと。

def find_price(soup):
    # 第1候補: CSSセレクタ
    price = soup.select_one("span.price")
    if price:
        return price.text

    # 第2候補: XPATH風(lxml使用時)
    price = soup.find("span", attrs={"data-testid": "price"})
    if price:
        return price.text

    # フォールバック: テキストベースの検索
    for span in soup.find_all("span"):
        if "円" in span.text:
            return span.text

    return None

3段構えにしておくことで、 サイト構造が一部変わっても止まらない ツールになります。

閾値チェック

取得件数が想定の50%を下回ったら、 「サイト構造が変わったのでは」という警告を出す 仕組みを入れておきます。

EXPECTED_MIN_COUNT = 50

if len(items) < EXPECTED_MIN_COUNT:
    logger.warning(
        f"取得件数が想定下限を下回っています: {len(items)}件 "
        f"(想定下限 {EXPECTED_MIN_COUNT}件)。サイト構造の変更を確認してください。"
    )

これがあるだけで、 「気づかないうちに壊れていた」という最悪のケースを防げる。クライアントから見ても「ちゃんと監視してくれている」という安心材料になります。

サイト構造変更を検知するスモークテスト

毎日動かす想定のツールなら、最初に サイトのトップページが200を返すか・想定キーワードが含まれるか だけ確認するスモークテストを入れておくと、エラーの一次切り分けが楽になります。


パッケージ化 — 単価を上げる商品設計

基本プラン+オプションの組み立て

ここまで紹介した5軸を実装すると、 同じ案件でも提供できる価値が階層化 されます。これをそのまま価格設計に反映するのがパッケージ化です。

プラン 内容 価格目安
ベーシック 単発スクレイピング+CSV/Excel納品 ¥5,000〜
スタンダード + EXE化・操作マニュアル・7日間の軽微修正対応 ¥10,000〜
プロ + リトライ&ログ&スモークテスト・スケジュール実行設定 ¥20,000〜
エンタープライズ + 設定ファイル化・複数サイト対応・Slack通知 ¥40,000〜

ベーシックの単価では到達できない金額が、 「価値を積み上げて見せる」ことで自然に取れる ようになります。

オプション例

プランとは別で「アドオン」として売れる項目もあります。

  • 対象サイト追加: 1サイトあたり ¥5,000
  • スケジュール実行設定: ¥3,000
  • GUI化(簡易ウィンドウ付き): ¥10,000
  • 多言語対応(日英など): ¥5,000
  • データの自社システム連携(API送信): ¥10,000〜

オプションは クライアントが「あったら便利」と感じる微差を可視化 するもの。すべてを最初から見せる必要はなく、要件確認の中で「こういうこともできますがどうですか」と提案する形で十分です。


値下げ交渉を回避する提案文

クライアントから「もう少し安くなりませんか」と言われたとき、 無条件で下げると単価のディスカウントが恒常化 します。私が使っているのは「成果・リスク・代替案」をセットで提示する型です。

ご予算の件、ご相談ありがとうございます。現在ご提案している ¥XX,000 のプランは、 (成果) スクレイピング・EXE化・運用マニュアルまで含めた一式で、納品後の保守も7日間つきます。これを ¥XX,000 まで下げることは可能ですが、 (リスク) その場合は運用マニュアルを簡略化、あるいは保守期間を3日に短縮させていただく形となります。 (代替案) ご予算優先であれば、まず単発スクレイピング(¥5,000)で動作確認いただき、ご満足いただいたうえで運用化のご相談、という段階的な進め方もご提案できますがいかがでしょうか。

これで 「下げるなら何かを差し引く/上げるなら全部含む」 という構造を可視化できます。クライアント側も合理的に判断しやすくなり、 単純な値引き交渉が消える


保守メニュー — 安定収入を作る

月額保守の価格設定

スクレイピングツールは 必ずいつかサイト仕様の変更で動かなくなります。これを「壊れたら都度有償対応」にするか「月額保守で先払い」にするかで、副業の安定性が大きく変わります。

私の標準は以下のような月額メニュー。

プラン 月額 含まれるもの
ライト保守 ¥3,000 月1回までのサイト仕様変更追随
スタンダード保守 ¥7,000 月3回までの追随+軽微な機能追加
プロ保守 ¥15,000 月5回まで+緊急対応24時間以内

月額保守は クライアントから見ても「予算化しやすい」 ので、年単位で契約してもらえることが多い。私の場合、月額保守で安定的に入る金額は単発案件と同等以上になっています。

都度修正と月額のどちらを勧めるか

依頼頻度が 月1回以上 あるクライアントには月額を勧めます。それ以下なら都度の方がお互い得。判断軸を最初から数値で持っておくと、提案が迷わなくなる。

保守契約のスコープ

月額保守で揉めないために、 「月額に含まれるもの/含まれないもの」を契約時に文章化 しておきます。

  • 含まれる: サイトHTML変更への追随、ログ確認、軽微な不具合対応
  • 含まれない: 新規サイト追加、新機能開発、別環境への移植
  • 条件付き: 月の上限回数を超えた依頼は追加見積もり

法的・倫理的な注意点

robots.txtと利用規約の確認手順

スクレイピング案件を受注するとき、 必ず最初に対象サイトの利用規約とrobots.txtを確認 します。

  • robots.txt: https://example.com/robots.txt で取得可能。User-agent: *Disallow: を確認
  • 利用規約: 「スクレイピング禁止」「自動アクセス禁止」のような文言があれば即停止

クライアント側に判断を委ねる場合も、 「利用規約の確認はクライアント様にてお願いします」 という文言を出品ページや見積もりに入れておくと、後でトラブルになっても責任分担が明確になります。

アクセス頻度の制御

DoS的にならない設計が必須です。具体的には、

  • リクエスト間に 1〜3秒のスリープ を入れる
  • 同時接続数は 1〜2 に絞る(スレッドで並列化しない)
  • 大量取得が必要なら 時間帯を夜間 にずらす

これは技術倫理であると同時に、 自分のIPがブロックされないための自衛 でもあります。

個人情報・著作権への配慮

取得データに個人情報(メールアドレス・電話番号・氏名)が含まれる場合は、 そもそも受注を控える か、契約段階でクライアントの利用目的を明確に書面化します。著作権で保護される画像や文章の取得も同様です。

免責事項テンプレ

私が見積もりに必ず添えている免責文言の例。

本ツールは公開情報の収集を目的としており、対象サイトの利用規約に従って運用してください。サイト側の仕様変更・利用制限により予期せず動作しなくなる可能性があります。本ツールの使用に伴う一切の法的責任はご利用者様に帰属します。

これだけで、 「サイトが規約変更してアクセス禁止になった」「アカウントBANされた」 などのトラブル時にも責任の所在が明確になります。


アカウント停止やツール不動作時の対応

動作環境の事前明文化

納品時の運用マニュアルに、必ず以下を明記します。

  • 動作確認済みのWindowsバージョン(例: Windows 11 22H2)
  • 必要なブラウザのバージョン(Selenium使用時)
  • ChromeDriverのバージョンと自動更新の有無

これがないと、 「動かない」の原因が環境差なのかコード側なのか切り分けられない トラブルが頻発します。

「サイト仕様変更は保守範囲」「アカウント要件はクライアント責任」の線引き

トラブル時の責任分担を、最初の契約段階で線引きしておきます。

  • 保守範囲(こちら側で対応): サイトHTMLの変更によるパース不能
  • 保守範囲外(クライアント責任): クライアントのアカウントBAN、利用規約違反、ネットワーク不通

ログによる原因切り分けの実例

「動かない」と連絡が来たら、まずログファイルの該当時間帯を見るだけで、ほぼ原因の8割は特定できます。 「ログを送ってください」と即返答できる仕組みがあるかどうか で、初動対応の所要時間が10分→1時間と差が出ます。


まとめ — 育てる副業の3要素

ここまでの内容を3つに圧縮するなら、こうなります。

  1. 再利用可能な土台を仕込む: モジュール分離・設定ファイル駆動で次案件の工数を圧縮
  2. 堅牢性を価格に変える: 例外処理・ログ・閾値チェックを「価値」としてパッケージ化
  3. 保守で安定収入を作る: 単発の積み上げではなく、月額契約で複利的に育てる

副業を 「時間の切り売り」から「ストック型のビジネス」へ 転換する技術投資は、最初こそ実装コストが重いですが、3〜6ヶ月で回収できる規模感だと感じます。


関連記事

副業を始めたばかりの方、まだ単発でしか案件が取れていない方は、以下の入門編・継続案件化編と合わせて読むと全体像がつかめます。

👉 入門編: ココナラでプログラミング副業を始める方法|Pythonスクレイピングで実績ゼロから1案件目を取るまで

👉 継続案件化編: ココナラで継続案件を取る方法 — 非エンジニアに優しいEXE納品の作り方