Python 강좌 / Beautiful Soup으로 HTML에서 데이터 추출하기

HTML에서 데이터를 추출할 때는 문서 파서로 요소를 선택하고 필요한 텍스트·속성을 읽습니다. 페이지 모양이나 선택자가 바뀌면 결과가 달라지므로 예상 항목 수와 필수 필드를 함께 검증해야 합니다.

정적 문서 파싱부터 시작하기

이 실습은 직접 만든 HTML 문자열을 파싱하며 웹사이트에 요청을 보내지 않습니다. HTML 파싱과 외부 사이트 수집은 다른 단계입니다. 외부 수집에서는 이용 조건·접근 권한·요청 빈도와 개인정보 취급도 확인해야 합니다.

관련 동작과 지원 조건은 HTML 문서에서 필요한 데이터 추출하기 관련 공식 문서에서 확인할 수 있습니다.

Beautiful Soup는 외부 라이브러리이며 여기서는 Python 표준 HTML 파서를 백엔드로 사용합니다. 설치 패키지명은 beautifulsoup4, import 이름은 bs4입니다.

python -m pip install beautifulsoup4

텍스트 패턴과 HTTP 응답의 차이은 정규 표현식으로 문자열 검색하고 추출하기에서 사용하는 규칙과 연결됩니다.

선택자나 서버 연결의 준비 과정은 CSS 강좌 / 관계 선택자와 가상 클래스로 요소 고르기 및 CSS 강좌 / 기본 선택자 - 요소, 클래스, ID, 속성의 해당 실습도 함께 확인할 수 있습니다.

전체 예제와 실행 결과

별도의 실습 폴더에서 아래 파일을 UTF-8로 저장합니다. 가상환경을 선택한 터미널에서 python parse_html.py 명령으로 실행합니다. macOS·Ubuntu에서 가상환경을 사용하지 않는 경우 명령 이름을 python3으로 바꿉니다.

parse_html.py 파일의 전체 내용입니다.

from bs4 import BeautifulSoup

html = """<ul id="expenses">
<li data-amount="3500"><span class="name">coffee</span></li>
<li data-amount="8000"><span class="name">lunch</span></li>
</ul>"""
soup = BeautifulSoup(html, "html.parser")
rows = soup.select("#expenses > li")
if len(rows) != 2:
    raise ValueError("unexpected row count")
total = 0
for row in rows:
    name = row.select_one(".name")
    amount_text = row.get("data-amount")
    if name is None or not amount_text or not amount_text.isascii() or not amount_text.isdecimal():
        raise ValueError("missing or invalid field")
    amount = int(amount_text)
    print(name.get_text(" ", strip=True), amount)
    total += amount
print("total:", total)

예상 결과는 다음과 같습니다.

coffee 3500
lunch 8000
total: 11500

CSS 선택자와 텍스트 추출

#expenses > li는 지정 ID의 바로 아래 li를 선택합니다. .name은 클래스 선택자입니다. select_one은 없으면 None을 반환하므로 바로 .text를 읽지 않고 검사합니다. get_text의 구분자와 strip은 중첩 요소의 텍스트가 붙어 보이거나 불필요한 공백이 남는 것을 조절합니다.

브라우저에서 보이는 데이터가 서버 HTML 안에 있다고 보장되지 않습니다. JavaScript가 나중에 만든 내용은 단순 HTTP 다운로드로 얻지 못할 수 있으므로 응답 본문과 실제 데이터 출처를 먼저 확인합니다.

Beautiful Soup가 보는 것은 전달한 HTML입니다. HTTP 응답에 없는 요소를 JavaScript가 나중에 만들면 select로 찾을 수 없습니다. 브라우저에서 보이는 화면과 응답 원문을 비교해 정적 HTML인지 먼저 확인합니다. 클래스 선택자와 자손·자식 관계를 구분하면 비슷한 요소가 여러 개 있어도 필요한 영역으로 범위를 좁힐 수 있습니다.

변경 감지와 실패 정책

선택 결과가 0개라고 정상적인 빈 목록으로 처리하면 사이트 구조 변경을 놓칠 수 있습니다. 이 실습은 예상 항목 수가 2라고 명시했고, 실제 서비스에서는 범위·필수 필드·수집 시각 등 합리적인 검증 기준을 정합니다.

  1. name 클래스를 바꿔 필수 필드 검사가 실패하는지 확인합니다.
  2. data-amount를 -1 또는 abc로 바꾸어 거부되는지 확인합니다.
  3. 중첩 태그를 이름 안에 추가해 텍스트가 읽히는지 확인하고 원래 구조로 되돌립니다.