Geonode logo
Geonode Team

Geonode Team

Dikemas kini: 7 Oktober 2026

Diterbitkan: 2 September 2026

XPath contains(): Panduan Lengkap Bersama Contoh-contohnya

`contains()` adalah fungsi XPath yang paling sering digunakan orang, namun paling sedikit dipahami. Fungsi ini tampak seperti pengujian substring dan berperilaku seperti itu hanya jika Anda memberinya sebuah string. Jika diberikan sekumpulan node — yang merupakan hasil dari sebagian besar ekspresi — fungsi ini secara diam-diam hanya menggunakan node pertama dan mengabaikan sisanya. Panduan ini membahas jebakan tersebut, idiom pencocokan kelas yang jarang dipahami dengan benar pada percobaan pertama, serta penanganan spasi kosong dan huruf besar/kecil yang dibuat lebih rumit daripada seharusnya oleh XPath 1.0.

Catatan singkat mengenai alasan kami menulis ini. Kami adalah Geonode; kami menjual proxy kepada orang-orang yang mengekstrak data, sehingga pertanyaan seputar selector sering kali kami terima. Pernyataan penafian yang relevan ini singkat: bug selector dan masalah proxy menimbulkan gejala yang sama sekali berbeda, dan mengacaukan keduanya hanya akan membuang-buang waktu berjam-jam. Permintaan yang diblokir akan menampilkan halaman tantangan atau status kesalahan. contains() yang rusak akan menampilkan halaman yang tampak normal namun hasilnya kosong. Jika HTML-nya ada dan ekspresi Anda tidak menemukan apa pun, artikel ini adalah tempat yang tepat dan jaringan Anda berfungsi dengan baik.

Fungsi Itu Sendiri

Spesifikasi XPath 1.0 cukup ringkas:

Fungsi contains mengembalikan nilai true jika string argumen pertama mengandung string argumen kedua, dan sebaliknya mengembalikan nilai false.

Kedua argumen tersebut adalah string. Dua argumen, hasil boolean, peka huruf besar-kecil, tanpa karakter pengganti, tanpa ekspresi reguler.

//a[contains(@href, 'download')]
//div[contains(@class, 'product')]
//p[contains(text(), 'Price')]

Perilaku yang menarik sepenuhnya terletak pada apa yang terjadi ketika argumen yang Anda berikan bukanlah string, yang biasanya terjadi.

Perangkap Node-Set: contains()

Hanya Mendeteksi Node Pertama Inilah bug yang menyebabkan "XPath saya berfungsi di beberapa halaman tetapi tidak di halaman lain", dan spesifikasinya menjelaskannya dengan tepat:

Sebuah node-set dikonversi menjadi string dengan mengembalikan nilai string dari node dalam node-set yang berada di urutan pertama dalam dokumen. Jika node-set kosong, maka string kosong yang dikembalikan.

Jadi, ketika Anda menulis kode yang menghasilkan node-set dan meneruskannya ke contains() , XPath secara diam-diam mengabaikan semua node kecuali yang pertama.

<div>
  <p>Introduction</p>
  <p>Price: £42</p>
  <p>Availability</p>
