본문 바로가기
목차
Python

[urllib] request 모듈

by ds31x 2025. 8. 28.
728x90
반응형

https://www.scaler.com/topics/urllib-python/

 

urllib은 Python의 표준 라이브러리로, URL 작업을 위한 여러 모듈을 제공함.
이 중에서 urllib.request 모듈은 HTTP/HTTPS 요청 처리를 위한 것임.

 

참고로 urllib 라이브러리의 주요 구성 모듈은 다음과 같음:

  • urllib.request - HTTP/HTTPS 요청 처리
  • urllib.parse - URL 파싱 및 구성
  • urllib.error - urllib 관련 예외
  • urllib.robotparser - robots.txt 파일 처리

urllib.request 모듈

웹 요청을 보내고 응답을 받는 기능을 제공.


urlopen() 함수

  • URL을 열고 파일형 객체인, http.client.HTTPResponse 객체를 반환.
  • 가장 기본적인 웹 요청 함수임.

Signature:

urllib.request.urlopen(
    url, 
    data=None, 
    timeout=socket._GLOBAL_DEFAULT_TIMEOUT, 
    *, cafile=None, capath=None, cadefault=False, context=None
    )

Parameters:

  • url : (str | Request)
    • 요청할 URL 또는 Request 객체
  • data : (bytes, optional)
    • POST 데이터.
    • None이면 GET 요청
    • None이 아니면 POST 요청 임.
  • timeout : (float, optional)
    • 타임아웃 시간(sec).
    • 기본값은 전역 설정 이용.
  • cafile : (str, optional)
    • CA 인증서 파일 경로 문자열.
  • capath : (str, optional)
    • CA 인증서 디렉터리 경로 문자열.
  • cadefault : (bool, optional)
    • 시스템 CA 인증서 사용 여부
  • context : (ssl.SSLContext, optional)
    • SSL 컨텍스트

return:

  • http.client.HTTPResponse 객체
    • HTTP 응답 객체

예제: GET 요청

아래의 모든 예제는 httpbin.org 를 이용함.

참고로, httpbin.org 는 테스트용 HTTP 요청/응답 서비스 를 제공하는 사이트임.

import urllib.request

# --------------------
# 1. 기본 GET 요청 보내기
# urlopen('https://httpbin.org/get') 는 GET 요청 전송
with urllib.request.urlopen('https://httpbin.org/get') as response:

    # ------------------
    # 2. 응답 상태 코드 확인
    print(f"상태 코드: {response.getcode()}") # 정상 응답이면 보통 200 (OK)

    # ---------------
    # 3. 응답 헤더 확인
    #    - response.headers : HTTP 응답 헤더를 담고 있는 객체
    #    - dict(...) 로 변환하면 dictionary 형태로 보기 편함
    print(f"헤더: {dict(response.headers)}")

    # --------------------
    # 4. 응답 본문(body) 읽기
    #    - response.read() : 바이트(bytes)로 반환됨
    #    - .decode('utf-8') → 문자열(str)로 변환
    data = response.read().decode('utf-8')

    # 응답 내용 전체는 길 수 있으므로 앞부분 100자만 출력
    print(f"응답 내용: {data[:100]}...")

 

httpbin.org/get 엔드포인트는 클라이언트가 보낸 GET 요청 정보를 JSON 형식으로 돌려줌.

  • 때문에 헤더에서 'Content-Type': 'application/json'를 확인 가능함.

참고로, Content-Type 헤더는 응답 body가 어떤 형식인지 알려주는 메타데이터임.

  • HTML : text/html
  • 일반 텍스트 : text/plain
  • JSON : application/json

예제: POST 요청

이는 웹 브라우저의 <form>태그를 통해 키와 값을 보내는 예제임.

참고로,
POST에서는 Content-Type: application/x-www-form-urlencoded를 사용하는 폼인코딩 (아래의 예제 코드) 이 외에도
REST API에서 흔히 사용되는 Content-Type: application/json
파일업로드에 사용되는 form-data인 멀티파트(Content-Type: multipart/form-data)도 있음.
urllib.request 빌트인 라이브러리를 통해서도 구현가능하나, 보통은 requests 라이브러리를 보다 많이 이용함:

