Geonode logo
Geonode Team

Geonode Team

Dikemas kini: 7 Oktober 2026

Diterbitkan: 2 September 2026

pandas.read_html(): Panduan Lengkap dengan Contoh

`pandas.read_html()` mengekstrak tabel HTML menjadi DataFrame dalam satu baris kode. Fungsi ini benar-benar berguna, namun juga merupakan fungsi yang paling mungkin membuat pemula salah mengira bahwa ekstraksi data web itu mudah. Fungsi ini mengembalikan *daftar* DataFrame, bukan hanya satu. Fungsi ini hanya mendeteksi elemen `<table>`. Dan dokumentasinya sendiri menyebutkan bahwa Anda perlu melakukan pembersihan data setelahnya. Panduan ini membahas apa yang dilakukannya, parameter-parameter yang perlu diketahui, serta situasi di mana alat lain merupakan pilihan yang lebih tepat.

Sikap kami: kami adalah Geonode dan kami menjual layanan proxy, sehingga tabel-tabel di web publik berkaitan erat dengan bisnis kami. Hal yang perlu diperhatikan adalah bahwa read_html saat mengambil URL secara langsung tidak memberi Anda kendali atas permintaan tersebut — tidak ada header khusus, tidak ada sesi, tidak ada logika percobaan ulang, dan tidak ada cara untuk mengarahkan permintaan tersebut melalui apa pun. Untuk tabel satu kali di situs yang kooperatif, hal itu tidak masalah dan merupakan cara tercepat yang tersedia. Untuk hal apa pun yang berulang, ambil HTML-nya sendiri dengan klien HTTP yang tepat dan kirimkan string tersebut ke read_html. Pemisahan tersebut hanya membutuhkan satu baris kode tambahan dan memberi Anda semua yang tidak disediakan oleh fungsi fetch bawaan.

Dasar-dasar

import pandas as pd

tables = pd.read_html("https://example.com/data")
print(len(tables))
df = tables[0]

Detail penting, dari dokumentasi pandas: fungsi ini mengembalikan "daftar DataFrame". Bukan satu DataFrame. Sebuah halaman dengan enam tabel akan menghasilkan enam DataFrame, sesuai urutan dalam dokumen, dan tables[0]

kemungkinan besar merupakan tata letak navigasi, bukan data yang Anda inginkan.

Dokumentasi tersebut juga menjelaskan dengan jelas bahwa fungsi ini "akan selalu mengembalikan daftar DataFrame atau gagal sepenuhnya" — fungsi ini tidak akan mengembalikan daftar kosong kecuali dalam kasus yang tidak biasa seperti "baris tunggal dengan <td>

yang hanya berisi spasi". Jadi, hasil yang kosong merupakan tanda bahwa terjadi sesuatu yang tidak biasa, bukan hasil yang normal.

Anda juga dapat meneruskan HTML secara langsung, yang merupakan format yang disarankan untuk hal-hal di luar sekadar melihat sekilas:

import requests
html = requests.get(url, headers={"User-Agent": "MyBot/1.0 (+https://example.com/bot)"}).text
tables = pd.read_html(html)

Apa yang Dapat dan Tidak Dapat Dilihat

Memahami cakupannya dapat mencegah sebagian besar kekecewaan.

Alat ini hanya membaca elemen <table>. Dokumentasi menyatakan bahwa alat ini "mencari elemen <table> dan hanya memproses elemen <tr>, <th> pada baris, serta elemen <td> di dalam tabel". Tata letak yang dibangun dari elemen <div> yang ditata sebagai grid — yang merupakan desain web paling modern — tidak mengandung tabel yang dapat ditemukan oleh alat ini, terlepas dari seberapa mirip tabel tampilan tersebut di layar.

Alat ini tidak menjalankan JavaScript. Jika tabel dirender di sisi klien, HTML yang dilihat oleh read_html tidak mengandung tabel tersebut. Inilah alasan paling umum mengapa muncul pesan "tidak ditemukan tabel" pada halaman yang secara visual memiliki tabel.

Alat ini menangani elemen spanning dengan benar. Atribut colspan dan rowspan "ditangani dengan benar", yang lebih baik daripada yang dapat dilakukan oleh banyak parser buatan sendiri.

Alat ini lebih mengutamakan <thead> untuk header, dan akan mencari header di bagian body jika tidak ada.

Alat ini menghormati display: none secara default. Pengaturan default "displayed_only=True" "mengecualikan elemen dengan display: none", yang biasanya sesuai dengan yang Anda inginkan dan terkadang menyembunyikan data yang sengaja dibuat tersedia oleh situs hanya untuk viewport tertentu.

