JSON Toolkit

Bahasa Pertanyaan JSONPath dalam Amalan

Inti pati: Pada 2024 JSONPath akhirnya mendapat standard rasmi, RFC 9535. Artikel ini menerangkan sintaks, semantik penapis, fungsi, dan perbezaan sejarah antara pelaksanaan.

Pertengahan~12 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk Bahasa Pertanyaan JSONPath dalam Amalan

Intinya: cari dengan laluan eksplisit dahulu dan tapis di atasnya, serta tulis ungkapan mengikut RFC 9535; gunakan akses sifat atau JSON Pointer untuk satu medan tetap, dan jq atau kod untuk pengagregatan.

Anda mungkin pernah melihat ungkapan seperti $.store.book[?(@.price < 10)].title, tetapi ia hanya menjadi standard antarabangsa pada 2024.

1. Daripada Standard De Facto kepada Standard Rasmi

JSONPath dicadangkan oleh Stefan Gössner pada 2007, dan untuk masa yang lama satu-satunya "spesifikasi" ialah satu catatan blog, dengan setiap pelaksanaan bahasa melakukan caranya sendiri:

  • sama ada penapis memerlukan kurungan (?(@.a) berbanding ?@.a);
  • sama ada indeks tatasusunan [-1] disokong;
  • sama ada hasil memulangkan "nilai" atau "nilai dan laluan";
  • sama ada ungkapan skrip (?(@.price < $.max)) dibenarkan.

RFC 9535, yang diterbitkan pada Februari 2024, menyatukan perbezaan ini kepada satu sintaks dan semantik serta menyediakan suite ujian pematuhan rasmi. Projek baharu harus mengikut RFC 9535; semasa berhijrah projek lama, sahkan versi pelaksanaan dahulu.

2. Lembaran Curang Sintaks

