Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ PY

BAB 22: Skrip yang Tidak Mudah Menyerah: Galat, Batch Tahan Galat, dan Ekspresi QGIS

Studi kasus: "Enam kiriman, satu berkas rusak, dan laporan yang menulis dirinya sendiri" #

Enam tim lapangan mengirim titik survei. Malam hari, skrip penggabung Anda berhenti di tengah jalan dengan pesan Inggris yang panjang. Anda tidak tahu berkas mana yang bermasalah. Esoknya Kepala Seksi hanya bertanya: berapa kiriman yang diterima, mana yang ditolak, dan mengapa? Ia juga minta satu kalimat ringkas per KPH, misalnya "KPH Beta, 4 ha, 5 titik panas", yang angkanya berubah tiap bulan. Bab ini mengajarkan dua hal. Pertama, membuat skrip yang menjawab pertanyaan itu sendiri. Kedua, menulis rumus laporan satu kali saja.

Konsep: galat dalam tiga kalimat #

Galat (exception) adalah tanda dari Python bahwa ada yang gagal. Tanpa penanganan, skrip langsung berhenti. Dengan try dan except, Anda menangkap galat itu, mencatat sebabnya, lalu meneruskan pekerjaan.

Bayangkan petugas gudang yang memeriksa tiap kiriman. Barang rusak dikembalikan dengan catatan. Gudang tidak ditutup hanya karena satu kardus basah. Bab 10 sudah mengajarkan dasarnya. Bab ini memakainya pada data GIS.

Ilustrasi 22.1: Tanpa dan dengan penanganan galat
Skema dua jalur: tanpa try-except satu galat menghentikan semua berkas, dengan try-except tiap berkas dicatat lalu laporan lengkap dibuat

Lima istilah yang perlu Anda ingat:

  • Galat (exception): peristiwa yang menghentikan skrip bila tidak ditangani.
  • try dan except: coba kerjakan, dan bila galat, tangkap lalu tangani.
  • raise: perintah untuk menimbulkan galat sendiri dengan pesan yang jelas.
  • Validasi: memeriksa masukan sebelum dipakai.
  • Log: catatan hasil tiap langkah, yang bisa dibaca belakangan.

Skrip 22.1: Memeriksa masukan sebelum dipakai #

Berkas b4_01_galat_dasar.py sama dengan yang dipakai di Buku I4. Di dalam berkasnya tertulis "Skrip 4.1". Skrip ini sudah dibedah baris demi baris di Bab 10. Di sini ia dipakai sebagai cetakan: fungsi penjaga di baris 10-22 muncul lagi dalam bentuk lain pada Skrip 22.2 dan 22.3. Karena itu penjelasannya dipadatkan.

Tujuan. Memeriksa empat berkas kiriman satu per satu. Berkas yang bermasalah dilewati dengan pesan jelas, dan berkas yang baik tetap dibaca.

Bahan. Paket data paket-i4, folder Kiriman_Lapangan. Isinya tim_a.gpkg (baik, 6 titik), tim_e.gpkg (rusak), dan tim_f.gpkg (kolom Kondisi hilang). Berkas tim_x.gpkg memang tidak ada. Ganti DATA di baris 6 sesuai komputer Anda. Dijalankan di Editor Skrip QGIS, seperti di Bab 14.

Gambar berikut menunjukkan empat penjaga di dalam fungsi muat_vektor.

Ilustrasi 22.2: Fungsi penjaga muat_vektor
Skema empat pemeriksaan berurutan: berkas ada, bisa dibuka, kolom lengkap, ada isinya; yang gagal memicu raise

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 4.1: Memeriksa masukan sebelum dipakai dan menangkap galat dengan try-except]
# Penulis: Badar Mubarok Yogaswara
import os
from qgis.core import QgsVectorLayer

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"


def muat_vektor(jalur, nama, field_wajib=()):
    """Membuka layer vektor. Bila ada yang tidak beres, berhenti dengan pesan yang jelas."""
    if not os.path.exists(jalur):
        raise FileNotFoundError("berkas tidak ada: " + jalur)
    layer = QgsVectorLayer(jalur, nama, "ogr")
    if not layer.isValid():
        raise ValueError("berkas tidak bisa dibuka (mungkin rusak): " + os.path.basename(jalur))
    kurang = [f for f in field_wajib if layer.fields().indexOf(f) < 0]
    if kurang:
        raise ValueError("field wajib tidak ada: " + ", ".join(kurang))
    if layer.featureCount() == 0:
        raise ValueError("layer kosong, tidak ada fitur")
    return layer


# Empat berkas dicoba satu per satu. Satu gagal tidak boleh menghentikan yang lain.
kasus = [
    DATA + "/Kiriman_Lapangan/tim_a.gpkg",      # baik
    DATA + "/Kiriman_Lapangan/tim_x.gpkg",      # tidak ada
    DATA + "/Kiriman_Lapangan/tim_e.gpkg",      # rusak
    DATA + "/Kiriman_Lapangan/tim_f.gpkg",      # field Kondisi tidak ada
]
for jalur in kasus:
    nama = os.path.basename(jalur)
    try:
        layer = muat_vektor(jalur, nama, field_wajib=["Kondisi", "Tinggi_Phn"])
    except FileNotFoundError as e:
        print("[LEWATI] %s -> %s" % (nama, e))
    except ValueError as e:
        print("[LEWATI] %s -> %s" % (nama, e))
    else:
        # blok else: hanya jalan bila TIDAK ada galat
        print("[OK]     %s -> %d fitur" % (nama, layer.featureCount()))
    finally:
        # blok finally: selalu jalan, berhasil atau gagal
        print("         selesai memeriksa", nama)

# Galat hitungan yang sering terjadi: pembagian dengan nol
def persen(bagian, total):
    try:
        return 100 * bagian / total
    except ZeroDivisionError:
        return None          # tidak ada jawaban yang masuk akal

print("Persen 3 dari 12:", persen(3, 12))
print("Persen 3 dari 0:", persen(3, 0))

Penjelasan singkat. Nomor baris mengikuti berkas di atas. Rincian tiap baris ada di Bab 10.

BarisBagianArtinya
1-7PembukaJudul, perkakas os dan QgsVectorLayer, serta dua alamat. Skrip ini hanya memakai DATA. HASIL tidak dipakai.
10-22Fungsi penjaga muat_vektorEmpat pemeriksaan berurutan. Begitu satu gagal, fungsi memicu raise dengan pesan jelas dan berhenti.
25-45Perulangan dengan trytry memanggil penjaga. Dua except menangkap FileNotFoundError dan ValueError. else jalan bila tidak ada galat. finally selalu jalan.
47-55Fungsi persenGalat ZeroDivisionError ditangkap, lalu fungsi mengembalikan None.

Dua hal perlu Anda amati di sini. Pertama, baris 36-39 berisi dua except dengan isi yang sama. Ini boleh, dan Anda akan melihat cara menggabungkannya pada latihan. Kedua, kolom wajib ditulis di baris 35, bukan di dalam fungsi. Jadi fungsi yang sama bisa dipakai untuk layer lain dengan kolom wajib yang lain.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
[OK]     tim_a.gpkg -> 6 fitur
         selesai memeriksa tim_a.gpkg
[LEWATI] tim_x.gpkg -> berkas tidak ada: D:/Latihan/paket-i4/Kiriman_Lapangan/tim_x.gpkg
         selesai memeriksa tim_x.gpkg
[LEWATI] tim_e.gpkg -> berkas tidak bisa dibuka (mungkin rusak): tim_e.gpkg
         selesai memeriksa tim_e.gpkg
[LEWATI] tim_f.gpkg -> field wajib tidak ada: Kondisi
         selesai memeriksa tim_f.gpkg
Persen 3 dari 12: 25.0
Persen 3 dari 0: None

Satu berkas baik dan tiga berkas bermasalah, tetapi keempatnya diperiksa. Perhatikan bahwa baris selesai memeriksa muncul untuk semuanya, karena itu tugas finally.

Bagian yang boleh diubah.

  • Baris 6, lokasi data. Ganti dengan lokasi paket-i4 di komputer Anda.
  • Baris 27-30, daftar berkas. Tambah atau ganti berkas yang ingin diperiksa. Akibatnya, tiap berkas baru ikut diperiksa dan dilaporkan.
  • Baris 35, kolom wajib. field_wajib=["Kondisi", "Tinggi_Phn"] boleh diganti nama kolom lain. Akibatnya, berkas yang tidak punya kolom itu dilewati.
  • Baris 20-21, syarat kosong. Hapus dua baris ini bila layer kosong boleh diterima.