Dua catatan mengenai dependensi. Mesin parsing yang digunakan adalah lxml sebagai pilihan utama, dengan opsi cadangan bs4 serta html5lib — dokumentasi menyebutkan bahwa "'bs4' dan 'html5lib' adalah sinonim" sebagai nama varian. Selain itu, ada hal unik terkait URL yang perlu diketahui: “lxml hanya menerima protokol URL http, ftp, dan file. Jika Anda memiliki URL yang dimulai dengan 'https', Anda dapat mencoba menghapus 's'.” Dalam praktiknya, mengambil halaman tersebut sendiri akan sepenuhnya menghindari masalah ini.

Parameter-Parameter yang Penting

Tanda tangan lengkap memiliki delapan belas parameter. Enam di antaranya melakukan sebagian besar pekerjaan.

**match

** (default '.+'

) menyaring tabel-tabel yang teksnya cocok dengan ekspresi reguler. Ini adalah parameter paling berguna dan sering kurang dimanfaatkan:

tables = pd.read_html(html, match="Population")

Alih-alih menebak indeks dalam daftar, Anda cukup menyebutkan sesuatu yang terdapat dalam tabel tersebut. Jauh lebih andal ketika tata letak halaman berubah, karena konten biasanya tetap utuh meskipun desain ulang memindahkan posisi tabel.

**attrs

** menyaring berdasarkan atribut HTML, yang merupakan cara lain untuk mengidentifikasi tabel tertentu:

tables = pd.read_html(html, attrs={"id": "results", "class": "data"})

**header

** menyebutkan baris header. Dokumentasi menyoroti detail urutan yang sering membingungkan pengguna: "argumen ``header`

diterapkan **setelah** ``skiprows

diterapkan". Jadi, jika Anda melewati dua baris lalu meminta ``header=0

`, Anda akan mendapatkan baris pertama setelah baris yang dilewati.

**skiprows

** menghapus baris sebelum proses parsing — berguna untuk tabel dengan baris judul di atas header sebenarnya.

**index_col

** menetapkan kolom sebagai indeks.

**thousands

** (default ','

) dan **decimal

** (default '.'

) menangani pemformatan numerik. Hal ini lebih penting daripada yang terlihat: tabel Eropa yang menggunakan 1.234,56

memerlukan thousands='.'

dan decimal=','

, dan tanpa keduanya, setiap angka secara diam-diam akan berubah menjadi string atau nilai yang salah.

Dua hal lain yang perlu diketahui:

**converters

** menerapkan fungsi per kolom pada saat parsing, yang lebih rapi daripada memperbaiki tipe data setelahnya.

**extract_links

** menangkap href

tautan di dalam sel, bukan hanya teksnya — sangat berguna ketika baris tabel menautkan ke halaman detail yang juga Anda inginkan.

Pembersihan yang Tak Bisa Dihindari

Dokumentasi tersebut menjelaskan ekspektasi dengan jujur, dan sebaiknya Anda membaca pernyataan penafian ini daripada menemukannya secara tak terduga:

Bersiaplah untuk melakukan pembersihan setelah memanggil fungsi ini. Misalnya, Anda mungkin perlu menetapkan nama kolom secara manual jika nama kolom tersebut diubah menjadi NaN saat Anda meneruskan argumen ``header=0`