그러므로 여기선 가장 간단한 Content-Type: application/x-www-form-urlencoded를 사용하는 폼인코딩 예제만 다룬다.

import urllib.request

# ----------------------
# 1. 전송할 POST 데이터 정의
#    - POST 요청 시 data 매개변수에 bytes 타입을 전달해야 함.
#    - "name=hong&age=30" : form 데이터 형식(key=value&key=value)
#    - application/x-www-form-urlencoded Content-Type 방식의 본문에 해당
post_data = b'name=hong&age=30'

# ----------------
# 2. POST 요청 전송
#    - urlopen() 호출 시 data 인자를 지정하면
#    - 자동으로 "POST" 방식 요청이 전송됨.
#    - (data=None 이면 GET 요청)
with urllib.request.urlopen('https://httpbin.org/post', data=post_data) as response:

    # ---------------
    # 3. 응답 본문 읽기
    result = response.read().decode('utf-8')
    # response.read() : 응답을 바이트(bytes) 데이터로 읽음
    # decode('utf-8') : UTF-8 문자열(str)로 변환

    # ------------------------------------
    # 4. 결과 출력
    print(result)
    # httpbin.org/post 는 요청 내용을 JSON으로 돌려주는 테스트 서버
    # 응답 JSON에는 우리가 보낸 post_data(name=hong, age=30),
    # 요청 헤더, IP, URL 등의 정보가 포함됨

timeout 설정

요청에 대한 응답을 기다리는 timeout을 설정.

지정된 timeout 안에 응답이 오지 않는 경우 타임아웃 예외가 발생함.

import urllib.request

try:
    # ------------------------------------------
    # httpbin.org/delay/5 는 응답을 5초 늦게 주는 테스트용 API
    # timeout=3 → 최대 3초 동안만 기다리도록 설정
    # 따라서 5초가 지나야 응답이 오는데, 3초까지만 기다리므로 타임아웃 발생
    with urllib.request.urlopen('https://httpbin.org/delay/5', timeout=3) as response:
        # 만약 3초 안에 응답이 오면 아래 코드 실행
        print("요청 성공")

except urllib.error.URLError as e:
    # ------------------------------------------
    # URLError:
    # - timeout 발생
    # - DNS 조회 실패
    # - 연결 거부(Connection Refused) 등
    # 네트워크 관련 오류가 발생했을 때 공통적으로 발생하는 예외
    print(f"타임아웃 오류: {e}")

e.reason 을 이용하면
타임아웃인지, 다른 네트워크 오류인지 구분할 수 있음.


Request 클래스

  • HTTP 요청을 추상화한 객체.
  • 헤더, 메서드 등을 보다 세밀하게 제어할 수 있음.

Signature:

class urllib.request.Request(
    url, 
    data=None, 
    headers={}, 
    origin_req_host=None, 
    unverifiable=False, 
    method=None
    )

Parameters:

  • url : (str)
    • 요청할 URL
  • data : (bytes, optional)
    • 요청 본문 데이터
  • headers : (dict, optional)
    • HTTP 헤더 딕셔너리
  • origin_req_host : (str, optional)
    • 원본 요청 호스트
  • unverifiable : (bool, optional)
    • 요청 검증 불가 여부
  • method : (str, optional)
    • HTTP 메서드 (GET, POST, PUT 등)

주요 메서드:

  • add_header(key, val): 헤더 추가
  • get_header(header_name, default=None): 헤더 값 조회
  • get_method(): HTTP 메서드 반환

예제: GET 요청

참고로, GET 요청은 원칙적으로 body가 없음

import urllib.request
import json

# -------------------
# 1. 보낼 HTTP 헤더 정의
headers = {
    'User-Agent': 'Python Client 1.0',   # 서버에 전달할 클라이언트 정보
    'Accept': 'application/json',        # 서버 응답으로 JSON 형식 원함
    'Authorization': 'Bearer token123'   # 인증 토큰 (예: OAuth2 방식)
}

# ------------------
# 2. Request 객체 생성
# URL: httpbin.org/headers → 서버가 받은 헤더를 그대로 JSON으로 돌려주는 테스트 API
request = urllib.request.Request('https://httpbin.org/headers', headers=headers)

