파이썬 requests 모듈 크롤링, 200인데 안 나오는 이유

status_code는 분명히 200입니다. 그런데 화면에는 아무것도 없습니다.

크롤링 입문에서 가장 많이 멈추는 지점이 바로 여기입니다. 코드는 맞는데 데이터가 안 나옵니다.

이 글은 그 “안 나오는 이유”를 7가지로 나눠서 하나씩 풀어 갑니다. 설치부터 법적 리스크까지, 실제로 막히는 순서대로 짚어 보겠습니다.

requests 모듈, 왜 크롤링 표준으로 불리나

파이썬 requests 모듈은 HTTP/1.1 요청을 보내는 라이브러리입니다. GET·POST·PUT·DELETE를 지원하고 세션, 쿠키 유지, SSL 검증, 타임아웃을 기본으로 제공합니다.

규모가 인상적입니다. PyPI 공식 페이지 기준으로 주당 약 3억 회 다운로드되고, 400만 개 이상의 GitHub 저장소가 이 패키지에 의존한다고 표기돼 있습니다. 웹 통신·크롤링에서 사실상 표준에 가깝습니다. (출처: PyPI)

버전은 자주 바뀝니다. 이 글 기준 최신 안정 버전은 2.34.2이고 Python 3.10 이상을 공식 지원합니다. 다만 보안 패치와 지원 버전이 바뀔 수 있으니, 작업 시점에 PyPI를 한 번 더 확인하는 편이 안전합니다.

표준이라는 건, 다르게 말하면 막히는 패턴도 거의 정해져 있다는 뜻입니다.

파이썬 requests 모듈 설치와 첫 요청

먼저 파이썬 3.10 이상이 깔려 있는지 확인합니다. 그다음 설치는 한 줄입니다.

파이썬 requests 모듈 예시代码如下:

“`bash

pip install requests

“`

HTML 파싱까지 하려면 BeautifulSoup도 함께 설치해 두면 편합니다.

“`bash

pip install beautifulsoup4

“`

첫 요청은 이렇게 보냅니다.

“`python

import requests

response = requests.get(“https://example.com”)

print(response.status_code) # 200이면 응답 도착

print(response.text[:200]) # 본문 앞부분 확인

“`

여기까지는 거의 누구나 성공합니다. 진짜 문제는 그다음입니다. text를 찍었는데 원하던 내용이 없을 때부터 본격적인 크롤링이 시작됩니다.

status_code 200인데 화면이 비어 있다면

응답은 정상인데 내용이 없는 경우는 크게 두 가지입니다.

첫째, 서버가 나를 봇으로 판단한 경우입니다. 브라우저로 보면 멀쩡한 페이지가 requests로는 로그인 안내나 빈 HTML만 내려옵니다. 헤더를 안 보냈기 때문입니다. 저도 처음엔 이걸 몰라서 한참 헤맸습니다. User-Agent를 넣자마자 같은 주소에서 정상 HTML이 내려왔습니다.

“`python

headers = {

“User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) “

“AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36”

}

response = requests.get(url, headers=headers)

“`

둘째, 데이터가 자바스크립트로 나중에 채워지는 경우입니다. React·Vue 같은 SPA는 처음 HTML이 거의 비어 있고, 화면은 JS가 그립니다. requests는 JS를 실행하지 않으니 빈 골격만 받습니다.

이때 흔한 오해 하나. “그럼 무조건 Selenium을 써야 한다”고 넘어가는 분이 많은데, 꼭 그렇지 않습니다. 브라우저 개발자 도구 Network 탭을 열어 보면, 화면 데이터가 사실은 별도 API에서 JSON으로 내려오는 경우가 많습니다. 그 API 주소를 직접 requests로 부르면 됩니다.

증상진짜 원인해결
200인데 로그인 페이지헤더 없음(봇 판정)User-Agent·쿠키 추가
200인데 빈 골격JS 렌더링 데이터Network 탭에서 API 찾기
일부만 보임세션 필요requests.Session() 사용

화면이 비었다고 코드가 틀린 게 아닙니다. 서버가 무엇을 보고 무엇을 내려 주는지를 먼저 봐야 합니다.

한글이 깨질 때 — 인코딩 문제

응답은 200, 내용도 있는데 한글이 �� 처럼 깨지는 경우가 있습니다. response.encoding이 실제 인코딩과 다르게 추정됐기 때문입니다.

오래된 한국 사이트는 euc-kr이나 cp949를 쓰는데, requests가 이를 잘못 잡으면 다 깨집니다. 저도 옛날 게시판을 긁다가 제목이 전부 깨져서, 처음엔 사이트가 막은 줄 알았습니다. 원인은 인코딩이었습니다.

“`python

response = requests.get(url)

response.encoding = response.apparent_encoding # 실제 인코딩 추정값으로 교정

print(response.text)

“`

