Indeedの求人タイトルをスプレッドシートに手で50回くらいコピペしていたあたりで、自分のキャリア選択をかなり本気で疑い始めました。Indeedから構造化データをプログラムで取ろうとしたことがあるなら、オチはもう見えているはずです。403エラーはバグじゃなくて、Indeedの防御システムの一部なんです。
Indeedは世界でも有数の求人サイトで、月間ユニーク訪問者は約、常時もの求人情報を抱え、で展開しています。つまり、求人市場データの宝庫である一方、スクレイピングがめちゃくちゃ難しいサイトでもあります。オープンソースのスクレイパー JobFunnel(GitHubスター数は数千規模)も、アンチボット対策との消耗戦に負け続けた末、2025年12月に。メンテナー本人の言葉を借りると、「すべてのユーザーは一部の求人を取得できるが、すぐにcaptchaに阻まれ、最終的にスクレイピングは失敗して求人データは取れない」 とのことです。別のコントリビューターは、と報告しています。というわけで、ここは全然“楽な相手”ではありません。本ガイドでは、PythonでIndeedをスクレイピングするための現実的な方法をすべて紹介し、403の壁をどう突破するかを解説します。さらに、デバッグを丸ごと飛ばしたい人向けに、 を使ったノーコード代替案も紹介します。
PythonでIndeedをスクレイピングするとはどういうことか?
そもそもWebスクレイピングとは、Webページから構造化データを自動で抜き出すことです。PythonでIndeedをスクレイピングするときは、Indeedの検索結果ページや求人詳細ページにアクセスするスクリプトを書き、HTML本体(または埋め込まれたデータ)を読み取り、求人タイトル、会社名、勤務地、給与、説明文などをCSV、データベース、Googleスプレッドシートなど扱いやすい形で取り出す、という意味になります。
よく使われるPythonライブラリは、Requests(HTTP通信)、BeautifulSoup(HTML解析)、SeleniumやPlaywright(ブラウザ自動操作)です。ただしIndeedは単純な静的サイトではありません。サーバー側で生成されたHTMLにJSONの状態データが埋め込まれており、さらにCloudflare Bot Managementで保護されたハイブリッド構成です。つまり、スクレイパーはJavaScriptでレンダリングされたコンテンツ、変動するCSSクラス名、強力なアンチボット対策のすべてに対応しなければならず、そのうえでようやく求人タイトルを1件取り出せる、という世界です。
しかも2026年時点では、Indeedに公式の無料・読み取り専用APIはありません。旧Publisher Jobs APIは2020年ごろに廃止され、現在残っているのは雇用主向けの機能(Job Sync、Sponsored Jobs)のみです。したがって、現実的な選択肢はスクレイピングするか、外部のデータプロバイダーにお金を払うかのどちらかになります。
なぜIndeedの求人データをスクレイピングするのか?
Indeedをスクレイピングするビジネス上の理由はシンプルです。何千件もの求人を手作業で見るのは現実的じゃないし、求人情報の中にあるデータにはかなりの価値があるからです。

