웹페이지 표 엑셀 저장 — pandas 없이 파이썬으로 웹 표를 엑셀 파일에 바로 담기 (실측)

통계나 가격표처럼 웹페이지에 있는 표를 엑셀로 옮길 때, 마우스로 드래그해 붙여넣으면 병합된 칸이 어긋나고 “51,780,579” 같은 숫자가 글자로 들어가 합계가 안 되는 경우가 많습니다. 그래서 웹 표를 엑셀로 저장하는 파이썬 스크립트를 이 노트북(윈도우 11, 파이썬 3.12)에서 직접 만들어 돌려봤습니다. 흔히 쓰는 방법은 pandas의 read_html() 한 줄이지만, 이 노트북에서는 pandas를 설치하자마자 “애플리케이션 제어 정책에서 이 파일을 차단했습니다” 오류로 import조차 되지 않았습니다. 결국 표준 라이브러리 HTML 파서와 openpyxl만으로 만들었고, 한국어 위키백과 “대한민국의 인구” 페이지(HTML 95만 자)의 표 25개를 엑셀 시트 25개로 저장하는 데 1.51초가 걸렸습니다. 그 과정에서 만난 403 오류, 병합 칸 밀림, 부동소수점 오차까지 순서대로 정리합니다.

먼저 시도한 pandas.read_html — 설치는 됐는데 실행이 막혔다

pandas가 있으면 pd.read_html(url)이 페이지의 표를 전부 DataFrame 목록으로 돌려주고, to_excel()로 바로 저장할 수 있습니다. 가장 짧은 방법이라 이것부터 시도했습니다. 이 노트북에는 pandas가 없어서 설치부터 했는데, 두 번 막혔습니다.

$ pip install pandas openpyxl
bash: .../Python312/Scripts/pip: Permission denied

$ python -m pip install pandas openpyxl      (15.46초, pandas 3.0.6 설치됨)

$ python -X utf8 -c "import pandas"
  File "pandas/_libs/missing.pyx", line 40, in init pandas._libs.missing
ImportError: DLL load failed while importing ops_dispatch: 애플리케이션 제어 정책에서 이 파일을 차단했습니다.

첫 번째는 pip 실행 파일을 직접 부를 때 난 Permission denied였습니다. python -m pip로 바꾸니 15초 만에 설치됐습니다. 두 번째가 진짜 문제였습니다. pandas는 속도를 위해 내부를 C로 컴파일한 DLL을 쓰는데, 이 노트북의 윈도우 앱 제어 정책이 그 DLL을 차단했습니다. 이 블로그 자동화를 운영하면서 같은 정책이 썸네일용 네이티브 모듈(@napi-rs/canvas)도 막은 적이 있어서, 회사 PC처럼 보안 정책이 걸린 환경에서는 충분히 생길 수 있는 일이라고 봤습니다.

참고로 이 오류 메시지는 처음에 ���ø����̼� ���� ��å����처럼 깨져서 보였습니다. 파이썬을 -X utf8 옵션으로 실행하자 한글로 제대로 읽혔습니다. 오류 메시지가 깨져 보이면 이 옵션부터 붙여 보는 게 원인 파악에 빠릅니다.

반면 엑셀 파일을 쓰는 openpyxl은 순수 파이썬이라 import openpyxl이 문제없이 됐습니다(3.1.5). 그래서 방향을 바꿨습니다. HTML 해석은 표준 라이브러리 html.parser, 저장은 openpyxl로 직접 만들기로 했습니다.

페이지 내려받기: User-Agent가 없으면 403

표준 라이브러리 urllib로 페이지를 받아오는 것부터 확인했습니다. 아무 헤더 없이 요청하니 위키백과가 바로 403을 돌려줬습니다.

$ python fetch_test.py <위키백과 URL>                 (User-Agent 지정 안 함)
HTTPError 403 Forbidden
0.47초