Jebakan.

  • Folder salah. Bila folder di baris 27-30 salah ketik, semua berkas dilaporkan tidak ada, termasuk tim_a.gpkg yang sebenarnya baik. Pada uji, nama folder Kiriman_Lapangan diganti Kiriman dan layar menampilkan [LEWATI] tim_a.gpkg -> berkas tidak ada: D:/Latihan/paket-i4/Kiriman/tim_a.gpkg. Pesan itu jujur: berkas memang tidak ada di alamat itu. Periksa alamatnya, bukan datanya.
  • Salah ketik di dalam fungsi tidak tertangkap. Skrip hanya menangkap FileNotFoundError dan ValueError. Pada uji, layer.fields() di baris 17 sengaja diubah menjadi layer.field(). Layar menampilkan selesai memeriksa tim_a.gpkg, lalu skrip berhenti dengan AttributeError: 'QgsVectorLayer' object has no attribute 'field'. Did you mean: 'fields'?. Baris finally tetap berjalan sebelum skrip berhenti. Ini bagus: galat yang tidak Anda duga memang harus terlihat.
  • None tidak bisa dihitung. Fungsi persen(3, 0) mengembalikan None. Bila hasilnya langsung dipakai hitung, misalnya persen(3, 0) + 1, muncul TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'. Periksa hasilnya dulu sebelum dihitung.

Latihan variasi.

  1. Tambahkan tim_b.gpkg dan tim_d.gpkg ke daftar kasus.
  2. Hapus "Kondisi" dari daftar kolom wajib. Berkas mana yang berubah status?
  3. Gabungkan dua except di baris 36-39 menjadi satu.

Contoh jawaban nomor 1, hanya bagian yang berubah (sisipkan sebelum tanda ] di baris 31):

PYTHON
    DATA + "/Kiriman_Lapangan/tim_b.gpkg",
    DATA + "/Kiriman_Lapangan/tim_d.gpkg",

Layar menambah dua baris: [OK] tim_b.gpkg -> 5 fitur dan [OK] tim_d.gpkg -> 3 fitur. Perhatikan bahwa tim_d.gpkg lolos walau sistem koordinatnya berbeda. Skrip ini tidak memeriksa CRS. Pemeriksaan itu baru ada di Skrip 22.2.

Contoh jawaban nomor 2: ubah baris 35 menjadi field_wajib=["Tinggi_Phn"]. Layar menampilkan [OK] tim_f.gpkg -> 3 fitur, karena kekurangannya hanya kolom Kondisi.

Contoh jawaban nomor 3 (sudah diuji, hasilnya sama persis):

PYTHON
    except (FileNotFoundError, ValueError) as e:
        print("[LEWATI] %s -> %s" % (nama, e))

Skrip 22.2: Batch tahan galat dengan laporan #

Skrip ini membawa pola penjaga ke pekerjaan nyata: menggabungkan semua kiriman lapangan. Bedanya, galat ditangkap lalu dicatat, bukan hanya dicetak. Catatan itu menjadi laporan untuk Kepala Seksi.

Tujuan. Menggabungkan semua kiriman lapangan menjadi satu layer, sambil mencatat berkas yang ditolak, CRS yang diubah, dan titik yang jatuh di luar semua KPH.

Bahan. Paket paket-i4: enam berkas Kiriman_Lapangan/tim_a.gpkg sampai tim_f.gpkg, serta Batas_KPH.gpkg. Berkas b4_02_batch_tahan_galat.py (di dalamnya tertulis "Skrip 4.2"). Ganti DATA di baris 9 dan HASIL di baris 10. Skrip membuat folder HASIL bila belum ada. Pekerjaan memakai alat Processing, yang dibahas di Bab 21. CRS (sistem koordinat) dibahas di Bab 14.

Skrip ini bekerja dalam lima tahap. Gambar berikut menunjukkan urutannya, lengkap dengan nomor baris.

Ilustrasi 22.3: Alur skrip 22.2
Skema lima tahap: siapkan, periksa tiap berkas, gabung yang lolos, cek titik di luar KPH, tulis laporan

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 4.2: Batch tahan galat: menggabungkan kiriman lapangan, mencatat yang gagal]
# Penulis: Badar Mubarok Yogaswara
import csv
import glob
import os
import processing
from qgis.core import QgsProject, QgsVectorLayer, QgsCoordinateReferenceSystem

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
os.makedirs(HASIL, exist_ok=True)

CRS_TUJUAN = "EPSG:32749"
FIELD_WAJIB = ["Kondisi", "Tinggi_Phn", "Foto"]
log = []                    # satu baris catatan untuk tiap berkas: (berkas, status, keterangan)
bagus = []                  # layer yang lolos pemeriksaan

