TL;DR
Web scraping adalah cara ngambil data dari halaman web dengan program, dan di Python kombinasi paling umum itu Requests buat ngunduh halamannya plus BeautifulSoup buat nyari elemen yang kamu mau. Alurnya empat langkah: cek robots.txt, unduh HTML, pilih elemen pakai CSS selector, lalu simpan ke DataFrame. Kasih jeda antar request dan pasang User-Agent yang jelas supaya server nggak keberatan.
Web scraping adalah cara ngambil data dari halaman web pakai program. Di Python, Requests yang ngunduh halamannya, BeautifulSoup yang nyari elemen yang kamu butuhin.
Kasus paling sering buat analis di Indonesia: ngumpulin harga produk sejenis dari marketplace, lalu bandingin sama harga sendiri. Kerjaan yang kalau manual makan dua jam seminggu, dan biasanya berhenti setelah minggu ketiga.
Di bawah ini alur lengkapnya, dari cek izin sampai file CSV yang siap dianalisis.
Web scraping adalah proses ngambil data dari tampilan halaman web secara otomatis, lalu ngubahnya jadi tabel. Ini pilihan kedua setelah API. Kalau situsnya punya API resmi, pakai API karena datanya lebih stabil dan izinnya jelas. Scraping baru masuk waktu datanya cuma ada di halaman web biasa.
Sebelum nulis satu baris kode pun, buka dua hal: file robots.txt situsnya dan halaman syarat layanan. Robots.txt ngasih tau bagian mana yang boleh diambil program.
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://books.toscrape.com/robots.txt")
rp.read()
print(rp.can_fetch("NgulikDataBot", "https://books.toscrape.com/catalogue/page-1.html"))
# True
Buat latihan di artikel ini aku pakai books.toscrape.com, situs yang emang dibikin khusus buat belajar scraping. Batas hukum dan etikanya aku bahas terpisah di artikel etika dan legalitas scraping di Indonesia.
pip install requests beautifulsoup4 pandas lxml
Empat paket, tugasnya beda-beda. Requests ngunduh halaman. BeautifulSoup4 baca HTML-nya. pandas nyimpen hasilnya jadi tabel. lxml itu parser tambahan yang lebih cepat, sifatnya opsional.
Dokumentasi resmi BeautifulSoup ada di crummy.com, dan itu rujukan paling lengkap buat semua metode pencarian elemennya.
Alurnya tiga langkah: unduh halaman pakai requests.get, masukin teksnya ke BeautifulSoup, lalu pilih elemen pakai CSS selector. Selalu pasang User-Agent yang nyebut nama dan kontak kamu supaya pemilik situs tau siapa yang lagi ngambil data.
import requests
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": "NgulikDataBot/1.0 (+https://ngulikdata.com)"
}
url = "https://books.toscrape.com/catalogue/page-1.html"
r = requests.get(url, headers=HEADERS, timeout=15)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "html.parser")
print(soup.title.get_text(strip=True))
Baris r.encoding = r.apparent_encoding itu penyelamat buat situs Indonesia. Tanpa itu, nama produk yang ada huruf beraksen sering berubah jadi karakter aneh.
Buka halamannya di browser, klik kanan di elemen yang kamu mau, pilih Inspect. Lihat nama tag dan class-nya, lalu tulis CSS selector yang paling pendek tapi masih spesifik. Selector pendek lebih tahan waktu desain situsnya diubah sedikit.
Di books.toscrape.com, tiap produk dibungkus article dengan class product_pod:
for card in soup.select("article.product_pod")[:3]:
judul = card.h3.a["title"]
harga = card.select_one("p.price_color").get_text(strip=True)
stok = card.select_one("p.instock.availability").get_text(strip=True)
print(judul, "|", harga, "|", stok)
A Light in the Attic | £51.77 | In stock
Tipping the Velvet | £53.74 | In stock
Soumission | £50.10 | In stock
Dua metode yang bakal kamu pakai terus:
select() balikin semua elemen yang cocok sebagai list.select_one() balikin satu elemen pertama, atau None kalau nggak ketemu.Pakai select_one() lalu cek None-nya, jangan langsung .get_text(). Satu produk yang kebetulan nggak punya harga bisa bikin seluruh loop berhenti.
Bungkus proses satu halaman jadi fungsi, lalu panggil berulang sambil kasih jeda. Jeda satu sampai tiga detik itu batas wajar buat situs kecil, dan bikin script kamu nggak keblokir di tengah jalan.
import time
import pandas as pd
def scrape_halaman(nomor):
url = f"https://books.toscrape.com/catalogue/page-{nomor}.html"
r = requests.get(url, headers=HEADERS, timeout=15)
if r.status_code == 404:
return None
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "html.parser")
hasil = []
for card in soup.select("article.product_pod"):
harga_el = card.select_one("p.price_color")
hasil.append({
"judul": card.h3.a["title"],
"harga_teks": harga_el.get_text(strip=True) if harga_el else None,
"rating": card.select_one("p.star-rating")["class"][1],
"halaman": nomor,
})
return hasil
semua = []
for n in range(1, 51):
batch = scrape_halaman(n)
if batch is None:
break
semua.extend(batch)
time.sleep(2)
df = pd.DataFrame(semua)
print(len(df), "produk")
Tiga pengaman di kode itu: batas 50 halaman, deteksi 404 buat berhenti otomatis, dan time.sleep(2) di tiap putaran.
Harga di halaman web selalu berupa teks, lengkap sama simbol mata uang dan pemisah ribuan. Bersihin dulu sebelum dihitung, kalau nggak semua agregasi kamu bakal salah atau langsung error.
import re
def ke_angka(teks):
if not teks:
return None
bersih = re.sub(r"[^0-9,.]", "", teks)
bersih = bersih.replace(".", "").replace(",", ".")
try:
return float(bersih)
except ValueError:
return None
print(ke_angka("Rp1.250.000")) # 1250000.0
print(ke_angka("Rp45.900")) # 45900.0
Fungsi di atas dipasang buat format Indonesia, titik sebagai pemisah ribuan dan koma sebagai desimal. Kalau kamu scraping situs berbahasa Inggris, aturannya kebalik.
Habis itu, cek dulu sebelum dianalisis:
print(df["harga"].isna().sum(), "harga gagal diparsing")
print(df["harga"].describe())
Toko Berkah jual 34 SKU aksesoris HP. Harga jualnya diset sekali di awal tahun dan belum pernah dicek ulang sama harga pasar.
Aku ambil harga produk sejenis dari halaman katalog publik, 3 halaman per kata kunci, jeda 2 detik. Total 41 menit buat 118 halaman, sekali jalan.
pasar = pd.read_csv("harga_pasar.csv")
median_pasar = (
pasar.groupby("kategori")["harga"]
.median()
.rename("median_pasar")
.reset_index()
)
toko = pd.read_csv("harga_toko_berkah.csv")
banding = toko.merge(median_pasar, on="kategori", how="left")
banding["selisih_pct"] = (
(banding["harga_jual"] - banding["median_pasar"]) / banding["median_pasar"] * 100
).round(1)
print(banding.sort_values("selisih_pct", ascending=False).head(8))
Hasil di dataset latihan ngulikdata: 9 dari 34 SKU dijual lebih dari 18 persen di atas median pasar. Yang paling jauh, kabel USB-C, dijual Rp42.000 sementara median pasarnya Rp29.500, selisih 42 persen.
Sisi lainnya juga muncul: 5 SKU dijual di bawah median pasar, dan salah satunya charger 20W yang laku 640 unit sebulan. Naikin Rp3.000 per unit di SKU itu doang nambah sekitar Rp1,9 juta sebulan tanpa ganti apa pun.
Median dipakai, bukan rata-rata, soalnya ada beberapa penjual yang masang harga aneh dan bikin rata-rata melenceng. Cara mikirin pencilan begini aku bahas di glossary outlier.
Banyak situs modern ngisi kontennya pakai JavaScript setelah HTML awal dimuat. Requests cuma ngambil HTML awal itu, jadi hasilnya kelihatan kosong.
Cara ngeceknya: klik kanan lalu View Page Source, bukan Inspect Element. View Page Source nampilin persis yang diterima Requests.
Kalau datanya nggak ada di situ, ada dua jalan:
| Kesalahan | Akibatnya | Perbaikannya |
|---|---|---|
| Nggak kasih jeda antar request | IP diblokir, data cuma separuh | time.sleep(2) di tiap putaran |
| Selector kepanjangan | Rusak begitu desain berubah | Pegang class semantik atau atribut data |
Langsung .get_text() tanpa cek None | Loop berhenti di tengah | Cek hasil select_one() dulu |
| Lupa set encoding | Huruf berubah jadi simbol aneh | r.encoding = r.apparent_encoding |
| Simpan harga sebagai teks | Agregasi salah atau error | Bersihin jadi float sebelum disimpan |
| Nggak nyimpen tanggal ambil | Nggak bisa bandingin antar waktu | Tambahin kolom tanggal_ambil |
Nomor enam sering kelewat, dan itu justru yang bikin proyek ini berguna jangka panjang. Simpan tiap hasil scraping dengan tanggalnya, tiga bulan kemudian kamu punya data tren harga yang nggak dipunya kompetitor. Pengecekan kualitas datanya jalanin tiap kali script selesai.
Tergantung apa yang kamu ambil dan gimana caranya. Ngambil data harga publik dengan jeda wajar dan tanpa nembus login umumnya aman. Yang bikin masalah itu ngambil data pribadi, ngelanggar syarat layanan situs, atau ngirim request begitu banyak sampai servernya terganggu. Cek robots.txt dan halaman syarat layanan sebelum mulai.
Kemungkinan besar halaman itu ngisi kontennya pakai JavaScript setelah HTML awal dimuat, sedangkan Requests cuma ngambil HTML awalnya. Cek dengan klik kanan lalu View Page Source, bukan Inspect Element. Kalau datanya nggak ada di situ, cari endpoint API yang dipanggil halamannya lewat tab Network.
Satu sampai tiga detik buat situs kecil, dan turutin nilai Crawl-delay kalau robots.txt nyebutin. Jeda ini bukan sekadar sopan santun, tapi juga bikin script kamu nggak kena blokir di tengah jalan. Kalau kamu butuh ribuan halaman, jalankan malam hari dan pecah jadi beberapa sesi.
html.parser udah bawaan Python jadi nggak perlu install apa-apa, dan cukup buat sebagian besar halaman. lxml lebih cepat dan lebih tahan sama HTML yang rusak, tapi harus diinstall terpisah. Buat proyek di bawah seribu halaman, bedanya nggak kerasa.
Script kamu bakal balikin kolom kosong atau langsung error. Makanya jangan pakai selector yang kepanjangan kayak div > div > div > span. Pilih pegangan yang stabil kayak nama class semantik atau atribut data. Tambahin juga pengecekan jumlah baris di akhir, supaya kamu langsung tau waktu hasilnya anjlok.
Empat hal yang bikin script scraping kamu layak dipakai berulang: cek robots.txt di awal, jeda di tiap request, parsing harga yang tahan format Indonesia, dan kolom tanggal ambil di tiap baris.
Jalanin seminggu berturut-turut, kamu udah punya dataset tren harga yang orisinal.
Coba mulai dari satu kategori produk yang kamu ngerti. Kalau mau tau batas amannya sebelum jalan, baca dulu rambu UU PDP untuk scraping.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom datetime nyimpen tanggal, jam, hari, semuanya nempel jadi satu. Accessor .dt di pandas ngeluarin tiap bagian jadi kolom sendiri buat dianalisa.
Nama toko ketik campur huruf besar-kecil, spasi nyasar di ujung, atau kode produk nempel jadi satu. Accessor .str di pandas ngerapiin semua itu tanpa loop.
Model machine learning cuma ngerti angka, bukan teks kayak merah atau biru. get_dummies ngubah kolom kategori jadi kolom 0/1 dalam satu baris kode.