# --------------------
# 3. 요청 보내고 응답 받기
with urllib.request.urlopen(request) as response:
    # 응답 본문 읽기 (바이트 -> 문자열 변환 -> JSON 디코딩)
    data = json.loads(response.read().decode('utf-8'))

    # ----------------------------
    # 4. 서버가 실제로 받은 헤더 확인
    print("서버가 받은 헤더:", data['headers'])

참고로 HTTP에서 GET 메서드의 request 의 구조는 다음과 같음:

GET <경로> <프로토콜>
<헤더1>: <값>
<헤더2>: <값>
...
<빈 줄>
[본문 - GET은 일반적으로 없음]

예제: POST 요청

import urllib.request
import json

# ----------------------------------------
# 1. 전송할 데이터 (Python dict)
post_data = {
    'name': '홍길동',
    'age': 30,
    'city': '서울'
}

# ----------------------------------------
# 2. JSON 직렬화 (dict -> JSON 문자열 -> bytes)
#    - json.dumps() : dict를 JSON 문자열로 변환
#    - ensure_ascii=False : 한글이 \uXXXX 로 깨지지 않고 그대로 출력되도록 함
#    - encode('utf-8') : 문자열을 UTF-8 바이트로 변환 (HTTP 요청 body는 bytes여야 함)
json_data = json.dumps(
    post_data,
    ensure_ascii=False
).encode('utf-8')

# -------------------
# 3. Request 객체 생성
#    - data=json_data : POST 요청으로 전송
#    - headers={'Content-Type': 'application/json'} : 본문이 JSON임을 명시
request = urllib.request.Request(
    'https://httpbin.org/post',
    data=json_data,
    headers={'Content-Type': 'application/json'}
)

# ---------------------
# 4. 요청 전송 및 응답 처리
with urllib.request.urlopen(request) as response:
    # response.read() : 응답 body (bytes)
    # decode('utf-8') : 문자열로 변환
    # json.loads() : JSON 파싱 (Python dict로 변환)
    result = json.loads(response.read().decode('utf-8'))

    # 서버가 받은 JSON 데이터 출력
    # httpbin.org/post 는 요청의 JSON 본문을 그대로 "json" 키에 담아 응답함
    print("전송된 JSON:", result['json'])

참고로 HTTP에서 POST 메서드의 request 의 구조는 다음과 같음:

POST <경로> <프로토콜>
<헤더1>: <값>
<헤더2>: <값>
...
<빈 줄>
<본문>

PUT 요청

POST가 새로 생성하거나 서버가 알아서 처리하는 용도로 사용되는 HTTP 메서드라면
PUT은 클라이언트가 특정 위치의 (기존) 리소스를 지정해서 교체하는 용도로 사용됨.

 

POST와 비슷한 형태이나, method='PUT' 을 통해 PUT요청임을 정확히 명시.

import urllib.request

# ----------------------------------------
# PUT 요청 생성
# - url: 요청할 주소
# - data: PUT 요청의 body (bytes 타입 필수)
# - method='PUT': HTTP 메서드를 PUT으로 지정
request = urllib.request.Request(
    'https://httpbin.org/put',
    data=b'updated data',   # 서버에 전송할 데이터
    method='PUT'            # HTTP 메서드 지정
)

# ----------------------------------------
# 요청 전송 및 응답 처리
with urllib.request.urlopen(request) as response:
    result = response.read().decode('utf-8')
    print(result)

urlretrieve()

URL의 내용을 파일로 직접 다운로드.

어느 정도 큰 파일 다운로드에 유용 (매우 큰 파일은 urlopen()으로 열고 chunk를 지정하여 읽어들여야 함).

  • 단순히 한 번에 다운로드를 통해 저장하는 구조.
  • 더 세밀한 제어(스트리밍, 커스텀 헤더, 예외 처리, 진행 상황 관리 등)를 하기 어려움.

대용량 파일에선 urlopen()을 통한 다음의 방식을 권장:

url = "https://httpbin.org/bytes/10485760"  # 10MB짜리 테스트 파일
filename = "downloaded_large_file.bin"
chunk_size = 8192  # 보통 8KB 또는 16KB 정도 사용

