File CSV dan Excel yang kita pelajari pada chapter sebelumnya bersifat statis: isinya berhenti pada saat file tersebut dibuat. Padahal banyak data paling berguna justru terus berubah — cuaca, kurs valuta, stok gudang, hingga jumlah pemesanan hari ini. Data seperti itu biasanya disajikan melalui API, yaitu layanan yang memungkinkan program kita meminta data langsung dari server penyedianya. Pada chapter ini kita memakai library requests untuk mengambil data dari REST API: mulai dari permintaan GET paling sederhana, parameter query, kode status, penanganan galat, hingga paginasi. Seluruh contoh dijalankan terhadap server HTTP lokal sebagai simulasi API sehingga hasilnya selalu dapat direproduksi.
Apa itu REST API #
REST API adalah API yang mengikuti gaya arsitektur REST: setiap sumber daya (data cuaca, daftar produk, dan sebagainya) dialamatkan lewat sebuah URL yang disebut endpoint. Program kita mengirim permintaan dengan salah satu method HTTP, lalu server membalas dengan data — paling sering dalam format JSON. Sebelum membahas kode, kita bedah dulu anatomi sebuah URL permintaan:
| Bagian | Contoh | Peran |
|---|---|---|
| Skema | https | Protokol komunikasi yang dipakai |
| Domain | api.example.com | Alamat server penyedia API |
| Endpoint (path) | /v1/cuaca | Sumber daya yang diminta: data cuaca |
| Query string | ?kota=Bandung | Saringan tambahan pada permintaan |
Method HTTP menunjukkan niat permintaan: GET mengambil data, POST mengirim data baru, PUT memperbarui data, dan DELETE menghapus data. Untuk keperluan pengumpulan data, kita hampir selalu cukup memakai GET. Format JSON sendiri adalah teks berpasangan kunci-nilai yang bentuknya mirip dictionary Python — dokumentasi modul json
menjelaskan pegangan lengkapnya, dan method respons.json() yang kita pakai nanti bekerja di atas modul tersebut.
Permintaan GET Pertama dengan requests #
Kita mulai dengan contoh paling sederhana: meminta data cuaca dari endpoint /api/cuaca. Contoh pada chapter ini menyalakan server HTTP kecil di dalam proses Python yang sama, mendengarkan alamat 127.0.0.1 (komputer sendiri) pada port acak. Pola ini membuat perilaku API sesungguhnya — koneksi, kode status, dan isi JSON — dapat kita amati tanpa bergantung pada layanan luar.
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
class HandlerCuaca(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/cuaca":
body = json.dumps({"kota": "Bandung", "suhu_c": 24, "kelembapan": 78}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
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), HandlerCuaca)
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}/api/cuaca", timeout=5)
print("Kode status :", respons.status_code)
print("Tipe konten :", respons.headers["Content-Type"])
print("Isi mentah :", respons.text)
data = respons.json()
print("Kota :", data["kota"])
print("Suhu (C) :", data["suhu_c"])
server.shutdown()Output yang Dihasilkan:
Kode status : 200
Tipe konten : application/json
Isi mentah : {"kota": "Bandung", "suhu_c": 24, "kelembapan": 78}
Kota : Bandung
Suhu (C) : 24Mari kita telaah baris-baris kode di atas:
- Baris 6-20 (
class HandlerCuaca): sisi server simulasi. Methoddo_GETmenentukan balasan untuk setiap path; untuk/api/cuacaserver menuliskan JSON hasiljson.dumps(), sedangkan path lain dibalas 404. Methodlog_messagedimatikan supaya server tidak mencetak log ke stderr. - Baris 22-24 (
server = ...hinggabasis_url = ...): server dinyalakan pada port0, artinya sistem operasi memilih port bebas — ini menghindari benturan dengan aplikasi lain. Alamat dasar API disimpan ke variabelbasis_url. - Baris 26 (
respons = requests.get(...)): inti chapter ini. Kita memanggilrequests.get()dengan URL endpoint dantimeout=5— batas waktu 5 detik agar program tidak menggantung menunggu balasan yang tidak pernah datang. - Baris 27-29:
respons.status_codeberisi kode status,respons.headersberisi metadata balasan, danrespons.textberisi isi mentah balasan sebagai string. - Baris 31-33 (
data = respons.json()): isi JSON diuraikan menjadi dictionary Python, sehingga setiap nilainya dapat diakses dengandata["kota"]dandata["suhu_c"]. - Baris 35 (
server.shutdown()): server dimatikan agar program berakhir dengan bersih.
Parameter Query dan Header Permintaan #
Endpoint yang sama dapat mengembalikan data berbeda tergantung parameter query-nya. Daripada menempelkan ?kota=Bandung secara manual ke URL, kita mengirim dictionary params dan requests akan menyusun query string untuk kita — termasuk mengubah spasi dan karakter khusus menjadi bentuk yang sah untuk URL. Header permintaan dikirim lewat dictionary headers; dua yang sering dipakai adalah User-Agent (identitas program kita) dan Accept (format balasan yang kita harapkan).
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import requests
CUACA = {
"Bandung": {"suhu_c": 24, "kelembapan": 78},
"Surabaya": {"suhu_c": 32, "kelembapan": 65},
}
class HandlerCuaca(BaseHTTPRequestHandler):
def do_GET(self):
hasil = urlparse(self.path)
if hasil.path == "/api/cuaca":
kota = parse_qs(hasil.query).get("kota", ["Bandung"])[0]
data = CUACA.get(kota)
if data is None:
body = json.dumps({"pesan": "kota tidak dikenal"}).encode()
kode = 404
else:
body = json.dumps({"kota": kota, **data}).encode()
kode = 200
self.send_response(kode)
self.send_header("Content-Type", "application/json")
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), HandlerCuaca)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"
header_kita = {"User-Agent": "TokoKopiKita/1.0", "Accept": "application/json"}
for kota in ["Bandung", "Surabaya"]:
respons = requests.get(
f"{basis_url}/api/cuaca",
params={"kota": kota},
headers=header_kita,
timeout=5,
)
print("Query pada URL :", urlparse(respons.url).query)
print("Kode status :", respons.status_code)
print("Data :", respons.json())
print()
server.shutdown()Output yang Dihasilkan:
Query pada URL : kota=Bandung
Kode status : 200
Data : {'kota': 'Bandung', 'suhu_c': 24, 'kelembapan': 78}
Query pada URL : kota=Surabaya
Kode status : 200
Data : {'kota': 'Surabaya', 'suhu_c': 32, 'kelembapan': 65}Poin penting dari blok di atas:
- Baris 14-16 (
hasil = urlparse(self.path)dst.): di sisi server,urlparsememisahkan path dari query string, laluparse_qsmengurai query menjadi dictionary. Server simulasi kita membalaskan data sesuai kota yang diminta. - Baris 40-47 (
header_kitadanrequests.get(...)): headerUser-Agentmengidentifikasi program kita di mata server, laluparams={"kota": kota}membuatrequestsmenambahkan?kota=Bandungpada URL secara otomatis — terlihat pada keluaranurlparse(respons.url).query. Pendekatanparamslebih aman daripada menggabungkan string manual karenarequestsmenangani pengodean URL.
Kode Status dan Penanganan Galat #
Setiap balasan HTTP membawa kode status tiga digit yang meringkas hasil permintaan. Tiga kode yang paling sering kita jumpai:
| Kode | Nama | Arti | Tindakan kita |
|---|---|---|---|
| 200 | OK | Permintaan berhasil | Baca isi balasan |
| 404 | Not Found | Sumber daya tidak ada | Periksa penulisan endpoint dan parameter |
| 500 | Internal Server Error | Galat pada sisi server | Coba beberapa saat lagi; laporkan bila berlanjut |
Galat saat memakai API terbagi dalam dua keluarga: respons berstatus galat (4xx dan 5xx) yang tetap menghasilkan objek respons, dan kegagalan jaringan (server tidak merespons, koneksi ditolak) yang langsung memunculkan exception sebelum balasan diterima. Method raise_for_status() mengubah keluarga pertama menjadi exception, sehingga keduanya dapat kita tangani dengan satu pola try/except yang sama.
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
class HandlerAneka(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/cuaca":
body = json.dumps({"kota": "Bandung", "suhu_c": 24}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif self.path == "/api/galat":
self.send_response(500)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), HandlerAneka)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"
for path in ["/api/cuaca", "/api/galat", "/api/tidak-ada"]:
respons = requests.get(f"{basis_url}{path}", timeout=5)
print(f"{path:18} -> {respons.status_code}")
try:
respons = requests.get(f"{basis_url}/api/galat", timeout=5)
respons.raise_for_status()
except requests.RequestException as galat:
print("Galat HTTP tertangkap :", type(galat).__name__)
print("Program tetap berjalan karena galat ditangani")
try:
requests.get("http://127.0.0.1:1/api/cuaca", timeout=5)
except requests.RequestException as galat:
print("Galat koneksi tertangkap:", type(galat).__name__)
server.shutdown()Output yang Dihasilkan:
/api/cuaca -> 200
/api/galat -> 500
/api/tidak-ada -> 404
Galat HTTP tertangkap : HTTPError
Program tetap berjalan karena galat ditangani
Galat koneksi tertangkap: ConnectionErrorMari kita telaah bagian-bagian kuncinya:
- Baris 29-31 (
for path in ...): tiga permintaan ke tiga path menghasilkan tiga kode status yang berbeda (200, 500, 404) — semuanya tetap menghasilkan objekrespons, hanya isi dan kodenya yang berbeda. - Baris 34-35 (
respons.raise_for_status()): karena status 500 tergolong galat, method ini memunculkanrequests.HTTPError. Tanparaise_for_status(), kode 500 akan lolos diam-diam dan analisis kita memroses balasan yang kosong. - Baris 36 (
except requests.RequestException as galat):RequestExceptionadalah induk dari seluruh jenis galatrequests, sehingga satu blokexceptcukup untuk menangkapHTTPError,ConnectionError,Timeout, dan lainnya. - Baris 41 (
requests.get("http://127.0.0.1:1/...")): port 1 hampir pasti tidak ada layanannya, sehingga permintaan ini gagal di tahap koneksi dan memunculkanConnectionError— tetap tertangkap olehexceptyang sama.
Paginasi: Mengambil Data Per Halaman #
API yang menyimpan banyak data hampir tidak pernah mengirim semuanya sekaligus; data dibagi menjadi halaman (paginasi) agar satu balasan tetap ringan. Pola umumnya: kirim parameter page, terima sepotong data beserta penunjuk halaman berikutnya, ulangi sampai penunjuk tersebut kosong. Simulasi berikut melayani enam produk dalam dua halaman, tiga produk per halaman.
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import pandas as pd
import requests
PRODUK = [
{"produk": "Kopi Susu", "harga": 24000},
{"produk": "Americano", "harga": 22000},
{"produk": "Matcha Latte", "harga": 28000},
{"produk": "Teh Tawar", "harga": 20000},
{"produk": "Kopi Susu Dingin", "harga": 26000},
{"produk": "Biji Kopi 200g", "harga": 85000},
]
class HandlerProduk(BaseHTTPRequestHandler):
def do_GET(self):
hasil = urlparse(self.path)
if hasil.path == "/api/produk":
halaman = int(parse_qs(hasil.query).get("page", ["1"])[0])
awal = (halaman - 1) * 3
potongan = PRODUK[awal:awal + 3]
berikutnya = halaman + 1 if awal + 3 < len(PRODUK) else None
body = json.dumps(
{"halaman": halaman, "halaman_berikutnya": berikutnya, "hasil": potongan}
).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
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), HandlerProduk)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"
semua_baris = []
halaman = 1
while True:
respons = requests.get(
f"{basis_url}/api/produk", params={"page": halaman}, timeout=5
)
data = respons.json()
semua_baris.extend(data["hasil"])
print(f"Halaman {halaman}: {len(data['hasil'])} baris, berikutnya: {data['halaman_berikutnya']}")
if data["halaman_berikutnya"] is None:
break
halaman += 1
df = pd.DataFrame(semua_baris)
print()
print(df)
server.shutdown()Output yang Dihasilkan:
Halaman 1: 3 baris, berikutnya: 2
Halaman 2: 3 baris, berikutnya: None
produk harga
0 Kopi Susu 24000
1 Americano 22000
2 Matcha Latte 28000
3 Teh Tawar 20000
4 Kopi Susu Dingin 26000
5 Biji Kopi 200g 85000Alur pengambilan bertahap di atas bekerja seperti ini:
- Baris 44-45 (
semua_baris = []danhalaman = 1): wadah hasil disiapkan dan penanda halaman dimulai dari satu. - Baris 46-55 (
while True: ...): pada setiap putaran, satu halaman diminta denganparams={"page": halaman}; isi kolomhasilditambahkan kesemua_barisdenganextend(). Ketikahalaman_berikutnyabernilaiNone, loop berhenti — inilah syarat berhenti yang ditentukan server, bukan ditebak oleh kita. - Baris 57-59 (
df = pd.DataFrame(semua_baris)): karena setiap baris hasil berupa dictionary dengan kunci yang sama,pd.DataFrame()langsung membentuk tabel yang rapi — titik temu antara pengambilan data dari API dan analisis pandas yang telah kita kenal di tutorial intro pandas .
Perlu dicatat bahwa penanda halaman berbeda-beda antar API: sebagian memakai nomor halaman berikutnya seperti di atas, sebagian memakai token khusus (cursor), dan sebagian cukup memberi tahu total halaman. Prinsip loop-nya sama: ambil, kumpulkan, periksa penanda, ulangi.
Rangkuman #
- REST API memaparkan data lewat endpoint; method
GETuntuk mengambil; balasan umumnya berformat JSON yang dapat diurai denganrespons.json(). requests.get(url, params=..., headers=..., timeout=...)adalah pola dasar permintaan;paramsmenangani query string danheadersmembawa identitas serta preferensi format.- Kode status 200, 404, dan 500 menuntut tindakan berbeda;
raise_for_status()bersamatry/except requests.RequestExceptionmenangani keluarga galat HTTP sekaligus kegagalan koneksi. - Paginasi diselesaikan dengan loop halaman yang berhenti mengikuti penanda dari server, lalu seluruh hasil digabung menjadi satu DataFrame.
Sejauh ini data kita datang dari layanan yang memang dirancang untuk dibaca program. Namun di banyak organisasi, data yang paling kaya justru tersimpan di database: tabel penjualan, tabel pelanggan, dan tabel stok yang hidup di sistem operasional. Pada chapter berikutnya kita belajar menyambungkan Python ke database SQL dengan modul sqlite3 dan membaca hasil query langsung ke pandas di chapter Pengambilan Data dari Database.