`.

Dan:

Kami berusaha mengasumsikan sesedikit mungkin mengenai struktur tabel dan menyerahkan keunikan HTML yang terkandung dalam tabel kepada pengguna.

Kalimat kedua tersebut merupakan filosofi desain yang dinyatakan secara gamblang. read_html

memberikan apa yang terkandung dalam HTML; menatanya agar rapi adalah tugas Anda.

Pembersihan yang selalu muncul:

Kolom MultiIndex dari header yang melintasi baris. Tabel dengan header dua baris menghasilkan MultiIndex

, yang memang benar namun terlihat aneh. Ratakan:

df.columns = [" ".join(str(c) for c in col).strip() for col in df.columns]

Spasi kosong dan spasi tak terpisah. HTML penuh dengan &nbsp;

, yang muncul sebagai \xa0

dan mengacaukan .strip()

biasa:

df = df.replace("\xa0", " ", regex=True)
df.columns = df.columns.str.replace("\xa0", " ", regex=False).str.strip()

Angka yang berupa string. Simbol mata uang, tanda persen, dan penanda catatan kaki:

df["Price"] = (df["Price"].astype(str)
               .str.replace(r"[^\d.,-]", "", regex=True)
               .str.replace(",", "")
               .pipe(pd.to_numeric, errors="coerce"))

errors="coerce"

mengubah nilai yang tidak dapat diparsing menjadi NaN

alih-alih memicu kesalahan, sehingga Anda dapat menghitung berapa banyak yang gagal daripada kehilangan seluruh operasi karena satu sel yang bermasalah.

Baris catatan kaki dan total. Banyak tabel diakhiri dengan baris ringkasan yang bukan merupakan data. Saring baris tersebut secara eksplisit daripada menganggap baris terakhir aman.

Memilih Tabel yang Tepat

Empat pendekatan, dalam urutan ketahanan yang semakin tinggi.

Berdasarkan indeks — tables[0]

. Cocok untuk eksplorasi, tetapi rentan jika digunakan dalam skrip. Tabel baru yang ditambahkan di atas tabel Anda akan merusaknya tanpa pemberitahuan, karena indeks 0 masih ada dan kini berisi data lain.

**Berdasarkan match

** — pilihan default terbaik. Tentukan string yang muncul di tabel yang Anda inginkan dan tidak ada yang lain.

**Berdasarkan attrs

** — paling baik jika tabel memiliki id atau kelas yang khas, karena hal-hal tersebut dipilih secara sengaja oleh pengembang.

Berdasarkan bentuk, setelah dimuat — ketika tidak ada hal lain yang mengidentifikasinya:

candidates = [t for t in pd.read_html(html)
              if {"Name", "Price"}.issubset(t.columns)]
if len(candidates) != 1:
    raise ValueError(f"expected 1 matching table, found {len(candidates)}")
df = candidates[0]

Pernyataan tersebut adalah bagian yang penting. Skrip yang secara diam-diam mengambil yang pertama dari tiga kecocokan akan menghasilkan data yang salah selama berbulan-bulan. Kegagalan ketika jumlahnya tidak terduga mengubah masalah data menjadi pesan kesalahan.

Membaca Beberapa Halaman ke dalam Satu DataFrame

Langkah selanjutnya yang wajar setelah satu tabel berhasil diproses, dan di sinilah beberapa kebiasaan baik dapat menghindarkan Anda dari masalah serius.

Gabungkan (concatenate) alih-alih menambahkan (append) dalam loop. Membangun DataFrame secara bertahap lambat dan menghasilkan indeks yang terfragmentasi. Kumpulkan frame-frame tersebut dan gabungkan sekaligus:

frames = []
for page in range(1, 11):
    df = fetch_table(f"https://example.com/data?page={page}",
                     match="Population",
                     expected_columns=["Country", "Population"])
    df["source_page"] = page
    frames.append(df)

combined = pd.concat(frames, ignore_index=True)

Catat dari mana setiap baris berasal. Kolom ``source_page`