| ユースケース | 恩恵を受ける人 | 具体例 |
|---|---|---|
| リード獲得 | 営業・採用チーム | 採用企業の一覧を連絡先付きで作成する |
| 求人市場調査 | アナリスト、人事チーム | トレンドスキルや地域別の給与水準を把握する |
| 競合調査 | 企業、派遣・人材会社 | 競合の採用動向や提示給与を追跡する |
| 個人向け転職自動化 | 求職者 | 条件に合う求人を地域横断でまとめる |
| 機械学習向け学習データ | データサイエンティスト | 過去の求人データから給与予測モデルを作る |
Indeed Hiring Labの研究でも、求人掲載データはBLSのJOLTSと高い相関があり、米国の労働市場状況をほぼリアルタイムで把握する代替指標になり得ることが。ヘッジファンドは求人掲載の増減をオルタナティブデータとして活用しています。人事部門はスクレイピングした給与レンジをもとに報酬水準を比較しますし、採用担当者は積極採用中の企業から見込み先リストを作ります。
ひとつ実務上の注意点があります。Indeedの給与データは改善してきているものの、まだ完全ではありません。2025年半ば時点で、米国の求人の約に給与情報が含まれていますが、しか正確な数値は記載されておらず、残りはレンジ表記です。Indeedデータを使った給与分析では、この欠損を前提に設計する必要があります。
PythonでIndeedをスクレイピングする方法の選び方
Indeedのスクレイピングに「これが唯一の正解」というものはありません。最適な方法は、あなたのスキル、必要なデータ量、どこまで保守に時間をかけられるかで変わります。主要な4手法を実際に比べると、こんな感じです。
| 比較項目 | BS4 + Requests | Selenium | 隠しJSON(window.mosaic) | ノーコード(Thunderbit) |
|---|---|---|---|---|
| 難易度 | 初級 | 中級 | 中級〜上級 | なし(2クリック) |
| 速度 | 速い | 遅い(ブラウザ描画あり) | 速い | 速い(クラウドスクレイピング) |
| JSレンダリング対応 | いいえ | はい | はい(埋め込みデータ) | はい |
| アンチボット耐性 | 低い | 中程度(検出されやすい) | 中〜高 | 高い(自動対応) |
| HTML変更時の保守負担 | 大きい(セレクタが壊れやすい) | 大きい | 中程度(JSON構造の方が安定) | なし(AIが追従) |
| 向いている用途 | ちょっとした試作 | 動的ページ、ログイン後ページ | 大量の構造化データ | 非開発者、素早く結果が欲しい場合 |
このガイドでは、それぞれの方法を順番に解説します。Python開発者なら、BS4、隠しJSON、Seleniumのセクションを読むといいでしょう。非エンジニア、あるいは403のデバッグにうんざりしているなら、Thunderbitのセクションまで飛んでください。
始める前に
- 難易度: 初級〜中級(Pythonセクション)、なし(Thunderbitセクション)
- 所要時間: Python環境構築と初回スクレイピングで約20〜60分、Thunderbitなら約2分
- 必要なもの: Python 3.9以上、コードエディタ、Chromeブラウザ、そしてノーコード手法では
Indeedスクレイピング用にPython環境を整える
スクレイピングコードを書く前に、まず環境を整えましょう。
必要なライブラリをインストールする
仮想環境を作成し、必要なパッケージを入れます。
1python -m venv indeed_env
2source indeed_env/bin/activate # Windowsの場合: indeed_env\Scripts\activate
3# HTTP + 解析方式
4pip install requests beautifulsoup4 lxml httpx
5# 隠しJSON方式(推奨)
6pip install curl_cffi parsel tenacity
7# ブラウザ自動操作方式
8pip install selenium
補足をいくつか。
curl_cffiは、2026年時点でCloudflare保護サイトをスクレイピングする際の定番です。普通のrequestsやhttpxでは再現できない、本物のブラウザに近いTLSフィンガープリントを装えます。これがなぜ重要かは、後のアンチボット対策で説明します。- Selenium 4.6以降 にはSelenium Managerが同梱されているため、ChromeDriverを自分でダウンロードする必要はありません。ブラウザの実体を自動で管理してくれます。
- BeautifulSoupのパーサーには
lxmlを使いましょう。標準のhtml.parserよりおよそです。
プロジェクト構成を作る
シンプルで十分です。
1indeed_scraper/
2├── scraper.py
3├── requirements.txt
4└── output/
以下のコード例はすべて scraper.py をベースにしています。
BeautifulSoupでPythonからIndeedをスクレイピングする方法
これは初心者向けの方法です。requests でページを取得し、BeautifulSoup でHTMLを解析します。セットアップは最速ですが、Indeed相手では最も壊れやすい方法でもあります。
ステップ1: Indeedの検索URLを作る
Indeedの検索URLは次のパターンです。
1https://www.indeed.com/jobs?q=<query>&l=<location>&start=<offset>
たとえば、「Austin, TX」で「data analyst」を検索し、1ページ目を開くならこうなります。
1from urllib.parse import urlencode
2params = {
3 "q": "data analyst",
4 "l": "Austin, TX",
5 "start": 0,
6}
7url = f"https://www.indeed.com/jobs?{urlencode(params)}"
8print(url)
9# https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX&start=0
Indeedのページネーションは10件単位で、最大1,000件(start <= 990)までです。990を超えるoffsetを指定すると、黙って同じページが返ってきます。
ステップ2: 適切なヘッダー付きでHTTPリクエストを送る
デフォルトのPython user-agent文字列では、Indeedにほぼ即ブロックされます。現実的なヘッダーを入れましょう。
1import requests
2headers = {
3 "User-Agent": (
4 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
6 ),
7 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8 "Accept-Language": "en-US,en;q=0.9",
9 "Accept-Encoding": "gzip, deflate, br",
10 "Referer": "https://www.indeed.com/",
11}
12response = requests.get(url, headers=headers, timeout=30)
13print(response.status_code)
200が返れば、とりあえず通っています。403ならCloudflareに止められています。(対処法は後述します)
ステップ3: HTMLから求人一覧を解析する
BeautifulSoup を使って求人カード要素を取得します。IndeedのランダムなCSSクラス名よりも、data-testid 属性を狙うほうが安定します。
1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "lxml")
3cards = soup.find_all("div", attrs={"data-testid": "slider_item"})
4jobs = []
5for card in cards:
6 title_el = card.find("h2", class_="jobTitle")
7 title = title_el.get_text(strip=True) if title_el else None
8 company = card.find(attrs={"data-testid": "company-name"})
9 location = card.find(attrs={"data-testid": "text-location"})
10 link = title_el.find("a")["href"] if title_el and title_el.find("a") else None
11 jobs.append({
12 "title": title,
13 "company": company.get_text(strip=True) if company else None,
14 "location": location.get_text(strip=True) if location else None,
15 "url": f"https://www.indeed.com{link}" if link else None,
16 })
17print(f"{len(jobs)}件の求人を取得しました")
ステップ4: ページネーションを処理する
start パラメータを増やしながらページを回します。
1import time, random
2all_jobs = []
3for page in range(0, 50, 10): # 最初の5ページ
4 params["start"] = page
5 url = f"https://www.indeed.com/jobs?{urlencode(params)}"
6 response = requests.get(url, headers=headers, timeout=30)
7 # ...上記と同様に解析...
8 all_jobs.extend(jobs)
9 time.sleep(random.uniform(3, 6))
この方法の限界
はっきり言うと、BS4 + Requests は2026年時点のIndeedに対してかなり弱い方法です。普通の requests はPython標準のTLSライブラリを使うため、Cloudflareが即座に「ブラウザじゃない」と判定するを出します。さらに、Indeedが配信するHTTP/2にも対応していません。数ページ取れればまだいいほうで、すぐにブロックされる可能性が高いです。CSSセレクタも、Indeedは css-1m4cuuf や jobsearch-JobComponent-embeddedBody-1n0gh5s のようなクラス名を変えるため、こうしたセレクタは時限爆弾みたいなものです。
この方法は、1ページだけの試作に使ってください。大規模にやるなら、隠しJSON方式を使うべきです。
隠しJSONデータを使ってPythonでIndeedをスクレイピングする方法
多くのPython開発者にいちばんおすすめしたい方法です。壊れやすいHTML要素を解析するのではなく、Indeedのページソースに埋め込まれたJavaScript変数 window.mosaic.providerData["mosaic-provider-jobcards"] から構造化データを抜き出します。
求人タイトル、会社名、勤務地、給与、求人キー、掲載日、リモート可否など、欲しい情報のほとんどはすでにこのJSON blobの中に入っています。JavaScriptを実行する必要もありません。スキーマはため、DOMセレクタよりかなり壊れにくいです。
ステップ1: ページHTMLを取得する
requests の代わりに curl_cffi を使いましょう。本物のブラウザに近いTLSフィンガープリントを再現できるので、Cloudflareを乗り越えるうえで重要です。
1from curl_cffi import requests as cffi_requests
2response = cffi_requests.get(
3 "https://www.indeed.com/jobs?q=python+developer&l=Remote&start=0",
4 impersonate="chrome124",
5 headers={
6 "Accept-Language": "en-US,en;q=0.9",
7 "Referer": "https://www.indeed.com/",
8 },
9 timeout=30,
10)
11print(response.status_code, len(response.text))
なぜ curl_cffi なのかというと、これは curl-impersonate のPythonバインディングで、実際のブラウザと同じTLS ClientHello、HTTP/2 SETTINGSフレーム、ヘッダー順序を再現できるからです。PythonのHTTPクライアントとして現役で保守されているものの中で、のはこれだけです。chrome120、chrome124、chrome131、Safari、Edge系のimpersonationがサポートされています。
ステップ2: 正規表現でJSONを抜き出す
JSON blobは <script> タグ内に埋め込まれています。正規表現で取り出しましょう。
1import re, json
2MOSAIC_RE = re.compile(
3 r'window\.mosaic\.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});',
4 re.DOTALL,
5)
6match = MOSAIC_RE.search(response.text)
7if match:
8 data = json.loads(match.group(1))
9 results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
10 print(f"隠しJSONから{len(results)}件の求人を見つけました")
11else:
12 print("隠しJSONが見つかりません。ブロックかページ変更の可能性があります")
ステップ3: JSONから求人項目を取り出す
results の各要素には、画面に見えている以上の情報が入っています。
1jobs = []
2for job in results:
3 jobs.append({
4 "jobkey": job["jobkey"],
5 "title": job["title"],
6 "company": job.get("company"),
7 "location": job.get("formattedLocation"),
8 "remote": job.get("remoteLocation"),
9 "salary": (job.get("salarySnippet") or {}).get("text"),
10 "posted": job.get("formattedRelativeTime"),
11 "job_type": job.get("jobTypes"),
12 "easy_apply": job.get("indeedApplyEnabled"),
13 "url": f"https://www.indeed.com/viewjob?jk={job['jobkey']}",
14 })
このJSONには、給与推定、スキルタグなどの分類属性、会社評価といった、レンダリングされたHTMLには常に出ていない情報が含まれることがあります。
ステップ4: 複数ページを取得する
JSON内の tierSummaries を使って総件数を把握し、そのうえでループします。
1import time, random
2all_jobs = []
3for start in range(0, 50, 10): # 最初の5ページ
4 url = f"https://www.indeed.com/jobs?q=python+developer&l=Remote&start={start}&sort=date"
5 response = cffi_requests.get(
6 url,
7 impersonate="chrome124",
8 headers={"Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.indeed.com/"},
9 timeout=30,
10 )
11 match = MOSAIC_RE.search(response.text)
12 if match:
13 data = json.loads(match.group(1))
14 results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
15 all_jobs.extend([{
16 "jobkey": j["jobkey"],
17 "title": j["title"],
18 "company": j.get("company"),
19 "location": j.get("formattedLocation"),
20 "salary": (j.get("salarySnippet") or {}).get("text"),
21 "url": f"https://www.indeed.com/viewjob?jk={j['jobkey']}",
22 } for j in results])
23 time.sleep(random.uniform(3, 7))
24print(f"合計{len(all_jobs)}件の求人を取得しました")
隠しJSONのほうが安定しやすい理由
window.mosaic.providerData の構造は、CSSクラス名より変更頻度が低いです。面倒なHTML解析をせずに、きれいな構造化データが取れます。とはいえ、ヘッダー、待機時間、プロキシといったアンチボット対策は引き続き必要です。次でそこを説明します。
SeleniumでPythonからIndeedをスクレイピングする方法
Seleniumはブラウザ自動化の手法です。求人詳細パネルをクリックしたい、ログイン後のコンテンツを扱いたい、初期HTMLにはない動的読み込みの説明文を取りたい、といった場合に向いています。
HTTPクライアントではなくSeleniumを使うべき場面
- Indeedの一部コンテンツが動的に読み込まれる場合(右側パネルの詳細説明など)
- セッション状態やログインが必要なページを取りたい場合
- スピードよりも小規模で確実に取りたい場合
簡単な流れ
1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.chrome.options import Options
4import time
5options = Options()
6options.add_argument("--disable-blink-features=AutomationControlled")
7# options.add_argument("--headless=new") # ヘッドレスは検出されやすいので注意
8driver = webdriver.Chrome(options=options)
9driver.get("https://www.indeed.com/jobs?q=data+engineer&l=New+York")
10time.sleep(3)
11cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid='slider_item']")
12for card in cards:
13 try:
14 title = card.find_element(By.CSS_SELECTOR, "h2.jobTitle").text
15 company = card.find_element(By.CSS_SELECTOR, "[data-testid='company-name']").text
16 location = card.find_element(By.CSS_SELECTOR, "[data-testid='text-location']").text
17 print(f"{title} | {company} | {location}")
18 except Exception:
19 continue
20driver.quit()
限界
Seleniumは遅いです。ページごとにブラウザ描画が必要になります。ヘッドレスChromeは、Cloudflareは navigator.webdriver、WebGLのベンダー文字列、プラグイン数などを確認します。undetected-chromedriver を使っても、検出を遅らせるだけで、恒久的に防げるわけではありません。BS4と同じく、IndeedがUIを更新すればセレクタも壊れます。
多くの用途では、隠しJSON方式のほうが同じデータをより速く、より少ない保守で取得できます。本当にブラウザが必要なケースだけSeleniumを使うのが賢明です。
PythonでIndeedをスクレイピングしたときの403エラーを回避する方法
ここがいちばん大事なパートです。イライラしながら検索してここに来たなら、まさに探していた場所です。

