kekavigi.xyz

Scraping RepoGempa

RepoGempa BMKG kembali mengubah situs dan tampilan basis data mereka. Waktunya bagi kita menulis ulang kode scraping data gempa.

Ditulis tanggal oleh A. Keyka Vigiliant. Revisi terakhir pada tanggal . Konten diterbitkan dibawah lisensi CC BY-SA 4.0.


Singkat cerita, selama beberapa tahun terakhir, saya berusaha menyusun ulang dataset event (kejadian) gempa yang dicatat BMKG, kedalam format yang manusiawi untuk dikelola oleh para peneliti/pelajar. Akan tetapi, sekitar pertengahan bulan Januari, BMKG mengubah tampilan situs dan konten basis data event gempa mereka. Berhubung saya memiliki waktu luang, akhirnya saya berhasil menulis ulang kode scraping dan menyusun kembali dataset. Kali ini saya akan mengajak Anda membuat kode scraping situs BMKG, dan beberapa komentar saya dalam prosesnya.

Pengamatan

Sebelum menulis kode, kita perlu mengamati tampilan situs BMKG yang baru, dan mencatat beberapa hal yang nantinya mungkin membantu kita. Saat ini, form pengisian data gempa dapat diakses di halaman /eventcatalog, yang terlihat lebih baik daripada tampilan sebelumnya. Bukan hanya secara visual, tapi dari tag-tag HTML yang digunakan. Berikut adalah potongan kode untuk form pengisian data gempa:

<form action="https://repogempa.bmkg.go.id/getEvent" method="get">
    <input type="hidden" name="_token" 
        value="6kE76Hz9KPfQdDDAcsL7YeudVlCfaiC692pz4unf">
    ...
</form>

Saya langsung mendapat firasat bahwa kita dapat meniru GET request pada form tersebut dengan Python. Lebih lanjut, setelah kita mengisi semua parameter pada form, kita akan diarahkan ke halaman dengan URL seperti berikut:

https://repogempa.bmkg.go.id/getEvent?
_token=6kE76Hz9KPfQdDDAcsL7YeudVlCfaiC692pz4unf
&date_range=2023-04-14T18:35:30
&min_date=2023-04-01T00:00:00&max_date=2023-04-14T23:59:59
&minmag=0.0&maxmag=4.9&mindepth=0&maxdepth=1000
&north=6&west=95&east=141&south=-11
&eventtype=preliminaryeq&parameter=origin
&email=kekavigi%40gmail.com&institution=ITB

Firasat saya bertambah kuat. Lebih lanjut, jika kita mengamati dengan seksama source code halaman tersebut, tepatnya di tag <script> ketiga dari terakhir, kita akan melihat

var locations =  [[1,"bmg2023hhyv","2023-04-14T23:15:07.618421Z",
"-8.578045845","109.0429382","3.500550505","M",58,"57","220.2638321",
"Java, Indonesia","BMKG"], ...]

Jackpot. Variabel itu berisi semua event gempa yang terjadi pada selang waktu yang diminta. Saya punya firasat kuat bahwa scraping kali ini akan jauh lebih mudah, tanpa perlu Selenium dan tanpa perlu menunggu selama satu minggu lebih (mungkin). Alhasil, tiga hal besar yang nanti perlu kita lakukan adalah:

  1. Tiru request event gempa
  2. Simpan berkas HTML yang didapat
  3. Ekstrak semua data dan kumpulkan menjadi satu dataset

Mari kita mulai.

Mempersiapkan Python

Ada beberapa pustaka (package) yang dapat membantu kita, atau setidaknya saya, dalam proses scraping nanti.

# Membantu mem-fetch halaman
import requests
from requests.models import PreparedRequest

from selectolax.parser import HTMLParser

# Berkaitan tentang waktu
from datetime import datetime, timedelta
from time import sleep

# Membantu mengolah data
import pandas as pd
from glob import glob
from ast import literal_eval

Selain itu, juga ada beberapa pustaka lain yang saya anggap akan meningkatkan “QOL” (Quality of Life):

from typing import Optional  # pengingat tipe variabel, karena saya pikun
from tqdm import tqdm  # akan menampilkan progress-bar ketika looping

Memformat waktu

Mari kita definisikan

DATE_RANGE  = timedelta(days=10)

Variabel ini menyatakan jangka waktu data gempa yang kita akan ambil. Dari hasil eksperimen, nilai days=30 cukup untuk data sebelum tahun 2018. Namun untuk data tahun itu dan setelahnya, hal ini dapat menyebabkan server database BMKG error – kemungkinan karena kelebihan beban request dalam waktu yang relatif singkat. Saya memilih pendekatan yang konservatif, secara personal karena saya kurang percaya dengan kemampuan server BMKG saja. Selanjutnya, mari kita buat fungsi untuk mengisi parameter waktu di dalam form:

def get_time(dt: datetime) -> str:
  '''Memformat waktu yang dipahami oleh form Event Request'''
  
  return dt.strftime('%Y-%m-%dT%H:%M:%S')

get_time(datetime(2019, 3, 8, 12, 30, 11))
# 2019-03-08T12:30:11

Menyimpan berkas

Dalam men-scraping, saya suka menyimpan berkas mentah (dalam kasus ini, berkas HTML) yang akan saya dapatkan. Anggaplah sebagai ‘bukti’ bahwa saya benar-benar mendownload data, dan bukan mengada-ada. Sisi positif yang lain, setelah semua berkas disimpan, proses meng-ekstrak data dari berkas-berkas HTML dapat dilakukan tanpa koneksi Internet.

def set_filename(min_date: datetime, max_date: datetime, dir: str) -> str:
  '''Menghasilkan nama berkas untuk HTML yang didapat'''

  _min = min_date.strftime('%Y-%m-%d')
  _max = max_date.strftime('%Y-%m-%d')
  return f'{dir}/{_min}-{_max}.html'

Lalu, mari kita membuat fungsi untuk menyimpan berkas HTML.

def save_html(
        min_date: datetime,
        max_date: Optional[datetime] = None,
        institution: str = 'school',
        email: str = '[email protected]',
        dir: str = 'raw'
    ) -> None:
    '''Mengunduh berkas HTML berisi data event gempa'''
    
    # Persiapan, secara personal, saya mengganggap `max_date`
    # tidak perlu wajib ada, karena secara implisit dapat
    # dihasilkan dengan bantuan `DATE_RANGE` 
    max_date = max_date if max_date else min_date+DATE_RANGE
    
    # Dapatkan token form
    url_form = 'https://repogempa.bmkg.go.id/eventcatalog'
    _html = requests.get(url_form)
    if _html.status_code!=200:
        raise ConnectionError(f'Gagal mem-fetch form. (Error {_html.status_code})')    
    _parsed = HTMLParser(_html.text)
    # lokasi token di berkas HTML
    token = _parsed.css_first('input[name="_token"]').attributes.get('value')
    
    # Lakukan request data gempa. Secara sederhana, kita
    # mencoba meminta data gempa tanpa perlu menge-klik
    # submit di halaman form Event Request
    url_event = 'https://repogempa.bmkg.go.id/getEvent'
    params = {
        '_token'        : token,
        'date_range'    : '',  # Tidak wajib
        'min_date'      : get_time(min_date),
        'max_date'      : get_time(max_date),
        'minmag'        : '0.0',
        'maxmag'        : '10.0',
        'mindepth'      : '0',
        'maxdepth'      : '1000',
        'north'         : '6',
        'west'          : '95',
        'east'          : '141',
        'south'         : '-11',
        'eventtype'     : 'preliminaryeq',
        'parameter'     : 'originfocal',  # Dapatkan mekanisme fokal gempa
        'email'         : email,
        'institution'   : institution,
    }
    req = PreparedRequest()
    req.prepare_url(url_event, params)
    _html = requests.get(req.url)
    if _html.status_code!=200:
        raise ConnectionError(f'Gagal mem-fetch hasil. (Error {_html.status_code})')    
    
    # simpan HTML yang didapat sebagai bukti
    with open(set_filename(min_date, max_date, dir), 'w') as f:
        f.write(_html.text)

Lalu, mari kita mulai proses scraping data gempa!

# Data gempa paling tua yang tercatat di Repo Gempa
start_day = datetime(2008, 11, 1, 0, 0, 0)

while start_day < datetime.now():
  # sebenarnya akan lebih baik untuk berhenti beberapa hari sebelum 
  # *hari ini*, tetapi saya rasa ini juga sudah cukup.

  try:
    save_html(start_day)
  except ConnectionError:
    # Server error, coba abaikan hari ini dan scraping dari hari besok.
    # Alasan error terjadi akan dianalisis secara manual. 
    start_day += timedelta(days=1)
  else:
    # Data gempa dalam jangka waktu `DATE_RANGE` berhasil diunduh.
    # Mulai scraping dari awal waktu yang baru.
    start_day += DATE_RANGE
  finally:
    # Apapun yang terjadi, pada akhirnya kita perlu tunggu beberapa
    # detik sebelum lanjut mendownload data. Kita tidak ingin secara
    # tidak sengaja membuat server BMKG *down*.
    sleep(30)  # detik

Mengekstrak semua data

Pada tahap ini, kita sudah mendapatkan semua berkas HTML berisi event gempa. Mari kita buat fungsi untuk mengambil data gempa dari satu berkas HTML:

def get_data(filename: str):
  ''' Ekstrak data gempa dari filename'''

  with open(filename) as f:
    # baca berkas HTML
    text = f.read()

  # Ambil data gempa. Data terletak di tag <script> ketiga dari terakhir.
  the_script = HTMLParser(text).css('script')[-3].text()
  data = the_script.split('var locations =')[1].split(';\n')[0].strip()
    
  # Proses di atas akan mendapatkan data berupa list dalam format string.
  # Kita perlu mengubahnya ke format yang lebih manusiawi, yakni list.
  data = literal_eval(data) if data!='null' else []
    
  return data

dan mari ambil semua gempa!

total = []

# Semua berkas HTML yang sudah kita unduh berada di folder `raw`
for filename in tqdm(glob('raw/*.html')):
    total.extend(get_data(filename))

# Ini ditulis dari masa depan. Semua data gempa yang bermasalah
# akan dicoba untuk didownload ulang; mereka akan diletakkan di
# folder `nasty`. 
for filename in tqdm(glob('nasty/*.html')):
    total.extend(get_data(filename))

len(total)

Setelah itu dilakukan, mari ubah struktur data list menjadi DataFrame

# Daftar kolom di data Gempa
columns = [
    'No', 'eventID', 'datetime', 'latitude', 'longitude', 'magnitude', 'mag_type',
    'depth', 'phasecount', 'azimuth_gap', 'location', 'agency',  'datetimeFM',
    'latFM', 'lonFM', 'magFM', 'magTypeFM', 'depthFM', 'phasecountFM', 'AzGapFM',
    'scalarMoment', 'Mrr', 'Mtt', 'Mpp', 'Mrt', 'Mrp', 'Mtp', 'varianceReduction',
    'doubleCouple', 'clvd', 'strikeNP1', 'dipNP1', 'rakeNP1', 'strikeNP2', 'dipNP2',
    'rakeNP2', 'azgapFM', 'misfit',
]

# buat DataFrame
df = pd.DataFrame(total, columns=columns)

# Beberapa perapian kecil:
# ubah kolom datetime dari string ke objek datetime
df.datetime = pd.to_datetime(df.datetime)
# buang kolom 'No' (Nomor)
df = df.drop(columns=['No'])
# buang data duplikat, jika ada
df = df.drop_duplicates()
# urutkan data berdasarkan datetime.
df = df.sort_values(by='datetime').reset_index(drop=True)

Mengecek dan mengurus data yang bermasalah

Secara sederhana, kita mencoba mencari jika ada dua event gempa yang terpisah lebih dari satu hari. Karena hampir mustahil negara yang luas dan aktif secara tektonik tidak mengalami satupun gempa dalam satu hari, kita berkesimpulan kita gagal mengunduh data gempa di hari itu. Atau memang tidak ada sensor BMKG yang mencatat gempa di hari itu.

# cek selisih waktu event gempa dengan event sebelumnya
time_diff = df.datetime.sort_values().diff(periods=1)

# tampilkan semua selisih hari antar event gempa
set(time_diff.apply(lambda x: x.days))
# {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, nan, 27.0}

Whoops! Mari kita dapatkan indeks event gempa yang mungkin bermasalah ini, dan coba unduh ulang data gempa diantara dua event yang selisih waktunya lebih dari satu hari.

nasty_timeline = time_diff[time_diff.apply(lambda x: x.days)>=1.0].sort_index()

for i in tqdm(nasty_timeline.index):
    a = df.datetime[i-1]
    b = df.datetime[i]
    try:
        save_html(a, b, dir='nasty')
    except ConnectionError:
        print('Memang bermasalah. Tidak ada yang bisa dilakukan(?).')
    sleep(30)

Setelah proses ini selesai, ulangi tahap Mengekstrak semua data, dan setelah itu, kita selangkah dari kata selesai.

df.to_csv('katalog_gempa_v2.tsv', sep='\t')

Berkas ini dapat Anda lihat di dataset Kaggle ini.

Firasat pribadi

Walaupun kita telah mendapatkan data event gempa, saya merasakan sesuatu yang aneh terjadi. Pertama, proses scraping selesai jauh lebih cepat daripada versi sebelumnya. Ini seharusnya pertanda baik bahwa BMKG telah mengalokasikan waktu dan anggaran untuk mengurus basis data. Namun saya merasa ada beberapa event gempa yang gagal saya dapatkan. Kedua, dari hasil scraping awal, saya melihat 27 hari adalah selang waktu terbesar antara dua gempa. Ini terasa mustahil karena pada scraping tampilan situs lawas, selang waktu yang terjadi jauh lebih besar, khususnya pada tahun 2010-2015. Jadi, antara BMKG berhasil memperbaiki basis data untuk event gempa yang terjadi diantara tahun 2010-2015, atau sesuatu yang aneh telah terjadi.

Apapun itu, seseorang perlu menganalisis dataset ini, dan membandingkannya dengan dataset lawas. Maukah Anda mencobanya? Berikut adalah halaman Kaggle hasil analisis singkat saya.