Inti masalah kami, secara gamblang: kami adalah Geonode dan kami menjual proxy kepada orang-orang yang melakukan scraping, sehingga XPath berkaitan erat dengan bisnis kami. Perlu ditekankan bahwa selektor yang tidak berfungsi bukanlah masalah proxy. Jika proses ekstraksi Anda berhenti berfungsi, kemungkinan besar situs tersebut telah mengubah markup-nya, dan tidak ada jumlah bandwidth atau rotasi alamat yang dapat mengatasi hal itu. Kedua masalah ini sering disalahartikan karena keduanya sama-sama ditandai dengan "scraper saya berhenti mengembalikan data" — namun, kegagalan proxy menghasilkan respons yang diblokir dan halaman kesalahan, sedangkan kegagalan selektor menghasilkan respons yang berhasil namun tidak menghasilkan apa-apa. Periksa HTML mentah sebelum memeriksa hal lain. Jika halaman tersebut ada dan XPath Anda mengembalikan himpunan node kosong, artikel ini relevan dan proxy Anda berfungsi dengan baik.
Apa yang Sebenarnya Dipilih oleh "preceding-sibling"
Spesifikasi XPath 1.0 mendefinisikannya dalam satu baris: sumbu "preceding-sibling" "berisi semua saudara kandung yang mendahului simpul konteks; jika simpul konteks adalah simpul atribut atau simpul ruang nama, sumbu "preceding-sibling" kosong".
Dua kata yang mengandung makna tersebut. Siblings berarti node yang memiliki orang tua yang sama — bukan sepupu, bukan leluhur, bukan apa pun yang berada pada kedalaman yang berbeda. Preceding berarti lebih awal dalam urutan dokumen.
<div>
<p>First</p>
<p>Second</p>
<span id="here">Context</span>
<p>Third</p>
</div>
Dengan span sebagai simpul konteks:
preceding-sibling::p → First, Second
following-sibling::p → Third
preceding-sibling::* → both p elements
Peringatan mengenai simpul atribut patut diingat karena hal ini menjelaskan sekelompok hasil yang kosong. Jika Anda telah menavigasi ke sebuah atribut — //@class — maka preceding-sibling dari sana secara definisi akan kosong, terlepas dari apa yang mengelilingi elemen tempat atribut tersebut berada. Atribut bukanlah saudara kandung dari apa pun.
Perangkap Sumbu Terbalik: [1]
Tidak Berarti Yang Pertama Inilah satu-satunya penyebab paling umum dari hasil yang salah, dan spesifikasi tersebut menjelaskan dengan tepat alasannya.
XPath mengklasifikasikan ancestor
, ancestor-or-self
, preceding
, dan preceding-sibling
sebagai sumbu terbalik. Untuk sumbu terbalik, posisi kedekatan ditentukan dengan mengurutkan node dalam urutan dokumen terbalik.
Jadi, pada preceding-sibling
, posisi 1 adalah saudara terdekat yang mendahului — yaitu yang tepat sebelum simpul konteks — bukan yang pertama dalam dokumen.
<div>
<p>Alpha</p>
<p>Beta</p>
<p>Gamma</p>
<span id="here">Context</span>
</div>
preceding-sibling::p[1] → Gamma (nearest)
preceding-sibling::p[2] → Beta
preceding-sibling::p[3] → Alpha (furthest)
(preceding-sibling::p)[1] → Alpha (first in document order)
Tanda kurung mengubah segalanya. Tanpa tanda kurung tersebut, [1]
adalah predikat yang diterapkan sepanjang sumbu terbalik dan berarti "terdekat". Dengan tanda kurung, hasil sumbu terlebih dahulu dikumpulkan ke dalam himpunan simpul sesuai urutan dokumen dan [1]
mengindeks ke dalamnya, yang berarti "pertama".
Bandingkan dengan following-sibling
, yang merupakan sumbu maju di mana keduanya bertepatan:
following-sibling::p[1] → the next one
(following-sibling::p)[1] → also the next one
Asimetri itulah sebabnya orang yang belajar dari following-sibling
terjebak oleh preceding-sibling
. Kebiasaan tersebut terbawa, namun semantiknya tidak.
Dalam praktiknya, bentuk tanpa tanda kurung hampir selalu yang Anda inginkan. "Label yang langsung berada di depan nilai ini" adalah persyaratan umum, dan itu adalah preceding-sibling::label[1]
. Gunakan tanda kurung hanya ketika Anda benar-benar bermaksud "yang pertama dalam dokumen", yang merupakan kebutuhan yang lebih jarang daripada yang terdengar.
preceding-sibling versus preceding
Dua sumbu yang berbeda dengan nama yang mirip hingga membingungkan, dan perbedaannya sangat penting.
Spesifikasi mendefinisikan "preceding" sebagai "semua node dalam dokumen yang sama dengan node konteks yang berada sebelum node konteks tersebut dalam urutan dokumen, tidak termasuk nenek moyang apa pun serta tidak termasuk node atribut dan node namespace".
Jadi, "preceding" mencakup semua yang berada lebih awal dalam dokumen pada kedalaman apa pun, dikurangi nenek moyang. "preceding-sibling" hanya mencakup yang memiliki orang tua yang sama dengan Anda.
<body>
<header><h1>Title</h1></header>
<div>
<p>One</p>
<span id="here">Context</span>
</div>
</body>
Dari "span":
preceding-sibling::* → the p only
preceding::* → the p, the h1, and the header
Pengecualian node leluhur adalah bagian yang sering mengejutkan orang. Elemen div yang berisi elemen span tidak termasuk dalam preceding, meskipun tag pembukanya muncul lebih awal dalam sumber. Node leluhur dikecualikan secara definisi, karena sumbu ini berfokus pada apa yang berada di depan Anda, bukan apa yang mengandung Anda.
Kapan menggunakan yang mana. preceding-sibling untuk hubungan terstruktur — label dan nilainya, judul dan paragraf di bawahnya, sel-sel dalam satu baris. preceding untuk hubungan yang benar-benar longgar, seperti "judul terdekat di mana pun di atas elemen ini terlepas dari tingkat penyarangan". preceding memiliki cakupan yang lebih luas, jauh lebih lambat, dan jauh lebih mungkin mencocokkan sesuatu yang tidak Anda maksudkan.
Pola Label-Nilai
Inilah alasan mengapa pola "preceding-sibling
" ada dalam pekerjaan scraping, dan pola ini layak dikembangkan dengan baik karena sebagian besar markup yang sebenarnya merupakan variasi dari pola ini.
Masalahnya: Anda ingin mendapatkan nilai yang hanya dapat diidentifikasi melalui label yang berada di sebelahnya. Nilai itu sendiri tidak memiliki kelas yang berguna, tidak ada id, dan tidak ada ciri khas apa pun.
Daftar definisi:
<dl>
<dt>Price</dt>
<dd>£42.00</dd>
<dt>Stock</dt>
<dd>In stock</dd>
</dl>
Memilih harga berarti menemukan dd
yang dt
terdekat di depannya bertuliskan "Price":
//dd[preceding-sibling::dt[1] = 'Price']
Bacalah dari dalam ke luar: untuk setiap dd
, ambil dt
terdekat di depannya; simpan dd
jika teks tersebut adalah "Price". Perhatikan bahwa [1]
sangat penting. Tanpa itu, preceding-sibling::dt = 'Price'
akan benar jika ada dt
sebelumnya yang cocok, sehingga dd
yang kedua juga akan memenuhi syarat. Itu adalah bug yang nyata dan sering terjadi.
Sel tabel:
<tr>
<td>SKU</td>
<td>ABC-123</td>
</tr>
//td[preceding-sibling::td[1] = 'SKU']
Atau dari kolom header melintasi baris pada tabel spesifikasi dua kolom:
//th[normalize-space() = 'Weight']/following-sibling::td[1]
Bentuk kedua biasanya lebih disukai ketika label berupa th
, karena dibaca secara berurutan dan sesuai dengan struktur tabel.
Peningkatan ketahanan yang membuat ekspresi ini tetap berfungsi pada markup nyata:
//dd[preceding-sibling::dt[1][normalize-space() = 'Price']]
normalize-space()
menghilangkan spasi internal dan memotong bagian akhir, yang menangani HTML yang diformat rapi yang sebaliknya akan merusak perbandingan string yang tepat. Ini adalah fungsi paling berharga dalam pengambilan data menggunakan XPath.
Untuk pencocokan parsial, contains()
lebih toleran namun kurang presisi:
//dd[preceding-sibling::dt[1][contains(., 'Price')]]
Hati-hati: contains(., 'Price')
juga mencocokkan "Price excluding VAT" dan "Historic Price". Jika halaman memiliki beberapa label semacam itu, Anda akan mendapatkan beberapa hasil dan kode Anda akan mengambil yang pertama tanpa pemberitahuan.
Judul dan konten setelahnya, yang merupakan pola yang sama namun dalam arah sebaliknya:
//h2[normalize-space() = 'Specifications']/following-sibling::table[1]
Tabel pertama setelah judul tersebut. Ini merupakan kebutuhan yang sangat umum pada halaman dokumentasi dan produk, dan tidak ada setara CSS yang mengekspresikan "tabel pertama setelah judul spesifik ini".
Penggabungan dengan Predikat dan Kondisi
preceding-sibling
dapat digabungkan dengan bagian lain dari XPath, dan ada beberapa kombinasi yang perlu diketahui.
Menghitung elemen saudara — berguna untuk menemukan elemen pertama atau terakhir, atau memeriksa posisi:
//li[count(preceding-sibling::li) = 0] first li
//li[count(preceding-sibling::li) < 3] first three
Memeriksa keberadaan — himpunan node dalam konteks boolean dianggap benar jika tidak kosong:
//p[preceding-sibling::h2] paragraphs with an h2 somewhere before
//p[not(preceding-sibling::p)] first paragraph among its siblings
Merangkai sumbu:
//span[@class='value']/preceding-sibling::*[1]/text()
Elemen yang langsung mendahului, terlepas dari tag-nya, beserta teksnya.
Beberapa kondisi:
//td[preceding-sibling::td[1] = 'Status'][normalize-space() != '']
Dua predikat yang diterapkan secara berurutan: sel setelah label "Status", dan tidak kosong.
Catatan mengenai pintasan ..
, yang seringkali lebih rapi daripada sumbu:
//dt[.='Price']/following-sibling::dd[1]
biasanya lebih mudah dibaca daripada bentuk preceding-sibling
untuk hasil yang sama, dan dibaca sesuai arah penulisan dokumen. Gunakanlah jika jangkarnya adalah label.
Kapan Selektor CSS Dapat dan Tidak Dapat Menggantikannya
Pandangan umum bahwa "CSS tidak dapat melihat ke belakang" perlu diperbarui.
CSS kini dapat menerapkan kondisi saudara kandung dengan :has(). Didukung secara luas di peramban modern, fitur ini memungkinkan selektor bergantung pada hubungan saudara kandung:
dt:has(+ dd) a dt immediately followed by a dd
li:has(~ li.active) an li with a later sibling that is active
Hal yang masih belum dapat dilakukan oleh CSS:
Memilih berdasarkan konten teks. Tidak ada padanan CSS untuk [text() = 'Price']. Inilah satu-satunya alasan mengapa pola label-nilai tetap menjadi wilayah XPath, karena pencocokan label adalah pencocokan teks.
Menavigasi ke leluhur sembarang. :has() menyediakan bentuk pemilihan induk, tetapi tidak ada sumbu leluhur umum.
Mengindeks sepanjang sumbu terbalik. Tidak ada konstruksi CSS yang berarti "elemen terdekat sebelumnya dari jenis ini".
Dan hal-hal yang dilakukan CSS dengan lebih baik: segala hal yang sederhana. Pemilihan kelas dan ID, hubungan keturunan, serta pencocokan atribut. Selektor CSS lebih mudah dibaca, didukung lebih baik oleh alat bantu, dan lebih cepat di sebagian besar mesin.
Kebijakan yang bijaksana adalah menggunakan CSS sebagai default dan beralih ke XPath pada titik spesifik di mana Anda memerlukan pencocokan teks atau navigasi mundur. Memadukan keduanya dalam satu basis kode tidak menjadi masalah, dan scraper yang menggunakan CSS untuk 90% selektornya serta XPath untuk 10% yang sulit lebih mudah dipelihara daripada scraper yang hanya mengandalkan salah satunya.
Kinerja dan Kerentanan
Dua kendala praktis.
Kinerja. preceding-sibling dibatasi oleh jumlah node saudara, yang biasanya sedikit — hal ini tidak menjadi masalah. preceding memindai semua elemen sebelum node konteks dalam dokumen, yang pada halaman besar memerlukan sumber daya yang besar, dan di dalam loop, kompleksitasnya kuadratik. Jika selektor yang menggunakan preceding berjalan lambat, hampir pasti itulah penyebabnya. Solusi umumnya adalah menulis ulang selektor tersebut menggunakan preceding-sibling dari node konteks yang lebih dekat.
Kerentanan. Selektor berbasis saudara bergantung pada struktur dokumen, yang justru diubah oleh perancangan ulang. preceding-sibling::td[1] akan rusak tanpa peringatan saat seseorang menyisipkan kolom. Tidak ada pesan kesalahan; selektor tersebut mencocokkan sel yang berbeda dan data Anda menjadi salah tanpa disadari.
Tiga langkah mitigasi yang benar-benar membantu:
Gunakan teks sebagai acuan, bukan posisi, jika memungkinkan. //dt[.='Price']/following-sibling::dd[1] tetap berfungsi meski daftar diurutkan ulang. (//dd)[3] tidak.
Pastikan apa yang Anda ekstrak. Jika harga harus sesuai dengan pola mata uang, periksa hal tersebut. Selektor yang mulai mengembalikan status stok alih-alih harga tidak akan terdeteksi kecuali ada yang memvalidasi formatnya.
Pilihlah data terstruktur yang tertanam jika tersedia. Jika halaman tersebut memuat JSON-LD dalam blok <script type="application/ld+json">, parsinglah data tersebut. Data ini dirancang agar dapat dibaca oleh mesin, jauh lebih stabil saat terjadi perombakan desain, dan menghilangkan seluruh kategori kerentanan selektor. Melakukan pengecekan terhadapnya sebelum menulis XPath apa pun sangatlah sepadan dengan waktu tiga puluh detik.
Kegagalan struktural yang tidak terdeteksi termasuk dalam kelas masalah yang sama seperti yang kami jelaskan dalam mengapa pengujian proxy penting — permintaan berhasil, penguraian berhasil, namun datanya salah.
Kapan Tidak Harus Menggunakannya
Ketika elemen tersebut memiliki pengenal yang dapat digunakan. Jika ada atribut id, class, atau data, gunakanlah itu. Selektor yang bergantung pada struktur jelas lebih rentan daripada selektor yang bergantung pada nama yang dipilih secara sengaja oleh pengembang.
Ketika data terstruktur tersedia. JSON-LD, microdata, atau muatan JSON di balik permintaan XHR. Salah satu dari opsi ini lebih baik daripada mengurai HTML yang sudah ditampilkan.
Ketika hubungan antar elemen benar-benar longgar. Jika Anda menemukan diri Anda menulis preceding::*[5], struktur tersebut sebenarnya tidak memberikan informasi yang berarti, dan selektor tersebut akan rusak pada deployment berikutnya. Pertimbangkan kembali pendekatan Anda.
Ketika CSS sudah cukup. Untuk seleksi sederhana, CSS lebih mudah dibaca dan didukung dengan lebih baik. Gunakan XPath hanya untuk pencocokan teks dan navigasi mundur.
Ketika Anda mengandalkan fitur XPath 2.0 di browser. Browser mengimplementasikan XPath 1.0 melalui document.evaluate, dan Selenium mengikuti implementasi browser tersebut. Jadi, tidak ada matches(), tidak ada ekspresi reguler, tidak ada upper-case(), dan tidak ada tipe urutan. Perpustakaan sisi server seperti lxml juga menggunakan XPath 1.0 untuk antarmuka pemrograman aplikasi (API) umum. Jika potongan kode yang Anda temukan secara daring tidak berfungsi, periksa apakah kode tersebut menggunakan fungsi yang hanya ada di versi yang lebih baru.
Pertanyaan Lainnya
Apa fungsi preceding-sibling dalam XPath?
Fungsi ini memilih semua node yang memiliki induk yang sama dengan node konteks dan muncul sebelum node konteks tersebut dalam urutan dokumen. Fungsi ini tidak mencakup leluhur, keturunan, atau node pada tingkat lain dalam pohon — hanya node saudara. Hasilnya kosong jika node konteks adalah node atribut atau node namespace.
Mengapa preceding-sibling[1] menghasilkan elemen yang salah?
Karena preceding-sibling merupakan sumbu terbalik, dan penomoran posisi pada sumbu terbalik berjalan sesuai urutan dokumen terbalik. Oleh karena itu, [1] berarti saudara terdekat yang berada di depan, bukan yang pertama dalam urutan dokumen. Untuk mendapatkan yang pertama dalam urutan dokumen, bungkus sumbu tersebut dengan tanda kurung: (preceding-sibling::p)[1].
Apa perbedaan antara preceding dan preceding-sibling?
preceding-sibling hanya mencakup node dengan orang tua yang sama. preceding mencakup setiap node yang muncul lebih awal dalam dokumen pada kedalaman apa pun, kecuali leluhur dan node atribut. preceding jauh lebih luas, jauh lebih lambat, dan jauh lebih mungkin mencocokkan sesuatu yang tidak diinginkan.
Bagaimana cara memilih nilai berdasarkan labelnya di XPath?
Cocokkan label berdasarkan teks, lalu ambil elemen yang berdekatan: //dt[normalize-space()='Price']/following-sibling::dd[1], atau dari sisi nilai, //dd[preceding-sibling::dt[1]='Price']. Penggunaan [1] sangat penting — tanpa itu, predikat akan bernilai benar jika ada label sebelumnya yang cocok.
Apakah selektor CSS dapat melakukan apa yang dilakukan oleh preceding-sibling?
Sebagian. :has() menyediakan seleksi bersyarat saudara di browser modern. Yang masih tidak dapat dilakukan oleh CSS adalah memilih berdasarkan konten teks atau indeks sepanjang sumbu terbalik, dan pencocokan teks adalah persis apa yang dibutuhkan oleh pola label-nilai. Gunakan CSS untuk seleksi sederhana dan XPath jika Anda memerlukan teks atau navigasi mundur.
Apakah preceding-sibling berfungsi di Selenium dan peramban?
Ya. Peramban mengimplementasikan XPath 1.0 melalui document.evaluate, dan Selenium menggunakan mesin peramban tersebut. Sumbu ini sesuai dengan XPath 1.0 dan tersedia secara universal. Yang tidak tersedia adalah fitur apa pun dari XPath 2.0 atau versi yang lebih baru — tidak ada ekspresi reguler, tidak ada "matches()", dan tidak ada "upper-case()".
Apakah "preceding-sibling" lambat?
Biasanya tidak. Kinerjanya dibatasi oleh jumlah saudara, yang umumnya sedikit. Axis preceding-lah yang lambat, karena memindai semua elemen sebelumnya dalam dokumen, dan di dalam loop yang menjadi kuadratik. Jika selektor berbasis saudara lambat, periksa apakah Anda benar-benar menggunakan preceding.
Bagaimana cara membuat selektor XPath lebih tangguh?
Gunakan teks sebagai titik acuan alih-alih posisi jika memungkinkan, gunakan normalize-space() untuk mengatasi perbedaan spasi kosong, utamakan pengenal (identifier) dan atribut data daripada struktur, periksa adanya JSON-LD yang tertanam sebelum mem-parsing HTML sama sekali, dan validasi bentuk data yang diekstraksi sehingga selektor yang mencocokkan elemen yang salah akan gagal secara jelas daripada diam-diam.
Kesimpulan `
`preceding-siblingadalah alat yang sangat spesifik dengan satu fungsi: menjelajahi ke belakang di antara node-node pada tingkat yang sama. Hal yang perlu diingat adalah bahwa ini menggunakan sumbu terbalik, sehingga[1]`` berarti "yang terdekat" alih-alih "yang pertama", dan penambahan tanda kurung akan membalikkan arti tersebut sepenuhnya. Perbedaan tunggal inilah yang menjadi penyebab sebagian besar hasil salah yang diperoleh pengguna dari fungsi ini.
Nilai sebenarnya terletak pada pola label-nilai — mengekstrak bidang yang hanya dapat diidentifikasi melalui teks yang berada di sebelahnya. CSS telah menutup sebagian celah tersebut dengan :has(), tetapi masih belum dapat memilih berdasarkan konten teks, dan teks itulah tepatnya yang dimaksud dengan label. Inilah situasi di mana XPath tetap menjadi alat yang tepat, bukan sekadar alat yang biasa digunakan.
Hal yang perlu diwaspadai adalah bahwa selektor struktural dapat gagal tanpa peringatan. Sebuah kolom baru, daftar yang diurutkan ulang, elemen div pembungkus, dan selektor Anda akan mencocokkan elemen lain meskipun tampilan tetap terlihat normal. Gunakan teks sebagai acuan jika memungkinkan, periksa data terstruktur yang tertanam sebelum menulis selektor apa pun, dan validasi hasilnya — karena kegagalan yang terlihat bukanlah yang paling merugikan.