for jalur in sorted(glob.glob(DATA + "/Kiriman_Lapangan/*.gpkg")):
    nama = os.path.basename(jalur)
    try:
        layer = QgsVectorLayer(jalur, nama, "ogr")
        if not layer.isValid():
            raise ValueError("berkas rusak atau bukan GeoPackage")
        kurang = [f for f in FIELD_WAJIB if layer.fields().indexOf(f) < 0]
        if kurang:
            raise ValueError("field hilang: " + ", ".join(kurang))
        catatan = "sesuai"
        if layer.crs().authid() != CRS_TUJUAN:
            asal = layer.crs().authid()
            layer = processing.run("native:reprojectlayer", {
                "INPUT": layer, "TARGET_CRS": QgsCoordinateReferenceSystem(CRS_TUJUAN),
                "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
            catatan = "CRS %s diubah ke %s" % (asal, CRS_TUJUAN)
        bagus.append(layer)
        log.append((nama, "DITERIMA", "%d titik; %s" % (layer.featureCount(), catatan)))
    except Exception as e:                       # satu berkas gagal, yang lain tetap diproses
        log.append((nama, "DITOLAK", str(e)))

if not bagus:             # tanpa berkas lolos, penggabungan gagal; tampilkan dulu sebab penolakan
    for baris in log:
        print("%-22s %-9s %s" % baris)
    raise RuntimeError("Tidak ada berkas yang lolos pemeriksaan; penggabungan dibatalkan")

# Gabungkan yang lolos
gabungan = processing.run("native:mergevectorlayers", {
    "LAYERS": bagus, "CRS": QgsCoordinateReferenceSystem(CRS_TUJUAN),
    "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
gabungan.setName("Titik_Survei_Gabungan")
QgsProject.instance().addMapLayer(gabungan)

# Pemeriksaan keberadaan: titik yang tidak jatuh di dalam KPH mana pun patut dicurigai
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
cek = processing.run("native:joinattributesbylocation", {
    "INPUT": gabungan, "JOIN": batas, "JOIN_FIELDS": ["NAMA_KPH"], "METHOD": 1,
    "PREDICATE": [0], "DISCARD_NONMATCHING": False, "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
luar = [f["Foto"] for f in cek.getFeatures() if not f["NAMA_KPH"]]
for foto in luar:
    log.append((foto, "CURIGA", "titik jatuh di luar semua KPH"))

# Laporan: layar dan berkas CSV
with open(HASIL + "/log_kiriman.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["berkas", "status", "keterangan"])
    w.writerows(log)
for baris in log:
    print("%-22s %-9s %s" % baris)
print("Ringkasan: %d diterima, %d ditolak, %d curiga; total titik gabungan %d" % (
    sum(1 for b in log if b[1] == "DITERIMA"), sum(1 for b in log if b[1] == "DITOLAK"),
    len(luar), gabungan.featureCount()))

Penjelasan baris demi baris. Skrip ini 69 baris, jadi dibahas dalam enam blok. Nomor baris mengikuti berkas di atas.

Blok 1: persiapan (baris 3-16).

BarisTulisanArtinya
3-6import csv, glob, os, processingPanggil perkakas: csv untuk menulis tabel teks, glob untuk mencari berkas dengan pola nama, os untuk urusan folder, dan processing untuk memanggil alat QGIS.
7from qgis.core import ...Panggil tiga perkakas QGIS: proyek, pembuka layer vektor, dan pengenal sistem koordinat.
9-11DATA, HASIL, os.makedirs(...)Dua alamat di satu tempat. os.makedirs(HASIL, exist_ok=True) membuat folder hasil. Bila folder sudah ada, tidak terjadi galat.
13CRS_TUJUAN = "EPSG:32749"Kode sistem koordinat yang diinginkan. EPSG:32749 adalah UTM zona 49S, sama dengan data paket ini.
14FIELD_WAJIB = [...]Tiga kolom yang harus ada di setiap berkas.
15-16log = [] dan bagus = []Dua daftar kosong. log menampung catatan, bagus menampung layer yang lolos.

Blok 2: periksa tiap berkas (baris 18-37). Ini jantung skrip. Isi try memuat tiga pemeriksaan berlapis. Pemeriksaan yang gagal memicu raise, dan except di baris 36 mencatatnya.

BarisTulisanArtinya
18for jalur in sorted(glob.glob(...)):glob.glob mencari semua berkas berpola *.gpkg di folder kiriman. Tanda bintang mewakili bagian nama apa saja. sorted mengurutkannya menurut abjad.
19-20nama = ... lalu try:Ambil nama berkas saja, lalu mulai bagian yang boleh gagal.
21-23layer = QgsVectorLayer(...) lalu raise ValueError(...)Pemeriksaan 1: berkas harus bisa dibuka. Berkas rusak menghasilkan layer tidak valid.
24-26kurang = [...] lalu raise ValueError(...)Pemeriksaan 2: semua kolom wajib ada. Caranya sama dengan Skrip 22.1.
27catatan = "sesuai"Catatan awal. Teks ini diganti bila CRS diubah.
28-33if layer.crs().authid() != CRS_TUJUAN:Pemeriksaan 3: authid() memberi kode CRS layer, misalnya EPSG:4326. Bila beda dari tujuan, alat native:reprojectlayer mengubahnya. "TEMPORARY_OUTPUT" berarti hasil disimpan sementara. ["OUTPUT"] mengambil layer hasil dari kamus jawaban alat.
34bagus.append(layer)Layer yang lolos masuk daftar bagus.
35log.append((nama, "DITERIMA", ...))Tambahkan satu catatan berisi tiga hal: nama, status, dan keterangan. Tiga hal dalam kurung ini disebut tuple (Bab 5).
36-37except Exception as e:Tangkap galat jenis apa pun. Catat berkas sebagai DITOLAK beserta teks galatnya, str(e). Exception adalah induk hampir semua galat biasa.

Blok 3: pengaman bila tidak ada yang lolos (baris 39-42). Daftar kosong dianggap salah (False) oleh Python. Karena itu if not bagus: berarti "bila tidak ada satu pun layer yang lolos". RuntimeError adalah jenis galat umum untuk "ada yang tidak beres saat skrip berjalan". Seperti raise ValueError di Skrip 22.1, raise menghentikan skrip dengan pesan buatan sendiri.

BarisTulisanArtinya
39if not bagus:Periksa apakah daftar bagus masih kosong.
40-41for baris in log: lalu print(...)Cetak dulu seluruh catatan, supaya sebab penolakan terlihat.
42raise RuntimeError("Tidak ada berkas yang lolos ...")Hentikan skrip dengan pesan jelas. Penggabungan tidak dicoba, karena tidak ada yang bisa digabung.

Blok 4: gabungkan yang lolos (baris 44-49).

BarisTulisanArtinya
45-47processing.run("native:mergevectorlayers", {...})Gabungkan semua layer di daftar bagus menjadi satu layer sementara, dalam CRS tujuan.
48-49gabungan.setName(...) lalu addMapLayer(...)Beri nama yang jelas, lalu tampilkan di proyek.

Blok 5: cek titik di luar KPH (baris 51-58). Titik survei yang tidak jatuh di dalam KPH mana pun patut dicurigai: mungkin salah koordinat.

BarisTulisanArtinya
52batas = QgsVectorLayer(...)Buka layer batas KPH.
53-55processing.run("native:joinattributesbylocation", {...})Tempelkan nama KPH ke tiap titik menurut letaknya. METHOD: 1 berarti ambil pasangan pertama saja. PREDICATE: [0] berarti titik dan KPH harus berpotongan (intersect). DISCARD_NONMATCHING: False berarti titik tanpa pasangan tetap disimpan, dengan kolom NAMA_KPH kosong.
56luar = [f["Foto"] for f in ... if not f["NAMA_KPH"]]Kumpulkan kolom Foto dari titik yang NAMA_KPH-nya kosong. Kolom Foto dipakai sebagai pengenal titik.
57-58for foto in luar:Untuk tiap titik di luar, tambahkan catatan CURIGA ke log.

Blok 6: laporan (baris 60-69).

BarisTulisanArtinya
61-64with open(... "log_kiriman.csv" ...)Tulis seluruh log ke berkas CSV: satu baris judul, lalu satu baris per catatan. newline="" mencegah baris kosong ekstra di Windows.
65-66print("%-22s %-9s %s" % baris)Cetak tiap catatan. %-22s menyediakan 22 kolom rata kiri, %-9s sembilan kolom. Tuple baris mengisi tiga tempat sekaligus.
67-69print("Ringkasan: ...")Hitung dan cetak ringkasan. sum(1 for b in log if b[1] == "DITERIMA") menambah angka 1 untuk tiap catatan berstatus itu.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
tim_a.gpkg             DITERIMA  6 titik; sesuai
tim_b.gpkg             DITERIMA  5 titik; sesuai
tim_c.gpkg             DITERIMA  4 titik; sesuai
tim_d.gpkg             DITERIMA  3 titik; CRS EPSG:4326 diubah ke EPSG:32749
tim_e.gpkg             DITOLAK   berkas rusak atau bukan GeoPackage
tim_f.gpkg             DITOLAK   field hilang: Kondisi
b_05.jpg               CURIGA    titik jatuh di luar semua KPH
Ringkasan: 4 diterima, 2 ditolak, 1 curiga; total titik gabungan 18

Berkas log_kiriman.csv berisi tujuh baris catatan yang sama, ditambah satu baris judul berkas,status,keterangan. Dari hasil ini Anda tahu empat hal. Empat berkas diterima. Dua berkas ditolak beserta alasannya. tim_d.gpkg diterima setelah diubah dari EPSG:4326. Satu titik, b_05.jpg, jatuh di luar semua KPH. Total titik gabungan 18, yaitu 6 + 5 + 4 + 3.

Bagian yang boleh diubah.

  • Baris 9-10, jalur. Sesuaikan dengan komputer Anda.
  • Baris 13, CRS tujuan. Ganti kode EPSG sesuai wilayah Anda. Akibatnya, berkas dengan CRS lain diubah ke kode itu.
  • Baris 14, kolom wajib. Ganti dengan kolom yang menurut Anda harus ada.
  • Baris 18, pola nama. *.gpkg boleh diganti, misalnya tim_*.gpkg.
  • Baris 39-42, pengaman. Pesan di baris 42 boleh diganti kalimat Anda sendiri. Jangan hapus pengaman ini. Tanpa baris itu, daftar bagus yang kosong membuat penggabungan gagal dengan pesan yang tidak menyebut sebabnya (lihat Jebakan).
  • Baris 54, METHOD dan PREDICATE. Angkanya mengikuti daftar pilihan alat. Menurut daftar alat di QGIS 4.0.2, PREDICATE 0 berarti intersect, 1 contain, 2 equal, 3 touch, 4 overlap, 5 are within, dan 6 cross. METHOD 0 membuat satu fitur per pasangan, 1 mengambil pasangan pertama, dan 2 mengambil pasangan dengan irisan terbesar.

Jebakan.

  • except Exception menelan salah ketik Anda. Bila layer.fields() di baris 24 salah ditulis menjadi layer.field(), setiap berkas yang bisa dibuka dicatat DITOLAK dengan teks 'QgsVectorLayer' object has no attribute 'field'. Daftar bagus jadi kosong. Pengaman di baris 39-42 lalu mencetak seluruh log dan berhenti dengan RuntimeError. Jadi sebab sebenarnya terlihat di layar. Tanpa pengaman itu, skrip berhenti di penggabungan dengan QgsProcessingException: Could not create memory layer, dan laporan tidak sempat tercetak. Bila Anda melihat teks galat yang sama pada banyak berkas baik, curigai skrip Anda, bukan datanya.
  • Folder kiriman salah. Bila pola di baris 18 tidak menemukan berkas, bagus juga kosong dan log kosong. Layar hanya menampilkan RuntimeError: Tidak ada berkas yang lolos pemeriksaan; penggabungan dibatalkan, tanpa daftar berkas. Pesan ini tidak menyebut folder. Bila Anda melihatnya dan log tidak berisi catatan apa pun, periksa dulu DATA di baris 9 dan pola di baris 18.
  • Kolom pertama laporan bercampur. Untuk berkas, kolom pertama berisi nama berkas (tim_a.gpkg). Untuk titik curiga, kolom pertama berisi nama foto (b_05.jpg). Itu bukan galat, tetapi hati-hati saat membaca. Untuk mengetahui berkas asal titik itu, buka kolom Foto di layer Titik_Survei_Gabungan.
  • Titik curiga tidak dibuang. Ringkasan menyebut 1 curiga, tetapi total titik tetap 18. Titik itu hanya ditandai.

Latihan variasi.

  1. Tambahkan pemeriksaan: tolak berkas yang tidak punya fitur sama sekali.
  2. Setelah laporan, cetak ulang hanya catatan yang berstatus DITOLAK.

Contoh jawaban nomor 1 (sudah diuji): sisipkan dua baris ini setelah baris 23.

PYTHON
        if layer.featureCount() == 0:
            raise ValueError("layer kosong")

Pada data paket ini tidak ada berkas kosong, jadi hasilnya tetap sama. Pemeriksaan ini baru bekerja bila ada kiriman kosong.

Contoh jawaban nomor 2 (sudah diuji): tambahkan di akhir skrip.

PYTHONUnduh berkas
print("--- hanya yang ditolak ---")
for baris in log:
    if baris[1] == "DITOLAK":
        print(baris)

Layar menambah tiga baris, yaitu judul dan dua catatan. Misalnya ('tim_f.gpkg', 'DITOLAK', 'field hilang: Kondisi').

Skrip 22.3: Pembungkus untuk alat Processing #

Skrip 22.2 menangkap galat di dalam perulangan. Skrip ini menyiapkan cara yang lebih rapi: pembungkus kecil yang memanggil alat Processing dan tidak pernah menghentikan skrip. Pembungkus itu selalu mengembalikan dua hal: hasil dan pesan.

Tujuan. Membuat fungsi jalankan_aman yang menjalankan alat Processing dan mengembalikan hasil atau pesan galat, lalu mengujinya pada empat kasus.

Bahan. Paket paket-i4, berkas Sungai.gpkg. Berkas b4_03_galat_alat.py (di dalamnya tertulis "Skrip 4.3"). Ganti DATA di baris 7. Skrip hanya mencetak teks.

Ilustrasi 22.4: Alur pembungkus jalankan_aman
Skema fungsi jalankan_aman dengan tiga kemungkinan: berhasil mengembalikan hasil dan teks kosong, galat Processing mengembalikan None dan pesan alat, galat Python lain mengembalikan None dan galat tak terduga

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 4.3: Menangkap galat dari alat Processing dengan pembungkus kecil]
# Penulis: Badar Mubarok Yogaswara
import traceback
import processing
from qgis.core import QgsVectorLayer, QgsProcessingException, QgsMessageLog, Qgis

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"


def jalankan_aman(alat, parameter):
    """Mengembalikan (hasil, pesan). Bila alat gagal: hasil = None dan pesan berisi sebabnya."""
    try:
        return processing.run(alat, parameter), ""
    except QgsProcessingException as e:          # galat milik Processing
        QgsMessageLog.logMessage("%s gagal: %s" % (alat, e), "Otomasi KPH", Qgis.Warning)
        return None, str(e).replace("\n", " | ")
    except Exception:                            # galat Python lain: simpan jejak untuk diagnosis
        return None, "galat tak terduga: " + traceback.format_exc().splitlines()[-1]


sungai = QgsVectorLayer(DATA + "/Sungai.gpkg", "Sungai", "ogr")

# Kasus 1: berjalan normal
hasil, pesan = jalankan_aman("native:buffer", {
    "INPUT": sungai, "DISTANCE": 30, "OUTPUT": "TEMPORARY_OUTPUT"})
print("Kasus 1:", "OK, %d fitur" % hasil["OUTPUT"].featureCount() if hasil else "GAGAL: " + pesan)

# Kasus 2: masukan tidak ada
hasil, pesan = jalankan_aman("native:buffer", {
    "INPUT": DATA + "/tidak_ada.gpkg", "DISTANCE": 30, "OUTPUT": "TEMPORARY_OUTPUT"})
print("Kasus 2:", "OK" if hasil else "GAGAL: " + pesan)

# Kasus 3: nama alat salah ketik
hasil, pesan = jalankan_aman("native:buffr", {})
print("Kasus 3:", "OK" if hasil else "GAGAL: " + pesan)

# Kasus 4: parameter wajib (OVERLAY) tidak diisi
hasil, pesan = jalankan_aman("native:clip", {"INPUT": sungai, "OUTPUT": "TEMPORARY_OUTPUT"})
print("Kasus 4:", "OK" if hasil else "GAGAL: " + pesan)

Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.

Blok 1: perkakas dan pembungkus (baris 3-19).

BarisTulisanArtinya
3import tracebackPerkakas untuk membaca jejak galat, yaitu daftar langkah yang berujung pada galat (Bab 10).
5from qgis.core import ...QgsProcessingException adalah galat khusus milik alat Processing. QgsMessageLog menulis ke catatan resmi QGIS. Qgis menyediakan tingkat pesan seperti Warning.
11def jalankan_aman(alat, parameter):Buat fungsi dengan dua bahan: ID alat dan kamus parameter.
13-14try: lalu return processing.run(alat, parameter), ""Jalankan alat. Bila berhasil, kembalikan dua hal sekaligus: hasil dan teks kosong. Dua hal yang dipisah koma disebut pasangan (tuple).
15-17except QgsProcessingException as e:Galat dari alat. Tulis pesan ke log QGIS dengan tag Otomasi KPH dan tingkat Warning. Kembalikan None dan teks galat. .replace(...) mengganti tiap pindah baris dengan garis tegak berspasi, agar pesan muat dalam satu baris.
18-19except Exception:Galat Python lain yang tidak terduga. traceback.format_exc().splitlines()[-1] mengambil baris terakhir jejak galat, yaitu jenis dan sebabnya.

Blok 2: empat kasus uji (baris 22-40).

BarisTulisanArtinya
22sungai = QgsVectorLayer(...)Buka layer sungai.
25-26hasil, pesan = jalankan_aman(...)Pasangan yang dikembalikan dipecah ke dua kotak: hasil dan pesan. Kasus 1 menjalankan native:buffer dengan jarak 30 meter.
27print("Kasus 1:", "OK, ..." if hasil else "GAGAL: " + pesan)Bentuk A if syarat else B. Bila hasil ada, cetak bagian A. Bila tidak, cetak bagian B.
30-32Kasus 2Masukan menunjuk berkas yang tidak ada.
35-36Kasus 3Nama alat salah ketik: native:buffr.
39-40Kasus 4Alat native:clip dijalankan tanpa parameter wajib OVERLAY.

Bentuk A if syarat else B mudah dibaca bila dipecah. Baris 27 berarti: "bila hasil ada, tulis OK dan jumlah fitur; bila tidak, tulis GAGAL dan pesannya". Bagian hasil["OUTPUT"].featureCount() hanya dihitung bila hasil ada. Karena itu tidak ada galat saat hasil kosong.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
Kasus 1: OK, 2 fitur
Kasus 2: GAGAL: Unable to execute algorithm | Could not load source layer for INPUT: D:/Latihan/paket-i4/tidak_ada.gpkg not found
Kasus 3: GAGAL: Error: Algorithm native:buffr not found | 
Kasus 4: GAGAL: Unable to execute algorithm | Could not load source layer for OVERLAY: no value specified for parameter

Empat kasus, empat jawaban, dan skrip tidak berhenti di mana pun. Kasus 1 menghasilkan 2 fitur karena dua garis sungai disangga sendiri-sendiri, tanpa digabung. Pesan Kasus 2 dan 4 dimulai dengan Unable to execute algorithm, lalu sebabnya setelah garis tegak. Pesan Kasus 3 diakhiri garis tegak kosong, tidak apa-apa.

Bagian yang boleh diubah.

  • Baris 7, lokasi data. Sesuaikan dengan komputer Anda.
  • Baris 25-26, 30-31, 35, dan 39, kasus uji. Ganti alat dan parameternya. Cara ini cocok untuk menguji alat baru sebelum dipakai di perulangan besar.
  • Baris 16, tag log. "Otomasi KPH" boleh diganti. Pesan muncul di panel Log Messages QGIS dengan tag itu [CEK: tampilan panel tidak dapat diuji tanpa antarmuka QGIS].
  • Baris 17, pemisah. " | " boleh diganti, misalnya "; ".

Jebakan.

  • Galat tak terduga masuk jalur kedua. Pada uji, parameter Kasus 3 diganti None, bukan kamus. Layar menampilkan Kasus 3: GAGAL: galat tak terduga: TypeError: QgsProcessingAlgorithm.checkParameterValues(): argument 1 has unexpected type 'NoneType'. Itu galat Python, bukan galat alat, sehingga ditangkap oleh except Exception di baris 18.
  • Nilai parameter salah. Pada uji, "DISTANCE": "abc" menghasilkan GAGAL: Unable to execute algorithm | Incorrect parameter value for DISTANCE. Pesan menyebut nama parameternya, jadi Anda tahu ke mana mencari.
  • Pesan berbahasa Inggris. Pesan galat alat berasal dari QGIS dan tidak diterjemahkan. Bila laporan akan dibaca orang lain, ganti dengan kalimat Anda sendiri, misalnya "masukan tidak ditemukan".
  • Hasil kosong berarti gagal. Skrip yang memakai jalankan_aman wajib memeriksa if hasil sebelum memakai hasil["OUTPUT"]. Bila lupa, muncul TypeError: 'NoneType' object is not subscriptable.

Latihan variasi.

  1. Tambahkan Kasus 5: jarak berupa teks "abc".
  2. Ubah Kasus 1 agar hasil penyangga digabung, dengan parameter DISSOLVE.

Contoh jawaban nomor 1 (sudah diuji), tambahkan di akhir skrip:

PYTHONUnduh berkas
# Kasus 5: jarak berupa teks
hasil, pesan = jalankan_aman("native:buffer", {"INPUT": sungai, "DISTANCE": "abc", "OUTPUT": "TEMPORARY_OUTPUT"})
print("Kasus 5:", "OK" if hasil else "GAGAL: " + pesan)

Layar menampilkan Kasus 5: GAGAL: Unable to execute algorithm | Incorrect parameter value for DISTANCE.

Contoh jawaban nomor 2 (sudah diuji): ubah baris 26 menjadi "INPUT": sungai, "DISTANCE": 30, "DISSOLVE": True, "OUTPUT": "TEMPORARY_OUTPUT"}). Layar menampilkan Kasus 1: OK, 1 fitur, karena dua penyangga digabung menjadi satu.

Konsep: ekspresi QGIS dalam tiga kalimat #

Ekspresi adalah rumus tertulis yang dihitung QGIS untuk setiap fitur, mirip rumus di sel lembar kerja. Rumus yang sama dipakai di banyak tempat: seleksi, kalkulator kolom, label, dan model. Agregasi menambah satu kemampuan, yaitu menghitung ringkasan dari layer lain, misalnya jumlah titik panas di dalam tiap KPH.

Bayangkan surat undangan bertuliskan "Yth. (nama tamu)". Rumusnya sama, tetapi hasilnya berbeda untuk tiap tamu. Tamu yang sedang diurus, lengkap dengan daftar tamunya, disebut konteks: fitur yang sedang dibaca, layernya, dan variabel proyek.

Ilustrasi 22.5: Ekspresi dalam konteks dan agregasi
Skema rumus teks masuk ke konteks lalu keluar sebagai nilai, dan agregasi yang menghitung titik di dalam KPH yang sedang dibaca

Lima istilah baru:

  • Ekspresi: rumus yang dihitung per fitur. Kutip ganda menunjuk nama kolom, kutip tunggal menulis teks.
  • Konteks: fitur, layer, dan proyek tempat rumus dihitung.
  • Agregasi (aggregate): ringkasan dari layer lain, seperti jumlah atau rata-rata.
  • @parent: fitur yang sedang dibaca di layer luar, dipakai di dalam agregasi.
  • $area: luas fitur yang sedang dibaca, dihitung menurut pengaturan proyek.

Skrip 22.4: Menghitung ekspresi dari Python #

Tujuan. Menghitung ekspresi QGIS dari Python: rumus dasar, agregasi dari layer lain, dan ekspresi yang salah, tanpa menghentikan skrip.

Bahan. Paket paket-i4: Batas_KPH.gpkg (3 KPH), Hotspot_Nasional.gpkg (12 titik panas), dan Survei.gpkg (titik survei). Berkas b5_01_ekspresi_python.py (di dalamnya tertulis "Skrip 5.1"). Ganti DATA di baris 6. Skrip menambah tiga layer ke proyek yang sedang terbuka, dan di akhir mengubah CRS serta ellipsoid proyek itu. Jalankan di proyek kosong.

Skrip ini 77 baris dan terdiri atas enam bagian kecil. Intinya satu fungsi, hitung. Gambar berikut menunjukkan isinya.

Ilustrasi 22.6: Fungsi hitung
Skema fungsi hitung: teks rumus diperiksa salah tulis, lalu salah hitung, lalu nilai dikembalikan; dua kemungkinan galat mengembalikan teks sebabnya

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 5.1: Mengevaluasi ekspresi QGIS dari Python, termasuk agregasi]
# Penulis: Badar Mubarok Yogaswara
from qgis.core import (QgsProject, QgsVectorLayer, QgsExpression, QgsExpressionContext,
                       QgsExpressionContextUtils, QgsCoordinateReferenceSystem)

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

proyek = QgsProject.instance()
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
survei = QgsVectorLayer(DATA + "/Survei.gpkg", "Titik_Survei", "ogr")
for lyr in (batas, hotspot, survei):
    proyek.addMapLayer(lyr)         # fungsi aggregate() mencari layer lain berdasarkan NAMA di proyek


def konteks(layer, fitur):
    """Membuat 'lingkungan' tempat ekspresi dihitung: variabel proyek, layer, dan fitur saat ini."""
    c = QgsExpressionContext()
    c.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))
    c.setFeature(fitur)
    return c


def hitung(teks, layer, fitur):
    e = QgsExpression(teks)
    if e.hasParserError():                      # salah tulis ekspresi
        return "GALAT TULIS: " + e.parserErrorString()
    nilai = e.evaluate(konteks(layer, fitur))
    if e.hasEvalError():                        # salah saat dihitung
        return "GALAT HITUNG: " + e.evalErrorString()
    return nilai


# 1. Ekspresi dasar: rumus, teks, dan pilihan (CASE)
dasar = {
    "luas (ha)": "round($area / 10000, 2)",
    "nama huruf besar": 'upper("NAMA_KPH")',
    "ukuran": "CASE WHEN $area > 50000 THEN 'besar' ELSE 'kecil' END",
}
for fitur in batas.getFeatures():
    print(fitur["NAMA_KPH"], {k: hitung(v, batas, fitur) for k, v in dasar.items()})

# 2. Agregasi: menghitung dari LAYER LAIN. @parent = fitur KPH yang sedang dibaca.
titik_panas = ("aggregate(layer:='Hotspot_Nasional', aggregate:='count', expression:=\"ID_Titik\", "
               "filter:=intersects($geometry, geometry(@parent)))")
tinggi_rata = ("round(aggregate(layer:='Titik_Survei', aggregate:='mean', expression:=\"Tinggi_Phn\", "
               "filter:=intersects($geometry, geometry(@parent))), 1)")
print("--- agregasi per KPH ---")
for fitur in batas.getFeatures():
    print("%-10s titik panas = %s | tinggi pohon rata-rata = %s" % (
        fitur["NAMA_KPH"], hitung(titik_panas, batas, fitur), hitung(tinggi_rata, batas, fitur)))

# 3. Agregasi tanpa filter: satu angka untuk seluruh layer
fitur = next(batas.getFeatures())
total = hitung("aggregate('Hotspot_Nasional', 'count', \"ID_Titik\")", batas, fitur)
print("Total titik panas (semua KPH):", total)

# 4. Ekspresi yang salah dikenali dengan aman
print(hitung("round($area / 10000, ", batas, fitur))
print(hitung("to_int('abc') + 1", batas, fitur))

# 5. Dua cara lain menghitung titik: daftar dan gabungan teks
lain = {
    "jumlah lewat overlay": "array_length(overlay_intersects('Hotspot_Nasional', \"ID_Titik\"))",
    "daftar ID": ("aggregate('Hotspot_Nasional', 'concatenate', \"ID_Titik\", "
                  "filter:=intersects($geometry, geometry(@parent)), concatenator:=', ')"),
}
for fitur in batas.getFeatures():
    print(fitur["NAMA_KPH"], {k: hitung(v, batas, fitur) for k, v in lain.items()})

# 6. Awas: $area mengikuti pengaturan ellipsoid proyek, area($geometry) memakai bidang datar peta
fitur = next(batas.getFeatures())
print("Tanpa ellipsoid  :", hitung("$area", batas, fitur), hitung("area($geometry)", batas, fitur))
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749"))
proyek.setEllipsoid("EPSG:7030")                 # WGS 84, pengaturan umum pada proyek baru
print("Dengan ellipsoid :", round(hitung("$area", batas, fitur), 2), hitung("area($geometry)", batas, fitur))

Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.

Blok 1: bahan (baris 3-14).

BarisTulisanArtinya
3-4from qgis.core import (...)Panggil perkakas ekspresi: QgsExpression (rumus), QgsExpressionContext dan QgsExpressionContextUtils (konteks). Tanda kurung memungkinkan daftar panjang ditulis di dua baris.
9proyek = QgsProject.instance()Ambil proyek yang sedang terbuka (Bab 14).
10-12Tiga QgsVectorLayer(...)Buka tiga layer. Perhatikan nama layer di isian kedua, misalnya "Hotspot_Nasional". Nama ini dipakai rumus di bawah.
13-14for lyr in (...): proyek.addMapLayer(lyr)Masukkan ketiganya ke proyek. Ini wajib, karena aggregate() mencari layer lain menurut nama di proyek.

Blok 2: dua fungsi pembantu (baris 17-32).

BarisTulisanArtinya
17-22def konteks(layer, fitur):Bangun konteks: wadah kosong, tambahkan variabel global, proyek, dan layer (appendScopes), lalu tetapkan fitur yang sedang dibaca (setFeature).
26e = QgsExpression(teks)Ubah teks rumus menjadi objek rumus.
27-28if e.hasParserError():Periksa galat tulis: apakah rumusnya terbaca. Bila tidak, kembalikan teks GALAT TULIS berisi sebabnya.
29nilai = e.evaluate(konteks(layer, fitur))Hitung rumus dalam konteks.
30-31if e.hasEvalError():Periksa galat hitung: rumus terbaca, tetapi nilainya tidak bisa dihitung. Kembalikan teks GALAT HITUNG.
32return nilaiBila tidak ada masalah, kembalikan nilainya.

Perhatikan bahwa fungsi hitung tidak memakai try dan except. QGIS tidak menimbulkan galat Python untuk rumus yang salah. Ia menyimpan tanda galat di objek rumus, dan Andalah yang harus bertanya. Itulah dua pertanyaan di baris 27 dan 30.

Blok 3: ekspresi dasar (baris 35-42).

BarisTulisanArtinya
36-40dasar = {...}Kamus berisi tiga rumus. Kuncinya label, nilainya teks rumus. round($area / 10000, 2) mengubah luas dari meter persegi ke hektare. upper("NAMA_KPH") mengubah huruf besar. CASE WHEN ... END memilih besar atau kecil menurut luas.
41-42for fitur in batas.getFeatures():Untuk tiap KPH, hitung ketiga rumus. {k: hitung(v, batas, fitur) for k, v in dasar.items()} membuat kamus baru dalam satu baris: tiap label dipasangkan dengan hasil rumusnya.

Blok 4: agregasi (baris 44-57).

BarisTulisanArtinya
45-48titik_panas = (...) dan tinggi_rata = (...)Dua rumus agregasi yang ditulis di dua baris. Python menyambung teks dalam kurung otomatis. Rumus pertama menghitung ('count') kolom ID_Titik di layer Hotspot_Nasional. Rumus kedua mengambil rata-rata ('mean') kolom Tinggi_Phn di layer Titik_Survei, dibulatkan satu desimal.
45-48filter:=intersects($geometry, geometry(@parent))Syarat: hanya titik yang bersinggungan dengan geometri KPH yang sedang dibaca (@parent). Titik di luar tidak dihitung.
50-52for fitur in batas.getFeatures():Untuk tiap KPH, cetak jumlah titik panas dan tinggi rata-rata. %-10s menyediakan sepuluh kolom rata kiri.
55-57fitur = next(batas.getFeatures())Ambil fitur pertama saja. Lalu hitung agregasi tanpa filter, sehingga hasilnya satu angka untuk seluruh layer.

Blok 5: ekspresi yang salah dan dua cara lain (baris 59-70).

BarisTulisanArtinya
60hitung("round($area / 10000, ", ...)Rumus sengaja dibiarkan terpotong. Hasilnya GALAT TULIS.
61hitung("to_int('abc') + 1", ...)Rumus benar, tetapi abc tidak bisa menjadi bilangan bulat. Hasilnya GALAT HITUNG.
64-68lain = {...}Dua cara lain menghitung titik. overlay_intersects('Hotspot_Nasional', "ID_Titik") menghasilkan daftar ID titik yang bersinggungan, dan array_length menghitung isinya. 'concatenate' menyambung ID menjadi satu teks, dipisah ', '.
69-70for fitur in batas.getFeatures():Cetak hasilnya per KPH.

Blok 6: dua jenis luas (baris 72-77).

BarisTulisanArtinya
73-74$area dan area($geometry)Hitung luas KPH pertama dengan dua rumus, saat proyek belum diberi ellipsoid.
75-76proyek.setCrs(...) lalu proyek.setEllipsoid("EPSG:7030")Ubah CRS proyek menjadi UTM 49S dan beri ellipsoid WGS 84. Ellipsoid adalah bentuk bumi yang dipakai untuk menghitung ukuran. Pengaturan ini umum pada proyek baru.
77print("Dengan ellipsoid :", round(hitung("$area", ...), 2), ...)Hitung lagi kedua luas. round(..., 2) di sini aman karena $area menghasilkan angka.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
KPH Alpha {'luas (ha)': 4.0, 'nama huruf besar': 'KPH ALPHA', 'ukuran': 'kecil'}
KPH Beta {'luas (ha)': 4.0, 'nama huruf besar': 'KPH BETA', 'ukuran': 'kecil'}
KPH Gamma {'luas (ha)': 8.0, 'nama huruf besar': 'KPH GAMMA', 'ukuran': 'besar'}
--- agregasi per KPH ---
KPH Alpha  titik panas = 2 | tinggi pohon rata-rata = 11.3
KPH Beta   titik panas = 5 | tinggi pohon rata-rata = 9.6
KPH Gamma  titik panas = 5 | tinggi pohon rata-rata = 8.9
Total titik panas (semua KPH): 12
GALAT TULIS: 
Incomplete expression. You might not have finished the full expression.
GALAT HITUNG: Cannot convert 'abc' to int
KPH Alpha {'jumlah lewat overlay': 2, 'daftar ID': 'HS-008, HS-012'}
KPH Beta {'jumlah lewat overlay': 5, 'daftar ID': 'HS-004, HS-005, HS-006, HS-009, HS-011'}
KPH Gamma {'jumlah lewat overlay': 5, 'daftar ID': 'HS-001, HS-002, HS-003, HS-007, HS-010'}
Tanpa ellipsoid  : 40000.0 40000.0
Dengan ellipsoid : 39997.03 40000.0

Cara membacanya. Baris pertama: tiga KPH, dua berluas 4 ha dan satu 8 ha, hanya Gamma yang besar (di atas 50.000 meter persegi). Agregasi: titik panas 2, 5, dan 5, totalnya 12, sama dengan jumlah titik di layer. Dua baris GALAT menunjukkan dua jenis galat ekspresi. Kata GALAT TULIS: diikuti baris baru, karena teks sebab dari QGIS memang diawali pindah baris. Baris terakhir: $area berubah dari 40000.0 menjadi 39997.03 setelah ellipsoid dipasang, sedangkan area($geometry) tetap 40000.0. Untuk satuan hektare dengan dua desimal, selisihnya tidak terlihat. Pada pekerjaan yang butuh ketelitian, pilih satu cara dan catat.

Bagian yang boleh diubah.

  • Baris 6, lokasi data. Sesuaikan dengan komputer Anda.
  • Baris 36-40, rumus dasar. Ganti atau tambah rumus Anda sendiri. Kutip ganda untuk kolom, kutip tunggal untuk teks.
  • Baris 39, angka 50000. Ambang besar dan kecil, dalam meter persegi.
  • Baris 45-48, nama layer dan kolom. Nama layer di dalam aggregate harus sama dengan nama di proyek, huruf demi huruf.
  • Baris 60-61, rumus yang salah. Ganti dengan salah ketik buatan Anda untuk melihat pesannya.

Jebakan.

  • Layer tidak ada di proyek. Pada uji, dua baris addMapLayer di baris 13-14 dilewati. Setiap agregasi berubah menjadi GALAT HITUNG: Cannot find layer with name or ID 'Hotspot_Nasional'. Skrip tetap berjalan, jadi galatnya tampil sebagai teks di tengah hasil. Bacalah hasilnya, jangan hanya menunggu skrip berhenti.
  • Nama layer salah. Pada uji, 'Hotspot_Nasional' di baris 45 diganti 'Hotspot'. Hasilnya GALAT HITUNG: Cannot find layer with name or ID 'Hotspot', hanya untuk rumus itu.
  • Nama kolom salah. Pada uji, "NAMA_KPH" di baris 38 diganti "NAMA". Hasilnya GALAT HITUNG: Field 'NAMA' not found.
  • Kurung kurang. Pada uji, tanda kurung tutup di baris 37 dihapus. Hasilnya GALAT TULIS yang berakhir dengan expecting COMMA or ')'. Bila Anda melihat GALAT TULIS, periksa tanda kurung dan kutip.
  • Membulatkan hasil yang berupa teks. Fungsi hitung bisa mengembalikan angka atau teks galat. Pada uji, baris 61 dibungkus round(...) dan skrip berhenti dengan TypeError: type str doesn't define __round__ method. Periksa jenis hasilnya lebih dulu dengan isinstance(hasil, str), yang bertanya: "apakah hasil ini teks?"
  • Proyek Anda berubah. Baris 13-14 menambah tiga layer, dan baris 75-76 mengubah CRS serta ellipsoid proyek yang terbuka [CEK: tampilan perubahan di jendela QGIS tidak dapat diuji dari sini]. Jalankan di proyek kosong.

Latihan variasi.

  1. Tambahkan rumus keliling KPH dalam meter ke kamus dasar.
  2. Turunkan ambang besar menjadi 30000. Apa yang berubah?

Contoh jawaban nomor 1 (sudah diuji): sisipkan setelah baris 39.

PYTHON
    "keliling (m)": "round($perimeter, 1)",

Layar menambah 'keliling (m)': 800.0 untuk Alpha dan Beta, dan 1200.0 untuk Gamma. Fungsi $perimeter memberi keliling fitur yang sedang dibaca.

Contoh jawaban nomor 2 (sudah diuji): ganti 50000 di baris 39 dengan 30000. Ketiga KPH menjadi besar, karena luas terkecil 40000 meter persegi sudah melewati 30000.

Skrip 22.5: Ekspresi di dalam alat Processing #

Skrip 22.4 menghitung rumus dengan tangan. Skrip ini menyerahkan rumus kepada alat Processing, sehingga hasilnya berupa kolom baru, pilihan fitur, dan teks laporan. Rumus agregasinya sama. Karena itu hanya bagian yang baru yang dijelaskan.

Tujuan. Menambah dua kolom dengan ekspresi, memilih KPH yang titik panasnya banyak, lalu membentuk teks laporan tiga baris untuk tiap KPH.

Bahan. Paket paket-i4: Batas_KPH.gpkg dan Hotspot_Nasional.gpkg. Berkas b5_02_ekspresi_di_alat.py (di dalamnya tertulis "Skrip 5.2"). Ganti DATA di baris 7. Skrip menambah layer ke proyek yang terbuka, jadi jalankan di proyek kosong.

Ilustrasi 22.7: Alur skrip 22.5
Skema tiga tahap: tambah dua kolom dengan kalkulator kolom, pilih fitur dengan ekspresi, tulis teks laporan

Skrip utuh:

PYTHONUnduh berkas
# [SKRIP 5.2: Ekspresi di dalam alat Processing: Field calculator, Extract by expression, label]
# Penulis: Badar Mubarok Yogaswara
import processing
from qgis.core import (QgsProject, QgsVectorLayer, QgsExpression, QgsExpressionContext,
                       QgsExpressionContextUtils)

DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"

proyek = QgsProject.instance()
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
proyek.addMapLayer(batas)
proyek.addMapLayer(hotspot)         # wajib ada di proyek agar aggregate() menemukannya

# 1. Menambah dua kolom baru dengan ekspresi
tahap1 = processing.run("native:fieldcalculator", {
    "INPUT": batas, "FIELD_NAME": "LUAS_HA", "FIELD_TYPE": 0,          # 0 = desimal
    "FIELD_LENGTH": 10, "FIELD_PRECISION": 2,
    "FORMULA": "round($area / 10000, 2)", "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tahap2 = processing.run("native:fieldcalculator", {
    "INPUT": tahap1, "FIELD_NAME": "JML_HOTSPOT", "FIELD_TYPE": 1,     # 1 = bilangan bulat
    "FIELD_LENGTH": 5, "FIELD_PRECISION": 0,
    "FORMULA": "aggregate('Hotspot_Nasional', 'count', \"ID_Titik\", "
               "intersects($geometry, geometry(@parent)))",
    "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tahap2.setName("Batas_KPH_Ringkas")
proyek.addMapLayer(tahap2)
print("Kolom:", [f.name() for f in tahap2.fields()])
for f in tahap2.getFeatures():
    print("  ", f["NAMA_KPH"], f["LUAS_HA"], "ha;", f["JML_HOTSPOT"], "titik panas")

# 2. Memilih fitur dengan ekspresi lewat alat
rawan = processing.run("native:extractbyexpression", {
    "INPUT": tahap2, "EXPRESSION": '"JML_HOTSPOT" >= 5', "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
print("KPH dengan titik panas >= 5:", sorted(f["NAMA_KPH"] for f in rawan.getFeatures()))

# 3. Teks laporan satu KPH, dibentuk dengan ekspresi (cara yang sama dipakai label di layout)
teks = ("'LAPORAN ' || upper(\"NAMA_KPH\") || '\n' || 'Luas: ' || \"LUAS_HA\" || ' ha\n' || "
        "'Titik panas: ' || \"JML_HOTSPOT\" || ' titik'")
ekspresi = QgsExpression(teks)
for f in tahap2.getFeatures():
    c = QgsExpressionContext()
    c.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(tahap2))
    c.setFeature(f)
    print(ekspresi.evaluate(c))
    print("---")

Penjelasan bagian yang baru. Baris 3-14 sama dengan Skrip 22.4: perkakas, dua layer, dan addMapLayer. Komentar di baris 14 mengingatkan bahwa layer Hotspot_Nasional wajib ada di proyek agar aggregate() menemukannya. Nomor baris mengikuti berkas di atas.

BarisTulisanArtinya
17-20processing.run("native:fieldcalculator", {...})Panggil alat Field calculator. FIELD_NAME nama kolom baru, FIELD_TYPE: 0 jenis desimal, FIELD_LENGTH dan FIELD_PRECISION panjang dan jumlah desimal, FORMULA rumusnya. Hasilnya layer baru dengan kolom LUAS_HA.
21-26tahap kedua dengan FIELD_TYPE: 1Kolom bilangan bulat JML_HOTSPOT. Masukannya hasil tahap pertama (tahap1). Rumusnya agregasi seperti di Skrip 22.4. Tanda \" menulis kutip ganda di dalam teks Python.
27-28tahap2.setName(...) lalu addMapLayerBeri nama Batas_KPH_Ringkas dan tampilkan di proyek.
29-31print("Kolom:", [f.name() for f in tahap2.fields()])Tampilkan nama kolom, lalu isi tiap KPH. Daftar nama kolom dibuat dengan comprehension (Bab 6).
34-35processing.run("native:extractbyexpression", {...})Alat Extract by expression: hanya fitur yang memenuhi "JML_HOTSPOT" >= 5 masuk ke hasil.
36sorted(f["NAMA_KPH"] for f in rawan.getFeatures())Cetak nama KPH yang lolos, urut abjad.
39-40teks = (...)Satu rumus panjang yang menyusun laporan tiga baris. Dua garis tegak berurutan pada rumus QGIS menyambung teks. Itu bukan operator Python. \n membuat baris baru.
41-47QgsExpression(teks) lalu perulanganUntuk tiap KPH, bangun konteks seperti di Skrip 22.4, lalu hitung dan cetak teksnya. --- memisahkan laporan.

Rumus laporan di baris 39-40 terlihat padat. Ditulis sebagai ekspresi QGIS biasa, bentuknya begini:

EKSPRESIUnduh berkas
'LAPORAN ' || upper("NAMA_KPH") || '
' || 'Luas: ' || "LUAS_HA" || ' ha
' || 'Titik panas: ' || "JML_HOTSPOT" || ' titik'

Teks ini sama dengan rumus label di layout peta (Bab 17), sehingga rumus yang Anda uji di sini bisa dipindahkan ke label.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
Kolom: ['fid', 'NAMA_KPH', 'LUAS_HA', 'JML_HOTSPOT']
   KPH Alpha 4.0 ha; 2 titik panas
   KPH Beta 4.0 ha; 5 titik panas
   KPH Gamma 8.0 ha; 5 titik panas
KPH dengan titik panas >= 5: ['KPH Beta', 'KPH Gamma']
LAPORAN KPH ALPHA
Luas: 4 ha
Titik panas: 2 titik
---
LAPORAN KPH BETA
Luas: 4 ha
Titik panas: 5 titik
---
LAPORAN KPH GAMMA
Luas: 8 ha
Titik panas: 5 titik
---

Perhatikan dua hal. Pertama, kolom yang muncul adalah fid, NAMA_KPH, LUAS_HA, dan JML_HOTSPOT. Kolom fid adalah nomor urut bawaan GeoPackage. Kedua, pada laporan tertulis Luas: 4 ha, bukan 4.0 ha. Penyambung teks di QGIS menulis angka bulat tanpa desimal. Bila Anda ingin dua desimal, bungkus dengan format_number("LUAS_HA", 2) [CEK: fungsi ini belum diuji di bab ini].

Bagian yang boleh diubah.

  • Baris 7, lokasi data. Sesuaikan dengan komputer Anda.
  • Baris 18 dan 22, nama kolom baru. Ganti LUAS_HA dan JML_HOTSPOT dengan nama Anda.
  • Baris 20 dan 24-25, rumus. Ganti dengan rumus lain. Akibatnya, isi kolom baru berubah.
  • Baris 35, ambang >= 5. Ubah syarat pemilihan. Akibatnya, daftar KPH yang terpilih berubah.
  • Baris 39-40, bunyi laporan. Ganti kata-katanya. Akibatnya, teks laporan berubah.

Jebakan.

  • Layer rujukan tidak ada di proyek. Pada uji, baris 14 diganti pass. Skrip berhenti dengan QgsProcessingException: Evaluation error in expression "aggregate('Hotspot_Nasional', 'count', "ID_Titik", intersects($geometry, geometry(@parent)))": Cannot find layer with name or ID 'Hotspot_Nasional'. Berbeda dengan Skrip 22.4, di sini galatnya benar-benar menghentikan skrip. Karena itu pembungkus jalankan_aman dari Skrip 22.3 berguna untuk alat jenis ini.
  • FIELD_PRECISION boleh dihilangkan. Pada uji, "FIELD_PRECISION": 2 di baris 19 dibuang dan skrip tetap jalan dengan hasil yang sama. Tetap isi supaya bentuk kolomnya jelas.
  • Angka tampil tanpa desimal. Lihat Hasil di atas. Itu bukan kesalahan data.

Latihan variasi.

  1. Pilih KPH dengan titik panas dua atau lebih, bukan lima.
  2. Pilih KPH yang luasnya lebih dari 5 ha.

Contoh jawaban nomor 1 (sudah diuji): ubah baris 35 menjadi "INPUT": tahap2, "EXPRESSION": '"JML_HOTSPOT" >= 2', "OUTPUT": "TEMPORARY_OUTPUT"}). Layar menampilkan ['KPH Alpha', 'KPH Beta', 'KPH Gamma'].

Contoh jawaban nomor 2 (sudah diuji): ganti syaratnya menjadi '"LUAS_HA" > 5'. Layar menampilkan ['KPH Gamma'], satu-satunya KPH berluas 8 ha.

Cek paham #

  1. Apa beda else dan finally pada blok try? Mengapa finally berguna pada Skrip 22.1?
  2. Pada Skrip 22.2, apa risikonya memakai except Exception, dan bagaimana cara mengurangi risiko itu?
  3. Fungsi hitung di Skrip 22.4 tidak memakai try. Bagaimana ia tahu bahwa rumusnya salah?

Jawaban.

  1. else hanya berjalan bila tidak ada galat. finally selalu berjalan. Pada Skrip 22.1, finally mencetak "selesai memeriksa" untuk setiap berkas, bahkan ketika skrip berhenti karena salah ketik.
  2. except Exception menangkap segala galat, termasuk salah ketik Anda sendiri, sehingga sebab asli tersembunyi. Cara mengurangi risiko: cetak atau simpan teks galatnya (str(e)), cetak laporan sebelum langkah berikutnya, dan hentikan skrip dengan pesan jelas bila hasilnya kosong. Skrip 22.2 sudah melakukan dua hal terakhir lewat pengaman di baris 39-42.
  3. QGIS tidak memicu galat Python untuk rumus yang salah. Ia menandai objek rumusnya. Fungsi hitung bertanya lewat hasParserError() (salah tulis) dan hasEvalError() (salah hitung).

Kesalahan umum #

  • Menangkap galat lalu diam saja. Perbaikan: selalu cetak atau catat sebabnya, seperti log di Skrip 22.2.
  • Lupa memasukkan layer rujukan ke proyek. Perbaikan: addMapLayer untuk setiap layer yang disebut di dalam aggregate(), dan samakan nama layer dengan teks di rumus.
  • Memakai hasil alat sebelum diperiksa. Perbaikan: periksa if hasil atau isValid() sebelum memakai hasil["OUTPUT"].
  • Menjalankan skrip contoh di proyek penting. Perbaikan: skrip 22.4 dan 22.5 menambah layer dan mengubah pengaturan proyek yang terbuka. Jalankan di proyek kosong.

Ringkasan dan latihan #

  • Galat bukan musuh. try dan except membuat satu berkas rusak tidak menghentikan yang lain.
  • Pola penjaga: fungsi memeriksa syarat dan memicu raise, pemakainya menangkap lalu mencatat.
  • Batch tahan galat menyimpan log berisi nama, status, dan keterangan, lalu menampilkannya sebagai laporan. Bila tidak ada berkas yang lolos, laporan dicetak dulu lalu skrip berhenti dengan pesan jelas.
  • Ekspresi QGIS dihitung dalam konteks. Dua jenis galatnya: galat tulis dan galat hitung. Keduanya dibaca lewat tanda di objek rumus.
  • Rumus yang sama bekerja di kalkulator kolom, pemilihan fitur, dan teks laporan.

Latihan. Buka Skrip 22.2 dan tambahkan satu pemeriksaan baru: tolak berkas yang jumlah titiknya kurang dari 4. Berkas mana yang akan ditolak pada data paket ini? Jawab dulu dari hasil di bab ini, baru jalankan untuk membuktikan.

Tabel perbandingan: jenis galat dan cara menanganinya #

Jenis galatContoh di bab iniDitangkap denganSkrip
FileNotFoundErrorBerkas tim_x.gpkg tidak adaexcept FileNotFoundError22.1
ValueErrorBerkas rusak, kolom hilangexcept ValueError22.1
ZeroDivisionErrorPersen dari total nolexcept ZeroDivisionError22.1
Exception (induk galat)Segala galat per berkasexcept Exception as e22.2
QgsProcessingExceptionAlat tidak ada, parameter kurangexcept QgsProcessingException22.3
Galat tulis rumusKurung tidak ditutuphasParserError()22.4
Galat hitung rumusto_int('abc')hasEvalError()22.4