Indeedの求人タイトルをスプレッドシートに50回ほどコピペしたあたりで、自分のキャリア選択に疑問を持ち始めました。Indeedから構造化データをプログラムで取得しようとしたことがあるなら、もうオチはわかっているはずです。403エラーはバグではなく、Indeedの防御システムの一部なのです。
Indeedは世界最大の求人サイトで、、常時もの求人情報があり、で展開しています。つまり、世界でも有数の豊富な求人市場データの宝庫である一方、最もスクレイピングが難しいサイトのひとつでもあります。オープンソースのスクレイパーJobFunnel(GitHubスター数は数千)は、アンチボットとの消耗戦に敗れ続けた末、2025年12月にメンテナー自身の手で。メンテナー本人の言葉を借りると、「すべてのユーザーが一部の求人は取得できますが、すぐにCAPTCHAに阻まれ、スクレイピングは失敗して求人は取得できません。」 別の貢献者は、CAPTCHAが出たと報告しています。なので、これは決して簡単なスクレイピング対象ではありません。このガイドでは、PythonでIndeedをスクレイピングするための実践的な方法をすべて紹介し、403の壁をどう乗り越えるかを解説します。さらに、デバッグ自体を飛ばしたい方のために、を使ったノーコードの代替案も紹介します。
PythonでIndeedをスクレイピングするとはどういう意味か?
ウェブスクレイピングとは、基本的には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自身の調査でも、、米国の労働市場状況をほぼリアルタイムで示す指標になり得ることが確認されています。ヘッジファンドは求人掲載数の増減を代替データのシグナルとして活用しています。人事チームはスクレイピングした給与レンジで報酬水準をベンチマークし、採用担当者は積極採用中の企業から見込み先リストを作ります。
ひとつ実務上の注意点があります。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保護サイトをスクレイピングする際の標準的な選択肢です。実際のブラウザに近いTLSフィンガープリントを装えるため、通常のrequestsやhttpxではできない対策が可能です。これが重要な理由は、後ほどアンチボットの章で説明します。- 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」を最初のページから検索する場合は次のようになります。
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を超えるオフセットを指定しても、同じページが返ってくるだけでエラーにはなりません。
ステップ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))
この方法の限界
先に率直に言うと、2026年のIndeedに対しては、BS4 + Requestsは最も弱い方法です。通常の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"]から構造化データを抜き出します。
求人タイトル、企業名、勤務地、給与、job key、掲載日時、リモート判定など、必要な情報はすべてこのJSONブロックに入っています。JavaScriptを実行する必要はありません。スキーマは、DOMセレクタよりはるかに堅牢です。
ステップ1: ページのHTMLを取得する
requestsの代わりにcurl_cffiを使いましょう。Cloudflareを突破するうえで重要な、実際のブラウザに近いTLSフィンガープリントを装えます。
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ブロックは<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は、Indeedのアンチボットシステムにです(Cloudflareはnavigator.webdriver、WebGLのベンダー文字列、プラグイン数などをチェックします)。undetected-chromedriverでも検出を遅らせることはできますが、恒久的に防げるわけではありません。さらにBS4と同様、IndeedがUIを更新するとセレクタは壊れます。
多くの用途では、非公開JSONの方法の方が、同じデータをより速く、保守負荷を抑えて取得できます。Seleniumは、本当にブラウザが必要な例外ケースにだけ使いましょう。
PythonでIndeedをスクレイピングする際に403エラーを避ける方法
ここが最も重要な部分です。もしフラストレーションのたまったGoogle検索からここにたどり着いたなら、正解です。

