Dua chapter sebelumnya mengambil data dari sumber yang memang dirancang untuk dibaca program: API dan database. Kenyataannya, sebagian data hanya tersaji sebagai halaman web untuk manusia — daftar harga di situs toko, jadwal di portal layanan, tabel ringkasan di laman pemerintahan. Web scraping adalah teknik mengambil data semacam itu langsung dari kode HTML halamannya. Sebelum menyentuh satu baris kode pun, kita harus bicara soal etika dan legalitasnya terlebih dahulu, karena scraping yang dilakukan sembarangan dapat melanggar ketentuan layanan, mengganggu server milik pihak lain, dan berujung pada pemblokiran.
Etika dan Legalitas Sebelum Baris Kode Pertama #
Halaman web adalah aset milik penyedianya. Prinsip dasar kita: kirim sedikit permintaan, pada jam yang wajar, hanya ke halaman yang mengizinkan, sambil menyebut identitas secara jujur. Sebelum menulis scraper, periksa dulu urutan berikut:
- Baca
robots.txt— berkas di akar situs (misalnyahttps://contoh.example/robots.txt) yang menyatakan jalur mana yang boleh dan tidak boleh dikunjungi otomatis. - Baca syarat dan ketentuan layanan — sebagian situs menutup pintu bagi pengambilan otomatis berapa pun jumlahnya; jika syaratnya melarang scraping, kita tidak melakukannya.
- Kirim permintaan dengan jeda — tambahkan
time.sleep()antar permintaan dan hindari mengambil ribuan halaman dalam hitungan detik; jangan membebani server yang dipakai banyak orang. - Gunakan
User-Agentyang jujur — identitas program yang jelas membuat pemilik situs dapat mengenali dan menghubungi kita bila ada masalah. - Hormati data pribadi — data orang per orang tidak kita kumpulkan tanpa dasar yang sah.
Scraping adalah pilihan terakhir, bukan pilihan pertama. Urutan preferensi yang lebih sehat:
| Kondisi | Sumber terbaik | Alasan |
|---|---|---|
| Situs menyediakan API publik | API | Struktur stabil, tak mudah rusak |
| Data dapat diunduh (CSV/Excel) | Berkas unduhan | Utuh, resmi, sekali unduh |
| Tidak ada keduanya | Web scraping | Paling rapuh, wajib santun |
Membaca robots.txt #
Modul urllib.robotparser dari pustaka standar dapat mengurai isi robots.txt dan menjawab pertanyaan “bolehkah jalur ini dikunjungi?”. Kita berlatih dengan isi robots.txt contoh sebagai string; pada scraper sungguhan, isinya diunduh dulu dari situs yang dituju.
from urllib import robotparser
isi_robots = """User-agent: *
Disallow: /admin/
Allow: /
"""
parser = robotparser.RobotFileParser()
parser.parse(isi_robots.splitlines())
print("Isi robots.txt:")
print(isi_robots)
for path in ["/produk.html", "/admin/harga", "/"]:
boleh = parser.can_fetch("TokoKopiScraper/1.0", path)
print(f"{path:15} -> {'boleh' if boleh else 'dilarang'}")Output yang Dihasilkan:
Isi robots.txt:
User-agent: *
Disallow: /admin/
Allow: /
/produk.html -> boleh
/admin/harga -> dilarang
/ -> bolehMari kita telaah baris-barisnya:
- Baris 3-6 (
isi_robots): aturan untuk semua robot (User-agent: *): seluruh situs boleh dikunjungi (Allow: /) kecuali jalur/admin/yang dilarang (Disallow). - Baris 8-9 (
parser.parse(...)):RobotFileParsermengisi aturan dari baris-baris teks —splitlines()mengubah string menjadi baris yang siap diurai. - Baris 14-16 (
parser.can_fetch(...)): untuk setiap jalur,can_fetchmengembalikanTrueatauFalse. Jalur/admin/hargaberada di bawah naunganDisallow: /admin/, sehingga jawabannyadilarang— scraper yang baik berhenti di sini, bukan memaksakan kunjungan.
Mengambil dan Mengurai Halaman HTML #
Tibalah di inti chapter: mengambil HTML dengan requests, lalu menguraikannya dengan BeautifulSoup dari library bs4. Sebagaimana chapter API, kita memakai server HTTP lokal sebagai simulasi situs — satu halaman /produk.html berisi tabel harga statis, sehingga seluruh alur ambil-uraikan dapat dijalankan dan diperiksa ulang tanpa menyentuh situs orang lain.
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
import pandas as pd
import requests
from bs4 import BeautifulSoup
HALAMAN_PRODUK = """<!DOCTYPE html>
<html>
<head><title>Daftar Harga - Toko Kopi Kita</title></head>
<body>
<h1>Daftar Harga</h1>
<table>
<tr><th>produk</th><th>harga</th></tr>
<tr><td>Kopi Susu</td><td>24000</td></tr>
<tr><td>Americano</td><td>22000</td></tr>
<tr><td>Matcha Latte</td><td>28000</td></tr>
<tr><td>Teh Tawar</td><td>20000</td></tr>
</table>
</body>
</html>
"""
class HandlerWeb(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/produk.html":
body = HALAMAN_PRODUK.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), HandlerWeb)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"
respons = requests.get(
f"{basis_url}/produk.html",
headers={"User-Agent": "TokoKopiScraper/1.0 (latihan belajar)"},
timeout=5,
)
print("Kode status :", respons.status_code)
print("Tipe konten :", respons.headers["Content-Type"])
print("Isi HTML :")
print(respons.text)
soup = BeautifulSoup(respons.text, "html.parser")
tabel = soup.find("table")
kolom = [th.get_text() for th in tabel.find_all("th")]
baris_data = []
for tr in tabel.find_all("tr")[1:]:
nilai = [td.get_text() for td in tr.find_all("td")]
baris_data.append(dict(zip(kolom, nilai)))
df = pd.DataFrame(baris_data)
df["harga"] = df["harga"].astype(int)
print("Hasil ekstraksi:")
print(df)
time.sleep(0.5)
respons_kedua = requests.get(f"{basis_url}/produk.html", timeout=5)
print("Permintaan kedua setelah jeda 0.5 detik:", respons_kedua.status_code)
server.shutdown()Output yang Dihasilkan:
Kode status : 200
Tipe konten : text/html; charset=utf-8
Isi HTML :
<!DOCTYPE html>
<html>
<head><title>Daftar Harga - Toko Kopi Kita</title></head>
<body>
<h1>Daftar Harga</h1>
<table>
<tr><th>produk</th><th>harga</th></tr>
<tr><td>Kopi Susu</td><td>24000</td></tr>
<tr><td>Americano</td><td>22000</td></tr>
<tr><td>Matcha Latte</td><td>28000</td></tr>
<tr><td>Teh Tawar</td><td>20000</td></tr>
</table>
</body>
</html>
Hasil ekstraksi:
produk harga
0 Kopi Susu 24000
1 Americano 22000
2 Matcha Latte 28000
3 Teh Tawar 20000
Permintaan kedua setelah jeda 0.5 detik: 200Alur kerjanya dari permintaan sampai DataFrame:
- Baris 44-48 (
respons = requests.get(...)): permintaan dilengkapi dua hal yang wajib menjadi kebiasaan —User-Agentyang jujur dantimeout.respons.textberisi HTML mentah yang persis seperti yang diterima peramban. - Baris 54 (
soup = BeautifulSoup(...)): parser"html.parser"milik pustaka standar mengubah string HTML menjadi struktur pohon yang dapat dicari. BeautifulSoup juga mendukung parser lain bila terpasang;"html.parser"selalu tersedia. - Baris 55 (
tabel = soup.find("table")):find()mengembalikan elemen pertama yang cocok dengan tag — di sini satu-satunya<table>.find_all()mengembalikan seluruh elemen yang cocok, seperti seluruh<tr>dan<td>. - Baris 56-60 (ekstraksi baris): nama kolom diambil dari
<th>, lalu setiap<tr>berisi data diurai menjadi<td>per sel.dict(zip(kolom, nilai))memasangkan nama kolom dengan isinya — idiom yang diperkenalkan di tutorial struktur data Python . Baris pertama hasilfind_all("tr")dilewati ([1:]) karena baris tersebut berisi kepala tabel, bukan data. - Baris 62-65 (
pd.DataFramedanastype): hasil berupa list of dictionary langsung menjadi DataFrame pada tutorial intro pandas ; teks harga dikonversi menjadi bilangan bulat agar siap dihitung. - Baris 67-69 (
time.sleep(0.5)dan permintaan kedua): jeda setengah detik memperlihatkan rate limiting dalam bentuk paling sederhana. Pada scraper nyata dengan ratusan halaman, jeda inilah yang menjaga server tetap sehat — dan menjaga alamat IP kita tetap diterima.
Keterbatasan: Halaman yang Dirender JavaScript #
Teknik pada chapter ini hanya bekerja bila data sudah tertulis di HTML yang diterima requests. Banyak situs modern bekerja secara berbeda: HTML awal hanya berisi kerangka halaman, sementara datanya diambil oleh kode JavaScript setelah halaman tiba di peramban — tabel yang kita lihat di layar tidak ada di respons.text. Untuk kondisi seperti itu ada dua jalur yang lebih sehat sebelum mengotomatiskan peramban: periksa apakah situs ternyata memanggil API JSON di balik layar (sering kali dapat kita pakai langsung), atau cari tautan unduhan resmi. Bila keduanya tertutup, pustaka otomatisasi peramban seperti Selenium atau Playwright dapat menjalankan JavaScript lalu membaca halaman yang sudah jadi — topik lanjutan di luar cakupan chapter ini.
Rangkuman #
- Etika mendahului teknik: baca
robots.txtdan syarat layanan, beri jeda antar permintaan, pakaiUser-Agentyang jujur, dan jangan membebani server. urllib.robotparsermengurairobots.txtdan menjawabcan_fetch()sebelum kita mengirim satu permintaan pun.- Alur scraping:
requests.get()mengambil HTML,BeautifulSoupmenguraikannya denganfind()/find_all(), hasilnya dirakit menjadi DataFrame. - Scraping hanya untuk HTML statis; halaman yang dirender JavaScript memerlukan API di baliknya atau pustaka otomatisasi peramban.
Sejauh ini data kita berbentuk tabel: baris, kolom, dan angka. Padahal sebagian data justru hidup di atas peta — titik lokasi, garis jalan, dan area wilayah. Chapter berikutnya mengenalkan data geospasial, struktur GeoJSON, dan cara membacanya dengan geopandas di Pengambilan Data Geospasial.