زبان پرسوجوی JSONPath در عمل
جمعبندی: در ۲۰۲۴ JSONPath سرانجام استاندارد رسمی گرفت، RFC 9535. این مقاله نحو، معناشناسی فیلتر، توابع و تفاوتهای تاریخی بین پیادهسازیها را توضیح میدهد.

جمعبندی: اول با مسیر صریح مکانیابی کنید و فیلتر را روی آن بگذارید، و عبارتها را بر اساس RFC 9535 بنویسید؛ برای یک فیلد ثابت از دسترسی به ویژگی یا JSON Pointer، و برای تجمیع از jq یا کد استفاده کنید.
احتمالاً عبارتهایی مانند
$.store.book[?(@.price < 10)].titleدیدهاید، اما فقط در ۲۰۲۴ استاندارد بینالمللی شد.
۱. از استاندارد عملی تا استاندارد رسمی
JSONPath توسط Stefan Gössner در ۲۰۰۷ پیشنهاد شد و مدت طولانی تنها «مشخصات» یک پست وبلاگی بود، با هر پیادهسازی زبانی که کار خودش را میکرد:
- آیا فیلترها پرانتز میخواهند (
?(@.a)در برابر?@.a)؛ - آیا اندیس آرایه
[-1]پشتیبانی میشود؛ - آیا نتیجه «مقدارها» برمیگرداند یا «مقدارها و مسیرها»؛
- آیا عبارتهای اسکریپتی (
?(@.price < $.max)) مجازند.
RFC 9535، منتشرشده در فوریه ۲۰۲۴، این واگراییها را به یک نحو و معناشناسی واحد همگرا کرد و مجموعه آزمون انطباق رسمی فراهم ساخت. پروژههای جدید باید از RFC 9535 پیروی کنند؛ هنگام مهاجرت پروژههای قدیمی، اول نسخه پیادهسازی را تأیید کنید.
۲. برگه تقلب نحو
| نحو | معنا | مثال |
|---|---|---|
$ |
گره ریشه | $.store |
@ |
گره جاری داخل فیلتر | $[?@.price < 10] |
.name |
گره فرزند (نام باید با [A-Za-z_][A-Za-z0-9_]* بخواند) |
$.store.book |
['name'] |
گره فرزند (هر نامی، شامل غیر ASCII و نقطه) | $['store']['书'] |
[0] |
اندیس آرایه؛ عدد منفی از انتها میشمارد | $.book[0]، $.book[-1] |
[0,2] |
چند اندیس / چند نام | $.book[0,2] |
[1:3] |
برش، شروع شامل و پایان انحصاری | $.book[1:3] |
[::2] |
برش با گام | $.book[::2] |
[*] |
نویسه عمومی برای همه گرههای فرزند | $.book[*].title |
..name |
نزول بازگشتی، جستوجو در هر عمق | $..price |
..[*] |
بازگشتی گرفتن همه گرهها | $..* |
[?<expr>] |
انتخابگر فیلتر | $.book[?@.price < 10] |
یک معنا که باید به یاد بسپارید: JSONPath مجموعه گره (nodelist) برمیگرداند، نه یک مقدار واحد. حتی وقتی فقط یک گره تطبیق میکند، نتیجه فهرستی با طول یک است.
۳. مرور نحو با یک نمونه
{
"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
}
| عبارت | نتیجه |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
۴ قیمت (شامل قیمت دوچرخه) |
$.store.book[*].title |
۳ عنوان |
$.store.book[?@.price < 10].title |
عنوان کتابهای ۱ و ۳ |
$.store.book[?@.tags] |
همه کتابهایی که tags دارند (آزمون وجود) |
$.store.book[-1].title |
عنوان آخرین کتاب |
$..book[0,1] |
دو کتاب اول |
$.store.book[?@.price < $.expensive] |
نحواً وابسته به پیادهسازی؛ RFC 9535 ارجاع بینریشهای را مجاز نمیداند |
ردیف آخر تله تاریخی است: بسیاری پیادهسازیها $.expensive را داخل فیلتر برای مقایسه بینگرهی مجاز میدانند، که RFC 9535 صراحتاً ممنوعش میکند —
فیلتر فقط میتواند به گره جاری @ دسترسی داشته باشد. وقتی باید با آستانه سراسری مقایسه کنید، آستانه را بهعنوان آرگومان تابع پرسوجو بدهید.
۴. فیلترها چه چیزی را میتوانند بیان کنند
عبارتهای فیلتر RFC 9535 چهار نوع ساختار را پشتیبانی میکنند:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
چند مثال عملی:
// 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() تطبیق کل رشته است و search() جستوجوی زیررشته؛ گویش regex پایه I-Regexp است (زیرمجموعه RFC 9485)،
که نه backreference پشتیبانی میکند نه lookaround — این هم محدودیت است و هم سپر دستی در برابر DoS regex.
۵. تفاوت پیادهسازیها: پیش از مهاجرت بررسی کنید
| پیادهسازی | زبان | پشتیبانی RFC 9535 | یادداشت |
|---|---|---|---|
jsonpath-plus |
JavaScript | عمدتاً سازگار | توابع افزونه و عبارت اسکریپتی پشتیبانی میکند؛ توانمندتر اما نه سختگیرانه استاندارد |
jsonpath (dchester) |
JavaScript | جزئی | کتابخانه دیرینه با نحو ?() و ساختار نتیجه متفاوت |
ojg / gjson |
Go | ojg پشتیبانی خوبی دارد |
gjson نحو مسیر دیگری استفاده میکند و JSONPath نیست |
jsonpath-ng |
Python | جزئی | افزونهها را پشتیبانی میکند و نسبتاً سهلگیر است |
Jayway JsonPath |
Java | جزئی | اکوسیستم گسترده؛ رفتار تاریخی با RFC متفاوت است |
سه بررسی پیش از مهاجرت:
- آیا پرانتز فیلتر الزامی است؛
- آیا اندیسهای منفی مانند
-1پشتیبانی میشوند؛ - آیا نتیجه «آرایهای از مقدارها» است یا «آرایهای از اشیای
{path, value}».
سومی آسانترین برای زمین خوردن است: فرانتاند result[0] میخواند و پس از تعویض کتابخانه شیء میگیرد و صفحه خالی میماند.
۶. عملکرد و امنیت
عملکرد
- JSONPath ذاتاً یک پیمایش با پیچیدگی O(تعداد گره) است و نمیتواند از ایندکس استفاده کند؛
- نزول بازگشتی با
..گرانترین است؛ اگر میتوانید اول با$.store.bookمکانیابی و بعد فیلتر کنید،$..bookننویسید؛ - عبارتها را یک بار کامپایل و بازاستفاده کنید (بیشتر کتابخانهها
compileیا کش ارائه میدهند) نه اینکه هر پرسوجو دوباره تجزیهشان کنید؛ - برای اسناد با بیش از ۱۰۰,۰۰۰ گره، اول استخراج دقیق با JSON Pointer یا پیشپردازش روی سرور با
jqرا در نظر بگیرید.
امنیت
- عبارتهای حاصل از ورودی کاربر باید محدودیت طول و عمق تودرتویی داشته باشند؛
- نگذارید JSONPath مجوز را مدیریت کند — فقط میتواند بخواند و نمیتواند بیان کند «کدام فیلدها نامرئیاند»؛
- وقتی regex استفادهشده توسط
match()از کاربران میآید، پیچیدگیاش را محدود کنید؛ هرچند از PCRE امنتر است، سقف طول همچنان توصیه میشود.
۷. چه زمانی از JSONPath استفاده نکنیم
- فقط یک فیلد ثابت لازم دارید → مستقیم
data.store.bicycle.color؛ - باید پس از تغییر بنویسید → JSON Pointer یا patch استفاده کنید؛ JSONPath برنمینویسد؛
- تجمیع لازم دارید (جمع، گروهبندی) →
jq، SQL یا کد؛ JSONPath فقط انتخاب میکند؛ - سند عظیم است و فقط چند فیلد لازم دارید → از پارسر جریانی استفاده کنید نه اینکه اول کل درخت را در حافظه بخوانید.
۸. چکلیست
- [ ] عبارتها از RFC 9535 پیروی میکنند و به ارجاع بینریشهای تکیه نمیکنند؛
- [ ] روشن است که نتیجه مجموعه گره است؛ برای یک مقدار از
[0]یاvalue()استفاده میشود؛ - [ ] عبارتها پیشکامپایل و بازاستفاده میشوند؛
- [ ] عبارتهای تحت کنترل کاربر محدودیت طول و پیچیدگی دارند؛
- [ ] هنگام تعویض پیادهسازی، موارد آزمون انطباق رسمی اجرا میشوند.
پرسشهای متداول
چگونه بین JSON Pointer و JSONPath انتخاب کنم؟
وقتی فقط یک فیلد ثابت لازم دارید از دسترسی ساده به ویژگی یا JSON Pointer استفاده کنید؛ وقتی به فیلتر، نویسه عمومی یا نزول بازگشتی نیاز دارید به JSONPath بروید. JSONPath فقط انتخاب میکند و هرگز برنمینویسد، پس نوشتن به JSON Pointer یا patch تعلق دارد.
چرا فیلتر نمیتواند به $.expensive ارجاع دهد؟
RFC 9535 صراحتاً ارجاع بینریشهای را ممنوع میکند؛ فیلتر فقط میتواند به گره جاری @ دسترسی داشته باشد. بهجایش آستانه را آرگومان تابع پرسوجو بدهید. پیادهسازیهای قدیمی نحو را میپذیرند، اما هنگام تعویض یا ارتقای کتابخانه میشکند.
آیا باید برای خواندن مقدار به result[0] تکیه کنم؟
فقط پس از تأیید اینکه پیادهسازی فعلی آرایهای از مقدارها برمیگرداند. برخی کتابخانهها اشیای {path, value} برمیگردانند، پس result[0] شیء میشود و صفحه خالی میماند. مقادیر واحد را از [0] یا value() بخوانید و پیش از تعویض پیادهسازی مجموعه انطباق رسمی را اجرا کنید.