메뉴 닫기

파이썬 BeautifulSoup 테이블 파싱 thead tbody tr td 순회와 rowspan colspan 처리 방법

파이썬 BeautifulSoup 테이블 파싱 thead tbody tr td 순회와 rowspan colspan 처리 방법

🐍 HTML 테이블을 깔끔하게 파싱하는 파이썬 BeautifulSoup 활용법

웹 크롤링을 하다 보면 뉴스 기사, 주식 데이터, 공공 데이터 페이지 등 다양한 곳에서 HTML 테이블을 마주치게 됩니다.
하지만 단순히 <table> 태그를 읽는 것만으로는 데이터를 제대로 가져오기 어렵습니다.
특히 <thead>, <tbody>, <tr>, <td> 구조와 함께 rowspan이나 colspan이 사용된 경우는 파싱 로직을 더 세심하게 짜야 합니다.
이번 글에서는 이런 복잡한 테이블을 파이썬 BeautifulSoup으로 다루는 방법을 정리해 보겠습니다.

단순한 크롤링 코드에서부터 시작해, 표의 구조를 순차적으로 탐색하고, 병합된 셀을 보정하는 방법까지 차근차근 살펴봅니다.
이 과정을 이해하면 금융 데이터 수집, 행정 문서 자동화, 통계 정보 파싱 등 실무에도 활용할 수 있는 강력한 기술을 갖추게 될 것입니다.
또한 실제 코드 예제와 함께 에러를 줄이는 팁도 소개하니, 기초부터 응용까지 단계별로 따라와 보세요.



🔗 BeautifulSoup 테이블 파싱 기본 원리

HTML에서 <table> 태그는 데이터를 행과 열의 구조로 담기 위해 사용됩니다.
파이썬 BeautifulSoup은 이 태그를 기반으로 데이터를 파싱할 수 있도록 다양한 기능을 제공합니다.
기본적으로 테이블은 <thead>, <tbody>, <tr>, <td> 계층 구조로 나눌 수 있으며, 이를 이해하는 것이 첫 단계입니다.

보통 헤더 정보는 <thead> 안에 포함되고, 실제 데이터 행은 <tbody>에 배치됩니다.
각 행은 <tr>로, 그 안의 셀은 <td> 또는 <th>로 표현됩니다.
따라서 테이블을 읽기 위해서는 우선 테이블 요소 전체를 찾아낸 뒤, 순서대로 행과 셀을 탐색해야 합니다.

  • 🔍find() 또는 find_all() 메서드로 <table> 요소 선택
  • 📑<thead><tbody>를 구분해 헤더와 본문 데이터 나누기
  • ➡️각 행(<tr>)을 순회하며 셀(<td>) 추출

아래 예시는 BeautifulSoup으로 HTML 테이블을 단순히 파싱하는 기본 구조입니다.

CODE BLOCK
from bs4 import BeautifulSoup

html = """
<table>
    <thead>
        <tr><th>이름</th><th>나이</th></tr>
    </thead>
    <tbody>
        <tr><td>홍길동</td><td>30</td></tr>
        <tr><td>김철수</td><td>25</td></tr>
    </tbody>
</table>
"""

soup = BeautifulSoup(html, "html.parser")
table = soup.find("table")
rows = table.find_all("tr")

for row in rows:
    cols = [col.get_text(strip=True) for col in row.find_all(["td", "th"])]
    print(cols)

이처럼 기본적인 계층 구조를 이해하고 순서대로 접근하면, 테이블 데이터를 리스트 형태로 손쉽게 뽑아낼 수 있습니다.
다만, rowspan이나 colspan이 적용된 복잡한 테이블은 단순 반복문만으로는 정확한 파싱이 어렵기 때문에, 다음 단계에서 보정 로직을 함께 다뤄보겠습니다.

🛠️ thead tbody tr td 순차 순회 방법

테이블 파싱의 핵심은 헤더와 본문을 분리하고, 각 행과 열을 순차적으로 순회하는 과정입니다.
단순히 find_all("tr")로 모든 행을 모으는 것보다, <thead><tbody>를 구분해 관리하면 데이터 구조가 더 명확해집니다.
예를 들어, <thead>에서 헤더명을 추출한 후, <tbody>에서 같은 순서로 데이터를 매칭하면 엑셀이나 데이터프레임으로 변환하기에도 유리합니다.

