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:

  1. Baca robots.txt — berkas di akar situs (misalnya https://contoh.example/robots.txt) yang menyatakan jalur mana yang boleh dan tidak boleh dikunjungi otomatis.
  2. Baca syarat dan ketentuan layanan — sebagian situs menutup pintu bagi pengambilan otomatis berapa pun jumlahnya; jika syaratnya melarang scraping, kita tidak melakukannya.
  3. Kirim permintaan dengan jeda — tambahkan time.sleep() antar permintaan dan hindari mengambil ribuan halaman dalam hitungan detik; jangan membebani server yang dipakai banyak orang.
  4. Gunakan User-Agent yang jujur — identitas program yang jelas membuat pemilik situs dapat mengenali dan menghubungi kita bila ada masalah.
  5. 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:

KondisiSumber terbaikAlasan
Situs menyediakan API publikAPIStruktur stabil, tak mudah rusak
Data dapat diunduh (CSV/Excel)Berkas unduhanUtuh, resmi, sekali unduh
Tidak ada keduanyaWeb scrapingPaling 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.

python
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:

text
Isi robots.txt:
User-agent: *
Disallow: /admin/
Allow: /

/produk.html    -> boleh
/admin/harga    -> dilarang
/               -> boleh

Mari kita telaah baris-barisnya:

  1. Baris 3-6 (isi_robots): aturan untuk semua robot (User-agent: *): seluruh situs boleh dikunjungi (Allow: /) kecuali jalur /admin/ yang dilarang (Disallow).
  2. Baris 8-9 (parser.parse(...)): RobotFileParser mengisi aturan dari baris-baris teks — splitlines() mengubah string menjadi baris yang siap diurai.
  3. Baris 14-16 (parser.can_fetch(...)): untuk setiap jalur, can_fetch mengembalikan True atau False. Jalur /admin/harga berada di bawah naungan Disallow: /admin/, sehingga jawabannya dilarang — 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.

python
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:

text
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: 200

Alur kerjanya dari permintaan sampai DataFrame:

  1. Baris 44-48 (respons = requests.get(...)): permintaan dilengkapi dua hal yang wajib menjadi kebiasaan — User-Agent yang jujur dan timeout. respons.text berisi HTML mentah yang persis seperti yang diterima peramban.
  2. 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.
  3. 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>.
  4. 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 hasil find_all("tr") dilewati ([1:]) karena baris tersebut berisi kepala tabel, bukan data.
  5. Baris 62-65 (pd.DataFrame dan astype): hasil berupa list of dictionary langsung menjadi DataFrame pada tutorial intro pandas ; teks harga dikonversi menjadi bilangan bulat agar siap dihitung.
  6. 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.txt dan syarat layanan, beri jeda antar permintaan, pakai User-Agent yang jujur, dan jangan membebani server.
  • urllib.robotparser mengurai robots.txt dan menjawab can_fetch() sebelum kita mengirim satu permintaan pun.
  • Alur scraping: requests.get() mengambil HTML, BeautifulSoup menguraikannya dengan find()/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.