副業を1〜2年やると必ず「単価の天井」にぶつかります。1件3,000〜5,000円のスクレイピング案件では時給換算で本業を超えにくい。単価を上げるのに必要なのは、依頼を受けてから書くコードの質ではなく、「依頼が来る前から再利用可能な土台を仕込んでおく」=副業をストック型のビジネスとして育てる発想です。
この記事でわかること(3部作の単価アップ編):
- 単価を上げる技術改善の5軸(再利用性・例外処理・ログ・出力・変更耐性)
- 価値を階層化するパッケージ化&オプション設計
- 値下げ交渉を回避する提案と、安定収入を作る保守メニュー
- スクレイピングの法的・倫理的な注意点
継続案件化までの基本は前記事継続案件化編を先にどうぞ。
改善軸1:再利用性を高める構成
最初に投資すべきは「次の案件で半分使い回せる構造」。1案件を main.py に書き殴ると次で全部書き直しになります。サイトごとの取得ロジックをモジュールに分離します。
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はハードコードせず設定ファイルに外出しします。
# 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不正 | スキップしてログに記録 |
| 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秒と待ってリトライする」処理が実装でき、見せられる堅牢性が一気に上がるので早めにやるべき軸です。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活用編】で扱う予定です。
改善軸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倍速くなり、保守単価を取りに行く前提条件になります。タスクスケジューラでの自動実行に備え、終了コードを正しく返すと失敗時だけ通知を出す運用ができます。
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通知も最小実装は10行程度。
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テンプレ(書式・関数・グラフ入り)にデータだけ流し込むと満足度が一段上がります。
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運用からの解放で、単価交渉の根拠になります。実装で踏みやすいのが全角半角・改行・前後空白の混在。取得値に一律で正規化をかけます。
import re
def normalize(text: str) -> str:
text = text.strip() # 前後空白を削除
text = re.sub(r"\s+", " ", text) # 連続する空白を1つに
text = text.replace(" ", " ") # 全角空白を半角に
return text
これでExcel側の重複検知や集計が綺麗に通ります。
改善軸5:変更耐性
サイトのHTMLは予告なく変わります。1つのセレクタに全依存せず、CSSセレクタを第1候補・代替を第2候補として持ちます。
def find_price(soup):
# 第1候補: CSSセレクタ
price = soup.select_one("span.price")
if price:
return price.text
# 第2候補: 属性ベース
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は https://example.com/robots.txt で取得し Disallow: を確認、利用規約に「スクレイピング禁止」「自動アクセス禁止」があれば即停止。クライアントに判断を委ねる場合も「利用規約の確認はクライアント様にてお願いします」を出品ページや見積もりに入れ、責任分担を明確にします。
アクセス頻度はDoS的にならない設計が必須です。リクエスト間に1〜3秒のスリープ/同時接続数は1〜2に絞る/大量取得は夜間にずらす。これは技術倫理であると同時に、自分のIPがブロックされないための自衛でもあります。取得データに個人情報(メール・電話・氏名)が含まれる場合はそもそも受注を控えるか、利用目的を書面化します。見積もりに必ず添える免責文言の例:
本ツールは公開情報の収集を目的としており、対象サイトの利用規約に従って運用してください。サイト側の仕様変更・利用制限により予期せず動作しなくなる可能性があります。本ツールの使用に伴う一切の法的責任はご利用者様に帰属します。
これで規約変更によるアクセス禁止やアカウントBANなどのトラブル時にも責任の所在が明確になります。
ツール不動作時の対応
納品マニュアルに動作環境を明記します(動作確認済みのWindowsバージョン、必要なブラウザのバージョン、ChromeDriverのバージョンと自動更新の有無)。これがないと「動かない」の原因が環境差かコード側か切り分けられません。責任分担も契約段階で線引きします。保守範囲(こちら側)=サイトHTML変更によるパース不能/保守範囲外(クライアント責任)=アカウントBAN・利用規約違反・ネットワーク不通。「動かない」と来たらログの該当時間帯を見るだけで原因の8割は特定でき、「ログを送ってください」と即返答できる仕組みがあるかで初動が10分→1時間と差が出ます。
まとめ — 育てる副業の3要素
- 再利用可能な土台を仕込む:モジュール分離・設定ファイル駆動で次案件の工数を圧縮
- 堅牢性を価格に変える:例外処理・ログ・閾値チェックを「価値」としてパッケージ化
- 保守で安定収入を作る:単発の積み上げでなく、月額契約で複利的に育てる
副業を「時間の切り売り」から「ストック型のビジネス」へ転換する技術投資は、最初こそ実装コストが重いですが、3〜6ヶ月で回収できる規模感だと感じます。
関連記事
副業を始めたばかりの方、まだ単発でしか取れていない方は入門編・継続案件化編と合わせてどうぞ。
👉 入門編:ココナラでプログラミング副業を始める方法|Pythonスクレイピングで実績ゼロから1案件目を取るまで
👉 継続案件化編:ココナラで継続案件を取る方法 — 非エンジニアに優しいEXE納品の作り方