📑 헤더와 본문 분리

일반적으로 <thead>는 한 번만 등장하고, 내부에 <tr>이 있어 컬럼명을 정의합니다.
반면 <tbody>는 여러 <tr>을 포함하고 있으며, 각 행이 데이터 레코드가 됩니다.
따라서 크롤링 시 헤더를 먼저 읽고, 이후에 본문 데이터를 매칭하는 방식이 안정적입니다.

CODE BLOCK
table = soup.find("table")

# thead 추출
headers = [th.get_text(strip=True) for th in table.find("thead").find_all("th")]

# tbody 추출
body_rows = table.find("tbody").find_all("tr")

for row in body_rows:
    cols = [td.get_text(strip=True) for td in row.find_all("td")]
    print(dict(zip(headers, cols)))

➡️ 순차 순회의 장점

순차적으로 순회하는 방법을 사용하면, 데이터 누락을 방지하고 헤더와 값이 정확히 매칭됩니다.
또한 pandas 데이터프레임으로 변환할 때도 key-value 형태의 딕셔너리로 바로 매핑할 수 있어 후처리가 간단합니다.
이 방식은 특히 컬럼이 많은 표를 다룰 때 큰 장점이 있습니다.

💡 TIP: 일부 테이블에는 <tbody> 태그가 생략된 경우가 있습니다.
이때는 find_all("tr")을 사용하여 전체 행을 가져온 후, 첫 번째 행을 헤더로 처리하는 방식으로 보완하면 됩니다.



⚙️ rowspan과 colspan 보정 로직 구현

HTML 테이블 파싱에서 가장 까다로운 부분은 rowspancolspan입니다.
이 속성은 하나의 셀이 여러 행이나 열을 차지하도록 만들어 주는데, 단순히 get_text()로 읽으면 데이터가 누락되거나 구조가 어긋날 수 있습니다.
따라서 파싱 과정에서 셀 병합을 풀어내는 보정 로직이 반드시 필요합니다.

📐 rowspan 처리

rowspan은 특정 셀이 아래 행까지 확장되는 경우입니다.
이를 보정하려면, 해당 셀의 값을 이후 행에도 채워 넣어야 합니다.
예를 들어 3행에 걸쳐 rowspan이 적용된 경우, 원래의 값이 이후 2개의 행에도 복사되어야 합니다.

📏 colspan 처리

colspan은 하나의 셀이 여러 열을 차지하는 경우입니다.
이때는 해당 셀의 값을 여러 번 반복하여 배열에 넣어 주어야 전체 열 수가 맞게 정렬됩니다.
이 로직을 적용하지 않으면 데이터프레임 변환 시 컬럼 수가 맞지 않아 에러가 발생할 수 있습니다.

CODE BLOCK
table = soup.find("table")
rows = table.find_all("tr")

parsed = []
span_map = {}

for row_idx, row in enumerate(rows):
    cols = []
    col_idx = 0
    for cell in row.find_all(["td", "th"]):
        # rowspan, colspan 추출
        rowspan = int(cell.get("rowspan", 1))
        colspan = int(cell.get("colspan", 1))
        value = cell.get_text(strip=True)

        # colspan 반영
        for i in range(colspan):
            while (row_idx, col_idx) in span_map:
                cols.append(span_map.pop((row_idx, col_idx)))
                col_idx += 1
            cols.append(value)
            # rowspan 반영
            for j in range(1, rowspan):
                span_map[(row_idx + j, col_idx)] = value
            col_idx += 1
    parsed.append(cols)

for r in parsed:
    print(r)

⚠️ 주의: rowspan과 colspan 보정 로직을 적용하지 않으면, 표의 데이터가 비정상적으로 밀리거나 누락될 수 있습니다.
특히 공공데이터나 금융 통계처럼 표 구조가 복잡한 경우 반드시 이 과정을 거쳐야 합니다.

🔌 데이터프레임 변환과 활용

