Craigslist는 여전히 약 을 700개 안팎의 지역 사이트에서 끌어모으고 있지만, 공개 API는 여전히 없습니다. 아파트 매물, 중고차, 구인 공고, 단기 일감 광고에서 구조화된 데이터를 얻고 싶다면, 사실상 스크래핑이 유일한 방법입니다.
다만 Craigslist의 자체 안티봇 시스템은 꽤 강력합니다. Cloudflare나 DataDome을 쓰는 게 아니라, 10년 넘게 다듬어 온 nginx 기반 자체 레이트 리미터를 운영합니다. 잘못 건드리면 두 번째 커피를 마시기도 전에 바로 403이 뜹니다. 저는 Craigslist의 방어 체계를 상대로 여러 접근법을 테스트해 왔고, 그 결과가 이 가이드입니다. 2025년 기준으로 최신 내용을 반영한, 카테고리 구분 없이 쓸 수 있는 Python 튜토리얼로서, 오래된 글보다 가장 큰 개선점인 JSON-LD 추출 방식, 차단을 피하는 현실적인 전략, 법적 이슈, 그리고 코드를 한 줄도 쓰고 싶지 않은 분들을 위한 노코드 대안까지 모두 담았습니다.
Python으로 Craigslist를 스크래핑한다는 건 무슨 뜻일까?
Craigslist 웹 스크래핑은 Python 스크립트를 이용해 Craigslist 페이지에 프로그램적으로 접속한 뒤, 제목, 가격, 설명, 이미지, 위치, 게시 날짜 같은 필요한 구조화 데이터를 추출해 스프레드시트, 데이터베이스, 또는 JSON 파일로 저장하는 작업을 말합니다.
Python이 이 용도에 가장 많이 쓰이는 이유는 라이브러리 생태계가 매우 탄탄하기 때문입니다. requests, BeautifulSoup, lxml, curl_cffi만 잘 조합해도 100줄도 안 되는 코드로 쓸 만한 Craigslist 스크래퍼를 만들 수 있습니다. 커뮤니티도 워낙 커서, Craigslist가 무언가를 바꾸더라도(실제로 자주 바꿉니다) 이미 누군가 해결책을 찾아 둔 경우가 많습니다.
꼭 알아둘 점은 Craigslist가 입니다. 공식적인 프로그래밍 인터페이스는 Bulk Posting Interface(BAPI)뿐인데, 이것은 쓰기 전용입니다. 승인받은 유료 게시자만 글을 올릴 수 있고, 데이터를 가져오는 용도는 아닙니다. 외부 플랫폼에서 보이는 모든 “Craigslist API” 제품은 공식 엔드포인트가 아니라 비공식 스크래퍼입니다. 대량 데이터가 필요하다면 결국 스크래핑해야 합니다.
왜 Craigslist를 스크래핑할까? 실제 활용 사례
Craigslist는 중고 소파를 찾는 곳에 그치지 않습니다. 수십 개 분야에 걸쳐 계속 업데이트되는 거대한 데이터셋입니다. 실제로 스크래핑의 이점을 보는 사람들은 다음과 같습니다.
| 활용 사례 | 주요 사용자 | 추출 데이터 |
|---|---|---|
| 아파트 및 임대료 모니터링 | 부동산 중개인, 임차인, PropTech 기업 | 가격, 면적, 침실 수, 동네, 위도/경도 |
| 중고차 시장 분석 | 딜러십, 소비자 앱, 연구자 | 가격, 제조사, 모델, 연식, 주행거리, 상태 |
| 채용 시장 리서치 | 리크루터, 노동경제학자, 인력 분석가 | 제목, 보상, 고용 형태, 게시일 |
| 리드 생성 | 영업팀, 서비스 제공업체 | 연락처 정보, 업체명, 서비스 지역 |
| 경쟁 가격 조사 | 지역 서비스 업체, 이커머스 운영팀 | 서비스 가격, 설명, 서비스 범위 |
가장 자주 인용되는 학술 사례는 입니다. 약 50만 건의 미국 중고차 매물과 26개 변수를 담고 있어, 2024년 ResearchGate의 미국 중고차 시장 동향 연구를 포함한 수십 편의 논문의 기반이 되었습니다. 헤지펀드들은 임대료 추세 연구를 위해 Craigslist 임대 데이터를 집계 형태로 구매하기도 했고, 영업팀은 서비스 및 일감 카테고리를 리드 생성용으로 꾸준히 스크래핑합니다.
계산은 단순합니다. 손으로 복붙하는 데 8시간 vs. 잘 만든 스크래퍼로 약 10분.

