학습일지

[스나이퍼팩토리] 한컴AI아카데미(26.06.04) urllib

cd-record 2026. 6. 4. 18:04

 

urllib(얼리브, 유얼리브)

# urllib.request의 핵심함수

  • urlopen(url): URL을 열고 HTTP 요청을 보내는 가장 기본적이고 핵심적인 함수로 url 인자에는 일반 문자열 주소나 Request 객체를 모두 넣을 수 있습니다.
  • Request(url, headers={}): 복잡한 HTTP 요청을 정의할 수 있는 클래스로 headers에 User-Agent(브라우저 정보)를 숨겨 넣거나, method에 'POST', 'PUT', 'DELETE' 등을 명시하여 단순 GET 외의 요청을 커스텀할 때 생성하여 urlopen에 전달합니다.
  • urlretrieve(url): 네트워크의 파일을 로컬 컴퓨터로 즉시 다운로드하고 저장하는 편리한 함수입니다.

## urllib을 활용한 데이터 수집

import urllib.request

response = urllib.request.urlopen("https://sfac.dothome.co.kr/")
# print(type(response)) → <class 'http.client.HTTPResponse'>

# pre = response.read() # bytes 단위로 출력
# print(pre) → b'<!DOCTYPE html>\r\n~   <!-- [\xed\x95\x84\xec\x88\x98~

html = response.read().decode("utf-8")
print(html)
  1. import urllib.request
    • 파이썬에 기본적으로 내장된 네트워크 관련 라이브러리입니다. 웹 페이지에 접속하고 데이터를 가져오는 기능을 수행합니다.
  2. response = urllib.request.urlopen("https://sfac.dothome.co.kr/")
    • 해당 URL로 HTTP GET 요청을 보냅니다.
    • 서버가 응답을 보내오면 그 결과를 response 객체에 담습니다. 이 객체 안에는 상태 코드(200 OK 등), 헤더 정보, 그리고 우리가 원하는 HTML 데이터가 들어있습니다.
  3. pre = response.read()
    • 서버로부터 받은 데이터를 바이트(bytes) 형태의 날것 그대로 읽어옵니다.
    • 사람이 읽을 수 있는 텍스트가 아니라 컴퓨터가 이해하는 이진 데이터(0과 1) 형태라, 눈으로 보면 깨진 문자처럼 보일 수 있습니다.
  4. html = response.read().decode("utf-8")
    • read()로 읽어온 바이트 데이터를 utf-8 방식(웹에서 가장 많이 사용하는 문자 인코딩)으로 디코딩(해석)합니다.
    • 이 과정을 거쳐야 비로소 우리가 웹 브라우저에서 보는 'HTML 태그가 포함된 텍스트'로 변환됩니다.
import urllib.request

url = "https://sfac.dothome.co.kr/"

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'}

request = urllib.request.Request(url, headers=headers)

response = urllib.request.urlopen(request)
html = response.read().decode('utf-8')
print(html)

 

  1. url = "https://sfac.dothome.co.kr/"
    • 데이터를 수집하고자 하는 대상 웹 페이지의 주소를 변수에 저장합니다.
  2. headers = {'User-Agent': ...}
    • [요청 헤더(Headers)]
    • 웹 서버에게 "나는 웹 브라우저야"라고 속이는 정보입니다. 서버는 보안을 위해 아무런 정보 없이 접속하는 파이썬 코드의 요청을 무시하거나 차단하는 경우가 많은데, 이 User-Agent 값을 넣어주면 서버가 실제 사용자가 접속한 것으로 간주하여 데이터를 허용합니다.
  3. request = urllib.request.Request(url, headers=headers)
    • [Request 클래스]
    • 단순히 주소만 넘기는 게 아니라, 주소와 헤더 정보를 결합하여 하나의 '요청 패키지'를 만드는 과정입니다. 서버에게 보낼 봉투에 '수신인 주소'와 '보내는 사람(브라우저 정보)'을 함께 적어 넣는 것과 같습니다.
  4. response = urllib.request.urlopen(request)
    • 앞서 만든 '요청 패키지'를 사용하여 실제로 서버에 접속하고 응답을 받아옵니다.
import urllib.request

image_url = "https://sfac.dothome.co.kr/images/kakao.png"
save_path = "kakao.png"