테이블 데이터를 추출했다면, 실제 분석이나 저장을 위해서는 pandas 데이터프레임으로 변환하는 과정이 필요합니다.
데이터프레임은 행과 열 구조가 명확하기 때문에, 추출한 리스트 데이터를 바로 넣을 수 있습니다.
특히 헤더를 먼저 추출해두면 컬럼명을 지정하여 깔끔한 구조를 유지할 수 있습니다.

📊 pandas로 변환하기

앞서 보정한 리스트 형태의 데이터를 pandas.DataFrame으로 변환하면 다양한 데이터 처리 기능을 활용할 수 있습니다.
이를 통해 데이터 정제, 필터링, 통계 분석, 시각화까지 확장할 수 있습니다.

CODE BLOCK
import pandas as pd

# 예시 데이터 (보정된 테이블)
headers = ["이름", "나이"]
data = [
    ["홍길동", "30"],
    ["김철수", "25"]
]

df = pd.DataFrame(data, columns=headers)
print(df)

# CSV 저장
df.to_csv("output.csv", index=False, encoding="utf-8-sig")

📂 실무 활용 예시

테이블 데이터를 데이터프레임으로 변환하면 실무에서 다음과 같이 활용할 수 있습니다.

  • 📈금융 데이터 수집 후 시계열 분석 적용
  • 📊공공데이터를 표준 포맷으로 CSV 저장
  • 🔎웹 크롤링 데이터를 데이터 시각화로 가공

즉, BeautifulSoup으로 추출한 데이터를 pandas로 연결하면, 데이터 분석과 머신러닝 전처리까지 확장할 수 있는 완성형 워크플로우를 구축할 수 있습니다.



💡 에러 방지와 코드 최적화 팁

현장에서 테이블 파싱을 운영하다 보면 페이지 구조 변경, 네트워크 지연, 인코딩 문제 등 다양한 이슈가 발생합니다.
사전에 예외를 설계하고, 파서를 적절히 선택하며, 불필요한 DOM 탐색을 줄이면 속도와 안정성이 모두 개선됩니다.
또한 rowspancolspan 보정 로직은 테스트 케이스로 반복 검증하는 것이 중요합니다.
페이지마다 <tbody> 생략, 중첩 테이블, 숨김(display:none) 열처럼 예외가 숨어 있으니, 조건 분기와 방어적 코드를 기본으로 두면 장애를 크게 줄일 수 있습니다.

  • ⏱️요청 타임아웃과 재시도(backoff) 설정으로 네트워크 지연 대비
  • 🧩파서 선택: html.parser, lxml, html5lib의 장단점 이해
  • 🧼텍스트 정제: get_text(strip=True)와 공백·개행 정리
  • 🛡️존재 확인: find() 결과가 None일 때의 방어 로직
  • 🧪샘플 HTML로 rowspan/colspan 단위 테스트 작성
  • 🚫숨김 열(style="display:none")과 중첩 테이블 필터링
CODE BLOCK
import requests, time, random
from bs4 import BeautifulSoup

def fetch(url, retries=3, timeout=8):
    for i in range(retries):
        try:
            r = requests.get(url, headers={"User-Agent":"Mozilla/5.0"}, timeout=timeout)
            r.raise_for_status()
            r.encoding = r.apparent_encoding
            return r.text
        except Exception as e:
            if i == retries - 1:
                raise
            time.sleep(1.2 * (i + 1) + random.random())

html = fetch("https://example.com/table")
soup = BeautifulSoup(html, "lxml")  # 파싱 정확도와 속도의 균형

table = soup.find("table")
if not table:
    raise ValueError("table 요소를 찾을 수 없습니다")

# display:none 셀 제거
for hidden in table.select('[style*="display:none"]'):
    hidden.decompose()

# 중첩 테이블 무시
nested = table.find_all("table")
for nt in nested[1:]:
    nt.decompose()

# 이후는 앞서 구현한 rowspan/colspan 보정 로직 적용

파서 장점 주의점
html.parser 표준 라이브러리, 설치 불필요 복잡한 DOM에서 정확도 낮을 수 있음
lxml 빠르고 정확, 대형 문서에 유리 별도 설치 필요
html5lib 브라우저 수준 복원력 상대적으로 느림

