agriflow-api / tools /fetch_bps_production.py
masterAAA123's picture
Space deploy v5: orphan snapshot of main, zero binary files
b81a86b
Raw
History Blame Contribute Delete
7.4 kB
"""
Fetch produksi pertanian per-kabupaten Jawa Timur dari BPS WebAPI (statictable).
=================================================================================
Kenapa skrip ini ada:
Sandbox build (server CI/agent) sering di-throttle/blok oleh Cloudflare BPS
sehingga webapi balas kosong. DI MESIN ANDA SENDIRI (IP rumah/kantor) biasanya
lolos. Jalankan skrip ini sekali di laptop Anda; hasilnya CSV bersih yang bisa
di-commit + dipakai offline untuk demo.
Cara pakai (di terminal mesin Anda, dari root repo):
pip install cloudscraper
python tools/fetch_bps_production.py
(Key dibaca dari .env -> BPS_API_KEY. Sudah ada di repo Anda.)
Output:
sample_data/bps_real/<komoditas>_produksi.csv (kab_id, produksi_ton, satuan, tahun, table_id)
sample_data/bps_real/_raw_<table_id>.json (raw response, untuk audit/parse ulang)
sample_data/bps_real/PROVENANCE.md (sumber tiap angka)
Kalau ada tabel yang gagal (balas kosong), skrip retry beberapa kali dengan jeda.
Kalau tetap gagal, ia lapor tabel mana yang perlu Anda download manual (Excel).
Skrip TIDAK pernah mengarang angka — hanya menulis yang benar-benar diterima dari API.
"""
from __future__ import annotations
import csv
import json
import os
import time
from pathlib import Path
try:
import cloudscraper
except ImportError:
raise SystemExit("Butuh cloudscraper. Jalankan: pip install cloudscraper")
ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "sample_data" / "bps_real"
DOMAIN = "3500" # Jawa Timur
BASE = "https://webapi.bps.go.id/v1/api"
# Tabel yang dicari (keyword -> komoditas yang diharapkan di dalamnya).
# table_id 716 (padi) sudah terkonfirmasi; sisanya dicari via keyword.
SEARCH = {
"padi": ["padi"],
"sayuran cabai": ["cabai", "cabe"],
"sayuran bawang": ["bawang"],
}
def _load_key() -> str:
key = os.getenv("BPS_API_KEY", "")
if not key:
env = ROOT / ".env"
if env.exists():
for line in env.read_text(encoding="utf-8").splitlines():
if line.startswith("BPS_API_KEY"):
key = line.split("=", 1)[1].strip()
if not key:
raise SystemExit("BPS_API_KEY tidak ditemukan (set di .env atau env var).")
return key
def _scraper():
return cloudscraper.create_scraper(
browser={"browser": "chrome", "platform": "windows", "desktop": True}
)
def _get(s, url: str, tries: int = 4, pause: float = 3.0):
"""GET dengan retry + jeda (hormati rate limit BPS ~100 req/menit)."""
for i in range(tries):
try:
r = s.get(url, timeout=60)
if r.status_code == 200 and r.text.strip():
try:
return r.json()
except json.JSONDecodeError:
pass # mungkin halaman challenge; retry
except Exception as e:
print(f" (attempt {i+1} error: {type(e).__name__})")
time.sleep(pause * (i + 1))
return None
def list_tables(s, key: str, keyword: str):
url = f"{BASE}/list/model/statictable/domain/{DOMAIN}/keyword/{keyword}/key/{key}/perpage/50/"
b = _get(s, url)
if not b or not isinstance(b.get("data"), list) or len(b["data"]) < 2:
return []
return [(r.get("table_id"), r.get("title", "")) for r in b["data"][1]]
def fetch_table(s, key: str, table_id):
url = f"{BASE}/view/model/statictable/lang/ind/domain/{DOMAIN}/id/{table_id}/key/{key}/"
b = _get(s, url)
if b:
(OUT_DIR / f"_raw_{table_id}.json").write_text(
json.dumps(b, ensure_ascii=False, indent=2), encoding="utf-8"
)
return b
def decode_per_kab(view_json):
"""Decode datacontent statictable -> {kab_id: {tahun: nilai}} (defensif).
Key datacontent = concat string mulai dgn kode verticalvar (region).
Kita hanya pertahankan region yang berupa kode kab BPS (35xx, 4 digit, bukan
agregat 'JAWA TIMUR'). Kembalikan juga metadata (tahun, turvar) untuk audit.
"""
d = view_json.get("data", {})
if not isinstance(d, dict):
return None
vervar = {str(v["val"]): v["label"] for v in d.get("verticalvar", [])}
tahun = {str(t["val"]): t["label"] for t in d.get("tahun", [])}
turvar = {str(t["val"]): t["label"] for t in d.get("turvar", [])}
dc = d.get("datacontent", {})
rows = []
for k, val in dc.items():
for vid, label in vervar.items():
if k.startswith(vid):
rows.append({"vervar_id": vid, "region": label, "key": k, "value": val})
break
return {
"title": d.get("title", ""),
"excel": d.get("excel", ""),
"tahun": tahun,
"turvar": turvar,
"vervar_count": len(vervar),
"rows": rows,
}
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
key = _load_key()
s = _scraper()
prov_lines = ["# BPS Production Data — Provenance", ""]
failed = []
# 1) padi terkonfirmasi (id 716) + cari tabel lain
target_ids = {"716": "Produksi Padi Menurut Kabupaten/Kota (Ton)"}
for kw in SEARCH:
print(f"[search] '{kw}' ...")
for tid, title in list_tables(s, key, kw.split()[-1]):
low = title.lower()
if "menurut kabupaten" in low and any(
w in low for w in ("produksi",)
) and any(w in low for w in ("padi", "cabai", "cabe", "bawang")):
target_ids[str(tid)] = title
time.sleep(2)
print(f"[tables] kandidat: {list(target_ids.keys())}")
for tid, title in target_ids.items():
print(f"[fetch] table {tid}: {title[:70]} ...")
view = fetch_table(s, key, tid)
if not view:
print(f" GAGAL (kosong) — perlu download manual.")
failed.append((tid, title))
continue
dec = decode_per_kab(view)
if not dec or dec["vervar_count"] < 10:
print(f" struktur tak terduga (vervar={dec['vervar_count'] if dec else '?'}) — cek _raw_{tid}.json")
failed.append((tid, title))
continue
out = OUT_DIR / f"table_{tid}.csv"
with out.open("w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["vervar_id", "region", "key", "value"])
w.writeheader()
w.writerows(dec["rows"])
print(f" OK -> {out.name} ({len(dec['rows'])} rows, tahun={list(dec['tahun'].values())})")
prov_lines += [
f"## table {tid}: {dec['title']}",
f"- URL: {BASE}/view/model/statictable/lang/ind/domain/{DOMAIN}/id/{tid}/key/<KEY>/",
f"- tahun: {', '.join(dec['tahun'].values())}",
f"- turvar (komoditas): {', '.join(list(dec['turvar'].values())[:8])}",
f"- excel: {dec['excel']}",
f"- fetched: {time.strftime('%Y-%m-%d')}",
"",
]
time.sleep(3)
(OUT_DIR / "PROVENANCE.md").write_text("\n".join(prov_lines), encoding="utf-8")
print("\n=== SELESAI ===")
print(f"Output di: {OUT_DIR}")
if failed:
print("\nTabel yang GAGAL (download manual Excel-nya dari jatim.bps.go.id):")
for tid, title in failed:
print(f" - id {tid}: {title}")
print("\nLangkah berikutnya: beri tahu Claude isi sample_data/bps_real/ -> DEA decode jadi surplus/deficit real.")
if __name__ == "__main__":
main()