여기서 .text.content의 차이를 알아 두면 편합니다.

  • .text : 문자열(str). 인코딩을 거쳐 디코딩한 결과
  • .content : 바이트(bytes). 원본 그대로

이미지·PDF 같은 바이너리는 .content로 받아야 하고, 텍스트는 .text를 쓰되 인코딩을 맞춰 줘야 합니다. 무조건 .text만 쓰는 습관이 깨짐의 원인일 때가 많습니다.

파이썬 requests 모듈 사용법

GET·POST와 params·payload 헷갈릴 때

브라우저에서는 검색·필터가 잘 되는데, requests로 같은 주소를 부르면 기본 리스트만 나옵니다. 요청 방식이나 파라미터 위치가 틀린 경우입니다.

GET 요청의 쿼리스트링은 params로 넣습니다. 주소 뒤 ?keyword=파이썬&page=2를 직접 붙이지 말고 딕셔너리로 전달합니다.

“`python

params = {“keyword”: “파이썬”, “page”: 2}

response = requests.get(url, params=params)

“`

POST 요청은 본문 데이터를 data 또는 json으로 보냅니다. 서버가 폼 데이터를 받으면 data=, JSON을 받으면 json=을 씁니다.

“`python

requests.post(url, data={“id”: “user”, “pw”: “1234”})

requests.post(url, json={“id”: “user”, “pw”: “1234”})

“`

GET인지 POST인지, data인지 json인지는 추측하지 않습니다. Network 탭에서 실제 요청의 Method와 Payload 형식을 그대로 확인해 옮기는 게 정답입니다.

403·429가 뜨기 시작하면 — 차단 대응

처음엔 잘 되다가 어느 순간부터 403(거부)이나 429(요청 과다)가 뜹니다. 짧은 시간에 너무 많이 두드린 겁니다.

여기서 대부분의 입문 글은 time.sleep 하나만 언급하고 끝납니다. 실전에서는 몇 가지를 같이 챙겨야 안정적입니다.

  • 요청 간격 : time.sleep에 약간의 랜덤 딜레이를 섞어 일정 패턴을 피합니다.
  • 세션 재사용 : requests.Session()으로 TCP 커넥션을 재활용해 부담을 줄입니다.
  • 타임아웃 : timeout=을 항상 걸어 멈춤을 방지합니다.
  • 예외 처리 : try/except로 일시 오류에 재시도 여지를 둡니다.

“`python

import requests, time, random

session = requests.Session()

session.headers.update(headers)

for page in range(1, 11):

try:

r = session.get(url, params={“page”: page}, timeout=10)

r.raise_for_status()

except requests.RequestException as e:

print(“요청 실패:”, e)

time.sleep(random.uniform(1.0, 2.5)) # 1~2.5초 랜덤 대기

“`

차단은 속도 욕심에서 옵니다. 빠르게 한 번 막히는 것보다, 느려도 끝까지 도는 쪽이 결국 빠릅니다.

👉 정확한 메서드·파라미터·세션 사용법은 Requests 공식 문서에서 확인하세요.

requests vs Selenium, 언제 갈아타야 하나

둘을 나란히 소개하는 자료는 많지만, “언제 넘어가야 하는지” 기준은 의외로 흐릿합니다. 판단 기준을 정리하면 이렇습니다.

상황권장 도구
정적 HTML, API로 JSON이 내려옴requests
헤더·세션만 맞추면 데이터가 보임requests
JS 렌더링 후에야 데이터가 채워짐API 먼저 탐색, 안 되면 Selenium
무한 스크롤·클릭 후 로딩Selenium / Playwright
캡차·Cloudflare 봇 차단브라우저 자동화(그래도 어려움)

핵심 순서는 이렇습니다. requests로 시도 → Network 탭에서 API 찾기 → 그래도 안 되면 Selenium. API로 JSON을 직접 받으면 더 빠르고, 유지보수도 쉽습니다. HTML 구조가 바뀌어도 API는 잘 안 바뀌기 때문입니다.

처음부터 Selenium으로 가면 느리고 무겁습니다. 갈아타는 건 requests가 정말 안 될 때입니다.

합법적으로 크롤링하려면 — 법적 체크리스트

기술보다 더 자주 묻는 게 “이거 해도 되나요?”입니다. 양 극단의 오해가 같이 돌아다닙니다. “공개 페이지는 무조건 괜찮다”와 “크롤링은 무조건 위험하다”, 둘 다 정확하지 않습니다.

기준점이 되는 판례가 있습니다. 대법원 2021도1533 판결에서, 누구에게나 공개된 숙박업소 정보(가격·위치 등)를 크롤링한 행위에 대해, 서버 본래 목적대로 정보를 전송받은 것일 뿐 “허위 정보 또는 부정한 명령 입력”이 아니라고 보아 형법상 업무방해죄 성립을 부정했습니다. 같은 판결에서, 수집·갱신에 상당한 투자 증거가 부족하다며 데이터베이스 저작권도 인정하지 않았습니다. (출처: 영남일보 기업법률가이드 – 대법원 2021도1533 판결 요약)