` di atas tidak memerlukan biaya apa pun dan menjawab pertanyaan yang pada akhirnya akan diajukan kepada Anda — halaman mana yang menghasilkan nilai aneh ini. Untuk data apa pun yang dikumpulkan seiring waktu, tambahkan juga cap waktu. Kumpulan data tanpa riwayat asal-usul sangat sulit untuk di-debug dan mustahil untuk diaudit.

Atur kecepatan loop. Mengambil sepuluh halaman secepat yang diizinkan koneksi merupakan lonjakan yang terlihat seperti serangan dari sisi server. Jeda satu detik di antara permintaan tidak hanya sopan, tetapi juga, di sebagian besar situs, menjadi pembeda antara berhasil menyelesaikan proses atau terkena pembatasan laju:

import time, random
time.sleep(1 + random.random())

Tangani kegagalan per halaman daripada menghentikan proses. Satu halaman yang tata letaknya berubah tidak seharusnya membuat Anda kehilangan sembilan halaman lainnya:

frames, failures = [], []
for page in range(1, 11):
    try:
        frames.append(fetch_table(url_for(page), match="Population", expected_columns=COLS))
    except Exception as exc:
        failures.append((page, str(exc)))

if failures:
    print(f"{len(failures)} pages failed:", failures)

Pastikan bentuk kolomnya sesuai sebelum menggabungkannya. Jika halaman tujuh memiliki kolom yang tidak ada di halaman lain, pd.concat

akan dengan senang hati menampilkan bingkai penuh NaN

untuk baris yang tidak cocok — valid, terstruktur dengan baik, namun salah. Membandingkan kumpulan kolom sebelum menggabungkannya akan mengubah hal tersebut menjadi kesalahan yang dapat Anda lihat.

Dan hilangkan duplikat setelahnya. Tabel yang dipaginasi sering kali mengulang baris melintasi batas halaman, terutama ketika data dasarnya berubah di tengah proses pengambilan data. combined.drop_duplicates()

pada subset kolom yang relevan merupakan langkah pencegahan yang murah untuk menghindari penghitungan catatan yang sama dua kali.

Kapan Harus Menggunakan Alat Lain

read_html adalah pembungkus (wrapper) yang memudahkan penggunaan. Ada empat situasi yang memerlukan alat yang berbeda.

Ketika data tidak berada dalam format <table>. Tata letak kartu, daftar definisi, <div> kisi-kisi. Gunakan parser yang sesungguhnya — lxml atau BeautifulSoup dengan selektor CSS atau XPath — dan buat DataFrame-nya sendiri:

from lxml import html as lh
tree = lh.fromstring(page)
rows = [{"name": c.cssselect("h3")[0].text_content().strip(),
         "price": c.cssselect(".price")[0].text_content().strip()}
        for c in tree.cssselect("div.product-card")]
df = pd.DataFrame(rows)

Ketika halaman memerlukan JavaScript. Render halaman tersebut terlebih dahulu dengan Playwright atau alat serupa, lalu kirimkan HTML yang telah dirender ke read_html. Kombinasi ini bekerja dengan baik dan seringkali merupakan cara tercepat:

html = page.content()          # rendered DOM, not source
tables = pd.read_html(html)

Saat Anda perlu mengontrol permintaan. Header, cookie, sesi, percobaan ulang, batas waktu, proxy — tidak ada satupun di antaranya yang diungkapkan oleh read_html saat mengambil data untuk Anda. Ambil data secara terpisah dan kirimkan stringnya.

Saat situs menyediakan data terstruktur. Unduhan CSV, API, atau JSON-LD yang tertanam di halaman. Salah satu dari opsi ini lebih unggul daripada mengurai HTML yang sudah ditampilkan dalam hal stabilitas, dan layak untuk dicek selama tiga puluh detik sebelum menulis kode apa pun.

Melakukannya dengan Benar dalam Skrip

Pola untuk segala hal yang dijalankan lebih dari sekali.

import pandas as pd
import requests

HEADERS = {"User-Agent": "AcmeDataBot/1.0 (+https://acme.example.com/bot)"}

def fetch_table(url, match, expected_columns):
    resp = requests.get(url, headers=HEADERS, timeout=30)
    resp.raise_for_status()

    tables = pd.read_html(resp.text, match=match)
    if len(tables) != 1:
        raise ValueError(f"{url}: expected 1 table matching {match!r}, got {len(tables)}")

    df = tables[0]
    df.columns = [str(c).replace("\xa0", " ").strip() for c in df.columns]

    missing = set(expected_columns) - set(df.columns)
    if missing:
        raise ValueError(f"{url}: missing columns {missing}; got {list(df.columns)}")

    if df.empty:
        raise ValueError(f"{url}: table matched but contains no rows")

    return df

Lima hal di sana menjadi pembeda antara skrip yang gagal secara mencolok dan skrip yang gagal tanpa gejala.

**raise_for_status()

** menangkap kesalahan HTTP, karena read_html

pada halaman kesalahan akan menemukan tidak ada tabel atau menemukan tabel yang salah.

**match

daripada menggunakan indeks**, sehingga perubahan tata letak menghasilkan kesalahan daripada mengarah ke tabel yang salah.

Memastikan hanya ada satu kecocokan, sehingga hasil yang ambigu akan dihentikan daripada diam-diam mengambil yang pertama.

Memverifikasi kolom yang diharapkan, yang mendeteksi perombakan desain yang mengganti nama atau mengubah urutan kolom — kegagalan yang sebaliknya akan menghasilkan data yang salah namun secara sintaksis benar tanpa henti.

Memverifikasi apakah tabel kosong, karena tabel yang cocok namun tidak memiliki baris hampir selalu merupakan gejala, bukan hasil akhir.

Agen pengguna yang jujur dengan URL kontak tidak memerlukan biaya apa pun dan menjadikan Anda sebagai klien yang dapat dipilih oleh operator untuk diizinkan, bukan yang harus diblokir.

Pertanyaan Lainnya

Apa fungsi pandas.read_html?

Fungsi ini mengurai HTML dan mengembalikan setiap elemen <table> yang ditemukan sebagai daftar DataFrame. Fungsi ini menangani colspan dan rowspan, menggunakan <thead> untuk header jika ada, dan secara default mengabaikan elemen yang disembunyikan dengan display: none.

Mengapa read_html mengembalikan daftar?

Karena sebuah halaman dapat berisi sejumlah tabel, dan pandas mengembalikan semuanya sesuai urutan dokumen. Fungsi ini selalu mengembalikan daftar atau gagal — ia tidak akan mengembalikan daftar kosong kecuali dalam kasus yang tidak biasa — sehingga hasil kosong itu sendiri merupakan tanda bahwa ada yang salah.

Mengapa read_html menampilkan pesan "No tables found"?

Bisa jadi halaman tersebut tidak memiliki elemen <table> — tata letak modern sering kali menggunakan elemen <div> yang diberi gaya sebagai gantinya — atau tabel tersebut dirender oleh JavaScript yang tidak dieksekusi oleh pandas. Periksa sumber HTML mentah, bukan tampilan yang dirender oleh browser, untuk mengetahui penyebabnya.

Bagaimana cara memilih tabel tertentu?

Gunakan match dengan ekspresi reguler yang cocok dengan teks di dalam tabel yang Anda inginkan, atau attrs untuk menyaring berdasarkan id atau class. Keduanya jauh lebih andal daripada mengindeks daftar, yang akan gagal tanpa pemberitahuan saat ada tabel baru ditambahkan di atas tabel Anda.

Apakah read_html berfungsi pada halaman yang dirender oleh JavaScript?

Tidak. Fungsi ini mengurai HTML dan tidak menjalankan skrip. Render halaman terlebih dahulu menggunakan alat otomatisasi browser, lalu berikan string HTML hasilnya ke read_html — kombinasi ini bekerja dengan baik dan biasanya merupakan cara tercepat.

Bagaimana cara membersihkan DataFrame setelahnya?

Anda perlu meratakan kolom-kolom yang melebar (MultiIndex) dari header yang melebar, menghapus spasi yang tidak dapat dipisahkan (non-breaking spaces) yang muncul sebagai \xa0, mengonversi string mata uang dan persentase menjadi angka dengan pd.to_numeric(errors="coerce"), serta menghapus baris catatan kaki atau total. Dokumentasi pandas secara eksplisit menyebutkan bahwa pembersihan ini perlu dilakukan.

Bisakah saya menggunakan proxy atau header kustom dengan read_html?

Tidak, jika read_html mengambil URL-nya sendiri — fungsi ini tidak menyediakan opsi permintaan apa pun. Ambil halaman tersebut menggunakan requests atau klien lain, di mana Anda dapat mengontrol header, batas waktu, sesi, dan proxy, lalu berikan string HTML tersebut ke read_html.

Apa fungsi parameter thousands dan decimal?

Parameter tersebut memberitahu pandas bagaimana angka diformat, dengan nilai default masing-masing adalah ',' dan '.'. Untuk format Eropa seperti 1.234,56, Anda memerlukan thousands='.' dan decimal=',' — tanpa parameter tersebut, nilai-nilai akan diparsing secara salah tanpa pemberitahuan atau dibiarkan sebagai string.

Kesimpulan `