with urllib.request.urlopen(url) as response, open(filename, 'wb') as out_file:
    # total_size = resp.headers.get("Content-Length")
    # total_size = int(total_size) if total_size is not None else -1
    while True:
        # 지정한 크기만큼 읽기
        chunk = response.read(chunk_size)
        if not chunk:  # 더 이상 읽을 데이터가 없으면 종료
            break
        out_file.write(chunk)  # 바로 파일에 기록

signature:

urllib.request.urlretrieve(
    url, 
    filename=None, 
    reporthook=None, 
    data=None,
    )

parameters:

  • url : (str)
    • 다운로드할 URL
  • filename : (str, optional)
    • 저장할 파일명.
    • None이면 임시파일 생성
  • reporthook : (callable, optional)
    • 진행률 콜백 함수
  • data (bytes, opotional)
    • POST 데이터

return:

  • tuple: (filename, headers)
    • 튜플 객체

예제:

import urllib.request
import sys

def progress_hook(block_count, block_size, total_size):
    downloaded = block_count * block_size
    if total_size is None or total_size <= 0:
        # 총 크기를 모를 때: 다운로드한 바이트만 표시
        sys.stdout.write(f"\r진행: {downloaded} bytes 다운로드 중...")
    else:
        percent = min(100, downloaded * 100 / total_size)
        sys.stdout.write(
            f"\r진행률: {percent:.1f}% ({downloaded}/{total_size} bytes)"
        )
    sys.stdout.flush()

# # 파일 다운로드
# # url = 'https://httpbin.org/bytes/10240'
# url = "https://github.com/google/fonts/raw/main/ofl/nanumgothic/NanumGothic-Regular.ttf"
# save_path = "NanumFont.ttf"
# 최신 CaskaydiaCove Nerd Mono 다운로드 URL (GitHub Release)
url = "https://sourceforge.net/projects/nerd-fonts.mirror/files/v3.2.1/CascadiaMono.zip/download"
save_path = "CaskaydiaCoveNerdMono.zip"
filename, headers = urllib.request.urlretrieve(
    url, 
    save_path,
    reporthook=progress_hook,
)

print(f'\n다운로드 완료: {filename}')

build_opener()

커스터마이즈된 URL opener를 생성하기 위한 함수.

  • urllib.request.urlopen() 은 기본 opener를 사용: 기본 opener는 표준 handler 들을 사용.
    • HTTP/HTTPS/리다이렉트/에러처리 핸들러 등으로 구성됨.
  • build_opener()로 만든 opener는 기본 opener에서 명시적 제어(쿠키·프록시·인증 등)를 위해 바꾸려는 기능에 해당하는 handler를 넘겨주어 해당 기능을 대체함.
    • 없는 핸들러의 경우에 기능이 추가됨.
    • 기본 opener에 있는 핸들러가 넘겨진 경우 대체됨.

urllibopener 는 쿠키, 프록시, 인증 리다이렉트, SSL, 디버그 등을 제어하는 handler를 조합하여 동작 체인 구성한 객체로 요청(Request)을 open 할 때, 내부적으로 각 handler를 순서대로 거치며 기능을 수행함.

  • handler들의 처리순서는 고정된 우선순위가 있음
  •  때문에 build_opner()에 handler들을 넘겨줄 때 순서는 고민할 필요가 거의 없음.

urllibhandler는 HTTP 요청·응답 처리 과정에서 쿠키, 프록시, 인증, 리다이렉트, SSL 등 특정 기능을 담당하는 모듈화된 구성 요소임.


signature :

urllib.request.build_opener(*handlers)

parameters :

  • *handlers:
    • Handler 객체들
    • HTTPCookieProcessor, ProxyHandler

return :

  • OpenerDirector 객체
    • 커스터마이즈된 opener 객체

예제: 쿠키 처리

HTTP 쿠키는 웹 서버가 클라이언트에 저장해두고 이후 요청마다 자동으로 전송되는 key=value 쌍의 작은 데이터로, 세션 유지·인증·사용자 설정 등에 활용됨 (값은 빈 문자열수도 있음).

 

다음 예제는 HTTPCookieProcessor(CookieJar) 를 통해,

  • 서버가 보낸 쿠키를 자동 저장하고
  • 이후 요청에 자동 전송하는 쿠키 처리 객체를 만들고,
  • 이를 build_opner()를 통해 오프너를 생성하여 요청을 보내는 방법을 보여줌.