</div>
contains(//p, 'Price')      → false

Salah, karena //p adalah node-set yang terdiri dari tiga node; konversi ke string hanya mengambil yang pertama — "Introduction" — dan itu tidak mengandung "Price". Dua paragraf lainnya sama sekali tidak dipertimbangkan.

Solusinya adalah membuat predikat diterapkan per node, bukan mengonversi kumpulan:

//p[contains(., 'Price')]   → the second paragraph

Di sini, contains() dievaluasi sekali untuk setiap p , dengan . sebagai node tersebut. Inilah perbedaan antara bertanya "apakah kumpulan ini mengandung ini?" dan "anggota mana dari kumpulan ini yang mengandung ini?", dan biasanya yang dimaksud orang adalah yang kedua.

Perangkap yang sama muncul pada ``text() , yang juga merupakan himpunan node: `

//div[contains(text(), 'Price')]

text() ` mengembalikan semua anak node teks langsung, dan konversi string mengambil yang pertama. Jika elemen tersebut memiliki teks yang terbagi ke beberapa node — yang terjadi setiap kali ada markup bersarang — Anda hanya menguji fragmen pertama.

.

versus text()

: Sisi Lain dari Masalah yang Sama Spesifikasi mendefinisikan nilai string suatu elemen sebagai:

gabungan dari nilai-nilai string semua keturunan simpul teks dari simpul elemen tersebut sesuai urutan dokumen

Itulah perbedaan krusial antara kedua bentuk tersebut.

<p>Total: <strong>£42.00</strong> including VAT</p>
contains(., '£42.00')          → true   (all descendant text, concatenated)
contains(text(), '£42.00')     → false  (first direct text node only: "Total: ")

.

menjangkau elemen-elemen bersarang. text()

hanya melihat anak langsung, dan hanya yang pertama di antaranya.

**Gunakan .

secara default.** Ini sesuai dengan apa yang pembaca anggap sebagai "teks elemen ini", dan tetap berlaku meskipun terjadi perubahan markup, seperti saat seseorang membungkus nilai dalam <span>

.

**Gunakan ``text()`

` secara sengaja** ketika Anda secara khusus ingin mengecualikan konten bersarang — misalnya, mencocokkan label tanpa mencocokkan teks di dalam badge atau tooltip anak.

Untuk "salah satu dari node teks mengandung ini", bentuk yang benar menerapkan predikat pada node teks itu sendiri:

//div[text()[contains(., 'Price')]]
```

` Panjang, tetapi benar.

Spasi Kosong: normalize-space() Bukanlah Hal yang Opsional

HTML yang sebenarnya ditampilkan dengan rapi, dan spasi kosong dimasukkan ke dalam nilai string.

<td>
    In stock
</td>

Nilai string dari sel tersebut adalah "\n In stock\n", sehingga perbandingan eksak terhadap 'In stock' akan gagal.

Spesifikasi mendefinisikan solusinya:

Fungsi normalize-space mengembalikan string argumen dengan spasi yang dinormalisasi dengan cara menghapus spasi di awal dan akhir serta mengganti urutan karakter spasi dengan satu spasi.

//td[normalize-space() = 'In stock']
//td[contains(normalize-space(), 'In stock')]

Perhatikan bahwa normalize-space() tanpa argumen beroperasi pada node konteks, yang merupakan hal yang Anda inginkan di dalam predikat.

Secara khusus untuk contains(), spasi kurang berpengaruh di bagian ujung dan sangat berpengaruh di bagian tengah. Pencarian untuk 'In stock' akan gagal jika dibandingkan dengan "In stock" kecuali Anda menormalisasinya terlebih dahulu. Jika Anda hanya menerapkan satu kebiasaan dalam selektor Anda, pastikan untuk membungkus perbandingan teks dengan normalize-space().

Memasangkan Kelas dengan Benar

Kesalahan penggunaan yang paling umum pada ``contains()`

, dan yang paling mungkin salah tanpa disadari.

//div[contains(@class, 'btn')]

`

Kode tersebut cocok dengan ``class="btn"`

. Kode tersebut juga cocok dengan ``class="btn-primary"

, ``class="unbtn"

, dan ``class="sidebar-btn-group"

. Karena ``@class

adalah string tunggal yang dipisahkan spasi dan ``contains()

` adalah pengujian substring biasa, kode tersebut tidak memperhitungkan batas kata.

Cara yang benar adalah menambahkan spasi di sekitar atribut dan target sehingga hanya token utuh yang cocok:

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')]

Bacalah sebagai berikut: ambil atribut kelas, normalisasikan spasi kosongnya, bungkus dengan spasi sehingga setiap token dibatasi spasi di kedua sisi, lalu cari target yang dikelilingi spasi. class="btn-primary"

menjadi " btn-primary "

, yang tidak mengandung " btn "

. class="icon btn large"

menjadi " icon btn large "

, yang mengandungnya.

Ini terlihat jelek. Namun, ini juga benar, dan inilah yang pada akhirnya akan ditemukan di setiap basis kode scraping yang matang. Bungkuslah dalam helper:

def has_class(name):
    return (f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')")

Saat Anda membutuhkan dua kelas:

//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')
      and contains(concat(' ', normalize-space(@class), ' '), ' primary ')]

Pada titik ini, selektor CSS — div.btn.primary

— jauh lebih mudah dibaca dan melakukan hal yang tepat. Jika Anda hanya mencocokkan berdasarkan kelas dan tidak ada yang lain, gunakan CSS. XPath berperan saat Anda memerlukan pencocokan teks atau navigasi mundur, bukan untuk hal-hal yang sudah dilakukan dengan baik oleh CSS. Kami membandingkan keduanya dalam XPath preceding-sibling.

Sensitivitas Huruf Besar-Kecil dan Solusi Alternatif untuk "translate()"

"contains()" peka terhadap huruf besar-kecil, dan XPath 1.0 tidak memiliki fungsi "lower-case()". Browser dan Selenium mengimplementasikan XPath 1.0, sehingga keterbatasan ini berlaku di mana pun hal itu paling krusial.

Solusi alternatif ini menggunakan translate(), yang didefinisikan sebagai pengembalian "string argumen pertama dengan karakter yang muncul dalam string argumen kedua diganti oleh karakter pada posisi yang sesuai dalam string argumen ketiga":

//p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                          'abcdefghijklmnopqrstuvwxyz'), 'price')]

Transliterasi karakter per karakter, hanya ASCII. Metode ini tidak akan mengubah huruf beraksen menjadi huruf kecil kecuali Anda memperluas kedua string untuk mencakupnya, yang dengan cepat menjadi rumit.

Tersedia dua opsi yang lebih baik:

Cocokkan substring yang tidak peka huruf besar-kecil. Jika halaman menampilkan "Price" atau "PRICE" tetapi tidak pernah "price", mencocokkan 'rice' memang terlihat tidak rapi tetapi berfungsi. Seringkali ini adalah solusi yang paling praktis.

Gunakan pustaka dengan XPath yang lebih kaya fitur. Parser sisi server seperti lxml mendukung ekstensi EXSLT, termasuk re:test() untuk ekspresi reguler, yang menangani huruf besar-kecil dan banyak hal lainnya. Browser tidak mendukungnya, sehingga potongan kode yang Anda temukan untuk lxml mungkin gagal di Selenium tepat karena alasan ini.

Jika Anda mendapati diri Anda menulis translate() yang panjang, itu merupakan tanda bahwa Anda sudah melampaui kemampuan XPath 1.0 untuk tugas ini.

Fungsi String Terkait `

contains()

` merupakan salah satu dari sekumpulan kecil fungsi, dan fungsi-fungsi lainnya seringkali lebih akurat.

** ``starts-with()`

** — "mengembalikan true jika string argumen pertama dimulai dengan string argumen kedua". Lebih spesifik daripada ``contains()

` dan karenanya lebih kecil kemungkinannya untuk menghasilkan kecocokan berlebihan:

//a[starts-with(@href, 'https://')]

Tidak ada fungsi ``ends-with()`

dalam XPath 1.0. Solusi alternatifnya menggunakansubstring()`

dan string-length()

, dan hal ini cukup merepotkan sehingga pendekatan lain biasanya lebih baik.

**substring-before()

dan substring-after()

** — yang pertama "mengembalikan substring dari string argumen pertama yang mendahului kemunculan pertama string argumen kedua... atau string kosong jika string argumen pertama tidak mengandung string argumen kedua". Berguna untuk memisahkan nilai di dalam ekspresi:

substring-after(//span[@class='price'], '£')

**normalize-space()

** — telah dibahas di atas, dan inilah yang sebaiknya paling sering Anda gunakan.

**translate()

** — mengubah huruf besar-kecil, serta menghapus karakter dengan memetakan karakter tersebut ke nilai kosong:

translate(., ',', '')

**string-length()

** — menyaring nilai kosong atau terpotong:

//td[string-length(normalize-space()) > 0]

Kombinasi dari fungsi-fungsi inilah yang memberikan nilai tambah:

//tr[contains(normalize-space(td[1]), 'Weight')]/td[2]

Sel kedua dari baris mana pun yang sel pertamanya menyebutkan "Weight", tanpa membedakan spasi.

Pola yang Sering Muncul

Ekspresi di bawah ini mencakup sebagian besar pekerjaan ekstraksi yang sebenarnya dan sebaiknya selalu disimpan sebagai referensi.

Menemukan nilai di sebelah sebuah label. Persyaratan paling umum dalam proses scraping halaman terstruktur:

//dt[contains(normalize-space(), 'Price')]/following-sibling::dd[1]
//th[contains(normalize-space(), 'Weight')]/following-sibling::td[1]

Perhatikan [1]

— tanpa itu, following-sibling::td

akan mengembalikan setiap sel berikutnya dalam baris tersebut, dan kode Anda secara diam-diam mengambil yang pertama sementara Anda menganggap itu satu-satunya.

Temukan tautan berdasarkan teks yang terlihat, bukan berdasarkan href-nya:

//a[contains(normalize-space(), 'Download')]

Lebih andal daripada mencocokkan URL ketika URL tersebut berupa pengenal yang di-hash, dan lebih rentan ketika situs tersebut diterjemahkan. Pilihlah berdasarkan mana yang lebih sering berubah.

Temukan sebuah wadah berdasarkan sesuatu di dalamnya:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')][.//span[contains(., 'Sold out')]]

Dua predikat secara berurutan: sebuah kartu, yang berisi elemen span yang menyebutkan "Sold out". Hal ini dapat digabungkan dengan baik dan lebih mudah dibaca daripada mencoba mengungkapkannya dalam satu kondisi.

Lebih baik mengecualikan daripada memasukkan. Seringkali formulasi ini lebih jelas:

//tr[not(contains(@class, 'header'))]
//li[not(contains(normalize-space(), 'Advertisement'))]

**Cocokkan tombol, baik itu button

maupun a

:**

//*[self::button or self::a][contains(normalize-space(), 'Continue')]

Temukan baris yang berisi nilai tertentu dan ambil kolom yang berbeda:

//tr[td[contains(normalize-space(), 'SKU-1234')]]/td[3]

Baca dari luar ke dalam: baris yang memiliki sel yang menyebutkan SKU, lalu sel ketiga dari baris tersebut. Ini adalah pola pencarian tabel, dan pola ini jauh lebih tahan terhadap pengurutan ulang kolom dibandingkan dengan indeks absolut ke seluruh tabel.

Waspadai kecocokan kosong. Predikat yang menyaring sel kosong tidak memerlukan biaya tambahan dan mencegah jenis kebingungan di tahap selanjutnya:

//td[string-length(normalize-space()) > 0][contains(., 'Ltd')]

Kapan contains() Bukan Alat yang Tepat

Saat Anda bermaksud mencari kesamaan. contains(., 'Price') juga akan mencocokkan "Historic Price" dan "Price excluding VAT". Jika Anda ingin tepatnya label tersebut, gunakan normalize-space() = 'Price'. Pencocokan berlebih tidak akan ditampilkan — kode Anda akan mengambil hasil pertama dan tidak akan pernah tahu bahwa ada tiga hasil.

Saat Anda mencocokkan kelas dan tidak ada yang lain. CSS melakukannya dengan benar dan mudah dibaca. Lihat di atas.

Saat Anda membutuhkan ekspresi reguler. XPath 1.0 tidak memilikinya. Ekstrak dengan contains() jika perlu, lalu terapkan ekspresi reguler dalam bahasa pemrograman Anda, di mana Anda juga dapat melihat apa yang cocok.

Saat ada pengenal yang dapat digunakan. Sebuah id, atribut data, atau JSON-LD yang tertanam lebih stabil daripada pencocokan teks apa pun. Teks adalah konten, dan konten dapat berubah — perancangan ulang, terjemahan, atau penyuntingan teks dapat merusak selektor yang mencocokkan teks, dan tidak ada peringatan apa pun yang akan Anda terima.

Saat string berasal dari masukan pengguna. Menyisipkan teks yang tidak terpercaya ke dalam ekspresi XPath disebut injeksi XPath. Gunakan pengikatan variabel dari pustaka Anda jika tersedia, dan lakukan pelarian karakter dengan benar jika tidak tersedia — terutama tanda kutip, karena XPath 1.0 tidak memiliki urutan pelarian untuk tanda kutip di dalam literal string dan Anda harus menggunakan concat() untuk membuatnya.

Pertanyaan Terkait

Apa fungsi contains() dalam XPath?

Fungsi ini mengembalikan nilai true jika argumen string pertama mengandung argumen kedua sebagai substring. Kedua argumen tersebut berupa string, perbandingan ini peka huruf besar-kecil, dan tidak menggunakan karakter pengganti (wildcard) maupun ekspresi reguler. Peran umumnya dalam ekstraksi adalah mencocokkan elemen berdasarkan bagian teks atau nilai atributnya.

Mengapa fungsi contains() di XPath saya tidak menemukan apa pun?

Penyebabnya paling sering adalah karena Anda memberikan node-set sebagai argumen. XPath mengubah node-set menjadi string dengan mengambil node pertama dalam urutan dokumen dan mengabaikan sisanya, sehingga contains(//p, 'x') hanya memeriksa paragraf pertama. Gunakan predikat per node sebagai gantinya: //p[contains(., 'x')].

Apa perbedaan antara contains(.) dan contains(text())? `

`.menggunakan nilai string elemen, yang menurut spesifikasi didefinisikan sebagai gabungan semua node teks keturunan — sehingga menjangkau markup bersarang.text()mengembalikan anak node teks langsung, dan konversi string hanya mengambil yang pertama. Gunakan.`` kecuali Anda secara khusus ingin mengecualikan konten bersarang.

Bagaimana cara mencocokkan kelas dengan XPath?

Gunakan contains(concat(' ', normalize-space(@class), ' '), ' name '), yang menambahkan spasi di sekitar atribut sehingga hanya token utuh yang cocok. contains(@class, 'btn') biasa juga cocok dengan btn-primary dan unbtn. Jika Anda hanya mencocokkan berdasarkan kelas, selektor CSS lebih jelas dan benar secara default.

Apakah fungsi contains() di XPath peka huruf besar-kecil?

Ya, dan XPath 1.0 tidak memiliki fungsi "lower-case()". Solusi standar adalah menggunakan translate() dengan huruf besar dan kecil yang dinyatakan secara eksplisit, yang hanya menangani karakter ASCII. Perpustakaan sisi server seperti lxml mendukung ekspresi reguler EXSLT; sedangkan browser dan Selenium tidak.

Bagaimana cara menggunakan contains() dengan beberapa kondisi?

Gabungkan predikat dengan and dan or: //div[contains(@class, 'card') and contains(., 'In stock')]. Setiap contains() merupakan pengujian boolean terpisah yang dievaluasi terhadap node konteks yang sama.

Apakah XPath memiliki fungsi starts-with atau ends-with?

starts-with() tersedia dan lebih disarankan daripada contains() jika sesuai, karena menghasilkan lebih sedikit hasil yang tidak relevan. Tidak ada ends-with() di XPath 1.0 — solusi alternatifnya menggunakan substring() dengan string-length(), namun cara ini cukup merepotkan sehingga pendekatan lain biasanya lebih baik.

Mengapa contains() mencocokkan lebih banyak elemen daripada yang diharapkan?

Karena ini adalah pengujian substring tanpa konsep batas kata. contains(., 'Price') juga mencocokkan "Historic Price" dan "Price excluding VAT". Gunakan normalize-space() = 'Price' untuk kesamaan, atau idiom "padded-concat" untuk token kelas.

Kesimpulan

contains() memiliki spesifikasi yang sederhana namun penuh dengan jebakan saat digunakan, dan hampir semua jebakan tersebut berasal dari satu hal: XPath mengubah himpunan node menjadi string dengan mengambil node pertama dan mengabaikan sisanya. Aturan tunggal itulah yang menjelaskan mengapa contains(//p, 'x') memberikan jawaban salah yang meyakinkan, mengapa contains(text(), 'x') melewatkan teks yang terbagi di antara node, dan mengapa ekspresi yang sama berfungsi di satu halaman namun gagal di halaman berikutnya.

Kebiasaan yang dapat menghindari hal ini tidak banyak. Gunakan contains() di dalam predikat agar dievaluasi per node. Gunakan . daripada text() kecuali Anda memiliki alasan khusus. Bungkus perbandingan teks dalam normalize-space(), karena HTML asli ditampilkan dengan rapi. Dan untuk pencocokan kelas, gunakan idiom padded-concat atau — lebih baik lagi — gunakan selektor CSS, yang dirancang khusus untuk tugas tersebut.

Gunakan XPath hanya untuk fungsi uniknya: pencocokan teks, dan navigasi mundur. Itu adalah kemampuan nyata yang tidak memiliki padanan dalam CSS, dan layak untuk sintaksnya. Menggunakannya untuk memilih div.card berarti menanggung biayanya tanpa mendapatkan manfaatnya.