Indeedがスクレイパーを止める理由
Indeedはを使っています。DataDomeでもPerimeterXでもありません。レスポンスヘッダーにも server: cloudflare、cf-ray、__cf_bm のボット管理cookieが出ます。CloudflareはTLSフィンガープリント(JA3/JA4)、HTTP/2のヘッダー順序、リクエストパターン、ブラウザの挙動シグナルを確認します。どれか一つでも人間っぽくなければ、403、429、503、あるいはもっと面倒な「200 OKなのに中身は求人データではなくTurnstileのチャレンジページ」という形で返されます。
User-Agentとリクエストヘッダーをローテーションする
固定のUser-Agentは、最速でブロックされる方法です。現行の、それっぽい文字列を複数用意して回しましょう。重要なのは、Chromeのマイナーバージョン欄はUser-Agent Reduction以降点です。存在しないマイナーバージョンを作ると、アンチボットに怪しまれます。
1import random
2USER_AGENTS = [
3 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
4 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
5 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
6 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
7 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
8 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
9 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
10 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
11 "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
12]
13headers = {
14 "User-Agent": random.choice(USER_AGENTS),
15 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
16 "Accept-Language": "en-US,en;q=0.9",
17 "Accept-Encoding": "gzip, deflate, br, zstd",
18 "Referer": "https://www.indeed.com/",
19 "Sec-Fetch-Dest": "document",
20 "Sec-Fetch-Mode": "navigate",
21 "Sec-Fetch-Site": "same-origin",
22}
さらに、sec-ch-ua のClient HintsがUAのバージョンと一致していることも確認してください。たとえば sec-ch-ua: "Chrome";v="131" の一方でUser-AgentがChrome 145を名乗っていたら、それだけで赤信号です。
リクエスト間にランダムな待機を入れる
固定間隔はパターン検知に引っかかります。ランダムな揺らぎを入れましょう。
1import time, random
2# 各リクエストの間
3time.sleep(random.uniform(3, 6))
4# ブロック後の再試行時
5def backoff_sleep(attempt):
6 base = 4
7 sleep_time = base * (2 ** attempt) + random.uniform(0, 2)
8 time.sleep(min(sleep_time, 60))
や の実務的な見解では、IPあたり3〜6秒の間隔が目安で、1セッションあたり約100リクエストを超える前にIPを切り替えるのがよいとされています。
プロキシをローテーションする
成功率を左右する最大の要素はこれです。AWS/GCP系のデータセンタープロキシは、Cloudflare Enterprise相手だと成功率が5〜15%程度で、ほぼ使い物になりません。住宅回線プロキシと正しいTLSフィンガープリントを組み合わせると、成功率は80〜95%まで上がります。
1PROXIES = [
2 "http://user:pass@us.residential.example:7777",
3 "http://user:pass@us.residential.example:7778",
4 "http://user:pass@us.residential.example:7779",
5]
6proxy = random.choice(PROXIES)
7response = cffi_requests.get(
8 url,
9 impersonate="chrome124",
10 headers=headers,
11 proxies={"https": proxy},
12 timeout=30,
13)
2026年の住宅回線プロキシ料金は、プロバイダーや契約条件にもよりますが、おおむねです。Indeed向けには、まず小規模なプールから始めて、必要に応じて増やしていくのが現実的です。
403、429、503を丁寧に扱う
闇雲にリトライしてはいけません。ステータスコードごとに意味が違います。
1def fetch_with_retry(url, proxy_pool, max_retries=5):
2 for attempt in range(max_retries):
3 proxy = random.choice(proxy_pool)
4 headers["User-Agent"] = random.choice(USER_AGENTS)
5 try:
6 r = cffi_requests.get(
7 url,
8 impersonate=random.choice(["chrome124", "chrome120", "edge101"]),
9 headers=headers,
10 proxies={"https": proxy},
11 timeout=30,
12 )
13 # 「200だけどチャレンジページ」の厄介なケースを確認
14 if r.status_code == 200 and "cf-turnstile" not in r.text and "Just a moment" not in r.text:
15 return r
16 if r.status_code == 403:
17 print(f"403 — ブロックされました。プロキシを切り替えて再試行します(試行 {attempt + 1})")
18 elif r.status_code == 429:
19 print(f"429 — レート制限です。速度を落とします。")
20 elif r.status_code == 503:
21 print(f"503 — サーバー高負荷、またはJSチャレンジです。")
22 backoff_sleep(attempt)
23 except Exception as e:
24 print(f"リクエストエラー: {e}")
25 backoff_sleep(attempt)
26 raise RuntimeError(f"{max_retries}回の再試行後も失敗しました: {url}")
中でも厄介なのが、200が返るのに中身がチャレンジページというケースです。200を成功扱いにする前に、レスポンス本文に cf-turnstile や Just a moment が含まれていないか必ず確認してください。
もっと簡単な代替案: Thunderbitにアンチボットを任せる
プロキシプールの構築・保守、ヘッダーのローテーション、TLSフィンガープリントの擬装を自前でやりたくないなら、 のクラウドスクレイピングがCAPTCHA、プロキシローテーション、アンチボット対策を自動で処理します。プロキシ設定も curl_cffi の調整もCAPTCHA解読ライブラリも不要です。ただデータが欲しいだけなら、いちばん手間の少ない選択肢です。
なぜIndeedスクレイパーは壊れ続けるのか、そしてどう直すか
403の壁は目先の痛みです。本当にしんどいのは保守です。今日動いていたスクレイパーが来週には壊れ、空データや古いデータを黙って返すようになります。
Indeedがセレクタを壊す仕組み
IndeedはCSSクラス名を頻繁に入れ替えます。Bright Dataのガイドでも、css-1m4cuuf や css-1rqpxry のようなクラスは「ランダム生成されているように見え、おそらくビルド時に作られている」と。A/Bテストのせいで、同時に違うユーザーが違うレイアウトを見ることもあります。DOM構造が予告なく変わることもあります。
JobFunnelの事例はかなり示唆的です。あるコントリビューターは、「CaptchaBusterはCAPTCHAをうまく回避できたが、それでもページのスクレイピングに失敗したのは、BeautifulSoupのセレクタが古かったからだ」 と報告しています。つまりブロックされたのではなく、別の要素を読んでいたわけです。
方針: DOM解析より隠しJSONを優先する
window.mosaic.providerData のblobは、少なくとも2023年以降、スキーマが安定しています。metaData.mosaicProviderJobCardsModel.results[] へのパスは、2026年時点でもです。DOMセレクタは毎月壊れますが、JSON抽出は壊れても年単位です。
方針: クラス名よりdata属性を使う
どうしてもDOMを触る必要があるなら、機能を表す属性を狙いましょう。
| セレクタ | 用途 |
|---|---|
[data-testid="slider_item"] | 各求人カードのコンテナ |
[data-testid="job-title"] または h2.jobTitle > a | 求人タイトルのリンク |
[data-testid="company-name"] | 会社名 |
[data-testid="text-location"] | 勤務地テキスト |
各カードの data-jk="<jobkey>" | 最も安定したフックのひとつ。2019年以降ほぼ不変 |
セレクタの劣化を検知するためにアサーションを入れる
結果がゼロでも黙って進むスクレイパーは危険です。取得のたびにチェックを入れましょう。
1results = parse_hidden_json(html)
2assert len(results) > 0, (
3 f"Indeedが start={start} で空の結果を返しました — "
4 "ブロック、CAPTCHA、またはセレクタの変更の可能性があります。 "
5 f"レスポンス先頭500文字: {html[:500]}"
6)
失敗時は、元のレスポンスの先頭500〜2000文字をログに残してください。そうすれば、Turnstileのチャレンジなのか、サインイン画面なのか、スキーマ変更なのかがすぐ分かります。q=python&l=remote のような固定クエリに対して、毎日CIレベルのスモークテストを走らせ、結果がゼロでないことを確認するのも有効です。
AIという選択肢: 壊れにくいスクレイパー
ThunderbitのAIは、毎回ページ構造を読み直します。ハードコードされたセレクタや正規表現に依存しません。IndeedのHTMLが変わっても、Thunderbitは自動で追従します。これは、コミュニティの利用者がいつも口にする「保守負担」という悩みにかなり効く解決策です。「また空行を返してるよ」とSlackで連絡が来るたびに直すあの苦労を、かなり減らせます。
Pythonを書かずにIndeedをスクレイピングする: ノーコード代替
競合記事の多くは、Pythonコードを書く前提で話を進めます。でもフォーラムの声は違います。「バグとエラーが絶えなくて本当に大変」 といった声や、データを取るためだけにFiverrで誰かを雇うべきだという意見まであります。もし心当たりがあるなら、このセクションが抜け道になります。
ThunderbitでIndeedをスクレイピングする方法(手順)
ステップ1: Chromeウェブストアからをインストールします。無料で始められます。
ステップ2: ブラウザでIndeedの検索結果ページを開きます。たとえば https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX のようなページです。
ステップ3: ブラウザのツールバーにあるThunderbitアイコンをクリックし、続けて 「AI Suggest Fields」 を押します。ThunderbitのAIがページを解析し、Job Title、Company、Location、Salary、Job URL、Posted Date などの列を自動検出します。提案された項目は確認・調整できます。不要な列は外し、ほしい項目があれば自然な日本語で説明して追加できます。
ステップ4: 「Scrape」 をクリックします。Thunderbitがページからデータを抽出し、構造化された表として表示します。設定した項目付きで求人一覧が行単位で並ぶはずです。
サブページスクレイピングで情報を拡張する
一覧ページを取得したあと、「Scrape Subpages」 をクリックすると、Thunderbitが各求人詳細ページを巡回します。求人詳細全文、応募条件、福利厚生、応募リンクまで取得でき、追加設定は不要です。これは /viewjob?jk=<jobkey> を順番に訪問する2本目のPythonスクレイパーを書くのと同じことですが、クリック1回で済みます。
ページネーションも自動処理
ThunderbitはIndeedのクリック式ページネーションも自動で処理します。offset付きURLを手で組み立てたり、ループを書いたりする必要はありません。ページをまたいで取得し、結果をまとめてくれます。
お好みのツールにエクスポート
取得したデータはCSV、Excel、Google Sheets、Airtable、Notionにエクスポートできます。csv.writer() や pandas.to_csv() を書く必要はありません。
PythonとThunderbit、どちらを使うべきか
| シナリオ | 最適なツール |
|---|---|
| 独自のデータパイプライン、cron/Airflowによる定期自動化 | Python |
| 大きなコードベースへの組み込み | Python |
| かなりカスタマイズされた解析ロジック | Python |
| 単発のリサーチや市場分析 | Thunderbit |
| 非技術職のメンバーにもデータが必要 | Thunderbit |
| 403のデバッグなしで今すぐデータが欲しい | Thunderbit |
| 追加設定なしでサブページを拡張取得したい | Thunderbit |
時間比較でいうと、Pythonのセットアップ+アンチボット対策のデバッグは数時間〜数日かかることがあります(特に初回は)。Thunderbitなら、同じデータを2分以内で取れることも珍しくありません。Pythonが間違っていると言いたいのではなくて、必要なもの次第だという話です。
Indeedのスクレイピングは合法なのか?知っておくべきこと
上位表示されるIndeedスクレイピング記事の多くは、合法性について触れていません。フォーラムでは 「Indeedのスクレイピングは合法なのか?」 という質問がかなり多いのに、これは少し意外です。以下は法的助言ではありませんが、現状の見取り図です。
Indeedの利用規約
Indeedの利用規約()には、包括的に「スクレイピング禁止」とは書かれていません。明示的に自動化を禁じているのはセクションA.3.5で、「Indeed Applyプロセスを自動化するために、あらゆる自動化、スクリプト、ボットを使用すること」 を禁止しています。これは応募フローに限定された話であり、公開求人情報を受動的に読むことまでは対象にしていません。Indeedが主に使うのは、法廷よりもCloudflareのチャレンジ、IPブロック、デバイスフィンガープリントといった技術的な対策です。
関連する判例
米国でいちばんよく引き合いに出されるのは hiQ Labs v. LinkedIn です。第9巡回区控訴裁判所はに、公開アクセス可能なデータのスクレイピングは「CFAA(Computer Fraud and Abuse Act)に違反しない可能性が高い」と判断しました。ただしhiQは後に、従業員が偽のLinkedInプロフィールを作成して利用規約に同意していたため、が認められました。
より最近では、Meta v. Bright Data(カリフォルニア北部地区、2024年1月)がさらに明確な判断を示しました。Chen判事はと判断しました。その後Metaは残る請求を翌月取り下げています。
Indeedのrobots.txt
Indeedのは、デフォルトの User-agent: * に対して /jobs/ と /job/ を広く禁止していますが、GooglebotとBingbotには個別求人詳細ページの /viewjob? へのアクセスを明示的に許可しています。GPTBot、CCBot、anthropic-aiといったAI学習クローラはかなり制限されています。robots.txt は米国では法的拘束力を持ちませんが、これに従うことはベストプラクティスであり、誠実な運用の証拠にもなります。
責任あるスクレイピングの実践指針
- 公開されているデータだけを取得する。ログインもしない、偽アカウントも作らない
- レート制限を守る: IPあたり3〜6秒に1リクエスト、同時実行数は少数に抑える
- 取得データを自分の求人サイトとして再公開しない
- 個人利用や社内調査に限定し、許可なく商用再販しない
- 不要な個人情報は破棄またはハッシュ化し、関連データの保持期間に上限を設ける
- 大規模運用やEU/UK圏での利用では弁護士に相談する。スクレイピングされた個人データにはGDPR第14条の通知義務が関わります
リスクの目安として、個人の転職支援レベルは低リスク、Indeedデータの大規模商用再販は高リスク寄りです。
まとめと重要ポイント
PythonでIndeedをスクレイピングすること自体は可能ですが、週末にサクッと作って放置できる類のものではありません。Cloudflare保護、変動するセレクタ、強力なアンチボット対策を考えると、適切なツールと現実的な期待値で臨む必要があります。
ここまでの要点をまとめると、次の通りです。
- IndeedはWeb上で最も豊富な求人市場データ源のひとつです。月間3.5億人、掲載1.3億件という規模ですが、スクレイパーにはかなり強く抵抗します。
- 隠しJSON抽出(
window.mosaic.providerData)が、Pythonでは最も堅牢な方法です。スキーマは何年も安定していますが、CSSセレクタは毎月壊れます。 - Cloudflare保護サイトに対する2026年時点のHTTPクライアント標準は、ブラウザを擬装できる
curl_cffiです。 普通のrequestsやhttpxはTLSフィンガープリントだけで止められます。 - 403エラーを避けるには、ヘッダーのローテーション、ランダムな待機、住宅回線プロキシを必ず使うべきです。データセンタープロキシはCloudflare Enterprise相手ではほぼ役に立ちません。
- アサーションチェックを入れて、セレクタが壊れた瞬間や、求人データの代わりにチャレンジページを受け取った瞬間に気づけるようにしましょう。
- 非技術者や、すぐに結果が欲しい人には、 のようなノーコードのAIツールが有効です。サイト変更にも自動追従し、プロキシ設定もデバッグも保守も不要です。
ノーコードを試したいなら、があるので、Indeedで気軽に試せます。Pythonで進める場合は、上記のコードサンプルがいい出発点になります。ただし、アンチボット耐性は後回しではなく、最初から最重要事項として扱ってください。
Webスクレイピングの手法やツールについてさらに知りたい方は、、、 もどうぞ。YouTubeではでもチュートリアルをご覧いただけます。
FAQ
Indeedのスクレイピングに最適なPythonライブラリは?
HTTPリクエストには、2026年時点では curl_cffi が最有力です。本物のブラウザに近いTLSフィンガープリントを装えるため、Cloudflare回避に重要です。httpx のHTTP/2対応も、保護が弱い対象なら悪くない代替手段です。HTML解析には、BeautifulSoup4 と lxml の組み合わせが定番です。ブラウザ自動化なら playwright-stealth 付きのPlaywrightや undetected-chromedriver も使えますが、どちらも検出されやすくなっています。隠しJSONを抜く window.mosaic.providerData 方式なら、重たい解析自体が不要です。
Indeedをスクレイピングすると403エラーが頻発するのはなぜ?
IndeedはCloudflare Bot Managementを使っており、TLSフィンガープリント(JA3/JA4)、HTTP/2ヘッダー順序、リクエストパターン、ブラウザ挙動を見ています。素の requests を使っていると、ヘッダーが読まれる前にTLSだけでPythonスクリプトだと判定され、403になります。対策としては、ブラウザを擬装する curl_cffi に切り替え、現実的なUser-Agentをローテーションし、3〜6秒のランダム待機を入れ、住宅回線プロキシを使うことです。また、cf-turnstile を含む「200だけどチャレンジページ」のケースもあるので、レスポンス本文のチェックも必須です。
コーディングなしでIndeedをスクレイピングできますか?
はい。 のようなツールを使えば、数クリックでIndeedの求人一覧を取得できます。Chrome拡張機能を入れてIndeedの検索ページを開き、「AI Suggest Fields」を押してから「Scrape」をクリックするだけです。ThunderbitのAIが求人タイトル、会社名、勤務地、給与などの項目を自動検出します。ページネーション、サブページ拡張取得(求人詳細全文)、アンチボット対策も自動処理します。CSV、Google Sheets、Airtable、Notionへのエクスポートも無料です。
IndeedのHTML構造はどのくらいの頻度で変わりますか?
IndeedはCSSクラス名(例: css-1m4cuuf のようなランダムなハッシュ文字列)を頻繁に変え、DOM要素の構造も予告なく組み替えます。A/Bテストの影響で、同時に異なるユーザーが異なるレイアウトを見ることもあります。隠しJSON方式(window.mosaic.providerData)はかなり安定しており、少なくとも2023年以降は一貫しています。どうしてもDOMセレクタを使う場合は、CSSクラスではなく data-testid 属性や data-jk(job key)を狙ってください。
Indeedをスクレイピングするのは合法ですか?
公開されているIndeedの求人URLをログアウト状態でスクレイピングすることは、米国ではCFAA違反になりにくいと考えられます。これは第9巡回区の hiQ v. LinkedIn 判決(2022年)や Meta v. Bright Data の判断(2024年)に基づくものです。Indeedの利用規約が明示的に禁じているのは、応募フローの自動化であって、公開一覧の受動的閲覧そのものではありません。ただし、常に責任ある運用をしてください。ログインしない、偽アカウントを作らない、レート制限を守る、データを自分の求人サイトとして再公開しない、また、採用担当者名やメールアドレスなどの個人データはGDPR/CCPAの観点から慎重に扱うべきです。商用規模で運用するなら、弁護士に相談してください。
さらに詳しく知る
