JMESPath للمبتدئين: استعلامات إسقاط تتفوق على JSONPath
الخلاصة: يعيد JMESPath تشكيل دفعات الكائنات بشكل أكثر مباشرة من JSONPath. يغطي هذا الدليل التمهيدي الصياغة، والإسقاطات، والأنابيب، والدوال المدمجة، واستخدام AWS CLI.

الخلاصة: استخدم JSON Pointer للتحديد حسب الموضع، وJSONPath للمرشحات أو العودية، وJMESPath عندما تحتاج إلى إعادة تشكيل النتيجة أو إسقاطها على سطر الأوامر؛ أما الحقل الواحد المعروف فلا يحتاج إلى أيٍّ منهما.
إذا كان هدفك إعادة تشكيل دفعة من الكائنات إلى شكل مختلف، فإن JMESPath غالبًا أكثر مباشرة من JSONPath. فهو لا يبحث عن عقد؛ بل يبني نتيجة.
1. ما هو JMESPath
JMESPath هي لغة استعلام مصممة خصيصًا لـ JSON. أُنشئت في الأصل لتصفية مخرجات AWS CLI، ثم انتشرت بعد ذلك إلى لغات كثيرة عبر تطبيقات مثل jmespath.js وjmespath.py وgo-jmespath. ويختلف هدفها عن JSONPath:
- JSONPath تركّز على اختيار مجموعة من العقد بالمسار والشرط، وتعيد مجموعة عقد؛
- JMESPath تركّز على تحويل JSON المُدخل، وتعيد قيمة بأي شكل.
هذا الاختلاف يجعل JMESPath جيدة بشكل خاص في إسقاطات القوائم، وإعادة تسمية الحقول، وتقليم النتائج، مثل تقليص عشرين حقلًا من استجابة API إلى الحقول الثلاثة التي تحتاجها الواجهة فعليًا. وإذا لم تكن قد قرأت أساسيات JSONPath بعد، فابدأ بـ لغة استعلام JSONPath في التطبيق العملي.
2. الصياغة الأساسية: من المعرّفات إلى الفهارس
تستخدم JMESPath رموزًا أقل من JSONPath، وتُقرأ أقرب إلى تعبير استخراج:
| الصياغة | المعنى | المثال |
|---|---|---|
name |
الوصول إلى عضو في كائن | people |
a.b |
الوصول إلى أعضاء متداخلة | store.bicycle.color |
[0] |
الوصول إلى فهرس في مصفوفة | people[0] |
[-1] |
الوصول إلى العنصر الأخير | people[-1] |
[0:2] |
شريحة، بدايتها مشمولة ونهايتها غير مشمولة | people[0:2] |
[*] |
إسقاط قائمة | people[*].name |
[] |
إسقاط تسطيح | people[].tags[] |
[?condition] |
إسقاط مرشح | people[?age > 18] |
{key: expr} |
تجزئة متعددة الاختيار | {name: name, age: age} |
[expr, expr] |
قائمة متعددة الاختيار | [name, age] |
| |
تمرير النتيجة اليسرى إلى الجانب الأيمن | people[*].age | [0] |
مثال بسيط:
{
"people": [
{ "name": "Alice", "age": 31, "city": "Shanghai" },
{ "name": "Bob", "age": 17, "city": "Beijing" }
]
}
people[0].name → "Alice"
people[-1].city → "Beijing"
people[0:2].name → ["Alice", "Bob"]
لاحظ لماذا يعيد people[0:2].name مصفوفة: الشريحة تعيد قائمة، والـ .name التالي يُسقَط تلقائيًا على كل عنصر في تلك القائمة. هذا هو أكبر فرق بين JMESPath والوصول العادي إلى الكائنات.
3. الإسقاطات: تحويل قائمة إلى قائمة أخرى
الإسقاط هو المفهوم المحوري في JMESPath. ويعمل في خطوتين: تقييم كل عنصر في القائمة، ثم تجميع النتائج في قائمة جديدة. والعناصر التي تُقيَّم إلى null تُسقَط تلقائيًا.
people[*].name // ["Alice", "Bob"]
people[*].{name: name, age: age} // قائمة كائنات
people[?age > 18].name // ["Alice"]
السطر الثاني تعبير إسقاط نمطي: people[*].{name: name, age: age}. وهو يعيد بناء كل عنصر على صورة { "name": ..., "age": ... } ويستبعد الحقل الإضافي city. وبالمقارنة مع JSONPath، التي تمنحك عقدًا تقوم أنت بتحويلها في الشيفرة، فإن هذا يوفّر عليك حلقة تكرار.
يجدر التمييز بين نوعين من الإسقاطات:
[*]يحافظ على بنية القائمة الأصلية ويتخطى القيم الفارغة؛[]إسقاط تسطيح يدمج أولًا مستوى واحدًا من القوائم المتداخلة، وهو مناسب لتعبيرات مثلpeople[].tags[]التي تحتاج إلى "كل الوسوم لكل الأشخاص".
4. الأنابيب والاختيار المتعدد
الأنبوب | يعني "أوقف الإسقاط الحالي وتعامل مع النتيجة كمدخل جديد". وأكثر استخداماته شيوعًا هو أخذ فهرس بعد إسقاط:
people[*].name | [0] → "Alice"
people[*].age | sort(@) | [-1] → أقصى عمر
بدون الأنبوب، يُفسَّر people[*].name[0] على أنه أخذ الحرف الأول من كل اسم، وهذا ليس ما تريده. والأنبوب هو الرمز الأكثر عرضة للنسيان والأكثر نفعًا في JMESPath.
وللاختيار المتعدد صيغتان:
// تجزئة متعددة الاختيار: النتيجة كائن
people[0].{name: name, age: age}
// قائمة متعددة الاختيار: النتيجة مصفوفة
people[0].[name, age]
تجزئات الاختيار المتعدد جيدة لبناء نتائج مسماة، أما قوائم الاختيار المتعدد فجيدة للاستهلاك الموضعي. ويمكن تداخل كليهما داخل الإسقاطات، لكن لاحظ: في تجزئة الاختيار المتعدد، يبقى المفتاح الذي قيمته null، بينما يُسقَط الإسقاط العنصر null بأكمله.
5. الدوال المدمجة
تعريف معيار JMESPath مجموعة من الدوال. ومن الشائعة:
| الدالة | الغرض | المثال |
|---|---|---|
length() |
الحصول على الطول | length(people) |
sort() |
ترتيب القيم | sort(people[*].age) |
sort_by() |
الترتيب حسب حقل | sort_by(people, &age) |
max() / min() |
الحد الأقصى / الأدنى | max(people[*].age) |
sum() |
جمع القيم | sum(people[*].age) |
keys() |
الحصول على مفاتيح الكائن | keys(people[0]) |
contains() |
التحقق من الاحتواء | contains(name, "Ali") |
to_string() |
التحويل إلى سلسلة | to_string(age) |
الوسيط الثاني لـ sort_by تعبير &، ويعني "خُذ هذا الحقل من كل عنصر":
sort_by(people, &age)[*].name
تتيح هذه الدوال لـ JMESPath إجراء تجميع بسيط، بينما صُممت JSONPath للاختيار فقط؛ فالجمع والتجميع يتطلبان عادة jq أو شيفرة.
6. مقارنة جانبية مع JSONPath
| البُعد | JSONPath | JMESPath |
|---|---|---|
| الهدف الأساسي | اختيار العقد | تحويل البيانات |
| رمز الجذر | $ |
لا يوجد؛ تُكتب أسماء الحقول مباشرة |
| العقدة الحالية | @ |
@ (في وسائط الدوال فقط) |
| المرشح | [?(@.age > 18)] |
[?age > 18] |
| حرف البدل | [*] |
[*] أو [] |
| بناء كائنات جديدة | غير مدعوم | {name: name} |
| الأنابيب | غير مدعومة | | |
| المعيار | RFC 9535 | مواصفة JMESPath |
دليل قرار في جملة واحدة:
- استخدم JSONPath عندما تحتاج فقط إلى العثور على عقد تطابق شرطًا؛
- استخدم JMESPath عندما تحتاج إلى إعادة تشكيل النتيجة إلى ما تتوقعه الواجهة؛
- لا تستخدم أيًّا منهما عندما تقرأ حقلًا واحدًا معروفًا؛ فاكتفِ بالوصول المباشر إلى الخاصية.
إذا كان يجب كتابة نتيجة الاستعلام إلى المستند، فلا اللغة هذه ولا تلك مناسبة. استخدم JSON Pointer والتصحيحات بدلًا منهما؛ انظر JSON Pointer وPatch وMerge Patch.
7. JMESPath في AWS CLI
المعامل --query في AWS CLI هو JMESPath، وهو أكثر حالات استخدامه انتشارًا:
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{Id: InstanceId, State: State.Name, Type: InstanceType}' \
--output table
يفعل هذا الأمر ثلاثة أشياء: يسهّل قائمتين متداخلتين بـ []، ويحتفظ بثلاثة حقول فقط عبر تجزئة متعددة الاختيار، ويعرض النتيجة كجدول بـ --output table. أما مع JSONPath فستحتاج إلى مستويات أكثر ولن تحصل على تأثير "إعادة التسمية مع التقليم".
صيغ شائعة أخرى:
# النسخ قيد التشغيل فقط
aws ec2 describe-instances --query 'Reservations[].Instances[?State.Name==`running`].[InstanceId,PublicIpAddress]'
# قيمة الوسم Name
aws ec2 describe-instances --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value[]'
لاحظ أن العلامات الخلفية (backticks) تُحيط بالحرفيات لا بالسلاسل. ففي AWS CLI تُستخدم علامات الاقتباس المفردة للسلاسل والعلامات الخلفية لحرفيات JSON. وهذا الفرق من أكثر أخطاء المبتدئين شيوعًا.
8. الأخطاء الشائعة
- الخلط بين
[*]و[]: الأول يحافظ على التداخل، والثاني يسهّل مستوى واحدًا، والبنيتان الناتجتان مختلفتان تمامًا؛ - نسيان الأنبوب:
people[*].name[0]يأخذ الحرف الأول من كل اسم، لا الاسم الأول؛ - كتابة
@داخل مرشح: تستخدم مرشحات JMESPath أسماء الحقول مباشرة، لذا[?@.age > 18]خطأ؛ - التعامل مع
nullكسلسلة فارغة: الإسقاطات تُسقِطnull، لكن تجزئات الاختيار المتعدد تحتفظ بالمفتاح؛ - استخدام علامات اقتباس مزدوجة حول الحرفيات في AWS CLI: يقوم الـ shell بتوسيعها أولًا فيُعيد كتابة التعبير.
نصيحة للتصحيح: تحقق من التعبير على أصغر مُدخل ممكن قبل تطبيقه على بيانات حقيقية، وأضف --output json في AWS CLI لرؤية النتائج الخام. وللتحرير والتنسيق اليومي لـ JSON، فإن الاقتران بـ محررات JSON وأدوات سطر الأوامر يجعل تحديد مسارات الحقول أسرع بكثير.
الأسئلة الشائعة
هل يمكن استخدام JMESPath وJSONPath معًا؟
نعم، لكنه عادة غير ضروري. والنهج الشائع هو تقليم كائن صغير بـ JMESPath، ثم استخدام الوصول العادي إلى الخصائص في الشيفرة. وإذا كنت حقًا تحتاج إلى التحديد بالمسار ثم التعديل، فإن JSONPath للقراءة وJSON Pointer للكتابة، وهو تقسيم أوضح للأدوار.
لماذا يعيد people[*].name شيئًا غير سلسلة واحدة؟
لأن [*] إسقاط، والنتيجة دائمًا قائمة، حتى عندما يكون هناك شخص واحد فقط. وللحصول على قيمة واحدة، أضف أنبوبًا وفهرسًا: people[*].name | [0].
ما الذي يحدث لـ null داخل إسقاط؟
يتخطى إسقاط القائمة تلقائيًا العناصر التي تُقيَّم إلى null، لذا قد تكون النتيجة أقصر من القائمة الأصلية. أما تجزئة الاختيار المتعدد فتحتفظ بالمفتاح وتحافظ على قيمة null، وهذا مهم عند بناء استجابات API.
هل تستطيع JMESPath إجراء التجميع والتجميع المجموعي؟
تستطيع إجراء تجميع أساسي بـ sum() وmax() وsort_by(). أما التجميع المجموعي والربط ودوال النوافذ فخارج المعيار. اترك التحليل المعقد لـ jq أو SQL أو الشيفرة.
كيف أقرر بين JMESPath وJSONPath؟
استخدم JSONPath عندما تحتاج إلى اختيار العقد بالمسار والشرط وتستقبل مجموعة عقد؛ واستخدم JMESPath عندما تحتاج إلى إعادة تشكيل النتيجة إلى ما تتوقعه الواجهة، مع إعادة تسمية الحقول وتقليمها. وعندما تقرأ حقلًا واحدًا معروفًا فلا تستخدم أيًّا منهما؛ فاكتفِ بالوصول إلى الخاصية.
هل يجب أن أستخدم أنبوبًا لأخذ أول قيمة مُسقَطة؟
نعم. people[*].name إسقاط، لذا فالنتيجة دائمًا قائمة؛ وبدون أنبوب يأخذ people[*].name[0] الحرف الأول من كل اسم. اكتب people[*].name | [0] للحصول على القيمة الأولى.