なぜIndeedはスクレイパーをブロックするのか
Indeedはを使っています。DataDomeでもPerimeterXでもありません。レスポンスヘッダーを見ればわかります。server: cloudflare、cf-ray、そしてボット管理用Cookieの__cf_bmが付いています。CloudflareはTLSフィンガープリント(JA3/JA4)、HTTP/2のヘッダー順、リクエストパターン、ブラウザ挙動のシグナルをチェックします。どれかひとつでも人間らしくなければ、403、429、503、あるいは最も厄介なケースとして、実データの代わりにTurnstileのチャレンジページが返る200 OKが起こります。
User-Agentとリクエストヘッダーをローテーションする
固定のUser-Agentだけを使うのは、最速でブロックされる方法です。現行で自然な文字列を複数用意し、そこから回しましょう。重要なのは、Chromeのマイナーバージョン表記はことです。存在しないマイナーバージョンを作ると、アンチボットに即座に怪しまれます。
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のバージョンと一致していることを確認してください。Chrome 145を名乗るUser-Agentなのにsec-ch-ua: "Chrome";v="131"となっているのは、即座に赤信号です。
リクエスト間にランダムな遅延を入れる
固定間隔はパターン検出で見つかります。ランダムなジッターを入れましょう。
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))
との見解では、1 IPあたり3〜6秒の間隔が目安で、セッション中の1 IPあたりの上限は約100リクエストとされています。
プロキシローテーションを使う
成功率を最も左右する要素はこれです。AWS/GCP系のデータセンタープロキシは、Cloudflare Enterprise対象では成功率が約5〜15%程度で、Indeedではほぼ使いものになりません。レジデンシャルプロキシと正しい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解決ライブラリも不要です。とにかくデータが欲しいときに、最小抵抗のルートです。
Pythonを書いたのにIndeedスクレイパーが壊れ続ける理由と、その直し方
403の壁は急性の痛みです。慢性的な痛みは保守です。今日動くスクレイパーが来週には壊れ、気づかないうちに空データや古い結果を返すようになります。
Indeedがセレクタを壊す仕組み
IndeedはCSSクラス名を積極的に入れ替えます。Bright Dataのガイドでも、css-1m4cuufやcss-1rqpxryのようなクラスは「ランダム生成されているように見え、おそらくビルド時に作られている」と。A/Bテストにより、セッションごとに異なるレイアウトが表示されることもあります。さらにDOM構造も予告なく変更されます。
JobFunnelの事例は示唆的です。ある貢献者は、*「CaptchaBusterはCAPTCHAをうまく回避できたが、ページのスクレイピングが成功しない理由は、古いBeautiful Soupセレクタにある」*と報告しました。つまり、ブロックされたのではなく、違う要素を読んでいたのです。
方針: DOM解析より非公開JSONを優先する
window.mosaic.providerDataのブロックは、少なくとも2023年以降スキーマが安定しています。metaData.mosaicProviderJobCardsModel.results[]のパスは、2026年時点でもです。DOMセレクタは月単位で壊れます。JSON抽出は、壊れてもせいぜい年単位です。
方針: クラス名よりデータ属性を使う
どうしてもDOMに触る必要がある場合は、機能を示す属性を狙います。
| セレクタ | 用途 |
|---|---|
[data-testid="slider_item"] | 各求人カードのコンテナ |
[data-testid="job-title"] または h2.jobTitle > a | 求人タイトルのリンク |
[data-testid="company-name"] | 企業名 |
[data-testid="text-location"] | 勤務地テキスト |
各カードの data-jk="<jobkey>" | 最も安定したフック。2019年以降変更なし |
セレクタの陳腐化を検知するためにアサーションを入れる
スクレイパーが0件のまま静かに動き続ける状態は避けましょう。取得のたびにチェックを入れます。
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で項目を提案」**をクリックします。ThunderbitのAIがページを解析し、求人タイトル、企業名、勤務地、給与、求人URL、投稿日などの列を自動検出します。提案された項目は確認・調整でき、不要な列を外したり、欲しい内容を自然な英語で説明して独自の列を追加したりできます。
ステップ4: **「スクレイプ」**をクリックします。Thunderbitがページからデータを抽出し、構造化された表で表示します。設定した項目を持つ求人一覧の行が見えるはずです。
サブページスクレイピングで情報を拡張する
一覧ページを取得したら、**「サブページをスクレイプ」**をクリックして、各求人詳細ページをThunderbitに巡回させます。求人全文、応募条件、福利厚生、応募リンクまで取得できます。追加設定は不要です。これは、各/viewjob?jk=<jobkey> URLにアクセスする2本目のPythonスクレイパーを書くのと同じですが、クリック1回で済みます。
ページネーションも自動処理
Thunderbitは、Indeedのクリック式ページネーションを自動で処理します。オフセットURLを手動で組み立てたり、ページ送りのループを書いたりする必要はありません。ページを順にたどって結果を集約してくれます。
お好みのツールへエクスポート
抽出したデータはCSV、Excel、Google スプレッドシート、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プロセスを自動化するために、あらゆる自動化、スクリプト、またはボットを使用すること」*を禁止しています。これはApplyフローに限定されたもので、公開求人情報を受動的に読む行為までは対象にしていません。Indeedの主な執行手段は法廷ではなく、Cloudflareチャレンジ、IPブロック、デバイスフィンガープリントといった技術的なものです。
関連する法的判例
米国で最もよく引用されるのはhiQ Labs対LinkedInです。第9巡回区控訴裁判所はに、公開アクセス可能なデータのスクレイピングは「CFAA(Computer Fraud and Abuse Act)に違反しない可能性が高い」と判断しました。ただし、hiQはその後、従業員が偽のLinkedInプロフィールを作成して利用規約に同意していたため、と認定されています。
さらに最近では、Meta対Bright Data(カリフォルニア北部地区、2024年1月)で、より明確な判断が示されました。Chen判事はとしました。その後、Metaは残りの請求を自発的に取り下げています。
Indeedのrobots.txt
Indeedのは、デフォルトのUser-agent: *に対して/jobs/と/job/を広く禁止していますが、GooglebotとBingbotには/viewjob?(個別求人詳細ページ)へのアクセスを明示的に許可しています。AI学習用クローラー(GPTBot、CCBot、anthropic-ai)は大きく制限されています。robots.txtは米国では法的拘束力を持ちませんが、それに従うことはベストプラクティスであり、誠実な運用の証拠にもなります。
責任あるスクレイピングの実践ガイド
- 公開されているデータだけを取得する。ログインしない、偽アカウントを作らない
- レート制限を守る。IPあたり3〜6秒に1リクエスト、同時実行は少数に抑える
- スクレイピングしたデータを自分の求人サイトとして再公開しない
- 個人利用や社内調査にとどめ、許可なく商用再販しない
- 必要のない個人情報は削除またはハッシュ化する。個人に近いデータの保持期間は最小限にする
- 大規模運用やEU/英国での運用では弁護士に相談する。GDPR第14条の透明性義務はスクレイピングした個人データにも適用されます
リスクの幅で言えば、個人の転職活動の自動化は低リスクです。Indeedのデータを大規模に商用再販するのは高リスクです。
まとめと重要なポイント
PythonでIndeedをスクレイピングすることは可能ですが、週末に作って放置できるような案件ではありません。IndeedのCloudflare保護、変動するセレクタ、強力なアンチボット対策を考えると、適切なツールと期待値で臨む必要があります。
ここまでをまとめると、次のとおりです。
- Indeedはウェブ上で最も豊富な求人市場データのひとつです。月間3.5億人、1.3億件の求人がありますが、スクレイパーには強硬に対抗してきます。
- 非公開JSON抽出(
window.mosaic.providerData)が、Pythonでは最も堅牢な方法です。 スキーマは何年も安定している一方、CSSセレクタは毎月のように壊れます。 - 2026年のCloudflare保護サイトでは、ブラウザ偽装付きの
curl_cffiが標準的なHTTPクライアントです。 素のrequestsやhttpxはTLSフィンガープリントの時点で止められます。 - 403エラーを避けるには、ローテーションするヘッダー、ランダム遅延、レジデンシャルプロキシを必ず使いましょう。 データセンタープロキシはCloudflare Enterpriseにはほぼ通用しません。
- セレクタが壊れたとき、またはデータの代わりにチャレンジページが返ったときにすぐ気づけるよう、アサーションチェックを入れてください。
- 非技術系ユーザーや、単純に早く結果が欲しい人には、のようなノーコード・AI駆動の手段が、サイト変更にも自動対応できる現実的な選択肢です。プロキシ設定も、デバッグも、保守も不要です。
ノーコードを試したいなら、があるので、実際にIndeedで気軽に試せます。Pythonで進める場合でも、上のコード例は十分良い出発点です。ただし、アンチボット耐性は後回しではなく最優先事項として扱ってください。
ウェブスクレイピングの手法やツールについてもっと知りたい方は、、、のガイドもご覧ください。でもチュートリアルを公開しています。
よくある質問
Indeedのスクレイピングに最適なPythonライブラリは何ですか?
HTTPリクエストには、2026年時点ではcurl_cffiが最有力です。実際のブラウザのTLSフィンガープリントを偽装でき、Cloudflare回避に不可欠だからです。httpxのHTTP/2対応版は、保護が弱い対象なら代替として使えます。HTML解析には、lxmlを使ったBeautifulSoup4が今も標準です。ブラウザ自動化では、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で項目を提案」をクリックしてから「スクレイプ」**を押すだけです。ThunderbitのAIが求人タイトル、企業名、勤務地、給与などの項目を自動検出します。ページネーション、サブページ拡張取得(求人全文)、アンチボット対策も自動です。CSV、Google スプレッドシート、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をスクレイピングすることは、米国では第9巡回区のhiQ対LinkedIn判決(2022年)やMeta対Bright Data判決(2024年)を踏まえると、CFAA違反にはなりにくいと考えられます。Indeedの利用規約は、公開求人の受動的な閲覧ではなく、Applyプロセスの自動化を特に禁止しています。ただし、必ず責任ある方法で行ってください。ログインしない、偽アカウントを作らない、レート制限を守る、データを自分の求人サイトとして再公開しない、個人データ(採用担当者名、メールアドレスなど)はGDPR/CCPAに配慮して丁寧に扱うことが重要です。商用規模で運用する場合は、弁護士に相談してください。
さらに詳しく知る