`read_htmladalah fungsi praktis yang benar-benar bagus dengan cakupan yang terbatas: fungsi ini mencari elemen<table>`` dalam kode HTML yang Anda berikan dan mengubahnya menjadi DataFrame. Dalam cakupan tersebut, fungsi ini menangani bagian-bagian yang rumit — sel yang melintasi baris, deteksi header, elemen tersembunyi — dengan lebih baik daripada kebanyakan parser yang ditulis secara manual.

Dua hal yang perlu dipahami adalah bahwa fungsi ini mengembalikan daftar, bukan DataFrame, dan bahwa dokumentasinya sendiri menyarankan Anda untuk mengantisipasi pembersihan data. Memilih berdasarkan match atau attrs alih-alih menggunakan indeks, serta memastikan bahwa hanya satu tabel yang cocok, akan mengubah kegagalan diam yang paling umum menjadi pesan kesalahan.

Untuk apa pun yang dijalankan lebih dari sekali, ambil HTML-nya sendiri. Satu baris kode tambahan memberi Anda header, batas waktu, upaya ulang, sesi, dan segala hal lain yang tidak disediakan oleh fungsi fetch bawaan — sekaligus menghindari kelemahan protokol URL pada lxml.

Dan jika halaman tidak memiliki tabel, hentikan pencarian parameter. Tabel yang dihasilkan oleh <div> atau halaman yang dirender di sisi klien merupakan masalah yang berbeda, dan solusinya adalah parser yang sesungguhnya, langkah rendering, atau — yang terbaik — data terstruktur yang kemungkinan besar sudah dipublikasikan oleh situs tersebut di tempat yang belum Anda periksa.