Topik ini membahas ekstraksi data dari API dengan library requests, mulai dari permintaan dasar, paginasi (pagination), sampai percobaan ulang dengan jeda yang membesar. Respons jaringan eksternal tidak deterministik sehingga tidak layak dipakai sebagai bahan demonstrasi, dan karena itu pola yang sama diuji terhadap server lokal yang menyajikan halaman statis. Pertimbangan etika ekstraksi ditutup di bagian akhir agar pipeline tidak membebani layanan sumbernya.
Permintaan dasar dengan requests#
Library requests menyederhanakan komunikasi HTTP menjadi pemanggilan fungsi, tetapi ekstraksi yang aman tetap membutuhkan tiga langkah pengamanan. Pertama, parameter timeout wajib disertakan pada setiap permintaan karena requests tidak menetapkan batas waktu secara default, sehingga tanpa itu permintaan dapat menggantung tanpa ujung. Kedua, metode raise_for_status() mengubah respons dengan kode status 4xx atau 5xx menjadi exception, sehingga kegagalan server tidak diperlakukan sebagai data sah. Ketiga, metode .json() mengurai tubuh respons JSON menjadi objek Python.
Pola dasarnya tampak pada berikut ini. Kode ini tidak dijalankan karena membutuhkan jaringan eksternal.
# tidak-dijalankan
import requests
respons = requests.get(
"https://api.example.com/v1/catatan",
params={"page": 1},
timeout=10,
)
respons.raise_for_status()
data = respons.json()
print(len(data))Argumen params diteruskan requests sebagai query string pada URL, sehingga permintaan di atas setara dengan menyertakan ?page=1. Dengan tiga langkah pengamanan tersebut, satu permintaan API berhenti dengan jelas baik pada kegagalan jaringan, kegagalan server, maupun respons yang tidak dapat diurai. Ketiga langkah itulah yang dipakai ulang oleh pola paginasi dan percobaan ulang pada bagian berikutnya.
Paginasi dan percobaan ulang#
API yang datanya lebih besar dari satu respons selalu menyediakan paginasi, yaitu pembagian hasil menjadi halaman-halaman yang diminta satu per satu. Pola yang lazim memakai parameter page beserta per_page, ditambah penanda pada respons yang menyatakan keberadaan halaman berikutnya. Perulangan ekstraksi wajib diberi batas jumlah halaman agar respons yang rusak tidak mengubah skrip menjadi perulangan tanpa ujung. Kesalahan yang bersifat sementara seperti koneksi terputus atau waktu tunggu habis ditangani dengan retry, yakni memanggil ulang permintaan yang sama. Jeda tunggu yang membesar setiap percobaan dikenal sebagai backoff eksponensial.
Berkas utuh berikut merangkum pola paginasi, batas halaman, timeout, dan percobaan ulang dalam satu modul. Kode ini tidak dijalankan karena membutuhkan API nyata di luar sandbox.
# tidak-dijalankan
"""Ekstraksi API dengan paginasi dan percobaan ulang (memerlukan jaringan)."""
import time
import requests
URL_API = "https://api.example.com/v1/catatan"
HALAMAN_MAKSIMAL = 10
PERCOBAAN_MAKSIMAL = 4
JEDA_DASAR = 1.0
def ambil_halaman(nomor: int, sesi: requests.Session) -> dict:
"""Mengambil satu halaman API dengan percobaan ulang dan backoff eksponensial."""
for percobaan in range(1, PERCOBAAN_MAKSIMAL + 1):
try:
respons = sesi.get(
URL_API,
params={"page": nomor, "per_page": 100},
timeout=10,
)
respons.raise_for_status()
return respons.json()
except (requests.ConnectionError, requests.Timeout):
if percobaan == PERCOBAAN_MAKSIMAL:
raise
jeda = JEDA_DASAR * 2 ** (percobaan - 1)
print(f"halaman {nomor} gagal, mencoba ulang dalam {jeda:.0f} detik")
time.sleep(jeda)
raise RuntimeError("tidak terjangkau")
def ambil_semua(sesi: requests.Session) -> list[dict]:
"""Menelusuri halaman sampai API tidak lagi menandai halaman berikutnya."""
catatan: list[dict] = []
nomor = 1
while nomor <= HALAMAN_MAKSIMAL:
isi = ambil_halaman(nomor, sesi)
catatan.extend(isi["data"])
if not isi.get("punya_lanjutan", False):
return catatan
nomor += 1
return catatan
if __name__ == "__main__":
with requests.Session() as sesi:
sesi.headers.update({"User-Agent": "pipeline-latihan/1.0"})
semua = ambil_semua(sesi)
print(f"total catatan: {len(semua)}")Struktur berkasnya memisahkan dua tanggung jawab. Fungsi ambil_halaman hanya mengurus satu permintaan beserta percobaan ulangnya, dengan jeda satu, dua, lalu empat detik mengikuti rumus JEDA_DASAR * 2 ** (percobaan - 1). Fungsi ambil_semua mengurus penelusuran halaman dari satu sampai penanda lanjut berhenti, dengan batas HALAMAN_MAKSIMAL sebagai rem. Kesalahan non-sementara seperti respons 404 tidak ditangkap blok except, sehingga gagal cepat tanpa percobaan ulang yang sia-sia.
Menguji paginasi dengan server lokal#
Server lokal membuat pola ekstraksi dapat diuji dengan keluaran yang dapat direproduksi. Perintah python3 -m http.server menyajikan isi direktori kerja sebagai situs statis, dan dua berkas JSON yang dibuat oleh perintah berikut berperan sebagai dua halaman API. Halaman pertama membawa tanda "lanjut": true, sedangkan halaman kedua membawa "lanjut": false, sehingga klien yang menelusurinya mempraktikkan logika paginasi yang sama dengan berkas utuh di atas.
# buat dua halaman statis yang berperan sebagai respons API
printf '%s\n' '{"lanjut": true, "data": [{"id": 1, "nilai": 120}, {"id": 2, "nilai": 340}]}' > halaman-1.json
printf '%s\n' '{"lanjut": false, "data": [{"id": 3, "nilai": 560}]}' > halaman-2.json
# jalankan server lokal pada port 8771 di latar belakang
python3 -m http.server 8771 >/dev/null 2>&1 & SRV_PID=$!; sleep 0.5Skrip klien berikut mengambil halaman demi halaman dari http://127.0.0.1:8771/ dengan requests, memakai timeout, raise_for_status(), dan pemeriksaan tanda lanjut.
"""Klien paginasi sederhana untuk API yang disajikan server lokal."""
import requests
POLA_URL = "http://127.0.0.1:8771/halaman-{nomor}.json"
def ambil_semua() -> tuple[list[dict], int]:
"""Menelusuri halaman sampai tanda lanjut bernilai false."""
catatan: list[dict] = []
nomor = 1
while True:
respons = requests.get(POLA_URL.format(nomor=nomor), timeout=5)
respons.raise_for_status()
isi = respons.json()
catatan.extend(isi["data"])
if not isi["lanjut"]:
return catatan, nomor
nomor += 1
if __name__ == "__main__":
semua, jumlah_halaman = ambil_semua()
for item in semua:
print(f"id={item['id']} nilai={item['nilai']}")
print(f"halaman diambil: {jumlah_halaman}")id=1 nilai=120
id=2 nilai=340
id=3 nilai=560
halaman diambil: 2Keluaran menunjukkan tiga catatan terkumpul dari dua halaman. Perulangan pada ambil_semua berhenti tepat setelah halaman kedua karena tanda lanjutnya bernilai false, dan jumlah halaman tercatat sebagai bagian dari hasil. Setelah pengujian selesai, server dimatikan dengan menyasar prosesnya di latar belakang.
# hentikan server lokal
kill $SRV_PIDEtika ekstraksi#
Ekstraksi yang bertanggung jawab menjaga layanan sumber tetap sehat, karena API publik adalah milik bersama yang kapasitasnya terbatas. Dokumentasi API biasanya menyebutkan batas laju (rate limit), yaitu jumlah permintaan maksimum dalam selang waktu tertentu, dan pelanggarannya berujung respons 429. Ketika respons 429 diterima, skrip sebaiknya membaca header Retry-After lalu menunggu sesuai isinya, bukan memaksakan percobaan berikutnya. Permintaan juga sebaiknya menyertakan header User-Agent yang jujur beserta identitas kontaknya, sehingga pengelola API dapat menghubungi pemilik skrip sebelum mengambil tindakan blokir.
Selain laju, kepatuhan juga menyangkut ruang lingkup. Kredensial API dirahasiakan dan tidak ditulis langsung di dalam kode pipeline. Hanya sebagian kolom yang benar-benar dibutuhkan yang diambil. Ketentuan penggunaan pada dokumentasi API diikuti sebagaimana tertulis.
Kesalahan umum#
Ekstraksi API menghasilkan kesalahan yang berulang dari proyek ke proyek, dan keempatnya biasanya lolos saat pengujian karena jaringan uji jarang bermasalah. Empat berikut paling sering ditemui beserta perbaikannya.
- Permintaan tanpa
timeout, sehingga skrip menggantung tanpa ujung ketika server tidak merespons. Perbaikannya, sertakan batas waktu pada setiap pemanggilan requests. - Percobaan ulang tanpa jeda, sehingga server yang sedang bermasalah justru mendapat beban tambahan. Perbaikannya, gunakan backoff eksponensial dengan batas jumlah percobaan.
- Perulangan paginasi tanpa batas halaman, sehingga respons yang rusak atau API yang selalu menandai lanjut membuat skrip berjalan terus. Perbaikannya, tetapkan
HALAMAN_MAKSIMALsebagaimanaambil-api.py. - Kode status 429 diabaikan, sehingga skrip terus menabrak batas laju sampai diblokir. Perbaikannya, tangani 429 secara khusus dan ikuti header
Retry-After.
Ringkasan#
- Setiap permintaan requests membawa
timeout, danraise_for_status()menjadikan kegagalan HTTP terlihat sebagai exception. - Paginasi ditelusuri dengan perulangan berbatas yang berhenti pada penanda halaman terakhir.
- Percobaan ulang hanya menyasar kesalahan sementara dan selalu disertai jeda backoff yang membesar.
- Server lokal membuat pola ekstraksi dapat diuji dengan keluaran yang dapat direproduksi tanpa jaringan eksternal.
- Ekstraksi yang sehat menghormati batas laju, mengidentifikasi dirinya, dan mengikuti ketentuan API sumbernya.
Referensi#
- Dokumentasi requests — Requests project — Rujukan utama library requests, meliputi permintaan dasar, parameter, dan sesi.
- Advanced Usage — Requests documentation — Bagian timeout dan penanganan error yang menjadi dasar pola percobaan ulang pada topik ini.