import urllib.request
from http.cookiejar import CookieJar

# ----------------------------------------
# 1. 쿠키를 저장할 CookieJar 객체 생성: 쿠키 저장소 
cookie_jar = CookieJar()

# ----------------------------------------
# 2. CookieJar와 연결된 HTTPCookieProcessor 생성
#    - HTTP 응답에서 받은 쿠키를 cookie_jar에 저장
#    - 이후 요청 시 cookie_jar에 저장된 쿠키를 자동 전송
# ----------------------------------------
cookie_processor = urllib.request.HTTPCookieProcessor(cookie_jar)

# ----------------------------------------
# 3. opener 생성
#    - 기본 urlopen 대신 opener.open()을 사용하면
#      쿠키를 포함한 고급 동작을 지원
opener = urllib.request.build_opener(cookie_processor)

# ----------------------------------------
# 4. 첫 번째 요청: 쿠키 설정
#    - httpbin.org/cookies/set/session/abc123
#    - "session=abc123" 쿠키를 응답에 담아 내려줌
#    - HTTPCookieProcessor가 자동으로 cookie_jar에 저장
response1 = opener.open('https://httpbin.org/cookies/set?session=abc123')

# ----------------------------------------
# 5. 두 번째 요청: 쿠키 자동 전송
#    - httpbin.org/cookies 는 서버가 받은 쿠키를 JSON으로 반환
#    - opener는 cookie_jar에 저장된 "session=abc123" 쿠키를 자동으로 전송
response2 = opener.open('https://httpbin.org/cookies')

# ----------------------------------------
# 6. 결과 출력
#    - {"cookies": {"session": "abc123"}} 형태의 JSON 반환
print(response2.read().decode('utf-8'))

참고로, 일반적으로 쿠키는

  • 서버가 Set-Cookie 헤더로 클라이언트에 전달하면,
  • 클라이언트는 이를 저장(메모리 또는 파일)해두었다가
  • 같은 도메인·경로 조건으로 또 요청할 경우 자동으로 Cookie 헤더에 포함해 전송하는 방식으로 동작함.

주의할 점은 http.cookiejar.CookieJar는 기본적으로 세션 쿠키 (메모리에 저장)만 관리한다.

 

Expires 또는 Max-Age가 지정된 persistent cookieMozillaCookieJarLWPCookieJar 같은 하위 클래스를 써서 파일로 저장되도록 구현해야함.

 

httpbin.org는 간단한 테스트용 사이트라, 세션 쿠키만 가능함.

  • /cookie/set 엔드포인트는 뒤에 넘겨지는 키와 값으로 쿠키를 설정하라는 응답헤더를 전송해줌: Set-Cookie: session=abc123; Path=/
    • 복수의 쿠키도 설정 가능: https://httpbin.org/cookies/set?name=hong&age=30&city=seoul
  • /cookie 엔드포인트는 요청에서 보내는 쿠키를 json으로 다시 전송해줌.

예제: Proxy 설정

프록시(Proxy)는 클라이언트와 서버 사이에 중계 역할을 하는 서버로, 클라이언트의 요청을 대신 전달하고 그 응답을 돌려주는 역할을 수행함.

  • Proxy를 통해 IP 주소를 숨기거나, 보안·캐싱·필터링 같은 기능을 추가할 수 있음.

다음의 예제는 이런 프록시 서버를 코드에서 지정해 요청을 경유하는 방법을 보여줌.

import urllib.request

# ----------------------------------------
# 1. ProxyHandler 생성
#    - 프로토콜별로 사용할 프록시 서버 지정
proxy_handler = urllib.request.ProxyHandler({
    'http': 'http://proxy.example.com:8080',
    'https': 'https://proxy.example.com:8080'
})

# ----------------------------------------
# 2. build_opener 로 오프너 생성
#   - 기본 opener에 ProxyHandler를 추가/대체한 opener를 만듦
proxy_opener = urllib.request.build_opener(proxy_handler)

# ----------------------------------------
# 3. custom opener 사용
#    - opener.open()을 사용하면 설정된 프록시를 거쳐 요청 전송
#    - 여기서는 예시라 실제 프록시 주소는 동작하지 않음
# with proxy_opener.open('http://httpbin.org/ip') as response:
#     print(response.read().decode('utf-8'))

