몇 줄이면 끝나는 파이썬 인터넷 데이터 가져오기
실제로 돌아가는 파이썬 프로그램을 만들다 보면 결국 네트워크 너머의 무언가와 통신해야 할 일이 생깁니다. REST API, 날씨 서비스, GitHub 엔드포인트, 파일 다운로드 같은 것들이죠. 표준 라이브러리의 urllib로도 할 수는 있지만, 파이썬 생태계에서 사실상 표준처럼 쓰이는 도구는 서드파티 라이브러리인 requests입니다. API가 훨씬 친절해서 pip install 한 번 할 가치는 충분합니다.
pip install requests
아직 가상 환경에서 작업하고 있지 않다면, 먼저 가상 환경부터 만들어 두자. 이렇게 하면 지금 설치하는 패키지들이 현재 프로젝트 안에만 적용되어 깔끔하게 관리된다.
첫 GET 요청 보내기
세 줄이면 끝납니다. get을 호출하고, 상태 코드를 확인한 뒤, 본문을 살펴보는 거죠. response.text는 응답 본문을 문자열로 돌려줍니다. 전체 JSON이 너무 길어서 여기서는 잘라서 보여드렸어요.
치트시트 수준에서 알아두면 좋은 상태 코드는 다음과 같습니다:
- 200 — OK, 정상 처리됐다는 뜻.
- 201 — Created (POST 요청에서 자주 보게 됩니다).
- 301 / 302 — 리다이렉트.
requests는 기본값으로 자동으로 따라갑니다. - 400 — 잘못된 요청. 보낸 내용에 문제가 있다는 신호입니다.
- 401 / 403 — 인증되지 않음 / 권한 없음.
- 404 — 해당 리소스가 존재하지 않음.
- 429 — 요청 제한에 걸림. 속도를 늦춰야 합니다.
- 500 — 서버 오류.
JSON 응답 파싱하기
엔드포인트가 JSON을 반환한다면, 응답 객체에 .json()을 호출하면 됩니다. 본문을 파싱해서 dict(또는 list)로 바로 넘겨줘요:
내부적으로 .json()은 json.loads(response.text)와 동일합니다. 자주 쓰는 패턴이다 보니 편하게 쓸 수 있도록 단축 메서드로 제공되는 것뿐이죠.
쿼리 파라미터 전달하기
URL 뒤에 ?key=value&... 형식으로 직접 이어 붙이지 마세요. 대신 딕셔너리를 만들어 params=로 넘기면 됩니다:
requests는 URL 인코딩을 알아서 처리해 줍니다. 공백이든, 특수 문자든, 유니코드든 안전하게 넘어갑니다.
실제로 어떤 URL로 요청이 나갔는지는 response.url에서 확인할 수 있는데, 디버깅할 때 꽤 유용합니다.
JSON 본문으로 POST 요청 보내기
데이터를 전송할 때, 즉 리소스를 생성하거나 폼을 제출하거나 상태를 변경하는 엔드포인트를 호출할 때는 requests.post를 사용합니다.
import requests
payload = {
"title": "Docs update",
"body": "Added HTTP requests page.",
"labels": ["docs"],
}
response = requests.post(
"https://api.example.com/issues",
json=payload,
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
print(response.status_code)
print(response.json())
json= 인자는 두 가지 일을 한꺼번에 해줍니다. payload를 JSON으로 직렬화하고, Content-Type: application/json 헤더까지 자동으로 붙여줍니다. 물론 data=json.dumps(payload)에 헤더를 직접 지정해서 똑같이 만들 수도 있지만, 그냥 json=을 쓰는 게 파이썬다운 지름길입니다.
일반 HTML 폼에서 보내는 것과 같은 폼 인코딩 데이터를 전송할 때는 data=를 대신 사용하면 됩니다:
requests.post("https://example.com/login", data={"user": "rosa", "password": "..."})
헤더 설정
커스텀 헤더를 추가하고 싶다면 딕셔너리 형태로 넘겨주면 됩니다:
import requests
response = requests.get(
"https://api.example.com/profile",
headers={
"Authorization": "Bearer abc123",
"User-Agent": "my-tool/1.0",
},
)
대부분의 API는 인증을 위해 Authorization 헤더를 요구합니다. 정확한 방식(Bearer, Basic, Token 등)은 해당 API 문서에 나와 있습니다.
timeout은 선택이 아닌 필수
requests는 기본적으로 응답이 올 때까지 무한정 기다립니다. 실제 서비스에서는 서버가 한 번만 삐끗해도 스크립트가 통째로 멈춰버리는 원인이 되죠. 그러니 timeout은 반드시 지정합시다:
import requests
try:
response = requests.get("https://api.example.com/slow", timeout=5)
except requests.Timeout:
print("Server took too long.")
숫자의 단위는 초입니다. timeout=5는 "5초 안에 응답이 안 오면 그냥 포기해라"라는 뜻이죠. 더 세밀하게 제어하고 싶다면 (connect_timeout, read_timeout) 형태의 튜플을 넘길 수도 있습니다.
에러 처리
문제가 발생하는 경우는 크게 두 가지입니다.
- HTTP 레벨 에러 (4xx, 5xx) — 서버가 응답은 했는데 그 내용이 에러인 경우입니다.
response.status_code로 확인할 수 있습니다. - 네트워크 레벨 문제 — 타임아웃, DNS 실패, 서버 접근 불가 같은 상황이죠. 이 경우엔 예외가 발생합니다.
보통은 두 가지를 함께 처리하는 방식으로 많이 씁니다.
raise_for_status()는 2xx 응답이면 아무 일도 하지 않고, 그 외의 경우에는 예외를 던집니다. RequestException은 requests가 발생시키는 모든 예외의 최상위 클래스라서, "이 엔드포인트와 통신하다 뭔가 잘못됐을 때"를 한 번에 잡아낼 수 있습니다.
파일 다운로드하기
용량이 큰 바이너리 파일을 받을 때는 응답을 스트리밍 방식으로 처리해야 메모리에 통째로 올라가지 않습니다:
import requests
url = "https://example.com/large.zip"
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open("large.zip", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
stream=True를 지정하면 requests가 응답 본문을 미리 전부 읽어오지 않습니다. 이어서 iter_content(chunk_size=...)가 본문을 청크 단위로 하나씩 넘겨주고, 이를 그대로 디스크에 기록하면 됩니다.
세션(Session): 커넥션 재사용과 기본값 설정
같은 서버에 여러 번 요청을 보내야 한다면 Session을 쓰는 게 정답입니다. 내부적으로 TCP 커넥션을 재사용하기 때문에 속도가 빨라지고, 공통으로 쓰는 설정을 한 번만 지정해두면 됩니다:
import requests
session = requests.Session()
session.headers.update({"Authorization": "Bearer abc123"})
# Every request through this session carries the header.
a = session.get("https://api.example.com/users/1")
b = session.get("https://api.example.com/users/2")
c = session.post("https://api.example.com/users", json={"name": "Rosa"})
같은 API를 수십 번 호출하는 스크립트라면 세션을 쓰는 것만으로도 체감될 만큼 빨라집니다.
실전 예제: 간단한 GitHub 클라이언트
특정 저장소의 최신 릴리스를 가져오는 코드입니다:
import requests
def latest_release(owner, repo):
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return {
"tag": data["tag_name"],
"name": data["name"],
"published": data["published_at"],
"url": data["html_url"],
}
release = latest_release("python", "cpython")
print(release)
15줄 남짓이면 끝납니다. URL 조립하고, 요청 보내고, 에러 체크하고, 필요한 필드만 뽑아내기. 여러분이 앞으로 작성할 API 클라이언트 대부분이 바로 이 틀에서 벗어나지 않습니다.
urllib은 어떨까?
파이썬 표준 라이브러리의 urllib.request로도 requests가 하는 일은 다 할 수 있습니다. 다만 코드가 길어지고 쓰기도 훨씬 번거롭죠. 외부 패키지를 도저히 못 붙이는 상황이라면 이런 대안도 있습니다.
import json
import urllib.request
with urllib.request.urlopen("https://api.github.com/repos/python/cpython") as r:
data = json.loads(r.read().decode("utf-8"))
print(data["name"])
간단한 스크립트 수준을 넘어서는 작업이라면, requests(비동기가 필요하다면 httpx)는 설치해 둘 만한 값어치를 충분히 합니다.
몇 가지 습관
- 타임아웃은 무조건 설정하세요. 예외 없습니다.
- 성공을 전제로 하는 스크립트라면
raise_for_status()를 활용하세요. 잘못된 응답을 요란한 예외로 바꿔 줍니다. params=와json=에는 dict를 넘기세요. 직접 문자열을 만들어 넣지 마세요.- 같은 API를 여러 번 호출한다면 관련 요청들을
Session으로 묶어서 처리하세요. - 디버깅할 때는
response.status_code와response.text를 로그로 남기세요. 서버가 뭘 못마땅해했는지는 보통 응답 본문이 알려 줍니다.
다음: 날짜와 시간
requests를 손에 익히고 나면 웬만한 최신 웹 API와 대화하고, 파일을 내려받고, 서비스 간의 작은 연동을 직접 만들 수 있습니다. 바로 앞의 JSON과 CSV 페이지까지 더하면, "데이터를 가져오고 → 읽고 → 뭔가 처리해서 → 다시 내보내는" 전체 흐름이 완성됩니다. 실제로 돌아가는 수많은 파이썬 스크립트의 전형적인 모습이죠. 그런데 그 데이터 대부분에는 타임스탬프가 붙어 있습니다. 다음 페이지에서는 파이썬이 날짜와 시간을 어떻게 다루는지, 그리고 피해야 할 타임존 함정들을 살펴봅니다.
자주 묻는 질문
파이썬에서 HTTP 요청은 어떻게 보내나요?
먼저 pip install requests로 requests 라이브러리를 설치한 다음, GET 요청은 requests.get(url), POST 요청은 requests.post(url, json=...)처럼 호출하면 됩니다. 응답 객체에서 .status_code, .text, .json(), .headers를 꺼내 쓸 수 있어요. 예: r = requests.get('https://api.example.com/users/1').
requests랑 urllib 중에 뭘 써야 하나요?
직접 코드를 짜서 요청을 보낼 거라면 requests가 답입니다. API가 훨씬 직관적이거든요. urllib는 표준 라이브러리라 외부 의존성을 추가할 수 없는 환경에서는 쓸 만하지만, JSON 바디를 보내거나 세션을 관리하는 일에 코드가 많이 필요합니다. 실무에서는 비동기까지 지원하는 httpx(requests와 API가 거의 호환됨)를 선택하는 팀도 많습니다.
POST 요청에 JSON은 어떻게 담아서 보내나요?
requests.post(...)에 json={'key': 'value'} 형태로 넘기면 됩니다. requests가 알아서 dict를 JSON 문자열로 직렬화하고 Content-Type: application/json 헤더도 붙여줍니다. 주의할 점은 data=와 json=을 동시에 넘기면 안 된다는 것 — 둘 중 하나만 쓰세요.
requests에서 에러는 어떻게 처리하나요?
response.status_code를 직접 확인하거나(200이면 성공), response.raise_for_status()를 호출해서 4xx/5xx 응답일 때 예외가 발생하도록 만들 수 있습니다. 타임아웃이나 DNS 실패 같은 네트워크 단계의 문제는 requests.RequestException의 하위 클래스로 올라오니, 이걸 잡으면 HTTP 오류와 네트워크 오류를 한 번에 처리할 수 있어요.