그렇다고 다 된다는 뜻은 아닙니다. 실무에서 검토하는 기준은 대략 다음과 같습니다.

  • 공개 vs 비공개 : 로그인·암호화로 보호된 영역까지 토큰·쿠키를 악용해 접근하면 정보통신망 침입 논의 대상이 될 수 있습니다(정보통신망법 제48조 등).
  • 서버 부하 : 과도한 반복 요청으로 서버 기능을 현저히 저해하면 형법상 업무방해죄(제314조)가 문제 될 수 있습니다.
  • DB 저작권 : 수집·검증·갱신에 상당한 투자가 들어간 데이터베이스(예: 채용·위키 데이터)를 무단·반복 수집하면 저작권법 위반 소지가 있습니다.
  • 개인정보 : 이름·주민등록번호 등 특정인을 알아볼 수 있는 정보를 동의 없이 수집하면 개인정보 보호법 제15조 위반으로 처벌 대상이 될 수 있습니다.
  • 약관·robots.txt : 법적 강제력 자체는 약하지만, 이를 무시하며 부하까지 유발하면 위법 판단에 불리하게 작용할 수 있습니다.

법령·판례 원문은 국가법령정보센터와 대법원 종합법률정보에서 직접 확인하는 게 가장 정확합니다. ⚠️ 개별 사안은 사실관계에 따라 결론이 달라질 수 있으니, 분쟁 소지가 있다면 전문가 검토를 권합니다.

실전 크롤러 뼈대 — 한 번 짜 두면 재사용

마지막으로 위 내용을 합친 최소 뼈대입니다. 헤더·세션·인코딩·예외·간격을 한 번에 챙긴 형태입니다.

“`python

import requests, time, random

from bs4 import BeautifulSoup

HEADERS = {

“User-Agent”: “Mozilla/5.0 … Chrome/124.0 Safari/537.36”

}

def fetch(url, params=None):

session = requests.Session()

session.headers.update(HEADERS)

try:

r = session.get(url, params=params, timeout=10)

r.raise_for_status()

r.encoding = r.apparent_encoding # 인코딩 교정

return r.text

except requests.RequestException as e:

print(“요청 실패:”, e)

return None

def parse(html):

soup = BeautifulSoup(html, “html.parser”)

return [a.get_text(strip=True) for a in soup.select(“a.title”)]

for page in range(1, 6):

html = fetch(“https://example.com/list”, params={“page”: page})

if html:

for title in parse(html):

print(title)

time.sleep(random.uniform(1.0, 2.5))

“`

함수를 나눠 두면 사이트가 바뀌어도 parse만 손보면 됩니다. 입문에서 한 번에 긁는 코드만 익히면, 두 번째 사이트에서 또 처음부터 헤맵니다. 처음부터 이렇게 구조를 잡아 두는 게 결국 가장 빠른 길입니다.

이 글은 2026년 6월 기준 정보로 작성했습니다. 라이브러리 버전과 법적 해석은 바뀔 수 있으니, 핵심 사실은 공식 출처에서 한 번 더 확인하시길 권합니다.

자주 묻는 질문

requests로 status_code가 200인데 데이터가 안 보입니다. 왜 그런가요?

두 가지가 가장 흔합니다. 헤더(User-Agent)를 안 보내 서버가 봇으로 판단했거나, 데이터가 자바스크립트로 나중에 채워지는 동적 페이지인 경우입니다. 후자는 Network 탭에서 데이터가 내려오는 API 주소를 찾아 requests로 직접 호출하면 해결되는 경우가 많습니다.

공개된 웹페이지는 크롤링해도 무조건 합법인가요?

아닙니다. 대법원 2021도1533 판결로 공개 데이터 크롤링에 다소 우호적인 흐름이 생겼지만, 서버에 과도한 부하를 주거나, 투자가 큰 데이터베이스를 무단 수집하거나, 개인정보를 동의 없이 모으면 업무방해·저작권·개인정보 보호법 위반이 될 수 있습니다. 공개 여부 하나만으로 합법이 결정되지 않습니다.

requests와 Selenium 중 무엇을 써야 하나요?

먼저 requests로 시도하고, Network 탭에서 JSON API를 찾아보는 게 순서입니다. 정적 페이지나 API 호출로 데이터를 받을 수 있으면 requests가 빠르고 가볍습니다. 무한 스크롤, 클릭 후 로딩, 캡차처럼 브라우저 동작이 꼭 필요할 때만 Selenium이나 Playwright로 넘어가면 됩니다.

함께 보면 좋은 글

Similar Posts

답글 남기기