# filename, headers = urllib.request.urlretrieve(image_url, save_path)

try:
    # 파일 다운로드 실행
    filename, headers = urllib.request.urlretrieve(image_url, save_path)
    print(f"다운로드 성공! 저장된 파일: {filename}")

except Exception as e:
    print(f"다운로드 중 오류 발생: {e}")

 

  1. urllib.request.urlretrieve(image_url, save_path)
    • [urlretrieve]
    • 웹에 있는 파일을 특정 경로로 직접 다운로드하여 저장하는 함수입니다. 이전의 urlopen이 데이터를 '읽어와서(read)' 변수에 담는 방식이었다면, 이 함수는 읽어오는 즉시 파일로 생성해 줍니다.
    • 반환값으로 저장된 파일의 경로(filename)와 서버 응답 정보(headers)를 넘겨줍니다. (반환값은 오직 2개)
    • 첫 번째 자리(filename): 함수는 무조건 '다운로드된 파일 경로'를 보냅니다.
    • 두 번째 자리(headers): 함수는 무조건 '서버의 응답 헤더 정보'를 보냅니다.
  2. try ... except Exception as e:
    • [예외 처리 (Exception Handling)]
    • 네트워크 환경은 변수가 많습니다(인터넷 끊김, 서버 접속 거부 등). 이때 프로그램이 무조건 멈추지 않도록, "에러가 발생하면(try) 그 내용을 출력하고(except) 정상적으로 종료하라"는 안전장치를 거는 코드입니다.
import urllib.request

# 다운로드 상태를 출력할 콜백 함수 정의
def show_progress(block_num, block_size, total_size):
    """
    block_num: 지금까지 다운로드된 블록 수
    block_size: 블록 한 개의 크기 (바이트)
    total_size: 전체 파일 크기 (바이트, 서버가 제공하지 않으면 -1)
    """
    if total_size > 0:
        downloaded = block_num * block_size
        percent = (downloaded / total_size) * 100
        # \r을 사용하여 한 줄에서 숫자가 계속 갱신되도록 처리
        print(f"\r다운로드 진행 중: {percent:.2f}%", end="")
    else:
        print("\r다운로드 중...", end="")

# 대용량 예시 파일
file_url = "https://sfac.dothome.co.kr/static/SQLyog855.zip"
save_path = "SQLyog855.zip"

print("다운로드를 시작합니다.")
urllib.request.urlretrieve(file_url, save_path, reporthook=show_progress)
print("\n다운로드가 완료되었습니다!")

 

  1. def show_progress(block_num, block_size, total_size):
    • [콜백(Callback) 함수]
    • 다운로드 진행 상황을 보고받기 위해 미리 정의해둔 함수입니다. urlretrieve 함수가 데이터를 조금씩 받아올 때마다, 중간중간 이 함수를 호출해서 현재 상태를 업데이트합니다.
  2. percent = (downloaded / total_size) * 100
    • 전체 크기 대비 현재까지 받은 데이터 양을 계산하여 백분율(%)을 구합니다.
  3. print(f"\r다운로드 진행 중: {percent:.2f}%", end="")
    • [\r (캐리지 리턴)]
    • 줄 바꿈(\n) 대신 \r을 사용하면 커서가 줄의 맨 앞으로 이동합니다. 이를 통해 이전 출력값을 지우고 그 자리에 숫자를 계속 덮어씌워, 진행률이 한 줄에서 변하는 것처럼 보이게 만듭니다.
    • end=""는 출력 후 자동으로 줄을 바꾸지 않도록 설정하는 옵션입니다.
  4. urllib.request.urlretrieve(..., reporthook=show_progress)
    • [reporthook]
    • urlretrieve의 옵션 중 하나입니다. "다운로드 상황을 보고할 때마다 show_progress 함수를 실행해달라"는 명령입니다.

# urllib.error

urllib.error에서 발생하는 두 예외는 오류가 발생한 시점(네트워크 연결 전 vs 연결 후)에 따라 명확히 구분됩니다.  
URLError는 서버를 찾지 못했을 때 발생하고, HTTPError는 서버에는 접속했으나 서버가 에러를 응답했을 때 발생합니다.

## 1. URLError (서버 접속 실패)

