웹 보안
Step 74. requests 라이브러리 — 파이썬으로 웹과 대화하기
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간
전제: Step 73을 마쳤다. HTTP 요청/응답 구조, GET과 POST, 헤더와 상태코드를 안다. 파이썬 스크립트를 작성·실행할 수 있고 pip을 써 봤다.
- 준비물: 파이썬, 터미널 두 개(서버용 하나 + 실험용 하나). 인터넷 연결은 필요 없습니다 — 오늘도 대화 상대는 우리가 직접 만드는 localhost 서버입니다.
- 주의: 오늘 실습은 자신이 소유한 로컬 실습 환경으로 범위를 제한합니다. 모든 요청은 127.0.0.1 안에서만 오갑니다.
지난 시간 우리는 curl로 서버와 대화했습니다. 명령 한 줄에 요청 하나. 훌륭하지만 한계가 있습니다 — 백 개의 페이지를 긁으려면 명령을 백 번 쳐야 하고, 응답에서 원하는 값만 뽑으려면 눈으로 찾아야 합니다. 자동화가 없으면 우리는 수집가가 아니라 타이핑 노예입니다. requests는 파이썬에서 HTTP 요청을 보내는 가장 유명한 라이브러리로, 이 하나로 GET/POST, 파라미터, 헤더, 쿠키를 코드 몇 줄로 다룹니다. 웹 스캐너, 수집기, 테스트 도구의 심장이 바로 이 라이브러리입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- POST와 쿠키를 이해하는 연습용 서버(lab.py)를 파이썬으로 직접 만든다
requests.get/post로 요청을 보내고 응답 객체의 status_code, headers, text를 읽는다- params(주소)와 data(본문)의 차이를 코드로 구분해 쓴다
- Session으로 쿠키를 유지하며 “로그인된 척 연속 행동”의 원리를 실험한다
- timeout과 예외 처리로 연결·읽기 대기를 제한하고 실패를 알리는 요청 코드를 만든다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 + requests 라이브러리 (pip install requests). 실습 상대는 127.0.0.1:8010의 자체 제작 서버 |
| 오늘의 도구 | requests.get/post, 응답 객체, requests.Session, 그리고 서버 쪽의 http.server 모듈 |
| 오늘의 함수 | requests.get(주소, params=, headers=, timeout=), requests.post(주소, data=), r.status_code / r.headers / r.text / r.json() / r.url / r.history |
| 필요한 개념 | 요청/응답 구조(Step 73), 파이썬 사전, 쿠키와 세션 |
| 오늘의 산출물 | lab.py — 연습용 서버, analyze.py — 응답 분석기 |
2-1. 응답 객체라는 선물 상자
requests.get(...)을 호출하면 돌아오는 것은 응답 객체(response object)입니다. “속성(주머니)이 여러 개 달린 선물 상자”라고 생각하세요.
| 속성 | 내용 | 예시 |
|---|---|---|
r.status_code |
상태코드 (숫자) | 200 |
r.headers |
응답 헤더 (사전처럼 사용) | r.headers["Content-Type"] |
r.text |
응답 본문 (문자열) | "<!DOCTYPE html>..." |
r.json() |
JSON을 객체·배열·스칼라에 대응하는 Python 값으로 해석하며 실패할 수 있음 | JSON 응답일 때만 |
r.url |
실제로 요청이 간 최종 주소 | 리다이렉트 확인용 |
r.history |
거쳐 온 리다이렉트 기록 | 경로 추적용 |
지난 시간 curl -v에서 눈으로 읽던 >와 <의 내용이, 이제는 코드에서 점(.)으로 꺼내 쓰는 속성이 된 것입니다.
2-2. params와 data — 싣는 위치의 차이
메서드의 의미와 매개변수 위치는 별개입니다. Requests의 params는 URL 쿼리를, data는 요청 내용을 구성하며 POST에서 둘 다 사용할 수 있습니다.
- params={“id”: “admin”}은 POST를 포함한 요청의 URL 쿼리를 만듭니다.
- data={“pw”: “dummy”}는 이 POST 예제에서 딕셔너리를 폼 본문으로 인코딩합니다. 가짜 입력값만 사용하세요.
파이썬 사전(dictionary, 이름표-값 쌍의 묶음)을 그대로 넘기면 requests가 알아서 HTTP 형식으로 포장합니다. 우리가 손으로 ?id=admin&pw=1234를 조립할 필요가 없습니다.
2-3. 세션 — 신분증을 계속 내미는 손
HTTP 요청은 독립적으로 처리됩니다. 애플리케이션은 쿠키나 다른 자격 증명으로 요청과 상태를 연결할 수 있습니다. requests.Session은 일치하는 쿠키와 공통 설정을 유지합니다. 쿠키가 반드시 인증 자격 증명인 것은 아니며 이 실습은 유지 동작만 보여 줍니다.
3. 따라 하기
3-1. 설치와 연습용 서버 만들기
입력
pip install requests
아래 lab.py를 만드세요. 앞의 SimpleHTTPRequestHandler 예제는 GET과 HEAD를 지원했고, 이번 핸들러는 제한된 POST와 쿠키 실습을 추가합니다. 로컬 학습용 서버입니다.
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
QUOTES_PAGE = """<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Quote Practice Ground</title></head>
<body>
<h1>Quotes of the Day</h1>
<div class="quote">
<span class="text">The greatest danger is a life without danger.</span>
<small class="author">Elbert Hubbard</small>
<div class="tags"><a class="tag" href="/tag/life">life</a></div>
</div>
<div class="quote">
<span class="text">Seeing once is better than hearing a hundred times.</span>
<small class="author">Proverb</small>
<div class="tags"><a class="tag" href="/tag/wisdom">wisdom</a></div>
</div>
<div class="quote">
<span class="text">Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.</span>
<small class="author">Antoine de Saint-Exupéry</small>
<div class="tags"><a class="tag" href="/tag/design">design</a> <a class="tag" href="/tag/wisdom">wisdom</a></div>
</div>
</body>
</html>"""
class LabHandler(BaseHTTPRequestHandler):
def _send(self, code, body, ctype="text/html; charset=utf-8", extra=None):
data = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
for k, v in (extra or {}).items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(data)
def do_GET(self):
path = urlparse(self.path).path
if path == "/":
self._send(200, QUOTES_PAGE)
elif path == "/headers":
lines = [f"{k}: {v}" for k, v in self.headers.items()]
self._send(200, "\n".join(lines), "text/plain; charset=utf-8")
elif path == "/set":
self._send(200, "A cookie has been planted.",
extra={"Set-Cookie": "session_id=abc123; Path=/"})
elif path == "/who":
cookie = self.headers.get("Cookie", "(no cookie)")
self._send(200, f"Cookie received by the server: {cookie}",
"text/plain; charset=utf-8")
elif path == "/old":
self.send_response(301)
self.send_header("Location", "/")
self.end_headers()
else:
self._send(404, "<h1>404 — no such page</h1>")
def do_POST(self):
if urlparse(self.path).path != "/echo":
self._send(404, "No such route", "text/plain")
return
try:
length = int(self.headers.get("Content-Length", 0))
self.connection.settimeout(3)
if length < 0:
raise ValueError("negative Content-Length")
if length > 8192:
# Drain only a bounded amount. A much larger rejected request
# may end with a connection error instead of a readable 413.
self.rfile.read(min(length, 8193))
self._send(413, "Body too large", "text/plain")
return
data = self.rfile.read(length)
if len(data) != length:
raise ValueError("incomplete request body")
raw = data.decode("utf-8")
except (OSError, ValueError, UnicodeError):
self._send(400, "Invalid request body", "text/plain")
return
fields = parse_qs(raw)
flat = {k: v[0] for k, v in fields.items()}
self._send(200, f"POST body received by the server: {raw}\nUnpacked result: {flat}",
"text/plain; charset=utf-8")
if __name__ == "__main__":
print("Practice server started: http://127.0.0.1:8010 (stop: Ctrl+C)")
HTTPServer(("127.0.0.1", 8010), LabHandler).serve_forever()
입력 (터미널 1 — 서버용)
python lab.py
출력 (2026-09-09 실측):
실습 서버 시작: http://127.0.0.1:8010 (종료: Ctrl+C)
읽는 법: 겁내지 마세요. 길지만 하는 일은 단순합니다 — 경로(path)마다 정해진 대답을 돌려주는 분기문입니다. /headers는 받은 헤더를 그대로 돌려주는 거울, /set은 쿠키를 심는 창구, /who는 받은 쿠키를 보여 주는 창구, /echo는 POST 본문을 돌려주는 창구입니다. 이 서버는 이 챕터와 크롤링 챕터에서 계속 씁니다.
왜: 서버를 직접 만들면 “서버는 요청 줄과 헤더를 읽고, 정해진 응답을 돌려주는 프로그램”이라는 것이 몸에 남습니다. 공격자의 눈으로 서버를 보려면 서버의 입장을 한 번은 되어 봐야 합니다.
3-2. 첫 요청과 응답 해부
터미널 2를 열고 req1.py를 만듭니다.
입력
import requests
r = requests.get("http://127.0.0.1:8010/", timeout=(2, 3))
print("상태코드:", r.status_code)
print("Content-Type:", r.headers["Content-Type"])
print("본문 앞 60자:", repr(r.text[:60]))
출력 (2026-09-09 실측):
상태코드: 200
Content-Type: text/html; charset=utf-8
본문 앞 60자: '<!DOCTYPE html>\n<html>\n<head><meta charset="utf-8"><title>명언'
읽는 법: 세 줄의 코드로 GET 요청이 날아갔고, 돌아온 상자에서 상태코드와 헤더와 본문을 꺼내 찍었습니다. 본문은 우리가 lab.py에 적은 그 HTML입니다. 지난 시간의 curl 한 줄이 코드 몇 줄로 바뀐 것에 불과해 보이지만, 이제 이 코드를 반복문에 넣을 수 있다는 점이 다릅니다.
3-3. 예측해 보기 — params는 주소 어디에 붙을까
예측입니다. 아래 코드에서 r.url에 찍히는 최종 주소는 어떤 모습일까요?
입력
r = requests.get("http://127.0.0.1:8010/headers", params={"q": "security", "page": "2"}, timeout=(2, 3))
print("최종 주소:", r.url)
출력 (2026-09-09 실측):
최종 주소: http://127.0.0.1:8010/headers?q=security&page=2
읽는 법: 사전으로 넘긴 두 개의 이름표-값이 ? 뒤에 &로 이어져 붙었습니다. 손으로 조립하던 GET 주소를 requests가 대신 만들어 준 것입니다. 참고로 우리 서버의 /headers는 주소 뒷부분을 무시하고 헤더만 돌려주지만, 주소 자체는 보시다시피 완성되어 날아갔습니다.
왜: 검색창에 단어를 칠 때 주소가 길어지는 현상의 정체가 이것입니다. 그리고 “이 주소의 파라미터를 바꾸면 서버가 다르게 반응할까?”라는 웹 테스트의 첫 질문이 바로 이 자리에서 나옵니다.
3-4. POST와 헤더 변장
입력
r = requests.post("http://127.0.0.1:8010/echo", data={"id": "admin", "pw": "1234"}, timeout=(2, 3))
print(r.text)
r2 = requests.get("http://127.0.0.1:8010/headers",
headers={"User-Agent": "MySecurityStudyBot/1.0"}, timeout=(2, 3))
print(r2.text)
출력 (2026-09-09 실측):
서버가 받은 POST 본문: id=admin&pw=1234
풀어 헤친 결과: {'id': 'admin', 'pw': '1234'}
Host: 127.0.0.1:8010
User-Agent: MySecurityStudyBot/1.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
읽는 법: 첫 번째는 POST 본문에 실린 데이터가 서버에 그대로 도착해 풀어 헤쳐진 증거입니다 — 이름=값&이름=값 형태로 실려 갔다는 것도 보입니다. 두 번째는 서버가 본 헤더 목록입니다. 내가 바꾼 User-Agent가 그대로 보이고, requests가 기본으로 붙인 헤더(Accept-Encoding, Connection 등)도 함께 보입니다.
왜: “요청의 모든 부분은 내가 조립할 수 있다”는 확신을 갖는 단계입니다. 웹 테스트 도구의 본질이 이 조립 자유도입니다.
3-5. 세션 — 쿠키가 유지되는지 실험
입력
s = requests.Session()
s.get("http://127.0.0.1:8010/set", timeout=(2, 3)) # the counter that plants a cookie
r = s.get("http://127.0.0.1:8010/who", timeout=(2, 3)) # check with the same session
print("With session:", r.text)
r2 = requests.get("http://127.0.0.1:8010/who", timeout=(2, 3)) # a fresh request without a session
print("Without session:", r2.text)
출력 (2026-09-09 실측):
세션으로: 서버가 받은 쿠키: session_id=abc123
세션 없이: 서버가 받은 쿠키: (쿠키 없음)
읽는 법: 세션으로 쿠키를 심은 뒤 같은 세션으로 물으니 쿠키가 살아 있습니다. 세션 없이 보낸 요청은 빈 쿠키입니다. 세션이 “신분증 지갑” 역할을 한다는 직접적인 증거입니다.
Session은 연속 요청에 편리하지만 쿠키를 보내는 유일한 방법은 아닙니다. 일부 애플리케이션의 세션 쿠키는 소지만으로 인증되어 탈취 시 제어나 만료 전까지 사칭에 악용될 수 있습니다. 인증과 관계없는 쿠키도 있습니다. 실습 쿠키로 로그인 성공을 판단하지 마세요.
3-6. 예측해 보기 — 리다이렉트는 어떻게 기록될까
우리 서버의 /old는 “이사갔다(301), 새 주소는 /”라고만 대답합니다. requests로 요청하면 최종 도착지는 어디이고, 그 기록은 어디에 남을까요?
입력
r = requests.get("http://127.0.0.1:8010/old", timeout=(2, 3))
print("최종 주소:", r.url)
print("거쳐온 안내:", [(x.status_code, x.url) for x in r.history])
출력 (2026-09-09 실측):
최종 주소: http://127.0.0.1:8010/
거쳐온 안내: [(301, 'http://127.0.0.1:8010/old')]
읽는 법: requests는 curl과 달리 리다이렉트를 기본으로 따라갑니다. 그리고 r.history에 거쳐 온 발자국(301 안내)이 남습니다. “최종적으로 어디에 도착했는가”와 “어떤 길로 왔는가”가 분리되어 보입니다.
왜: 단축 주소의 실제 목적지 확인, 피싱 사이트의 우회 경로 추적 — 이런 분석이 전부 이 history 속성에서 출발합니다.
3-7. 연결 및 읽기 대기 시간 제한
자동화 도구의 생존 장치를 붙입니다.
입력
import requests
for url in ["http://127.0.0.1:59999/", "http://127.0.0.1:8010/"]:
try:
r = requests.get(url, timeout=(2, 3))
r.raise_for_status()
print("Success:", r.status_code)
except requests.Timeout:
print("Connection/read timeout; not a total-duration deadline")
except requests.RequestException as exc:
print("Request failed:", type(exc).__name__)
출력 (2026-09-09 실측):
요청 실패: 서버에 닿지 않습니다
성공: 200
연결 거부는 ConnectionError를, 느리거나 침묵하는 연결은 Timeout을 일으킬 수 있습니다. 59999는 비어 있을 것으로 가정한 후보 포트일 뿐이므로 내 컴퓨터에서 서비스 중인지 먼저 확인하세요. Requests timeout은 연결 및 읽기 대기를 제한하며 응답 전체를 받는 총 경과 시간 제한은 아닙니다.
명시적 timeout과 RequestException 처리를 사용하고 페이지 수와 응답 크기도 제한하세요. 상대가 데이터를 조금씩 보내면 읽기가 계속될 수 있으므로 엄격한 전체 마감 시간에는 별도 제어가 필요합니다. timeout=3은 모든 처리가 3초 안에 끝난다는 보장이 아닙니다.
4. 미션과 연습문제
미션 — 응답 분석기 만들기
analyze.py를 만들어 다음을 수행하세요 (lab.py 서버가 켜져 있어야 합니다).
/headers에 params 두 개를 달아 GET 요청하고, 최종 URL과 상태코드를 출력합니다- 응답 본문에서 서버가 본 User-Agent 줄만 골라 출력합니다
- User-Agent를 자기만의 문자열로 바꿔 같은 요청을 다시 보내고, 서버가 본 내 신상이 바뀌었는지 출력으로 확인합니다
/echo에 POST로 이름표-값 세 개를 보내고, 서버가 풀어 헤친 결과를 출력합니다- 모든 요청을 try/except로 감싸고 timeout=3을 붙여, 서버가 꺼져 있어도 “요청 실패” 안내를 남기고 정상 종료하게 합니다
연습문제
Q1. params와 data에 전달한 딕셔너리는 각각 어디에 나타나나요? POST가 쿼리와 본문을 함께 가질 수 있는 이유를 설명하세요.
문제 2. r.json()을 호출했더니 JSONDecodeError: Expecting value: line 1 column 1 (char 0)가 떴습니다 (2026-09-09 실측). 무슨 상황이며, 호출 전에 무엇을 확인해야 하나요?
Q3. requests.Session은 실습 쿠키를 유지하지만 개별 호출은 자동으로 공유하지 않는 이유가 무엇인가요? 유지 자체가 인증은 아닌 이유도 설명하세요.
문제 4. r.url과 r.history의 차이를 “최종 도착지”와 “거쳐 온 길”로 설명해 보세요.
해설과 완료 기준 · 펼치기/접기
5. 모범 답안과 완료 기준
미션 모범 답안
import requests
BASE = "http://127.0.0.1:8010"
try:
# 1. params 달기
r = requests.get(f"{BASE}/headers", params={"q": "security", "page": "2"}, timeout=3)
print("최종 URL:", r.url)
print("상태코드:", r.status_code)
# 2. 서버가 본 User-Agent 줄 고르기
for line in r.text.splitlines():
if line.startswith("User-Agent"):
print("서버가 본 UA:", line)
# 3. 변장 후 재확인
r2 = requests.get(f"{BASE}/headers",
headers={"User-Agent": "MyAnalyzer/2.0"}, timeout=3)
for line in r2.text.splitlines():
if line.startswith("User-Agent"):
print("변장 후 서버가 본 UA:", line)
# 4. POST
r3 = requests.post(f"{BASE}/echo",
data={"id": "admin", "pw": "1234", "memo": "첫 실습"},
timeout=3)
print(r3.text)
except requests.exceptions.ConnectionError:
print("요청 실패: lab.py 서버가 켜져 있는지 확인하세요")
except requests.exceptions.Timeout:
print("요청 실패: 서버가 3초 안에 답하지 않았습니다")
실행 결과 (2026-09-09 실측, 일부):
최종 URL: http://127.0.0.1:8010/headers?q=security&page=2
상태코드: 200
서버가 본 UA: User-Agent: python-requests/2.33.1
변장 후 서버가 본 UA: User-Agent: MyAnalyzer/2.0
서버가 받은 POST 본문: id=admin&pw=1234&memo=%EC%B2%AB+%EC%8B%A4%EC%8A%B5
풀어 헤친 결과: {'id': 'admin', 'pw': '1234', 'memo': '첫 실습'}
아무 변장도 안 했을 때 requests는 스스로를 python-requests/버전이라고 신고한다는 것 — 이것도 오늘의 발견입니다. 그리고 POST 본문에서 한글 “첫 실습”이 %EC%B2%AB...으로 바뀐 것을 보세요. 폼 데이터는 URL 인코딩(주소에 실을 수 있는 글자로의 변환)되어 전송되고, 서버는 그것을 다시 풀어서 씁니다. Step 50의 인코딩 감각이 여기서도 쓰입니다.
검증하는 법: ① 서버를 끄고 실행했을 때 프로그램이 죽지 않고 안내를 남기는가. ② 변장 전후의 User-Agent 줄이 다른가. ③ POST의 세 이름표가 풀어 헤친 결과에 모두 있는가. 셋이 전부 ‘예’이면 완성입니다.
연습문제 해답
Q1: params는 URL 쿼리가 됩니다. data에 전달한 딕셔너리는 폼 본문으로 인코딩됩니다. POST는 둘 다 가질 수 있으며 메서드 이름으로 쿼리와 본문을 절대 구분하지 않습니다.
문제 2 해답. 응답 본문이 JSON이 아닌 상황입니다 — 우리 실습 서버의 /처럼 HTML이 돌아온 경우입니다 (char 0, 즉 첫 글자부터 JSON이 아니라는 뜻). json() 호출 전에 r.status_code를 보고 r.text[:200]으로 본문 앞부분을 먼저 들여다보는 습관이 필요합니다.
Q3: Session은 받은 쿠키를 저장하고 다음 요청에 맞는 것을 보냅니다. 독립된 requests 호출에는 별도 제공하지 않는 한 공통 쿠키 저장소가 없습니다. 실습은 쿠키 유지를 증명할 뿐 인증을 구현하지 않으며 실제 애플리케이션이 자격 증명과 상태를 검증합니다.
문제 4 해답. r.url은 리다이렉트를 다 따라간 뒤의 최종 주소이고, r.history는 그때까지 거쳐 온 안내(301 등)의 목록입니다. 3-6 실측처럼 최종 주소는 /인데 history에는 (301, .../old)가 남는 식입니다.
완료 기준 체크리스트
- [ ] lab.py 연습 서버를 만들고 켜고 끌 수 있다
- [ ] requests.get/post로 요청하고 status_code와 headers를 읽을 수 있다
- [ ] params와 data의 차이(주소 vs 본문)를 설명할 수 있다
- [ ] Session으로 쿠키가 유지되는 것을 실험으로 확인했다
- [ ] timeout과 try/except를 습관적으로 붙인다
- [ ] r.url과 r.history로 리다이렉트를 추적할 수 있다
- [ ] 미션: 응답 분석기를 완성했다
6. 흔한 실수와 해결
벽 1. 서버를 안 켜고 클라이언트부터 실행했다
증상 (2026-09-09 실측):
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8010): Max retries exceeded ... (Caused by NewConnectionError(... [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다))
원인: 대화 상대(lab.py)가 켜져 있지 않습니다.
해결: 터미널 1에서 python lab.py로 서버를 먼저 켜세요. 서버 창은 계속 떠 있어야 합니다.
벽 2. 서버를 켤 때 포트가 막혀 있다
증상: OSError: [WinError 10048] ... — 지정한 포트를 이미 쓰고 있습니다.
원인: 꺼지지 않은 이전 서버나 다른 프로그램이 그 번호를 점유 중입니다.
해결: lab.py의 8010을 8020 등으로 바꾸고, 클라이언트 코드의 주소도 같이 바꾸세요.
벽 3. r.json()에서 오류가 난다
증상 (2026-09-09 실측): requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
빈 본문·HTML·잘못된 JSON은 JSONDecodeError를 일으킬 수 있습니다. 200이라고 JSON이 보장되거나 오류 응답이 반드시 JSON이 아닌 것은 아닙니다. 상태·예상 Content-Type을 확인하고 204 같은 빈 응답을 처리하며 JSONDecodeError를 잡으세요.
벽 4. 요청이 한참 멈춰 있다
Requests의 기본값에는 timeout이 없습니다. 연결·읽기 제한을 지정하고 Timeout과 다른 RequestException을 처리하며 작업량을 제한하세요. 읽기 timeout은 데이터를 기다리는 시간이며 다운로드 전체 시간이 아닙니다. 실패를 성공한 빈 결과로 표시하지 말고 원인을 기록하세요.
벽 5. 분명 쿠키를 받았는데 다음 요청에서 사라진다
증상: /set으로 쿠키를 심었는데 /who에서 “(쿠키 없음)”이 옵니다.
원인: 매번 requests.get(...)으로 새 요청을 보냈습니다. 쿠키가 저장될 지갑이 없습니다.
해결: s = requests.Session()을 만들고 모든 요청을 s.get(...), s.post(...)로 보내세요 (3-5 실측이 그 증거입니다).
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| requests | 파이썬의 대표 HTTP 클라이언트 라이브러리 |
| 응답 객체 | status_code, headers, text, json() 등 주머니 달린 선물 상자 |
| params / data | params는 URL 쿼리, data는 요청 본문을 제공합니다. POST에도 쿼리가 있을 수 있습니다. |
| 쿠키 | Set-Cookie로 받고 쿠키 규칙에 따라 보내는 값이며 그 자체로 인증 증명은 아닙니다. |
| Session | 쿠키를 자동으로 보관·첨부하는 지갑 객체 |
| timeout | “이 시간 안에 답 없으면 포기” — 자동화의 생존 장치 |
오늘의 함수
| 함수·속성 | 하는 일 |
|---|---|
requests.get(주소, params=, headers=, timeout=) |
GET 요청 |
requests.post(주소, data=, timeout=) |
POST 요청 |
r.status_code / r.headers / r.text |
상태코드 / 응답 헤더 / 본문 문자열 |
r.json() |
JSON을 Python 값으로 해석하며 잘못되거나 빈 입력을 처리 |
r.url / r.history |
최종 주소 / 거쳐 온 리다이렉트 목록 |
requests.Session() |
쿠키 지갑 만들기 |
python lab.py |
연습용 서버 켜기 (종료: Ctrl+C) |
명령어보다 중요한 감각
오늘의 핵심 감각은 응답을 의심하는 습관입니다. 초보는 “응답이 왔으니 성공”이라 믿지만, 실무자의 확인 순서는 정해져 있습니다 — ① status_code 보기 ② r.text 앞 200자 찍어 보기 ③ 그다음에 json()이나 파싱. 이 삼단이 “왜 안 되지?” 하는 디버깅 시간의 절반을 지워 줍니다. 보안과의 연결: 오늘 배운 것은 남용하면 위험합니다. 같은 요청을 쉬지 않고 때리는 코드는 그 자체로 서비스 방해가 되므로, 반복 요청에는 time.sleep(1) 같은 쉼표를 넣는 것이 기술 이전의 태도입니다. 그리고 오늘 직접 만들어 본 lab.py를 기억하세요 — 서버는 결국 “요청을 읽고 정해진 대답을 내놓는 분기문”입니다. 이 그림이 있으면 웹의 어떤 신기술을 만나도 겁이 나지 않습니다.
전부 체크되면 Step 74 완료입니다.
한 단계 더 가까이
수업을 마쳤나요?
본문의 완료 기준을 확인한 뒤 직접 표시하세요.