$ python fetch_test.py <위키백과 URL> "codetipbox-table-test/1.0"
200 text/html; charset=UTF-8 985,707바이트
table 태그 25

$ python -c "import urllib.request; print(urllib.request.build_opener().addheaders)"
[('User-agent', 'Python-urllib/3.12')]

urllib은 기본으로 Python-urllib/3.12라는 User-Agent를 보내는데, 위키백과는 이런 기본값 요청을 막습니다. 스크립트 이름과 연락처를 담은 User-Agent를 넣자 200과 함께 98만 바이트짜리 HTML이 내려왔고, 그 안에 <table 태그가 25개 있었습니다. 다른 사이트도 비슷한 이유로 막는 경우가 많으니, 403이 나면 User-Agent부터 확인하세요. 단, 사이트 이용약관이나 robots.txt가 수집을 금지한다면 헤더를 바꿔서 우회할 일이 아닙니다.

병합된 칸(rowspan·colspan)을 풀지 않으면 생기는 일

실제 업무 표에는 “지역”처럼 여러 행에 걸친 칸(rowspan)이나, “매출” 아래 8월·9월이 있는 칸(colspan)이 흔합니다. 이를 시험하려고 다음 HTML을 직접 만들었습니다. 각주 <sup>와 위키백과식 마이너스 기호(−)도 넣었습니다.

<html><head><meta charset="utf-8"><title>지점별 매출</title></head>
<body>
<table>
  <caption>2026년 3분기 지점별 매출</caption>
  <tr><th rowspan="2">지역</th><th rowspan="2">지점</th><th colspan="2">매출(원)</th><th rowspan="2">증감</th></tr>
  <tr><th>8월</th><th>9월</th></tr>
  <tr><td rowspan="2">서울</td><td>강남</td><td>12,500,000</td><td>13,100,000</td><td>4.8%</td></tr>
  <tr><td>마포</td><td>8,200,000</td><td>7,950,000</td><td>&minus;3.0%</td></tr>
  <tr><td rowspan="2">부산</td><td>서면</td><td>9,800,000</td><td>10,050,000</td><td>2.6%</td></tr>
  <tr><td>해운대<sup>[1]</sup></td><td>6,400,000</td><td>6,400,000</td><td>0.0%</td></tr>
  <tr><td colspan="2">합계</td><td>36,900,000</td><td>37,500,000</td><td>1.6%</td></tr>
</table>
<p><sup>[1]</sup> 9월 중 리모델링 휴점 5일.</p>
</body></html>

먼저 셀을 나오는 순서대로만 담는 단순한 파서로 읽어 봤습니다.

"""rowspan/colspan을 무시하고 셀을 순서대로만 담는 단순 버전 (비교용)."""
import sys
from html.parser import HTMLParser
from pathlib import Path


class Naive(HTMLParser):
    def __init__(self):
        super().__init__()
        self.rows, self.cell = [], None

    def handle_starttag(self, tag, attrs):
        if tag == 'tr':
            self.rows.append([])
        elif tag in ('td', 'th'):
            self.cell = ''

    def handle_endtag(self, tag):
        if tag in ('td', 'th'):
            self.rows[-1].append(self.cell.strip())
            self.cell = None

    def handle_data(self, data):
        if self.cell is not None:
            self.cell += data


p = Naive()
p.feed(Path(sys.argv[1]).read_text(encoding='utf-8'))
for row in p.rows:
    print(row)
$ python naive.py sample.html
['지역', '지점', '매출(원)', '증감']
['8월', '9월']
['서울', '강남', '12,500,000', '13,100,000', '4.8%']
['마포', '8,200,000', '7,950,000', '−3.0%']
['부산', '서면', '9,800,000', '10,050,000', '2.6%']
['해운대[1]', '6,400,000', '6,400,000', '0.0%']
['합계', '36,900,000', '37,500,000', '1.6%']