Sintaks Maksud Contoh
$ Nod akar $.store
@ Nod semasa di dalam penapis $[?@.price < 10]
.name Nod anak (nama mesti sepadan dengan [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] Nod anak (sebarang nama, termasuk bukan ASCII dan titik) $['store']['书']
[0] Indeks tatasusunan; nombor negatif dikira dari hujung $.book[0], $.book[-1]
[0,2] Berbilang indeks / berbilang nama $.book[0,2]
[1:3] Hirisan, permulaan inklusif dan penghujung eksklusif $.book[1:3]
[::2] Hirisan dengan langkah $.book[::2]
[*] Kad liar untuk semua nod anak $.book[*].title
..name Penurunan rekursif, mencari pada mana-mana kedalaman $..price
..[*] Ambil semua nod secara rekursif $..*
[?<expr>] Pemilih penapis $.book[?@.price < 10]

Satu semantik yang mesti anda ingat: JSONPath memulangkan set nod (senarai nod), bukan nilai tunggal. Walaupun hanya satu nod sepadan, hasilnya ialah senarai dengan panjang satu.

3. Menjalankan Sintaks dengan Satu Contoh

{
  "store": {
    "book": [
      { "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
      { "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
      { "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "expensive": 10
}
Ungkapan Hasil
$.store.bicycle.color ["red"]
$..price 4 harga (termasuk harga basikal)
$.store.book[*].title 3 tajuk
$.store.book[?@.price < 10].title Tajuk buku 1 dan 3
$.store.book[?@.tags] Semua buku yang mempunyai tags (ujian kewujudan)
$.store.book[-1].title Tajuk buku terakhir
$..book[0,1] Dua buku pertama
$.store.book[?@.price < $.expensive] Bergantung pada pelaksanaan secara sintaksis; RFC 9535 tidak membenarkan rujukan akar silang

Baris terakhir ialah perangkap sejarah: banyak pelaksanaan membenarkan $.expensive di dalam penapis untuk perbandingan merentas nod, yang secara eksplisit dilarang oleh RFC 9535 — penapis hanya boleh mengakses nod semasa @. Apabila anda perlu membandingkan dengan ambang global, hantarkan ambang itu sebagai argumen kepada fungsi pertanyaan.

4. Apa yang Boleh Diungkapkan oleh Penapis

Ungkapan penapis RFC 9535 menyokong empat jenis konstruk:

Logic:       &&  ||  !          e.g. ?@.price < 10 && @.tags
Comparison:  ==  !=  <  <=  >  >=
Existence:   a bare path        e.g. ?@.tags (tests existence)
Functions:   length() count() match() search() value()

Beberapa contoh praktikal:

// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]

// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// More than one tag
$.store.book[?length(@.tags) > 1]

// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()

match() ialah padanan rentetan penuh dan search() ialah carian subrentetan; dialek ungkapan biasa asasnya ialah I-Regexp (subset RFC 9485), yang tidak menyokong rujukan belakang mahupun lookaround — itu adalah kedua-dua kekangan dan perisai berguna terhadap DoS ungkapan biasa.

5. Perbezaan Pelaksanaan: Semak Sebelum Berhijrah

Pelaksanaan Bahasa Sokongan RFC 9535 Nota
jsonpath-plus JavaScript Kebanyakannya serasi Menyokong fungsi sambungan dan ungkapan skrip; lebih berkemampuan tetapi tidak mengikut standard secara ketat
jsonpath (dchester) JavaScript Separa Pustaka lama dengan sintaks ?() dan struktur hasil yang berbeza
ojg / gjson Go ojg mempunyai sokongan baik gjson menggunakan sintaks laluan lain dan bukan JSONPath
jsonpath-ng Python Separa Menyokong sambungan dan agak permisif
Jayway JsonPath Java Separa Ekosistem luas; tingkah laku sejarah berbeza daripada RFC

Tiga semakan sebelum berhijrah:

  1. Sama ada kurungan penapis diperlukan;
  2. Sama ada indeks negatif seperti -1 disokong;
  3. Sama ada hasil ialah "tatasusunan nilai" atau "tatasusunan objek {path, value}".

Yang ketiga paling mudah tersandung: frontend membaca result[0], dan selepas menukar pustaka ia menerima objek, menyebabkan halaman kosong.

6. Prestasi dan Keselamatan

Prestasi

  • JSONPath pada asasnya ialah satu penjelajahan dengan kerumitan O(bilangan nod), dan ia tidak boleh menggunakan indeks;
  • Penurunan rekursif dengan .. paling mahal; jika anda boleh mencari dengan $.store.book dahulu dan kemudian menapis, jangan tulis $..book;
  • Kompil ungkapan sekali dan gunakannya semula (kebanyakan pustaka menawarkan compile atau cache) dan bukannya menghuraikan semula pada setiap pertanyaan;
  • Untuk dokumen dengan lebih daripada 100,000 nod, pertimbangkan pengekstrakan tepat dengan JSON Pointer dahulu, atau prapemprosesan pada pelayan dengan jq.

Keselamatan

  • Ungkapan daripada input pengguna mesti mempunyai had panjang dan kedalaman penyarangan;
  • Jangan biarkan JSONPath mengendalikan kebenaran — ia hanya boleh membaca, dan tidak boleh menyatakan "medan mana yang tidak kelihatan";
  • Apabila ungkapan biasa yang digunakan oleh match() datang daripada pengguna, hadkan kerumitannya; walaupun ia lebih selamat daripada PCRE, had panjang masih disyorkan.

7. Bila Tidak Menggunakan JSONPath

  • Anda hanya perlu satu medan tetap → gunakan data.store.bicycle.color secara langsung;
  • Anda perlu menulis kembali selepas mengubah → gunakan JSON Pointer atau patch; JSONPath tidak menulis kembali;
  • Anda perlu pengagregatan (penjumlahan, pengelompokan) → gunakan jq, SQL, atau kod; JSONPath hanya memilih;
  • Dokumen itu besar dan anda hanya perlukan beberapa medan → gunakan penghurai penstriman dan bukannya membaca keseluruhan pokok ke dalam memori dahulu.

8. Senarai Semak

  • [ ] Ungkapan mengikut RFC 9535 dan tidak bergantung pada rujukan akar silang;
  • [ ] Sudah jelas bahawa hasilnya ialah set nod; gunakan [0] atau value() untuk mendapatkan satu nilai;
  • [ ] Ungkapan dikompil terlebih dahulu dan diguna semula;
  • [ ] Ungkapan yang dikawal pengguna mempunyai had panjang dan kerumitan;
  • [ ] Kes ujian pematuhan rasmi dijalankan semasa menukar pelaksanaan.

FAQ

Bagaimana saya memilih antara JSON Pointer dan JSONPath?

Gunakan akses sifat biasa atau JSON Pointer apabila anda hanya perlukan satu medan tetap; gunakan JSONPath apabila anda perlukan penapis, kad liar, atau penurunan rekursif. JSONPath hanya memilih dan tidak pernah menulis kembali, jadi penulisan kembali adalah milik JSON Pointer atau patch.

Mengapa penapis tidak boleh merujuk $.expensive?

RFC 9535 secara eksplisit melarang rujukan akar silang; penapis hanya boleh mengakses nod semasa @. Hantarkan ambang sebagai argumen kepada fungsi pertanyaan sebaliknya. Pelaksanaan lama menerima sintaks itu, tetapi ia rosak apabila anda menukar atau menaik taraf pustaka.

Patutkah saya bergantung pada result[0] untuk membaca nilai?

Hanya selepas mengesahkan bahawa pelaksanaan semasa memulangkan tatasusunan nilai. Sesetengah pustaka memulangkan objek {path, value}, jadi result[0] menjadi objek dan halaman menjadi kosong. Baca nilai tunggal melalui [0] atau value(), dan jalankan suite pematuhan rasmi sebelum menukar pelaksanaan.