مرشحات JSONPath بعمق: العودية والشرائح والمطابقة متعددة الشروط
الخلاصة: النزول العودي والشرائح والمرشحات متعددة الشروط هي مواضع قوة JSONPath وسوء استخدامها. يغطي المقال دلالات المرشحات وأشكال النتائج وفروق المكتبات.

الخلاصة: حدّد عمقًا معروفًا أولًا، ثم أضف الشرائح والمرشحات بدلًا من التوسل بـ $..؛ فالمسار المجرد في المرشح يفحص الوجود فقط، لذا اختبر قيمة منطقية بـ @.stock == true.
يستطيع أي شخص كتابة
$.store.book[0].title. لكن ما يكسر JSONPath فعلًا هو الجمع بين$..والشرائح والشروط المتعددة، ثم الحصول على نتائج أكثر من اللازم، أو أقل من اللازم، أو بترتيب لم تتوقعه.
1. مستند نموذجي
{
"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"
}
كل تعبير أدناه يستخدم هذا المستند كمدخل. وتذكّر مقدّمتين: النتيجة مجموعة عقد، وترتيبها يتبع ترتيب المستند لا ترتيب كتابة الشروط. وداخل المرشح، يشير @ إلى العقدة المرشحة الحالية فقط.
2. النزول العودي $..: مريح لكنه مكلف
يعني .. "ابحث عوديًا على أي عمق من الموضع الحالي". ويعرّفه RFC 9535 كمقطع سليل، ويمكن دمجه مع الأسماء وأحرف البدل والفهارس والشرائح والمرشحات:
$..price // كل عضو اسمه price على أي عمق
$..book[?(@.price < 10)] // أي book على أي عمق، ثم الترشيح
$..[?(@.stock)] // أي عقدة على أي عمق يوجد فيها stock
ثلاثة أمور يجب فهمها:
$..bookيجد كل عقدة اسمهاbook، بصرف النظر عن عمقها؛- الاجتياز بالعمق أولًا، وترتيب الخروج يعتمد على كيفية زيارة التطبيق للأبناء؛
- لا يستطيع
..استخدام أي فهرس، لذا فتكلفته تتناسب مع العدد الكلي للعقد.
ونتيجة لذلك، قد يختلف $..price و$.store.book[*].price بسعر الدراجة فقط في مخرجهما، ومع ذلك يختلفان بمقدار رتبة في التكلفة. وعندما تعرف العمق، اكتب المسار الصريح دائمًا. وإذا كنت تريد فقط مقارنة حدود لغتي المسار، فانظر JSONPath مقابل JSON Pointer.
3. شرائح المصفوفات وأحرف البدل: ثلاثة فخاخ
صياغة الشريحة هي [start:end:step]، وتشمل البداية وتستبعد النهاية، والأرقام السالبة تُعد من النهاية:
| التعبير | المعنى | النتيجة (مع أربعة كتب) |
|---|---|---|
[0:2] |
أول كتابين | الكتابان 1 و2 |
[1:] |
من الكتاب الثاني إلى النهاية | الكتب 2 و3 و4 |
[:2] |
أول كتابين | الكتابان 1 و2 |
[-2:] |
آخر كتابين | الكتابان 3 و4 |
[::2] |
كل كتاب ثانٍ | الكتابان 1 و3 |
[::-1] |
ترتيب معكوس | الكتب 4 و3 و2 و1 |
ثلاثة فخاخ:
- الخطوة الصفرية غير صالحة. يشترط RFC 9535 خطوة غير صفرية، بينما تعيد بعض التطبيقات مجموعة فارغة بصمت؛
- الخطوات السالبة غير مدعومة عالميًا. يدعمها تطبيق Goessner الأصلي، لكن بعض التطبيقات المصغّرة تقبل الخطوات الموجبة فقط؛
- الترشيح بعد شريحة لا يوسّع النطاق.
[0:2][?(@.stock)]يرشّح فقط الكتابين الناتجين عن الشريحة؛ ولا يعود إلى المصفوفة بأكملها.
يمكن دمج حرف البدل [*] مع الشرائح، لكن [*] يعيد "كل العقد الفرعية". فإذا كانت تلك الأبناء كائنات، فإن إلحاق .title يعطي كل العناوين. وإذا كان أحد الأبناء مصفوفة بنفسه، فلن يسهّل .title مستوى آخر ضمنًا. اكتب [*] مرة أخرى عندما تحتاج إلى التوسيع؛ ولا تتوقع تسطيحًا ضمنيًا.
4. مرشحات متعددة الشروط: المقارنات والمنطق والوجود
يمكن أن يحتوي المرشح [?(...)] على أربعة أنواع من التراكيب: المقارنات، والمعاملات المنطقية، وفحوص الوجود، والدوال. ويبدو استعلام متعدد الشروط نمطي هكذا:
$..book[?(@.price < 10 && @.stock)]
ويعني "السعر أقل من 10 و المخزون صحيح". وانتبه إلى عدة تفاصيل دلالية:
@.stockفحص وجود، لا "القيمة صحيحة". فإذا كان الحقل موجودًا وقيمتهfalse، فإن الشرط يظل قائمًا. ولاستخدام قيمة منطقية يجب كتابة@.stock == true؛&&أقوى ارتباطًا من||. أضف أقواسًا عند خلطهما بدلًا من الاعتماد على الذاكرة؛- يجب أن يكون طرفا المقارنة من النوع نفسه. ولا تُحوَّل الأرقام والسلاسل ضمنيًا؛ فـ
@.price < "10"خطأ أو خطأ تنفيذي في التطبيقات الصارمة؛ - الحقل المفقود يجعل المقارنة خطأ دائمًا. فـ
@.missing < 10لا يرفع خطأً؛ بل لا يطابق شيئًا ببساطة.
هذا أسهل موضع للتعثر عند كتابة المرشحات كما لو كانت JavaScript: فـif (obj.stock) يفحص صحة القيمة في JavaScript، بينما JSONPath يفحص الوجود. ولمعرفة الصياغة المعيارية الكاملة والدوال، راجع لغة استعلام JSONPath في التطبيق العملي.
5. تعبيرات السكربت: قدرة أكبر وقابلية نقل أقل
يسمح تطبيق Goessner الأصلي وjsonpath-plus بمقارنات عبر العقد مثل [?(@.price < $.max)]، بل وبسكربتات اعتباطية. أما RFC 9535 فيصرّح بالوصول إلى العقدة الحالية @ فقط ويمنع $ داخل المرشح.
// يعمل في التطبيقات الأقدم؛ ويمنعه RFC 9535
$..book[?(@.price < $.expensive)]
// النهج المعياري: مرّر الحد من المستدعي
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
تخلق تعبيرات السكربت ثلاث مشكلات:
- قابلية النقل تنعدم. ويعني تبديل المكتبة إعادة كتابة كل تعبير؛
- حد الأمان يختفي. فعندما تأتي التعبيرات من المستخدمين، يسلّم تقييم السكربت تنفيذ الشيفرة؛
- التحسين الساكن يصبح مستحيلًا. فلا يستطيع التطبيق إلا التفسير عقدة بعقدة، وهو بطيء وصعب التخزين المؤقت.
إذا كنت حقًا تحتاج إلى مقارنة عبر العقد، فمرّر الحد كمعامل استعلام ورشّح مرة ثانية في شيفرة التطبيق. وهذا عادة أكثر استقرارًا من الاعتماد على صياغة امتدادية.
6. أشكال النتائج: تظن أنك تحصل على قيم، لكنك تحصل على عقد
قد يعيد التعبير نفسه ثلاثة أشكال على الأقل عبر المكتبات:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // مصفوفة قيم
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // مسار + قيمة
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + قيمة
يؤثر هذا في ثلاثة أمور مباشرة: كيفية قراءة الواجهة للنتائج، وكيفية إزالة التكرار منها، وكيف تتحول نتائج الاستعلام إلى عمليات كتابة. فقائمة العقد المعرّفة في RFC 9535 هي مجموعة مراجع عقد، لكن تطبيقات JavaScript تعيد القيم فقط عادة لأن المراجع لا تعبر حدود العمليات.
نصيحة عملية:
- اقرأ دائمًا قيمة واحدة عبر
[0]أوvalue()؛ ولا تفترض أبدًا أن النتيجة قيمة مفردة؛ - عندما تحتاج إلى الكتابة إلى الخلف، اختر تطبيقًا يعيد JSON Pointers أو ابنِ المؤشر بنفسك؛
- النتيجة الفارغة ونتيجة
[null]مختلفتان؛ افحص الطول بدلًا من صحة القيمة.
7. فروق المكتبات: شغّل مجموعة الاختبار المطابقة قبل الترحيل
| القدرة | تطبيق Goessner الأصلي | jsonpath-plus | RFC 9535 الصارم |
|---|---|---|---|
أقواس المرشح ?() |
مطلوبة | متوافقة | اختيارية |
مرجع عبر الجذر $.x |
مدعوم | مدعوم | ممنوع |
| تعبيرات السكربت | مدعومة | مدعومة | ممنوعة |
| الفهارس والخطوات السالبة | مدعومة | مدعومة | مدعومة |
| إعادة المسارات | قابلة للتهيئة | قابلة للتهيئة | غير محددة |
دوال مثل length() |
لا شيء | امتدادات | دوال معيارية |
قبل الترحيل، تحقّق من أربعة أمور على الأقل: هل الأقواس مطلوبة، وهل يجوز ظهور $ داخل المرشح، وكيف تتصرف الخطوات السالبة، وبنية النتيجة. وتغطي مجموعة اختبار المطابقة الرسمية معظم فروق الصياغة، وتمنع أعطالًا صامتة مثل "اختفى ثلاثة سطور بعد تبديل المكتبة".
8. الأداء وقائمة سوء الاستخدام
يتلخص الأداء في جملة واحدة: JSONPath اجتياز كامل، و.. أغلى أنواع الاجتياز الكامل.
- للمستندات التي تزيد عقدها على 100000، ضيّق النطاق بمسار صريح أولًا، ثم رشّح المجموعة الفرعية؛
- اترجم التعبير مرة واحدة وأعد استخدامه؛ ولا تُعد تحليل السلسلة داخل حلقة تكرار؛
- انتقل إلى
jqأو الشيفرة للتجميع (الجمع، والتجميع المجموعي، والترتيب)؛ فـ JSONPath يختار فقط؛ - ضع الشروط في أقدم موضع ممكن: فـ
$.store.book[?(@.price < 10)]يمسح عقدًا غير ذات صلة أقل بكثير من$..[?(@.price < 10)].
قائمة سوء الاستخدام:
- [ ] التعامل مع فحص الوجود كاختبار لقيمة منطقية؛
- [ ] استخدام
$..بدل عمق معروف؛ - [ ] الاعتماد على تعبيرات السكربت للمقارنة عبر العقد؛
- [ ] افتراض أن النتيجة دائمًا مصفوفة قيم؛
- [ ] السماح بتعبيرات يتحكم فيها المستخدم دون حد للطول والتداخل.
الأسئلة الشائعة
لماذا يظل @.stock مطابقًا عندما تكون قيمة الحقل false؟
لأن المسار المجرد داخل المرشح يعني "الحقل موجود"، لا "القيمة صحيحة". ولاستخدام القيمة المنطقية قارن صراحةً بـ @.stock == true. وهذا هو أكبر فرق دلالي بين JSONPath وصحة القيم في JavaScript.
هل ترتيب النزول العودي مستقر؟
لا. يحدد RFC 9535 دلالات مجموعة العقد، ويعتمد ترتيب الاجتياز على كيفية زيارة التطبيق للأبناء. وعندما تحتاج إلى ترتيب ثابت، حدّد المصفوفة واقرأ بالفهرس، أو رتّب النتائج في شيفرة التطبيق.
هل يمكن دمج الشرائح والمرشحات؟
نعم، لكن تذكّر النطاق. فـ$.store.book[0:2][?(@.stock)] يرشّح الكتابين الناتجين عن الشريحة فقط. وللترشيح أولًا ثم التقطيع، اكتب $.store.book[?(@.stock)][0:2]؛ فالترتيب المختلف يعطي نتيجة مختلفة.
لماذا يدعم jsonpath-plus السلسلة $.expensive بينما يمنعها المعيار؟
لأن المرشحات معرّفة بدلالة العقدة الحالية @ فقط. فالمرجع عبر الجذر يجعل التعبير معتمدًا على سياق المستند بأكمله، وهو صعب التحليل ساكنًا والنقل بين اللغات. مرّر الحد كمعامل بدلًا من تضمينه في التعبير.
كيف أمنع النزول العودي من مسح عقد غير ذات صلة؟
اكتب المسار الصريح كلما عرفت العمق، وضيّق النطاق أولًا، ورشّح المجموعة الفرعية فقط بدلًا من استخدام $.. بديلًا عن مستوى معروف. وضع الشروط في أقدم موضع ممكن: فـ$.store.book[?(@.price < 10)] يمسح عقدًا غير ذات صلة أقل بكثير من $..[?(@.price < 10)]. وللمستندات التي تزيد عقدها على 100000، افعل ذلك أولًا.