결과가 그럴듯해 보이지만 엑셀에 그대로 넣으면 틀립니다. “마포” 행은 지역 칸이 비어 있어서 한 칸씩 왼쪽으로 밀렸고(지점 열에 매출이 들어감), 두 번째 머리글 행은 두 칸뿐이며, “해운대[1]”에는 각주 번호가 붙었습니다. HTML에서는 위 행의 rowspan이 그 칸을 차지하고 있어서 해당 <td> 자체가 없기 때문입니다. 표를 제대로 옮기려면 rowspan·colspan을 실제 칸으로 펼쳐야 합니다.

최종 스크립트: web_table_to_excel.py

표준 라이브러리와 openpyxl만 씁니다. 하는 일은 다음과 같습니다.

  1. URL이면 User-Agent를 붙여 내려받고, 파일이면 UTF-8 → CP949 순으로 읽습니다.
  2. HTMLParser로 <table>마다 행·칸·caption을 모읍니다. <sup>(각주), <script>, <style> 안의 글자는 버립니다.
  3. to_grid()가 rowspan은 아래 행의 같은 위치에 값을 미리 예약해 두고, colspan은 옆 칸에 같은 값을 채워 빈틈없는 2차원 표로 만듭니다.
  4. to_value()가 “12,500,000”은 정수, “4.8%”는 0.048(엑셀 서식 0.0%), “−3.0%”는 -0.03으로 바꿉니다. 나머지는 글자 그대로 둡니다.
  5. 표마다 시트를 하나씩 만들고, 첫 행 굵게·틀 고정·열 너비 자동 조정을 해서 저장합니다.
"""웹페이지(또는 저장한 HTML 파일)의 표를 엑셀로 저장합니다. 표준 라이브러리 + openpyxl.

사용법:
  python web_table_to_excel.py <URL 또는 파일> --list
  python web_table_to_excel.py <URL 또는 파일> 결과.xlsx [--match 글자] [--pick 0,3]
"""
import argparse
import re
import sys
import time
import urllib.request
from html.parser import HTMLParser
from pathlib import Path

from openpyxl import Workbook
from openpyxl.styles import Font

UA = 'Mozilla/5.0 (table-to-excel script; +https://codetipbox.com)'
SKIP_TAGS = {'style', 'script', 'sup'}  # sup: 위키백과 각주 [1] 같은 것