서버와 네트워크 연결 자체가 이루어지지 않은 경우에 발생합니다. HTTP 프로토콜 단계까지 가지도 못한 상태입니다.

  • 발생하는 경우:
      - 인터넷 연결 끊김: 랜선이 뽑혔거나 와이파이가 연결되지 않았을 때잘못된 
      - URL 주소: 존재하지 않는 도메인 주소를 입력했을 때 (예: https://not-exists-domain-12345.com)
      - 오타: 오타로 인해 없는 도메인을 요청했을 때
      - 서버 다운: 요청한 웹 서버의 전원이 꺼져 있거나 물리적으로 작동하지 않을 때
      - 방화벽 차단: 네트워크 방화벽이 해당 주소로의 접근을 막았을 때

## 2. HTTPError (서버 응답 오류)

서버에는 정상적으로 연결되었지만, 서버가 요청을 처리하는 과정에서 에러 코드(4xx 또는 5xx)를 반환한 경우에 발생합니다. HTTPError는 URLError 자식 클래스(하위 클래스)입니다.

  • 발생하는 경우:
      - 404 Not Found: 요청한 페이지나 파일(경로)이 서버에 존재하지 않을 때
      - 403 Forbidden: 해당 페이지에 접근할 권한이 없을 때 (예: 로그인 필요, 크롤링 차단 등)
      - 400 Bad Request: 요청 양식이 잘못되었을 때
      - 500 Internal Server Error: 웹 서버 자체에 프로그램 오류가 발생했을 때
      - 503 Service Unavailable: 서버가 과부하 상태이거나 점검 중일 때
import urllib.request
import urllib.error

# 다운로드할 정확한 파일 주소 (예시)
file_url = "https://sfac.dothome.co.kr/static/SQLyog855.zip"
save_path = "SQLyog855_agent.zip"

# 1. 브라우저로 위장할 User-Agent 설정
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
req = urllib.request.Request(file_url, headers=headers)

try:
    print("다운로드를 시작합니다.")

    # 2. urlopen에 Request 객체 전달 및 로컬 파일 쓰기 모드(wb) 오픈
    with urllib.request.urlopen(req) as response, open(save_path, "wb") as out_file:

        # 3. 서버가 알려준 전체 파일 크기 확인
        total_size = int(response.getheader("Content-Length", 0))
        downloaded = 0
        block_size = 1024 * 8  # 8KB 단위로 끊어서 다운로드

        while True:
            # 4. 블록 단위로 읽기
            buffer = response.read(block_size)
            if not buffer:
                break

            downloaded += len(buffer)
            out_file.write(buffer)

            # 5. 실시간 진행률 출력
            if total_size > 0:
                percent = (downloaded / total_size) * 100
                print(f"\r다운로드 진행 중: {percent:.2f}%", end="")
            else:
                print(f"\r다운로드 중... ({downloaded} bytes)", end="")

    print("\n다운로드가 완료되었습니다!")

except urllib.error.HTTPError as e:
    print(f"\nHTTP 오류 발생 ({e.code}): 주소가 틀렸거나 봇 차단 필터에 걸렸습니다.")
except Exception as e:
    print(f"\n오류 발생: {e}")
  1. urllib.error.HTTPError
    • 왜 발생하는 가? file_url이 잘못되었거나(404), 서버가 자동화된 접근(봇)을 차단했을 때 (403) 발생합니다.
    • 중요포인트: e.code를 통해 400번대, 500번대 에러를 구분하여 디버깅할 수 있습니다.
  2. 일반 Exception (그 외 에러)
    1. urllib.error.URLError이 발생하면 이쪽으로 걸리게 됩니다.
    2. 대표적인 URLError 상황:
      • DNS 오류: 도메인 주소를 IP로 변환하지 못할 때 (인터넷 끊김, 도메인 이름 오타)
      • Connection Refused: 서버가 아예 응답을 거부하거나 서버 자체가 내려갔을 때
      • Timeout: 서버 응답이 너무 오래 걸릴 때

# urllib.parse

## URL 파싱(Parsing)이란?

'파싱'이란 긴 문자열로 된 데이터를 컴퓨터가 이해할 수 있는 작은 단위(구성 요소)로 쪼개는 과정을 말합니다.

단순히 문자열을 자르는 것(split)보다 훨씬 안전합니다. 왜냐하면 URL에는 복잡한 규칙(프로토콜, 도메인, 포트, 경로 등)이 존재하는데, urlparse는 이 규칙에 맞춰 가장 완벽하고 안전하게 요소를 분리해주기 때문입니다.

from urllib.parse import urlparse, urlencode

url = 'http://www.example.com/path?query=1#fragment'

# URL 파싱
parsed_url = urlparse(url)
# http://www.example.com/path?query=1#fragment라는 통 문자열을 분석하여 객체(Object) 형태로 반환
print("파싱된 URL 구성 요소:")
print("프로토콜:", parsed_url.scheme) # http (데이터 통신 규약)
print("호스트:", parsed_url.hostname) # www.example.com (서버 주소)
print("경로:", parsed_url.path) # /path (서버 내 상세 경로)
print("쿼리:", parsed_url.query) # query=1 (전송할 데이터)
print("프래그먼트:", parsed_url.fragment) # fragment (페이지 내 특정 위치)

# 쿼리 문자열 인코딩 (새로운 파라미터 추가)
query_params = {'key1': 'value1', 'key2': 'value2'}
encoded_query = urlencode(query_params)
print("인코딩된 쿼리 문자열:", encoded_query)

# 새로운 URL 생성
new_url = f"{parsed_url.scheme}://{parsed_url.hostname}{parsed_url.path}?{encoded_query}"
print("새로운 URL:", new_url)
  • 쿼리 문자열 인코딩: urlencode(query_params)
    • 개념:  웹에서는 한글이나 공백, 특수문자를 그대로 URL에 넣으면 서버가 이해하지 못하거나 오류가 날 수 있습니다. 이를 안전하게 컴퓨터용 언어(Percent Encoding)로 바꾸는 작업이 인코딩
    • urlencode는 딕셔너리 데이터를 key1=value1&key2=value2 형태의 올바른 웹 URL 형식으로 자동 변환해 줍니다.
  • 핵심 요약
    • urlparse: URL을 분석(쪼개기)해서 각 정보를 추출할 때 사용합니다.
    • urlencode: 딕셔너리(데이터)를 변환(인코딩)해서 URL에 붙이기 좋은 문자열로 만들 때 사용합니다.

Request

# requests란 무엇인가?

우리가 브라우저 주소창에 주소를 입력하면 웹 서버로부터 정보를 받아오는 것처럼, 파이썬 코드로 이 과정을 대신 수행하는 도구

GET

import requests

# url = "https://sfac.dothome.co.kr/"
url = "http://ssacademy.dothome.co.kr/001.html"
response = requests.get(url)
print(response.text)  # 문자열로 출력
# print(response.status_code)  # HTTP 상태 코드 출력
# print(response.headers)  # 응답 헤더 출력
# print(response.content)  # bytes로 출력
# print(response.json())  # JSON으로 출력 (응답이 JSON 형식일 때) --> 딕셔너리로 자동 변환 반환
# print(response.encoding)  # 응답 인코딩 출력
# print(response.url)  # 최종 URL 출력 (리다이렉션이 있을 때)
# print(response.history)  # 리다이렉션 히스토리 출력
# print(response.cookies.get_dict())  # 서버에서 받은 쿠키 출력 (딕셔너리 형태)
# print(response.elapsed)  # 요청-응답 시간 출력
# print(response.cookies.get("cookie_name"))  # 특정 쿠키 값 출력 (쿠키 이름이 "cookie_name"인 경우)

코드의 핵심 동작 분석

  • requests.get(url): 해당 URL에 "데이터를 주세요"라고 요청(GET)을 보냅니다.
  • response 객체: 서버로부터 받은 응답 전체를 담고 있는 상자입니다.

response.status_code

- 200 OK(요청 성공)

서버가 클라이언트의 요청을 성공적으로 처리했을 때 사용하는 가장 일반적인 상태 코드입니다. 보통 데이터를 조회하거나 수정, 삭제할 때 반환됩니다. 

- 주요 사용 목적: 데이터 조회(GET), 기존 데이터 수정(PUT/PATCH), 데이터 삭제(DELETE)
- 의미: "네가 요청한 작업을 잘 끝냈고, 결과 데이터를 응답에 담아 보낼게."
- 응답 본문(Body): 클라이언트가 요청한 데이터나 처리 결과 메시지가 포함되는 경우가 많습니다.

 

- 201 Created(자원 생성 성공)

서버가 요청을 받아들여 새로운 자원(데이터)을 내부에 성공적으로 만들어냈을 때 사용하는 상태 코드입니다. 주로 회원가입이나 게시글 작성 같은 작업에서 사용됩니다. 

- 주요 사용 목적: 새로운 데이터 추가(POST)
- 의미: "네가 요청한 데이터를 기반으로 서버에 새 레코드(자원)를 성공적으로 생성했어."
- 응답 헤더/본문: 새로 생성된 데이터의 고유 주소(URL) 가 응답 헤더의 Location 항목에 담겨 오거나, 본문에 새로 만들어진 객체 정보(예: 새로 발급된 id 번호 등)가 포함되는 것이 웹 표준 관례입니다.

POST

import requests

url = "https://example.com"
headers = {"User-Agent": "Mozilla/5.0"}

payload = {
    "username": "my_id",
    "password": "my_password"
}

response = requests.post(url, headers=headers, data=payload)

 

코드 핵심 동작 분석

  • url: 데이터를 보낼 목적지 주소입니다.
  • headers: 서버에게 "나는 브라우저(Mozilla/5.0)를 사용 중이야"라고 알려주어, 서버가 이를 로봇이 아닌 일반 사용자의 요청으로 인식하게 합니다.
  • data=payload: 실제로 서버에 전달할 정보입니다. payload라는 딕셔너리에 담긴 username과 password 키-값 쌍이 서버로 전송됩니다.

requests.post()를 사용할 때 데이터를 보내는 방식은 크게 세 가지(params, data, json)로 나뉩니다.

 

1. params (URL 파라미터)

데이터를 URL 주소 뒤에 쿼리 스트링(?key=value)으로 붙여서 보냅니다. 주로 GET 요청에서 사용하지만, POST에서도 주소에 정보를 담아야 할 때 사용합니다.

2. data (폼 데이터 - Form Data)

데이터를 웹 페이지의 HTML <form> 태그를 통해 전송하는 방식과 동일하게 보냅니다.

  • 용도: 일반적인 로그인, 파일 업로드 등.
  • Content-Type: 주로 application/x-www-form-urlencoded 헤더를 사용합니다.
  • 형태: key1=value1&key2=value2 (문자열 직렬화)
  • 코드: requests.post(url, data={"username": "my_id", "password": "123"})

3. json (JSON 데이터)

데이터를 JSON 형식(직렬화된 객체)으로 몸체(Body)에 담아 보냅니다. 최근 REST API 통신에서 가장 많이 사용하는 방식입니다.

  • 용도: API 서버와 통신할 때, 복잡한 데이터 구조를 보낼 때.
  • Content-Type: application/json으로 자동 설정됩니다.
  • 형태: {"key1": "value1", "key2": "value2"} (파이썬 딕셔너리를 JSON 문자열로 변환)
  • 코드: requests.post(url, json={"username": "my_id", "password": "123"})
구분 params data json
전달 위치 URL 뒤 (Query String) 요청 본문 (Body) 요청 본문 (Body)
주 목적 데이터 필터링/요청 폼 데이터 전송 API 데이터 교환
데이터 형식 key=value key=value (URL-encoded) JSON 문자열
주요 헤더 없음 application/x-www-form-urlencoded application/json

 

* GET VS POST: 데이터 전송 방식 차이

구분 GET POST
주 목적 정보를 가져올 때 사용 정보를 보낼 때 사용
데이터 전달 URL 뒤에 데이터가 붙음 (예: ?id=123) 메시지 **몸체(Body)**에 데이터가 숨겨짐
보안성 낮음 (주소창에 다 보임) 상대적으로 높음 (Body에 숨겨짐)
주 사용처 검색, 페이지 이동 로그인, 회원가입, 글쓰기

 

 

 

 

 

 

 

 

 

 

 

 

——————————————————————————

본 후기는 [한글과컴퓨터x한국생산성본부x스나이퍼팩토리] 한컴 AI 아카데미 (B-log) 리뷰로 작성 되었습니다.