예제: password 인증

특정 서버의 자원이 패스워드 인증을 요구할 경우 이를 처리하는 handler사용 예제.

  • build_opener의 사용법만을 보여주는 예제라 암호를 코드에 그대로 사용하는 등의 간단한 방식으로 구현함
    • 이는 보안이 매우 취약하므로 실무에서 절대 이 방식으로 해선 안됨.
    • 사용된 HTTPPasswordMgrWithDefaultRealm은 인증 자격(사용자명·비밀번호)을 메모리에 단순 저장하는 구조임.
    • 실무에서는 환경 변수, 비밀 관리 시스템(AWS Secrets Manager, Kubernetes Secret 등등)을 활용하여 자격 증명을 안전하게 관리하는 방식을 사용.
    • 실무에서 사용되는 자격 관리에 대한 부분은 이 문서의 범위 밖의 내용임.
  • 또한 사용된 HTTPBasicAuthHandler 객체는 HTTP Basic Authentication을 수행하며, 이 경우 사용자명과 비밀번호가 모두 Base64인코딩되어 전송되므로 암호화 안된 평문과 다름없음.
    • 이것도 매우 보안에 취약한 부분임.
    • 단, HTTPS 환경에서 사용해야할 경우, 해결됨.
    • uri 에서 항상 "https:// 로 시작해야함.

uri 는 uniform resourece identifier로 리소스를 식별하는 모든 문자열을 의미한다.
이 문서에서 url 대신에 uri라고 기재한 이유는
실제 URL의 앞부분에 붙여지는 문자열을 가리키고 있으며, 파이썬 공식문서에서도 uri라고 기재했기 때문임.

import urllib.request

# ----------------------------------------
# 1. 패스워드 매니저 생성
#    - 요청 대상 서버와 사용자 계정을 관리
#    - add_password(realm, uri, user, passwd)
#      realm=None : 모든 realm에 적용 (해당 uri에서 패스워드가 필요한 자원을 realm으로 설정)
password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(
    None,                   # realm: None이면 기본 영역(Realm 무시하고 해당 uri 모조리 지정)
    'https://httpbin.org',  # 보호된 리소스가 있는 서버/도메인
    'user',                 # 사용자명
    'pass'                  # 비밀번호
)

# ----------------------------------------
# 2. BasicAuth 핸들러 생성
#    - HTTP Basic 인증 처리기
#    - 서버가 401 Unauthorized + WWW-Authenticate 헤더를 내려주면
#      이 핸들러가 사용자/비밀번호를 포함한 Authorization 헤더 생성
#    - 즉, HTTPBasicAuthHandler는 401 응답 시 Authorization 헤더 자동 생성
auth_handler = urllib.request.HTTPBasicAuthHandler(password_manager)

# ----------------------------------------
# 3. opener 생성
#    - build_opener()로 auth_handler를 포함한 opener 구성
#    - opener.open() 사용 시 자동으로 인증 헤더 추가
auth_opener = urllib.request.build_opener(auth_handler)

# ----------------------------------------
# 4. 요청 예시 (httpbin 기본 인증 테스트 엔드포인트)
#    - /basic-auth/<user>/<passwd>
#    - 올바른 user/pass를 넣으면 200 OK 반환
url = 'https://httpbin.org/basic-auth/user/pass'
with auth_opener.open(url) as response:
    print(response.read().decode('utf-8'))

참고로, 401 응답코드는 요청이 서버에 도달했지만, 인증이 필요하거나 제공된 인증 정보가 잘못되었음을 의미함.


같이 보면 좋은 자료들

2025.08.29 - [Python] - [requests] Python의 requests 라이브러리 사용법.

 

[requests] Python의 requests 라이브러리 사용법.

requests는 Python에서 HTTP 요청을 간단하고 직관적으로 보낼 수 있게 해주는 가장 널리 쓰이는 라이브러리임.복잡한 소켓 프로그래밍이나 urllib 모듈보다 훨씬 쉬운 인터페이스를 제공GET/POST/PUT/PATCH

ds31x.tistory.com

 

728x90