⚠️ 주의: 동적으로 렌더링되는 페이지는 초기 HTML에 테이블이 없을 수 있습니다.
이때는 서버 사이드 렌더링 엔드포인트를 찾거나, 네트워크 패널로 실제 데이터 API를 확인한 뒤 JSON을 직접 수집하는 전략이 더 안정적입니다.
무리하게 렌더링 결과를 파싱하려고 하면 성능 저하와 불안정이 커집니다.

💡 TIP: 보정 로직의 정확도를 높이려면 최소 5개 이상의 형태가 다른 표 샘플을 준비해 자동 테스트를 돌리세요.
헤더 병합, 첫 열 병합, 중간 열 병합, 빈 셀 포함, 중첩 테이블 등 시나리오를 포괄하면 운영 중 구조 변경에도 강해집니다.

자주 묻는 질문 (FAQ)

BeautifulSoup으로 테이블을 파싱할 때 가장 먼저 해야 할 일은 무엇인가요?
우선 <table> 요소를 찾고, <thead><tbody>를 구분하여 구조를 파악하는 것이 중요합니다.
thead와 tbody를 구분하지 않고 tr만 순회해도 되나요?
가능은 하지만, 헤더와 데이터를 정확히 매칭하기 어렵습니다. 가급적 theadtbody를 구분하는 것이 안정적입니다.
rowspan은 어떤 방식으로 처리해야 하나요?
rowspan이 적용된 셀은 아래 행까지 확장되므로, 같은 값을 해당 행들에 복사하여 채워 넣어야 올바른 데이터 구조가 유지됩니다.
colspan은 어떻게 보정하나요?
colspan은 하나의 셀이 여러 열을 차지하는 경우이므로, 같은 값을 여러 번 반복해 리스트에 넣어 열 개수를 맞춰야 합니다.
데이터프레임 변환은 꼭 필요한가요?
필수는 아니지만, 데이터프레임으로 변환하면 CSV 저장, 통계 처리, 시각화 등 다양한 작업에 바로 활용할 수 있습니다.
tbody 태그가 없는 경우는 어떻게 처리하나요?
일부 HTML에는 tbody가 생략되어 있습니다. 이 경우에는 tr 전체를 불러와 첫 행을 헤더로 간주하고 나머지를 데이터로 처리하면 됩니다.
lxml, html.parser, html5lib 중 어떤 파서를 쓰는 게 좋나요?
일반적으로 lxml이 속도와 정확성 면에서 균형이 좋습니다. 다만 환경 제약이나 호환성 문제에 따라 다른 파서를 선택할 수도 있습니다.
동적 렌더링된 테이블은 어떻게 가져오나요?
자바스크립트로 렌더링되는 경우에는 BeautifulSoup만으로는 불가능합니다. 네트워크 요청을 분석해 API 엔드포인트를 직접 호출하는 방식이 권장됩니다.

🧾 BeautifulSoup 테이블 파싱 핵심 체크포인트 총정리

이번 글에서는 파이썬 BeautifulSoup으로 <table>을 안정적으로 파싱하는 전 과정을 정리했습니다.
핵심은 thead · tbody · tr · td 구조를 정확히 이해하고, 헤더와 본문을 분리한 뒤 순차적으로 행과 열을 순회하는 것입니다.
실무에서 자주 만나는 rowspancolspan은 값 누락과 열 불일치를 만들기 쉬우므로, 셀 병합을 해제하는 보정 로직을 별도로 구현해야 합니다.
또한 숨김 열, 중첩 테이블, tbody 생략 같은 예외를 방어적으로 처리하면 파싱 품질이 크게 향상됩니다.
마지막으로 보정된 리스트를 pandas DataFrame으로 변환하면 저장과 분석이 쉬워지고, CSV 내보내기와 시각화, 후속 전처리까지 매끄럽게 이어집니다.
위의 원칙과 예제 코드를 바탕으로 자신만의 모듈을 만들어두면, 다양한 도메인의 표 데이터를 일관된 방식으로 수집·가공할 수 있습니다.


🏷️ 관련 태그 : BeautifulSoup, Python웹크롤링, HTML테이블파싱, thead_tbody_tr_td, rowspan보정, colspan보정, pandas데이터프레임, 웹스크래핑, 데이터전처리, 크롤링팁