Python으로 Craigslist 스크래핑하기: 자동차만이 아니다, 모든 카테고리
제가 본 대부분의 Craigslist 스크래핑 가이드는 중고차만 다룹니다. 마치 Google 튜토리얼인데 이미지 검색만 설명하는 것과 비슷하죠. Craigslist는 카테고리가 아주 많고, URL 패턴도 각기 다릅니다.
기본 구조는 항상 이렇습니다: https://{city}.craigslist.org/search/{category_slug}
도시 서브도메인과 슬러그만 바꾸면 완전히 다른 분야를 스크래핑하게 됩니다. 아래는 가장 많이 쓰이는 카테고리 참고표입니다(2025년 4월 검증).
| 카테고리 | URL 슬러그 | 주로 추출하는 필드 |
|---|---|---|
| 아파트 / 주거 | /search/apa | 가격, 면적, 침실 수, 위치, 반려동물 정책 |
| 자동차 & 트럭 | /search/cta | 가격, 제조사, 모델, 연식, 주행거리 |
| 채용 | /search/jjj | 제목, 회사, 급여, 고용 형태 |
| 서비스 | /search/bbb | 제목, 설명, 전화번호, 지역 |
| 일감 | /search/ggg | 제목, 보상, 날짜, 카테고리 |
| 판매(일반) | /search/sss | 제목, 가격, 상태, 위치 |
필터링용 쿼리 파라미터도 함께 쓸 수 있습니다.
| 파라미터 | 용도 | 예시 |
|---|---|---|
query | 전체 텍스트 키워드 | ?query=studio |
min_price / max_price | 가격 범위 | &min_price=1500&max_price=3000 |
hasPic | 이미지가 있는 게시물만 | &hasPic=1 |
postedToday | 최근 24시간 | &postedToday=1 |
sort | 정렬 방식 | &sort=priceasc |
s | 페이지네이션 오프셋(페이지당 120개) | ?s=120 |
즉, https://newyork.craigslist.org/search/apa?min_price=1500&max_price=3000&hasPic=1 같은 URL은 사진이 있는 뉴욕 아파트 매물 중 $1,500~$3,000 범위를 보여줍니다. 이 가이드의 Python 스크래퍼는 모두 이 모든 카테고리에서 동작하며, 슬러그만 바꾸면 됩니다.
2025 Craigslist HTML 셀렉터: 옛 방식 vs. 새 방식(그리고 JSON 지름길)
Craigslist 스크래퍼가 깨지는 가장 큰 이유는 HTML 구조가 바뀌기 때문입니다. 2022년 튜토리얼처럼 .result-row나 .result-info를 타깃으로 잡고 있다면, 이미 그 스크래퍼는 끝난 거나 다름없습니다.
Craigslist는 2023~2024년에 검색 결과 마크업을 다시 작성했습니다. 예전 클래스명은 새 래퍼 안에 들어가 있지만, DOM 최상위에서 그것만 노리면 빈 리스트가 나옵니다. 달라진 점은 아래와 같습니다.
| 요소 | 기존 셀렉터(2024년 이전) | 현재 셀렉터(2025) |
|---|---|---|
| 매물 컨테이너 | .result-info | .cl-search-result |
| 제목 링크 | .result-title | .posting-title a |
| 가격 | .result-price | .priceinfo |
| 메타데이터(지역) | .result-hood | .meta |
하지만 진짜 핵심은 따로 있습니다. 2025년 기준으로 제대로 된 스크래퍼라면 HTML을 아예 파싱할 필요가 없다는 점입니다.
Craigslist는 이제 보이는 모든 매물을 <script id="ld_searchpage_results"> 태그 안에 구조화된 JSON-LD 데이터로 심어 둡니다. requests.get() 한 번이면 페이지에 있는 모든 매물의 schema.org ItemList를 그대로 받을 수 있습니다. 제목, 가격, 통화, 위치, 이미지 URL, 상세 페이지 링크까지 전부 들어 있습니다. JavaScript 렌더링도 필요 없고, CSS 셀렉터가 깨질 걱정도 없습니다.
JSON-LD 방식은 더 빠르고, 더 안정적이며, Craigslist가 UI를 조금 바꿔도 깨질 가능성이 훨씬 낮습니다. 현재 활발히 유지보수되는 GitHub 저장소들이 이 방식을 쓰고 있고, 아래 튜토리얼도 이 방식을 사용합니다.
한 가지 예외는 있습니다. JSON-LD 블록은 , 즉 아파트(apa), 판매(sss), 자동차(cta), 주거(hhh)에서는 보통 존재합니다. 반면 채용(jjj), 일감(ggg), 커뮤니티(ccc), 서비스(bbb)는 schema.org/Offer 가격 정보가 없기 때문에 대체로 없거나 내용이 빈약합니다. 이런 경우에는 .cl-search-result 기반 HTML 경로로 돌아가면 됩니다.
Python 스택 고르기: Requests + BS4 vs. Selenium vs. Playwright
스크래핑 포럼에서 늘 나오는 질문이 있습니다. “어떤 라이브러리를 써야 하나요?” Craigslist에 한해서는 대부분의 사이트보다 답이 훨씬 명확합니다.
| 비교 항목 | requests + BeautifulSoup | Selenium | Playwright |
|---|---|---|---|
| 속도 | 초당 5~15페이지 (네트워크 병목) | 초당 0.3~1페이지 | 초당 0.5~2페이지 |
| JS 렌더링 콘텐츠 | 아니오 | 예 | 예 |
| 메모리 | 약 30~60MB | 약 400~700MB | 약 300~500MB |
| 설정 복잡도 | 낮음 | 중간 | 중간 |
| 안티봇 대응력 | 낮음(헤더/프록시 필요) | 중간(실제 브라우저) | 중간~높음 |
| Craigslist에 가장 적합한 용도 | 검색 결과(JSON-LD) | 동적 콘텐츠가 있는 상세 페이지 | 대규모 비동기 스크래핑 |
| 학습 난이도 | 초보자 친화적 | 보통 | 보통 |
Craigslist 페이지는 서버에서 렌더링됩니다. JSON-LD 블록도 초기 HTML 안에 들어 있습니다. 읽기 경로에는 JavaScript 챌린지가 없습니다. 활발히 유지되는 도 대부분 requests + BeautifulSoup 또는 Scrapy를 씁니다. Selenium이나 Playwright는 거의 쓰지 않습니다. 우연이 아닙니다. 브라우저 자동화 프레임워크는 수백 MB의 메모리와 10~100배의 속도 손실을 가져오면서, 오히려 더 눈에 띄는 지문만 남기고 실익은 거의 없습니다.
제 추천은 다음과 같습니다.
- requests + BS4: 여기서 시작하세요. JSON-LD 추출 방식과 궁합이 가장 좋고, Craigslist 스크래핑의 95%는 이것으로 충분합니다.
- Selenium: 특정 상세 페이지에서 동적 콘텐츠와 상호작용해야 할 때만 사용하세요(크레이그리스트에서는 드뭅니다).
- Playwright: 수천 페이지를 비동기 병렬 처리로 확장할 때 고려할 수 있지만, 솔직히 Craigslist의 병목은 라이브러리 성능이 아니라 레이트 리미터입니다.
더 자세한 비교가 필요하다면 와 정리 글도 따로 준비되어 있습니다.
노코드 대안: Python 없이 Craigslist 스크래핑하기
코드 설명에 들어가기 전에 잠깐 우회하겠습니다. 이 섹션은 개발자가 아닌 분들을 위한 것입니다. 부동산 중개인, 영업팀, 운영 관리자처럼 Python을 배우는 데 관심이 없고 데이터만 필요하다면 더 빠른 방법이 있습니다.
은 Chrome 확장 프로그램 형태로 동작하는 AI 웹 스크래퍼입니다. 코딩 없이 Craigslist를 약 두 번의 클릭만으로 스크래핑할 수 있습니다. 흐름은 다음과 같습니다.
- 아무 Craigslist 검색 결과 페이지로 이동합니다(아파트, 자동차, 채용 등 모든 카테고리 가능).
- Thunderbit 사이드바에서 **"AI Suggest Fields"**를 클릭합니다. AI가 페이지를 읽고 제목, 가격, 위치, 링크 같은 열을 자동으로 감지합니다.
- **"Scrape"**를 클릭하면 데이터가 몇 초 안에 추출됩니다.
- 서브페이지 스크래핑을 사용해 각 매물 상세 페이지를 방문하고, 전체 설명, 전화번호, 이미지, 속성 정보까지 보강합니다.
- Google Sheets, Excel, Airtable, Notion으로 바로 내보낼 수 있으며, 모두 무료입니다.
매일 아파트 가격을 모니터링하거나 매주 구인 공고를 수집하는 식의 반복 작업이라면, Thunderbit의 Scheduled Scraper를 이용해 일정을 자연어로 설명하기만 하면 자동으로 실행됩니다. cron job도 필요 없고, 서버 세팅도 필요 없습니다.
Thunderbit는 Cloud Scraping 모드를 통해 안티봇 대응도 알아서 처리하므로, 프록시를 돌리거나 헤더를 직접 꾸밀 걱정이 없습니다. 직접 써보고 싶다면 을 설치해 보세요.
직접 제어하고 커스터마이징하고 싶다면, 아래 Python 단계별 가이드를 계속 읽어보시면 됩니다.
단계별: Python으로 Craigslist를 스크래핑하는 방법(전체 튜토리얼)
- 난이도: 중급
- 소요 시간: 약 30분(설정 + 첫 스크래핑)
- 준비물: Python 3.8+, Chrome 브라우저(페이지 검사용), 터미널
1단계: Python 환경 설정하기
필요한 라이브러리를 설치합니다.
1pip install requests beautifulsoup4 lxml
lxml은 선택 사항이지만 BeautifulSoup 파싱 속도를 꽤 끌어올립니다. 이후 TLS 지문 문제를 만나면(차단 방지 섹션에서 자세히 설명) curl_cffi도 설치할 수 있습니다.
1pip install curl_cffi
임포트 코드는 다음과 같습니다.
1import requests
2from bs4 import BeautifulSoup
3import json
4import csv
5import time
6import random
이제 필요한 의존성이 모두 설치된 깨끗한 Python 환경이 준비되었습니다.
2단계: 어떤 카테고리든 Craigslist URL 만들기
도시 + 카테고리 슬러그 + 선택 필터를 이용해 목표 URL을 동적으로 구성합니다.
1from urllib.parse import urlencode
2BASE = "https://{city}.craigslist.org/search/{slug}"
3def build_url(city, slug, **params):
4 return f"{BASE.format(city=city, slug=slug)}?{urlencode(params)}"
5# 예시: 뉴욕 아파트, $1500~$3000, 사진 포함
6url = build_url("newyork", "apa", min_price=1500, max_price=3000, hasPic=1)
7print(url)
8# https://newyork.craigslist.org/search/apa?min_price=1500&max_price=3000&hasPic=1
"apa"를 "cta"(자동차), "jjj"(채용), "bbb"(서비스) 등 위 표의 다른 슬러그로 바꾸면 됩니다. "newyork"는 "sfbay", "chicago", "losangeles" 등으로 바꿀 수 있습니다.
3단계: 페이지를 가져와 내장 JSON 추출하기
적절한 헤더와 함께 GET 요청을 보내고, JSON-LD 블록을 파싱합니다.
1HEADERS = {
2 "User-Agent": (
3 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
4 "AppleWebKit/537.36 (KHTML, like Gecko) "
5 "Chrome/124.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.craigslist.org/",
11 "Sec-Fetch-Dest": "document",
12 "Sec-Fetch-Mode": "navigate",
13 "Sec-Fetch-Site": "same-origin",
14 "Upgrade-Insecure-Requests": "1",
15}
16session = requests.Session()
17r = session.get(url, headers=HEADERS, timeout=20)
18r.raise_for_status()
19soup = BeautifulSoup(r.text, "html.parser")
20tag = soup.select_one("script#ld_searchpage_results")
21data = json.loads(tag.text) if tag else {"itemListElement": []}
tag가 None이라면 해당 카테고리에는 JSON-LD 블록이 없는 것입니다. 이 경우 HTML 파싱으로 전환하세요(위 셀렉터 표 참고). 아파트, 자동차, 판매 카테고리는 대체로 JSON-LD 블록이 안정적으로 존재합니다.
4단계: 매물 데이터를 구조화된 레코드로 파싱하기
JSON 항목을 순회하면서 필요한 필드를 추출합니다.
1listings = []
2for entry in data["itemListElement"]:
3 item = entry["item"]
4 offers = item.get("offers", {}) or {}
5 addr = (offers.get("availableAtOrFrom") or {}).get("address", {})
6 listings.append({
7 "name": item.get("name"),
8 "url": offers.get("url"),
9 "price": offers.get("price"),
10 "currency": offers.get("priceCurrency"),
11 "locality": addr.get("addressLocality"),
12 "region": addr.get("addressRegion"),
13 "image": item.get("image"),
14 })
15print(f"{len(listings)}개의 매물을 찾았습니다")
예를 들어 "Found 120 listings" 같은 결과가 나와야 합니다(Craigslist는 페이지당 120개 결과를 보여줍니다). 일부 매물은 게시자가 가격을 입력하지 않았을 수 있으니, 후속 로직에서 None 값을 자연스럽게 처리하세요.
5단계: 상세 페이지를 스크래핑해 더 풍부한 데이터 얻기
검색 결과에는 요약 정보만 나옵니다. 전체 설명, 속성 정보(침실 수, 면적, 반려동물 정책), 위도/경도, 이미지까지 얻으려면 각 매물의 상세 URL에 들어가야 합니다.
1def fetch_detail(url, session):
2 r = session.get(url, headers=HEADERS, timeout=20)
3 r.raise_for_status()
4 s = BeautifulSoup(r.text, "html.parser")
5 body = s.select_one("#postingbody")
6 mp = s.select_one("#map")
7 return {
8 "description": body.get_text("\n", strip=True) if body else None,
9 "attributes": [x.get_text(" ", strip=True)
10 for x in s.select("p.attrgroup span, div.attrgroup .attr")],
11 "lat": mp.get("data-latitude") if mp else None,
12 "lng": mp.get("data-longitude") if mp else None,
13 "images": [img["src"] for img in s.select("div.gallery img")],
14 }
15for item in listings:
16 item.update(fetch_detail(item["url"], session))
17 time.sleep(random.uniform(3, 6)) # 중요: 안티봇용 지터
time.sleep(random.uniform(3, 6))는 선택이 아닙니다. 이걸 빼면 몇십 번 요청 안에 403을 맞기 쉽습니다. 상세 페이지는 #titletextonly, #postingbody, #map 같은 안정적인 셀렉터를 사용하며, Craigslist에서 드물게 믿을 만한 부분 중 하나입니다.
6단계: 페이지네이션으로 모든 결과 스크래핑하기
Craigslist는 페이지네이션에 ?s=120 오프셋 파라미터를 사용합니다. 페이지당 120개 결과가 나오며, 최대 오프셋은 보통 2999입니다.
1def iter_all(city, slug, max_pages=25, **filters):
2 for page in range(max_pages):
3 offset = page * 120
4 url = build_url(city, slug, s=offset, **filters)
5 r = session.get(url, headers=HEADERS, timeout=20)
6 r.raise_for_status()
7 soup = BeautifulSoup(r.text, "html.parser")
8 tag = soup.select_one("script#ld_searchpage_results")
9 if not tag:
10 break
11 data = json.loads(tag.text)
12 items = data.get("itemListElement", [])
13 if not items:
14 break
15 for entry in items:
16 item = entry["item"]
17 offers = item.get("offers", {}) or {}
18 yield {
19 "name": item.get("name"),
20 "url": offers.get("url"),
21 "price": offers.get("price"),
22 }
23 time.sleep(random.uniform(2.5, 5.0))
수천 페이지를 짧은 시간에 몰아서 스크래핑하려고 하지 마세요. Craigslist의 레이트 리미터는 IP 단위로 동작하며, 어떤 라이브러리를 쓰더라도 단일 IP의 지속 가능한 처리량은 대략 초당 0.3~0.5 요청 수준입니다. 이 한계는 Python이 아니라 Craigslist가 정한 것입니다.
7단계: Craigslist 데이터를 CSV, JSON 또는 Google Sheets로 내보내기
결과를 저장합니다.
1# CSV
2with open("craigslist.csv", "w", newline="", encoding="utf-8") as f:
3 w = csv.DictWriter(f, fieldnames=listings[0].keys())
4 w.writeheader()
5 w.writerows(listings)
6# JSON
7with open("craigslist.json", "w", encoding="utf-8") as f:
8 json.dump(listings, f, indent=2, ensure_ascii=False)
내보내기 코드를 아예 쓰지 않고 싶다면, Thunderbit는 브라우저에서 Google Sheets, Excel, Airtable, Notion으로 무료 내보내기를 지원합니다. 다만 Python 파이프라인에서는 CSV와 JSON이 표준 출력 형식입니다. pandas로 바로 넘겨 분석하거나 sqlite3로 데이터베이스에 저장할 수도 있습니다.
Python으로 Craigslist를 스크래핑할 때 차단을 피하는 방법
대부분의 튜토리얼은 이 부분을 대충 넘어갑니다. 하지만 Craigslist의 안티봇 시스템은 자체 제작된 것으로, 몇 가지 독특한 특징이 있습니다.

현실적인 요청 헤더를 사용하기
Craigslist는 헤더의 순서와 완성도를 검사합니다. Sec-Fetch-Dest가 빠졌거나 오래된 User-Agent를 쓰면, 콘텐츠에 닿기도 전에 차단될 수 있습니다. 위 3단계에서 보여준 Chrome 120+ 헤더 세트가 최소 기준입니다. 세션마다 최근 Chrome/Firefox 데스크톱 UA 5~10개 사이에서 돌려 쓰되, 세션 도중에 바꾸지는 마세요. 그렇게 하면 오히려 부자연스럽게 보입니다.
Sec-Fetch-* 헤더가 빠지는 것이 처음 스크래퍼를 만들 때 즉시 차단당하는 가장 흔한 이유입니다.
요청 사이에 랜덤 지연 추가하기
에서 수렴하는 커뮤니티의 권장치는 검색 결과 페이지 간에는 랜덤 2~5초, 상세 페이지 간에는 3~6초입니다. 일정한 간격은 봇처럼 보입니다. time.sleep(2)가 아니라 time.sleep(random.uniform(2, 5))를 쓰세요.
프록시 로테이션 사용하기(대규모 스크래핑일 때)
Craigslist는 AWS, GCP, Azure 전체 IP 대역을 선차단하는 경우가 많습니다. 데이터센터 프록시는 시작하자마자 막히는 일이 흔합니다. 수백 페이지를 넘게 스크래핑하려면 주거용 회전 프록시가 필요하고, 20~30 요청마다 바꿔야 합니다. 모바일 프록시는 탐지 위험이 가장 낮지만 비용이 GB당 $8~30 수준입니다.
| 프록시 유형 | Craigslist에서의 탐지 위험 | 비용(2025) |
|---|---|---|
| 데이터센터 | 매우 높음 — 첫 요청부터 막히는 경우가 많음 | $0.50~2/GB |
| 주거용 회전 | 낮음 — 권장 | $5~15/GB |
| 모바일 | 가장 낮음 | $8~30/GB |
Thunderbit의 Cloud Scraping 모드를 사용하면 프록시 로테이션을 자동으로 처리하므로, 직접 관리하고 싶지 않다면 이 방법도 좋습니다.
CAPTCHA는 부드럽게 처리하기
Craigslist의 읽기 경로에서 CAPTCHA는 드문 편이고, 주로 게시/응답 흐름에서 나타납니다. 만약 뜬다면 최소 60초 이상 멈추고, IP를 바꾸고, 쿠키를 지우고, 속도를 낮추세요. 계속 CAPTCHA가 보인다면 솔버로 억지로 푸는 문제가 아니라, 요청 속도가 너무 빠르다는 신호입니다.
레이트 리밋을 존중하고 백오프 구현하기
Craigslist는 레이트 리밋에 도달하면 429가 아니라 403을 돌려줍니다. 403은 현재 IP가 거부 목록에 올랐다는 뜻이므로, 무작정 재시도하면 안 됩니다. IP를 바꾸고, UA를 바꾸고, 기다리세요.
1from requests.adapters import HTTPAdapter
2from urllib3.util.retry import Retry
3retry = Retry(
4 total=5,
5 backoff_factor=1.5, # 1.5, 3, 6, 12, 24초
6 status_forcelist=[429, 500, 502, 503, 504],
7 allowed_methods={"GET"},
8 respect_retry_after_header=True,
9)
10adapter = HTTPAdapter(max_retries=retry)
11session.mount("https://", adapter)
또 하나의 팁: 커뮤니티 보고서들에 따르면 대상 도시의 현지 시간 기준 새벽 2~6시가 가장 안전한 스크래핑 시간대이며, 낮 시간보다 차단율이 약 30~40% 낮습니다.
숨은 함정, TLS 핑거프린팅
Craigslist의 봇 레이어는 TLS ClientHello를 검사합니다. Python의 requests 라이브러리(OpenSSL 기반)는 실제 브라우저와 일치하지 않는 JA3 지문을 남깁니다. 완벽한 User-Agent 헤더와 브라우저가 아닌 TLS 지문이 함께 있으면, 이 불일치 자체가 탐지될 수 있습니다. 해결책은 impersonate="chrome124"를 지원하는 입니다. Chrome의 TLS 핸드셰이크를 흉내 냅니다.
1from curl_cffi import requests as cffi_requests
2r = cffi_requests.get(url, headers=HEADERS, impersonate="chrome124")
주거용 IP를 쓰고 헤더도 제대로 넣었는데도 설명되지 않는 403이 계속 난다면, TLS 지문이 원인일 가능성이 매우 높습니다.
Craigslist robots.txt, 이용약관, 그리고 윤리적 스크래핑
대부분의 가이드는 이 부분을 완전히 건너뛰거나 FAQ에 한 줄만 넣습니다. 하지만 Craigslist는 스크래퍼 RadPad를 상대로 을 받아낸 적이 있으므로, 이 문제는 가볍게 볼 수 없습니다.
Craigslist robots.txt가 실제로 말하는 것
은 놀랄 만큼 짧습니다. User-agent: * 블록 하나와, 단 7개의 금지 경로만 있습니다.
1Disallow: /reply
2Disallow: /fb/
3Disallow: /suggest
4Disallow: /flag
5Disallow: /mf
6Disallow: /mailflag
7Disallow: /eaf
이 7개는 모두 상호작용/변경용 엔드포인트입니다. reply, flag, suggest, email-a-friend 같은 기능이죠. 매물 페이지(/search/..., 개별 게시물 URL)는 금지되어 있지 않습니다. Crawl-delay 지시문도 없지만, Craigslist는 IP 차단으로 사실상 이를 강제합니다.
도시 서브도메인에는 사이트맵도 게시되어 있습니다. 예를 들어 https://newyork.craigslist.org/sitemap/index.xml는 매물 페이지를 공식적으로 발견할 수 있는 경로입니다.
중요한 법적 선례
Craigslist v. 3Taps (2013, 2015년 합의): 3Taps는 Craigslist 매물을 스크래핑해 재판매했습니다. Craigslist가 중지 요청을 보내고 IP를 차단하자, 3Taps는 회전 프록시로 차단을 우회했습니다. 법원은 명시적 철회 이후 IP 차단을 우회하는 행위가 CFAA상 “without authorization”에 해당한다고 판단했습니다. 3Taps는 했습니다.
Meta v. Bright Data (2024): 더 최근 판결에서는 Meta의 이용약관이 로그아웃된 상태의 공개 데이터 스크래핑을 금지할 수 없다고 보았습니다. 법원은 로그아웃 상태의 스크래퍼를 “방문자와 같은 지위”라고 판단했습니다. 2024~2025년 스크래퍼에게 가장 중요한 판결입니다. Craigslist 계정을 만들지 않고, 로그인하지 않으며, 공개적으로 보이는 페이지만 접근한다면, 이용약관을 계약 위반으로 적용하기 어려울 수 있습니다.
실무상 요점: Van Buren(2021)과 hiQ v. LinkedIn(2022) 이후, 공개적으로 접근 가능한 페이지에 대해서는 CFAA 리스크가 상당히 줄었습니다. 하지만 주법상 불법행위 청구(trespass-to-chattels, misappropriation)는 여전히 살아 있습니다. 3Taps 합의와 $6,050만 RadPad 판결도 바로 이 지점에서 나왔습니다.
이 내용은 법률 자문이 아니라 정보 제공 목적입니다. Craigslist를 상업적으로 스크래핑하려면 변호사와 상담하세요.
실무용 윤리적 스크래핑 체크리스트
- ✅ robots.txt의 모든
Disallow를 지키기 — 특히 7개의 액션 엔드포인트 - ✅ IP당 24시간에 1,000페이지를 넘기지 않기(Craigslist 이용약관은 그 이상에 대해 페이지당 을 명시)
- ✅ 로그아웃 상태 유지 — 스크래핑용으로 Craigslist 계정을 만들지 않기
- ✅ 명시적 차단 이후 프록시로 IP 금지를 우회하지 않기(3Taps가 이 때문에 무너졌습니다)
- ✅ 요청 사이에 지연 추가 — 최소 2~5초
- ✅ 스팸 목적으로 개인 연락처 정보를 수집하지 않기
- ✅ 원본 Craigslist 데이터를 재배포하거나 내 플랫폼인 것처럼 보여주지 않기
- ✅ 합법적인 리서치, 분석, 개인적 용도로만 사용하기
- ✅ 가능하면 무작정 크롤링보다 공개 사이트맵 우선 활용하기
- ✅ 저장 전에 PII(이메일, 전화번호)를 제거하기
에 대해 더 깊이 다룬 가이드도 있습니다.
Python vs. 노코드: 어떤 방법이 나에게 맞을까?
| 비교 항목 | Python (requests + BS4) | Thunderbit (노코드) |
|---|---|---|
| 준비 시간 | 30~60분(설치, 코드 작성) | 2분(Chrome 확장 설치) |
| 필요한 기술 수준 | 중급 Python | 없음 |
| 커스터마이징 | 로직, 필드, 흐름을 완전히 제어 가능 | AI가 필드를 자동 감지, 사용자가 조정 가능 |
| 규모 확장 | 무제한(프록시, 스케줄링 활용) | 반복 작업용 Scheduled Scraper |
| 차단 대응 | 수동 설정(헤더, 지연, 프록시, TLS) | 내장(Cloud Scraping) |
| 내보내기 옵션 | CSV, JSON(직접 구현) | Google Sheets, Excel, Airtable, Notion — 무료 |
| 적합한 사용자 | 개발자, 데이터 과학자, 커스텀 파이프라인 | 영업팀, 부동산 중개인, 운영 관리자 |
Python은 완전한 커스터마이징이 필요하거나, 더 큰 데이터 파이프라인과 연동할 계획이거나, 내부 동작을 정확히 이해하고 싶을 때 적합합니다. 반대로 코드를 쓰거나 관리하지 않고 빠르게 결과가 필요하다면 이 더 낫습니다. 둘 다 유효한 선택입니다. 결국 사용 사례와, 터미널에서 시간을 보낼지 브라우저에서 시간을 보낼지에 달려 있습니다.
마무리
Craigslist는 주거, 자동차, 채용, 서비스, 일감 등 여러 분야를 아우르는 풍부하고 계속 업데이트되는 데이터 소스입니다. 공개 API가 없기 때문에 구조화 데이터를 대규모로 얻는 방법은 결국 스크래핑뿐입니다. 2025년 기준 실제로 잘 되는 방법은 검색 결과에서 내장된 JSON-LD를 추출하고(깨지기 쉬운 CSS 셀렉터 대신), Selenium이 아니라 requests + BeautifulSoup을 사용하며, Sec-Fetch-*를 포함한 현실적인 헤더를 넣고, 지연 시간을 랜덤화하고, 수백 페이지를 넘게 스크래핑할 때만 주거용 프록시를 사용하는 것입니다.
JSON-LD 방식은 오래된 가이드보다 가장 큰 개선점입니다. 더 빠르고, 레이아웃 변경에 강하며, JavaScript 렌더링이 전혀 필요 없습니다. 위의 안티차단 전략과 함께 쓰면 대부분의 스크래퍼가 겪는 403 문제를 피할 수 있습니다.
코드를 아예 건너뛰고 싶다면, 을 사용해 어떤 Craigslist 카테고리든 몇 번의 클릭만으로 스크래핑하고 원하는 스프레드시트나 데이터베이스로 바로 내보낼 수 있습니다. 더 깊이 배우고 싶다면 과 가이드를 참고해 보세요.
자주 묻는 질문
Craigslist를 스크래핑하는 건 합법인가요?
Craigslist 이용약관은 자동 스크래핑을 금지하고 있으며, 일일 1,000페이지를 넘기면 페이지당 $0.25의 손해배상 조항이 포함되어 있습니다. 다만 최근 판결, 특히 Meta v. Bright Data(2024)와 hiQ v. LinkedIn(2022)은 로그아웃 상태에서 공개 데이터를 스크래핑하는 행위에 대한 CFAA 책임 범위를 좁혔습니다. 주법상 불법행위 청구(trespass-to-chattels)는 여전히 위험 요소이며, 특히 상업적 재배포의 경우 더 그렇습니다. robots.txt를 지키고, 로그아웃 상태를 유지하고, 지연을 두고, 원본 데이터를 재배포하지 마세요. 이는 일반 정보이며 법률 자문이 아닙니다.
Craigslist에 공개 API가 있나요?
아니요. Craigslist는 승인된 유료 게시자를 위한 쓰기 전용 Bulk Posting Interface(BAPI)만 제공합니다. 공개 읽기 API도, 개발자 포털도, 데이터 조회용 레이트 리미트 계층도 없습니다. 외부 플랫폼에서 보이는 모든 “Craigslist API”는 비공식 스크래퍼입니다.
왜 Craigslist 스크래퍼가 계속 깨지나요?
대부분 HTML 구조 변경 때문입니다. Craigslist는 2023~2024년에 검색 결과 마크업을 다시 작성했고, .result-row나 .result-info 같은 옛 셀렉터를 쓰는 가이드는 더 이상 작동하지 않습니다. 훨씬 견고한 방법으로는 내장 JSON-LD를 파싱하는 방식(script#ld_searchpage_results)이 있습니다. 또한 헤더에 Sec-Fetch-* 필드가 포함되어 있는지 확인하세요. 이게 빠지면 즉시 차단될 수 있습니다.
Python 없이 Craigslist를 스크래핑할 수 있나요?
네. Thunderbit의 AI 웹 스크래퍼 Chrome 확장 프로그램은 아파트, 자동차, 채용, 서비스 등 어떤 Craigslist 페이지에서도 작동합니다. "AI Suggest Fields"를 클릭해 열을 자동 감지하고, "Scrape"를 눌러 데이터를 추출한 뒤, Google Sheets, Excel, Airtable, Notion으로 무료 내보내기 할 수 있습니다. 코딩도, 설정도, 프록시 관리도 필요 없습니다.
차단당하지 않고 얼마나 자주 Craigslist를 스크래핑할 수 있나요?
단일 주거용 IP 기준으로는 페이지 사이에 랜덤 2~5초 지연을 두었을 때 지속 가능한 처리량이 대략 초당 0.3~0.5 요청입니다. 차단과 Craigslist 이용약관의 손해배상 기준을 피하려면 IP당 24시간에 1,000페이지를 넘기지 마세요. 대상 도시 현지 시간 기준 새벽 2~6시처럼 비혼잡 시간대에 스크래핑하면 차단율이 약 30~40% 낮습니다. 더 많은 양이 필요하다면 20~30 요청마다 주거용 프록시를 교체하세요.
더 알아보기
