Penapis JSONPath Secara Mendalam: Rekursi, Hirisan, dan Padanan Berbilang Syarat
Inti pati: Penurunan rekursif, hirisan dan penapis berbilang syarat ialah tempat JSONPath menjadi berkuasa dan disalahgunakan. Merangkumi semantik penapis, bentuk hasil dan perbezaan pustaka.

Intinya: cari dahulu kedalaman yang diketahui, kemudian tambah hirisan dan penapis dan bukannya menggunakan $..; laluan kosong dalam penapis hanya menguji kewujudan, jadi uji boolean dengan @.stock == true.
Sesiapa sahaja boleh menulis
$.store.book[0].title. Yang sebenarnya merosakkan JSONPath ialah menggabungkan$..dengan hirisan dan berbilang syarat, kemudian mendapat terlalu banyak hasil, terlalu sedikit hasil, atau susunan yang tidak dijangka.
1. Dokumen Contoh
{
"store": {
"book": [
{ "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
{ "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
{ "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
{ "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"updatedAt": "2026-10-31"
}
Setiap ungkapan di bawah menggunakan dokumen ini sebagai input. Ingat dua premis: hasilnya ialah set nod, dan susunannya mengikut susunan dokumen bukan susunan syarat ditulis. Di dalam penapis, @ merujuk hanya kepada nod calon semasa.
2. Penurunan Rekursif $..: Mudah tetapi Mahal
.. bermaksud "cari secara rekursif pada mana-mana kedalaman dari kedudukan semasa". RFC 9535 mentakrifkannya sebagai segmen keturunan, dan ia boleh digabungkan dengan nama, kad liar, indeks, hirisan, dan penapis:
$..price // every member named price at any depth
$..book[?(@.price < 10)] // any book at any depth, then filter
$..[?(@.stock)] // any node at any depth where stock exists
Tiga perkara untuk difahami:
$..bookmencari setiap nod bernamabook, tanpa mengira kedalamannya;- Penjelajahan adalah kedalaman-dahulu, dan susunan output bergantung pada cara pelaksanaan melawati anak;
..tidak boleh menggunakan mana-mana indeks, jadi kosnya bertambah mengikut jumlah nod keseluruhan.
Akibatnya, $..price dan $.store.book[*].price mungkin berbeza hanya pada harga basikal dalam output mereka, namun berbeza sebanyak satu urutan magnitud dalam kos. Apabila anda tahu kedalamannya, sentiasa tulis laluan eksplisit. Jika anda hanya ingin membandingkan sempadan kedua-dua bahasa laluan, lihat JSONPath vs JSON Pointer.
3. Hirisan dan Kad Liar Tatasusunan: Tiga Perangkap
Sintaks hirisan ialah [start:end:step], termasuk permulaan dan tidak termasuk penghujung, dengan nombor negatif dikira dari hujung:
| Ungkapan | Maksud | Hasil (dengan empat buku) |
|---|---|---|
[0:2] |
Dua buku pertama | Buku 1 dan 2 |
[1:] |
Daripada buku kedua hingga akhir | Buku 2, 3, dan 4 |
[:2] |
Dua buku pertama | Buku 1 dan 2 |
[-2:] |
Dua buku terakhir | Buku 3 dan 4 |
[::2] |
Setiap buku berselang | Buku 1 dan 3 |
[::-1] |
Susunan terbalik | Buku 4, 3, 2, dan 1 |
Tiga perangkap:
- Langkah sifar tidak sah. RFC 9535 memerlukan langkah bukan sifar, sedangkan sesetengah pelaksanaan secara senyap memulangkan set kosong;
- Langkah negatif tidak disokong secara universal. Pelaksanaan Goessner asal menyokongnya, tetapi sesetengah pelaksanaan minimum hanya menerima langkah positif;
- Menapis selepas hirisan tidak meluaskan skop.
[0:2][?(@.stock)]hanya menapis dua buku yang dihasilkan oleh hirisan; ia tidak kembali kepada keseluruhan tatasusunan.
Kad liar [*] boleh digabungkan dengan hirisan, tetapi [*] menghasilkan "semua nod anak". Jika anak-anak itu objek, menambah .title memberikan semua tajuk. Jika anak itu sendiri tatasusunan, .title tidak akan secara tersirat meratakan satu tahap lagi. Tulis [*] sekali lagi apabila anda perlu mengembangkan; jangan harapkan perataan tersirat.
4. Penapis Berbilang Syarat: Perbandingan, Logik, dan Kewujudan
Penapis [?(...)] boleh mengandungi empat jenis konstruk: perbandingan, pengendali logik, ujian kewujudan, dan fungsi. Pertanyaan berbilang syarat biasa kelihatan seperti ini:
$..book[?(@.price < 10 && @.stock)]
Ia bermaksud "harga di bawah 10 dan stok adalah benar". Beri perhatian kepada beberapa butiran semantik:
@.stockialah ujian kewujudan, bukan "nilainya benar". Jika medan wujud tetapi nilainyafalse, syarat itu masih berlaku. Untuk menguji nilai boolean anda mesti menulis@.stock == true;&&mengikat lebih kuat daripada||. Tambah kurungan apabila mencampurkannya dan bukannya bergantung pada ingatan;- Kedua-dua belah perbandingan mesti mempunyai jenis yang sama. Nombor dan rentetan tidak ditukar secara tersirat;
@.price < "10"adalah palsu atau ralat dalam pelaksanaan yang ketat; - Medan yang tiada sentiasa menjadikan perbandingan palsu.
@.missing < 10tidak menimbulkan ralat; ia hanya tidak memadankan apa-apa.
Ini adalah tempat paling mudah untuk tersandung apabila menulis penapis seolah-olah ia JavaScript: if (obj.stock) menguji kebenaran dalam JavaScript, sedangkan JSONPath menguji kewujudan. Untuk sintaks standard penuh dan fungsi, ulangi Bahasa Pertanyaan JSONPath dalam Amalan.
5. Ungkapan Skrip: Lebih Banyak Kuasa, Kurang Kebolehportan
Pelaksanaan Goessner asal dan jsonpath-plus membenarkan perbandingan merentas nod seperti [?(@.price < $.max)], malah skrip sewenang-wenangnya. RFC 9535 secara eksplisit hanya membenarkan akses kepada nod semasa @ dan melarang $ di dalam penapis.
// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]
// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
Ungkapan skrip mencipta tiga masalah:
- Kebolehportan jatuh kepada sifar. Menukar pustaka bermaksud menulis semula setiap ungkapan;
- Sempadan keselamatan hilang. Apabila ungkapan datang daripada pengguna, penilaian skrip menyerahkan pelaksanaan kod;
- Pengoptimuman statik menjadi mustahil. Pelaksanaan hanya boleh mentafsir nod demi nod, yang perlahan dan sukar disimpan dalam cache.
Jika anda benar-benar memerlukan perbandingan merentas nod, hantarkan ambang sebagai parameter pertanyaan dan tapis kali kedua dalam kod aplikasi. Itu biasanya lebih stabil daripada bergantung pada sintaks sambungan.
6. Bentuk Hasil: Anda Fikir Anda Mendapat Nilai, tetapi Anda Mendapat Nod
Ungkapan yang sama boleh memulangkan sekurang-kurangnya tiga bentuk merentas pustaka:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + value
Ini mempengaruhi tiga perkara secara langsung: cara frontend membaca hasil, cara hasil dideduplikasi, dan cara hasil pertanyaan menjadi operasi tulis. Senarai nod yang ditakrifkan oleh RFC 9535 ialah satu set rujukan nod, tetapi pelaksanaan JavaScript biasanya memulangkan nilai sahaja kerana rujukan tidak boleh merentas sempadan proses.
Nasihat praktikal:
- Sentiasa baca satu nilai melalui
[0]atauvalue(); jangan sekali-kali menganggap hasilnya ialah skalar; - Apabila anda perlu menulis kembali, pilih pelaksanaan yang memulangkan JSON Pointer atau bina penuding itu sendiri;
- Hasil kosong dan hasil
[null]adalah berbeza; semak panjang dan bukannya kebenaran.
7. Perbezaan Pustaka: Jalankan Suite Pematuhan Sebelum Berhijrah
| Keupayaan | Goessner asal | jsonpath-plus | RFC 9535 ketat |
|---|---|---|---|
Kurungan penapis ?() |
Diperlukan | Serasi | Pilihan |
Rujukan akar silang $.x |
Disokong | Disokong | Dilarang |
| Ungkapan skrip | Disokong | Disokong | Dilarang |
| Indeks dan langkah negatif | Disokong | Disokong | Disokong |
| Laluan pulangan | Boleh dikonfigurasi | Boleh dikonfigurasi | Tidak dinyatakan |
Fungsi seperti length() |
Tiada | Sambungan | Fungsi standard |
Sebelum berhijrah, sahkan sekurang-kurangnya empat perkara: sama ada kurungan diperlukan, sama ada $ boleh muncul di dalam penapis, cara langkah negatif berkelakuan, dan struktur hasil. Suite ujian pematuhan rasmi merangkumi kebanyakan perbezaan sintaks dan menghalang kegagalan senyap seperti "tiga baris hilang selepas menukar pustaka".
8. Senarai Semak Prestasi dan Salah Guna
Prestasi bermuara pada satu ayat: JSONPath ialah penjelajahan penuh, dan .. ialah jenis penjelajahan penuh yang paling mahal.
- Untuk dokumen dengan lebih daripada 100,000 nod, sempitkan skop dengan laluan eksplisit dahulu, kemudian tapis subset;
- Kompil ungkapan sekali dan gunakannya semula; jangan huraikan semula rentetan di dalam gelung;
- Beralih kepada
jqatau kod untuk pengagregatan (penjumlahan, pengelompokan, pengisihan); JSONPath hanya memilih; - Letakkan syarat seawal mungkin:
$.store.book[?(@.price < 10)]mengimbas jauh lebih sedikit nod tidak relevan daripada$..[?(@.price < 10)].
Senarai semak salah guna:
- [ ] Menganggap ujian kewujudan sebagai ujian kebenaran boolean;
- [ ] Menggunakan
$..dan bukannya kedalaman yang diketahui; - [ ] Bergantung pada ungkapan skrip untuk perbandingan merentas nod;
- [ ] Menganggap hasilnya sentiasa tatasusunan nilai;
- [ ] Membenarkan ungkapan yang dikawal pengguna tanpa had panjang dan penyarangan.
FAQ
Mengapa @.stock masih sepadan apabila medan itu false?
Kerana laluan kosong di dalam penapis bermaksud "medan itu wujud", bukan "nilainya benar". Untuk menguji boolean, bandingkan secara eksplisit dengan @.stock == true. Ini ialah perbezaan semantik terbesar antara JSONPath dan kebenaran JavaScript.
Adakah susunan penurunan rekursif stabil?
Tidak. RFC 9535 menentukan semantik set nod, dan susunan penjelajahan bergantung pada cara pelaksanaan melawati anak. Apabila anda memerlukan susunan tetap, cari tatasusunan dan baca mengikut indeks, atau susun hasilnya dalam kod aplikasi.
Bolehkah hirisan dan penapis digabungkan?
Ya, tetapi ingat skop. $.store.book[0:2][?(@.stock)] menapis hanya dua buku yang dihasilkan oleh hirisan. Untuk menapis dahulu dan menghiris kedua, tulis $.store.book[?(@.stock)][0:2]; susunan berbeza memberikan hasil berbeza.
Mengapa jsonpath-plus menyokong $.expensive sedangkan standard melarangnya?
Kerana penapis ditakrifkan hanya dari segi nod semasa @. Rujukan akar silang menjadikan ungkapan bergantung pada konteks keseluruhan dokumen, yang sukar dianalisis secara statik dan dipindahkan merentas bahasa. Hantarkan ambang sebagai parameter dan bukannya membenamkannya dalam ungkapan.
Bagaimana saya menghalang penurunan rekursif daripada mengimbas nod tidak relevan?
Tulis laluan eksplisit setiap kali anda tahu kedalaman, sempitkan skop dahulu, dan tapis hanya subset dan bukannya menggunakan $.. sebagai ganti tahap yang diketahui. Letakkan syarat seawal mungkin: $.store.book[?(@.price < 10)] mengimbas jauh lebih sedikit nod tidak relevan daripada $..[?(@.price < 10)]. Untuk dokumen dengan lebih daripada 100,000 nod, lakukan ini dahulu.