class TableParser(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.tables = []   # [{'caption': str, 'rows': [[(text, colspan, rowspan, is_th)]]}]
        self.stack = []    # 중첩 표 대비
        self.cell = None
        self.skip = 0
        self.in_caption = False

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag in SKIP_TAGS:
            self.skip += 1
        elif tag == 'table':
            self.stack.append({'caption': '', 'rows': []})
        elif not self.stack:
            return
        elif tag == 'caption':
            self.in_caption = True
        elif tag == 'tr':
            self.stack[-1]['rows'].append([])
        elif tag in ('td', 'th'):
            self.cell = {'text': [], 'colspan': _int(a.get('colspan')), 'rowspan': _int(a.get('rowspan')), 'th': tag == 'th'}
        elif tag == 'br' and self.cell is not None:
            self.cell['text'].append(' ')

    def handle_endtag(self, tag):
        if tag in SKIP_TAGS:
            self.skip = max(0, self.skip - 1)
        elif tag == 'table' and self.stack:
            self.tables.append(self.stack.pop())
        elif tag == 'caption':
            self.in_caption = False
        elif tag in ('td', 'th') and self.cell is not None and self.stack:
            rows = self.stack[-1]['rows']
            if not rows:
                rows.append([])
            text = re.sub(r'\s+', ' ', ''.join(self.cell['text'])).strip()
            rows[-1].append((text, self.cell['colspan'], self.cell['rowspan'], self.cell['th']))
            self.cell = None

    def handle_data(self, data):
        if self.skip:
            return
        if self.cell is not None:
            self.cell['text'].append(data)
        elif self.in_caption and self.stack:
            self.stack[-1]['caption'] += data.strip()


def _int(v):
    try:
        return max(1, int(str(v).strip()))
    except (TypeError, ValueError):
        return 1


def to_grid(rows):
    """colspan/rowspan을 풀어서 빈칸 없는 2차원 리스트로 만듭니다."""
    grid, pending = [], {}  # pending[(r, c)] = 위에서 rowspan으로 내려온 값
    for r, row in enumerate(rows):
        out, c = [], 0
        cells = iter(row)
        while True:
            if (r, c) in pending:
                out.append(pending.pop((r, c)))
                c += 1
                continue
            cell = next(cells, None)
            if cell is None:
                break
            text, colspan, rowspan, _ = cell
            for dc in range(colspan):
                out.append(text)
                for dr in range(1, rowspan):
                    pending[(r + dr, c + dc)] = text
            c += colspan
        grid.append(out)
    width = max((len(r) for r in grid), default=0)
    return [r + [''] * (width - len(r)) for r in grid]


NUM = re.compile(r'^[+-]?\d{1,3}(,\d{3})+(\.\d+)?$|^[+-]?\d+(\.\d+)?$')


def to_value(text):
    """'51,780,579' -> 51780579, '-0.3' -> -0.3, 나머지는 글자 그대로."""
    t = text.replace('−', '-')  # 위키백과의 마이너스 기호(−)
    if NUM.match(t):
        t = t.replace(',', '')
        return float(t) if '.' in t else int(t)
    if t.endswith('%') and NUM.match(t[:-1]):
        return round(float(t[:-1].replace(',', '')) / 100, 10)  # 2.6/100 = 0.026000000000000002 방지
    return text


def display_len(s):
    """한글·한자는 영문 두 글자 폭으로 셉니다."""
    return sum(2 if ord(ch) >= 0x1100 else 1 for ch in s)


def load(src):
    if re.match(r'https?://', src):
        req = urllib.request.Request(src, headers={'User-Agent': UA})
        with urllib.request.urlopen(req, timeout=30) as res:
            charset = res.headers.get_content_charset() or 'utf-8'
            return res.read().decode(charset, errors='replace')
    raw = Path(src).read_bytes()
    for enc in ('utf-8', 'cp949'):
        try:
            return raw.decode(enc)
        except UnicodeDecodeError:
            pass
    return raw.decode('utf-8', errors='replace')


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('src')
    ap.add_argument('out', nargs='?')
    ap.add_argument('--list', action='store_true', help='표 목록만 보기')
    ap.add_argument('--match', help='이 글자가 들어간 표만')
    ap.add_argument('--pick', help='번호로 고르기 (예: 0,3)')
    ap.add_argument('--min-rows', type=int, default=2)
    args = ap.parse_args()

    t0 = time.perf_counter()
    html = load(args.src)
    t1 = time.perf_counter()
    parser = TableParser()
    parser.feed(html)
    tables = []
    for t in parser.tables:
        grid = to_grid(t['rows'])
        if len(grid) >= args.min_rows and grid and len(grid[0]) >= 2:
            tables.append((t['caption'], grid))
    t2 = time.perf_counter()
    print(f'HTML {len(html):,}자, 표 {len(tables)}개 (불러오기 {t1 - t0:.2f}초, 분석 {t2 - t1:.2f}초)')

    picked = list(enumerate(tables))
    if args.match:
        picked = [(i, t) for i, t in picked if any(args.match in cell for row in t[1] for cell in row) or args.match in t[0]]
    if args.pick:
        nums = {int(n) for n in args.pick.split(',')}
        picked = [(i, t) for i, t in picked if i in nums]

    if args.list or not args.out:
        for i, (cap, grid) in picked:
            head = ' | '.join(grid[0][:4])
            print(f'[{i}] {len(grid)}행 x {len(grid[0])}열  {cap[:30]}  첫 줄: {head[:60]}')
        return

    if not picked:
        sys.exit('고른 표가 없습니다. --list로 번호를 확인하세요.')
    wb = Workbook()
    wb.remove(wb.active)
    for i, (cap, grid) in picked:
        name = re.sub(r'[\\/*?:\[\]]', '', cap)[:20] or f'표{i}'
        ws = wb.create_sheet(f'{i}_{name}'[:31])
        for row in grid:
            ws.append([to_value(v) for v in row])
            for cell, text in zip(ws[ws.max_row], row):
                if text.endswith('%') and isinstance(cell.value, float):
                    cell.number_format = '0.0%'
        for cell in ws[1]:
            cell.font = Font(bold=True)
        ws.freeze_panes = 'A2'
        for col in ws.columns:
            longest = max(display_len(str(c.value or '')) for c in col)
            ws.column_dimensions[col[0].column_letter].width = min(50, longest * 1.1 + 2)
        print(f'시트 {ws.title}: {len(grid)}행 x {len(grid[0])}열')
    wb.save(args.out)
    print(f'저장: {args.out} ({Path(args.out).stat().st_size:,}바이트), 전체 {time.perf_counter() - t0:.2f}초')


if __name__ == '__main__':
    main()

표가 여러 개인 페이지가 대부분이라 --list로 번호와 첫 줄을 먼저 보고, --pick 0,4로 번호를 고르거나 --match 글자로 특정 글자가 든 표만 고르게 했습니다. 행이 1개뿐이거나 열이 1개뿐인 표(레이아웃용 표가 흔함)는 기본으로 뺍니다.

테스트 표로 확인: 칸 밀림이 사라졌는지

앞의 sample.html을 엑셀로 저장한 뒤, openpyxl로 파일을 다시 열어 셀 값을 그대로 출력했습니다.

$ python web_table_to_excel.py sample.html sample.xlsx
HTML 765자, 표 1개 (불러오기 0.00초, 분석 0.00초)
시트 0_2026년 3분기 지점별 매출: 7행 x 5열
저장: sample.xlsx (5,362바이트), 전체 0.01초

$ python check_xlsx.py sample.xlsx 7          (엑셀을 다시 열어 셀 값 확인, 따옴표 표기만 정리)
['지역', '지점', '매출(원)', '매출(원)', '증감']
['지역', '지점', '8월', '9월', '증감']
['서울', '강남', 12500000, 13100000, 0.048]
['서울', '마포', 8200000, 7950000, -0.03]
['부산', '서면', 9800000, 10050000, 0.026]
['부산', '해운대', 6400000, 6400000, 0]
['합계', '합계', 36900000, 37500000, 0.016]
값 종류: {'str': 15, 'int': 11, 'float': 4}

“마포” 행 앞에 “서울”이 채워져 다섯 칸이 정확히 맞고, 매출은 정수(int), 증감은 소수(float)로 들어갔습니다. 엑셀에서 증감 열은 0.0% 서식이 걸려 4.8%로 보이고, 매출 열은 바로 SUM이 됩니다. 각주 [1]도 빠졌습니다.

다만 이 결과가 처음부터 나온 건 아닙니다. 중간에 두 가지를 고쳤습니다.

  • 열 너비: 처음엔 글자 수에 1.6을 곱했더니 “해운대” 열 너비가 6.8로 나와 한글 세 글자가 다 안 들어갔습니다. 한글은 영문 두 글자 폭이라 display_len()에서 한글을 2칸으로 세도록 바꿨습니다.
  • 증감 열 너비가 24: 다른 열은 6~11인데 증감 열만 24로 넓어졌습니다. 확인해 보니 퍼센트를 100으로 나눌 때 생긴 부동소수점 오차 때문이었습니다.
$ python float_demo.py
4.8% -> 0.048  (round: 0.048)
2.6% -> 0.026000000000000002  (round: 0.026)
1.6% -> 0.016  (round: 0.016)
12.4% -> 0.124  (round: 0.124)

2.6을 100으로 나누면 0.026이 아니라 0.026000000000000002가 되고, 이 긴 숫자 길이로 너비를 계산했던 겁니다. 엑셀 화면에는 2.6%로 보이니 그냥 넘어가기 쉽지만, 값을 비교하거나 다른 프로그램으로 넘기면 문제가 될 수 있어 round(..., 10)으로 정리했습니다. 수정 후 증감 열 너비는 7.5가 됐습니다.

실제 페이지: 위키백과 표 25개를 1.51초에

이제 실제 페이지에 돌렸습니다. 먼저 목록부터 봅니다.

$ python web_table_to_excel.py "https://ko.wikipedia.org/wiki/대한민국의_인구" --list
HTML 950,447자, 표 25개 (불러오기 1.11초, 분석 0.17초)
[0] 13행 x 3열  연도별 인구  첫 줄: 연도 | 인구 | ±%
[1] 27행 x 5열    첫 줄: 연령 | 남자 | 여자 | 명
[4] 21행 x 9열    첫 줄: 연도 (년) | 추계인구(명) | 출생자수(명) | 사망자수(명)
[12] 22행 x 9열    첫 줄:  | 2020 통계청 인구주택총조사 | 2020 통계청 인구주택총조사 | ...
[16] 292행 x 11열    첫 줄:  | 가구주의 연령 | 2020 행정구역별 가구주의 연령 및 가구원수별 가구(일반가구) | ...
[20] 53행 x 3열  혼인건수, 조혼인율  첫 줄: 시점 | 혼인건수(건) | 조혼인율(천명당)
[24] 6행 x 3열    첫 줄: vte아시아의 인구 | vte아시아의 인구 | vte아시아의 인구
(일부 생략)

HTML 95만 자를 파싱해 표 25개를 찾는 데 0.17초, 내려받는 데 1.11초였습니다. 처음 시험할 때는 내려받기만 6.99초가 걸린 적도 있어서, 전체 시간은 대부분 네트워크가 좌우한다고 보면 됩니다. 필요한 표만 고르거나 전부 저장해 봤습니다.

$ python web_table_to_excel.py "<같은 URL>" population.xlsx --pick 0,4,12
HTML 950,447자, 표 25개 (불러오기 1.12초, 분석 0.17초)
시트 0_연도별 인구: 13행 x 3열
시트 4_표4: 21행 x 9열
시트 12_표12: 22행 x 9열
저장: population.xlsx (9,636바이트), 전체 1.31초

$ python web_table_to_excel.py "<같은 URL>" all.xlsx
HTML 950,447자, 표 25개 (불러오기 1.11초, 분석 0.19초)
(시트 25개)
저장: all.xlsx (62,216바이트), 전체 1.51초

25개 시트를 모두 저장해도 1.51초, 파일은 62KB였습니다. 저장한 엑셀을 다시 열어 내용을 확인했습니다.

$ python check_xlsx.py pop0.xlsx 13          (일부 행만 발췌, 따옴표 표기만 정리)
== 0_연도별 인구 (13행 x 3열)
['연도', '인구', '±%']
[1930, 13900805, '—']
[1940, 15627186, 0.124]
[2020, 51829136, 0.067]
[2025, 51667000, -0.003]
['출처:', '출처:', '출처:']

== 16_표16 (292행 x 11열)
[None, '가구주의 연령', '2020 행정구역별 가구주의 연령 및 가구원수별 가구(일반가구)', ...]
[None, '가구주의 연령', '일반가구', '가구원수 1명', '가구원수 2명']
['전국', '합계', 20926710, 6643354, 5864525]
['서울특별시', '합계', 3982290, 1390701, 1033901]
값 종류: {'NoneType': 1, 'str': 799, 'int': 2165, 'float': 236}

연도별 인구는 정수로, 12.4% 증가는 0.124로, 위키백과가 쓰는 유니코드 마이너스 “−0.3%”도 -0.003으로 제대로 들어갔습니다. 292행짜리 가구 통계 표도 rowspan으로 묶인 “서울특별시”가 각 행에 채워져 있어서, 엑셀에서 바로 필터나 피벗 테이블을 걸 수 있습니다. 이 표 안의 정수 셀 2,165개가 모두 숫자로 들어갔습니다.

결과를 쓰기 전에 확인할 것

자동으로 옮겨도 사람 손이 조금 필요한 부분이 있었습니다. 실제 결과에서 확인한 것만 정리합니다.

현상실제 예처리
머리글이 2줄인 표표 16 첫 줄이 같은 제목으로 반복첫 줄을 지우거나 두 줄을 합쳐 열 이름 만들기
colspan 바닥글연도별 인구 마지막 줄 “출처:” 3칸 복제마지막 행 삭제
데이터 없음 표시“—”가 글자로 남음숫자 열 계산 전에 빈칸으로 바꾸기
데이터가 아닌 표표 24는 하단 내비게이션 상자–list로 보고 –pick으로 제외
페이지 수정위키백과는 편집되면 표 번호·행 수가 바뀜정기 실행이면 –pick 대신 –match 사용
  • 자바스크립트로 나중에 그려지는 표는 이 방법으로 안 잡힙니다. 내려받은 HTML에 <table>이 실제로 있는지 --list로 먼저 확인하세요. 0개라면 브라우저 자동화 도구가 필요합니다.
  • 로그인이 필요한 페이지는 브라우저에서 “다른 이름으로 저장”한 HTML 파일을 넣으면 됩니다. 스크립트는 URL 대신 파일 경로도 받습니다.
  • 같은 사이트를 짧은 간격으로 반복 요청하지 마세요. 표 몇 개를 받는 데는 한 번 요청이면 충분합니다.
  • 회사 PC처럼 보안 정책이 있는 환경에서는 pandas·numpy 같은 컴파일된 패키지가 막힐 수 있습니다. 순수 파이썬 패키지(openpyxl 등)와 표준 라이브러리 조합이 가장 확실하게 돌아갔습니다.

pandas를 쓸 수 있다면

보안 정책이 없는 PC라면 pandas가 더 짧습니다. pd.read_html()도 rowspan·colspan을 펼쳐 주고, to_excel()로 저장할 수 있습니다. 다만 이 노트북에서는 import가 막혀 실행해 보지 못했기 때문에 결과 비교는 하지 않았습니다. 이 글의 스크립트는 pandas가 막혔거나, 설치를 최소화하고 싶거나, 숫자·퍼센트 변환과 시트 서식을 직접 제어하고 싶을 때 쓰면 됩니다.

정리

항목실측 결과
pandas 3.0.6python -m pip로 15.46초에 설치, import는 앱 제어 정책에 차단
기본 urllib 요청위키백과 403 Forbidden, User-Agent 지정 후 200
병합 칸 무시“마포” 행이 한 칸 왼쪽으로 밀림
최종 스크립트테스트 표 7행 x 5열 정확히 복원, 숫자·퍼센트 변환
위키백과 “대한민국의 인구”HTML 95만 자, 표 25개, 전체 저장 1.51초(62KB)

가장 작은 첫걸음은 위 web_table_to_excel.py를 저장하고, 자주 보는 표가 있는 페이지 주소로 python web_table_to_excel.py 주소 --list를 한 번 돌려 보는 것입니다. 파일은 아무것도 만들어지지 않고 표 목록만 나오니, 원하는 번호를 확인한 뒤 결과.xlsx --pick 번호를 붙이면 됩니다.

다음 글에서는 JSON 파일을 명령줄에서 다루는 방법(jq 입문)을 다루겠습니다.

댓글 남기기