اختيار أداة diff لـ JSON: diff نصي وdiff بنيوي وتمثيل بصري
الخلاصة: يكتشف diff النصي موضع اختلاف الملفات، ويقرر diff البنيوي ما إذا كانت المحتويات متكافئة. اختيار الطبقة الصحيحة يمنع اعتبار ترتيب المفاتيح حادثًا.

الخلاصة: يكتشف diff النصي التغيير (رتّب المفاتيح بـ jq -S أولًا)، ويقرر diff البنيوي التكافؤ (حاذِ العناصر حسب المفتاح الأساسي)، ولا يقدم التمثيل البصري سوى الشرح أو المراجعة؛ أبقِ الطبقات الثلاث منفصلة في CI.
أن يبدو مستندا JSON مختلفين لا يعني أن محتوييهما مختلفان. اختر طبقة المقارنة الصحيحة قبل اختيار الأداة.
1. أنواع "diff" الثلاثة ليست شيئًا واحدًا
تعامل فرق كثيرة "مقارنة JSON" كفعل واحد، ثم تصطدم بالمشكلات نفسها في سياقات مختلفة. من المفيد فصل ثلاث طبقات:
| الطبقة | ما يُقارن | ما تكشفه | الاستخدام النموذجي |
|---|---|---|---|
| diff نصي | البايتات والأسطر الخام | الإزاحة البادئة، ترتيب المفاتيح، المسافات، الأسطر الجديدة | انحراف الإعداد، مراجعة الشيفرة |
| diff بنيوي | الكائنات والمصفوفات المحللة | مفاتيح مضافة أو محذوفة، قيم متغيرة، تغيرات عناصر المصفوفة | انحدار API، ترحيل البيانات |
| diff بصري | كيفية عرض الاختلافات | الطبقة التي ينتمي إليها التغيير، وحجم أثره | المراجعة البشرية، النقاش داخل الفريق |
diff النصي بلا تبعيات وسريع ويتكامل طبيعيًا مع Git. عيبه أنه لا يعرف شيئًا عن دلالات JSON. المستندان المتكافئان تمامًا سيتوهجان بالأحمر والأخضر إن اختلف ترتيب مفاتيحهما أو إزاحتهما. diff البنيوي هو العكس: يحلل أولًا ثم يقارن، فيدرك أن قيمة ما انتقلت فحسب. لكنه يحتاج إلى استراتيجية محاذاة صحيحة للمصفوفات، وإلا قُرئ إدخال واحد على أنه إعادة كتابة كاملة.
التمثيل البصري ليس خوارزمية ثالثة. إنه يعرض مخرجات الطبقتين الأخريين حتى يتأكد الإنسان سريعًا مما إذا كان الاختلاف متوقعًا. إنه مناسب للحكم البشري وسيئ كبديل عن الفحوص الآلية. خلط الطبقات معًا يُنتج عادة خط أنابيب CI أحمر دائمًا وفريقًا تعلم تجاهله.
2. diff النصي: نظّف ترتيب المفاتيح بـ jq -S أولًا
لنظرة سريعة على ملفين، التركيبة الأرخص هي jq -S مع diff -u. يرتب العلم -S مفاتيح الكائنات تكراريًا، فيوحّد المستندات المتطابقة في المحتوى والمختلفة في ترتيب مفاتيحها:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
يزيل هذا أكثر النتائج الإيجابية الزائفة شيوعًا. تعرّف RFC 8259 الكائنات كمجموعات غير مرتبة، لذا فإن {"a":1,"b":2} و{"b":2,"a":1} قيمتان متطابقتان. أما diff النصي فيعلّم كل سطر كمتغير. الترتيب أولًا يجعل الكتل الحمراء أقرب بكثير إلى المشكلات الحقيقية.
لاحظ أن jq -S يرتب المفاتيح فقط؛ ولا يرتب المصفوفات. ترتيب المصفوفة ذو معنى في JSON، وأن [1,2] ليست القيمة نفسها التي [2,1]، لذا لن تتخذ الأداة ذلك القرار نيابة عنك. إن كانت المصفوفة مفهوميًا مجموعة غير مرتبة، فرتبها بمفتاح أساسي بنفسك قبل المقارنة.
هناك تفصيل آخر يجدر تذكره: يخرج diff بالرمز 1 عند إيجاد اختلافات، لكن ملفًا مفقودًا أو خطأ صلاحيات ينتج أيضًا خروجًا غير صفري. في CI، لا تعامل كل رمز غير صفري على أنه "تغيّر المحتوى"، وإلا فسيُبلَّغ عن مسار مكتوب خطأً كتغير في البيانات. النمط الأكثر أمانًا يتحقق من الملفات أولًا ثم يتعامل مع الاختلاف:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON content differs" >&2
exit 1
fi
delta يجعل diff النصي مقروءًا
مخرجات diff -u الخام تعمل، لكن الأسطر الطويلة وJSON المتداخل بعمق مرهقان للقراءة. delta هو مرشّح diff يضيف تلوين الصياغة وأرقام الأسطر وألوان خلفية أوضح:
diff -u a.sorted.json b.sorted.json | delta
بدأ حياته كأداة ترقيم صفحات لـ Git، فيستطيع أيضًا تولي git diff. لمن يقرأ عشرة فروق إعدادات يوميًا، تحسّن أدوات كهذه سرعة القراءة لا الدقة الخوارزمية. خيار شائع آخر هو git diff --no-index --word-diff، الذي يقارن على مستوى الكلمات ويناسب JSON المضغوط المخزّن في أسطر طويلة جدًا.
تحذير واحد: التجميل لا يزيل الفروق الزائفة. إن لم يُطبَّع ترتيب المفاتيح، فسيجعل delta الضجيج أجمل فحسب. الترتيب الصحيح دائمًا هو التطبيع أولًا والتزيين ثانيًا.
3. diff بنيوي: jsondiffpatch يفهم الدلالات
عندما يكون السؤال هو ما إذا كانت قطعتا بيانات متكافئتين دلاليًا، استخدم diff بنيويًا. الخيار الأكثر شيوعًا في منظومة JavaScript هو jsondiffpatch، الذي يعيد كائن delta يصف التغييرات بدلًا من أسطر نصية بعلامتي + و-:
import { create } from 'jsondiffpatch';
const instance = create();
const delta = instance.diff(
{ id: 1, tags: ['a', 'b'], enabled: true },
{ id: 1, tags: ['a', 'c'], enabled: false },
);
console.log(JSON.stringify(delta, null, 2));
يميّز delta المفاتيح المضافة والمحذوفة والقيم المستبدلة وعناصر المصفوفة المنقولة. يعني ذلك أنك تستطيع المرور على delta مباشرة في التحققات: undefined عند عدم وجود اختلاف، أو وصف مسار بمسار عند وجوده. وخلافًا لـ diff النصي، لا تتأثر بالإزاحة البادئة أو ترتيب المفاتيح أو صياغة الأعداد مثل 1e3 مقابل 1000.
وبالقدر نفسه من الأهمية، يمكن إطعام مخرجات diff البنيوي سير عمل التصحيح. يوفر jsondiffpatch الدالتين patch وreverse، فيمكن تطبيق delta على الكائن الأصلي ثم التراجع عنه لاحقًا. تحذير واحد: delta المولّد تلقائيًا ليس بالصيغة نفسها التي لـ JSON Patch المكتوب يدويًا، والتحويل بينهما يتطلب جهدًا. للمقاربة الأشمل للمقارنة، راجع diff ومقارنة JSON.
4. كيف نزيل الفروق الزائفة الناتجة عن ترتيب المفاتيح
ترتيب المفاتيح ليس مجرد إزعاج؛ بل قد ينتج استنتاجات خاطئة. اتبع ثلاث خطوات عند التحقيق:
- قبل المقارنة، طبّع ترتيب المفاتيح تكراريًا بـ
jq -Sأو أداة مكافئة. - أزل الحقول التي تتغير في كل تشغيل، مثل
timestampوrequestIdوtraceId. - تأكد من عدم وجود مفاتيح مكررة. تُحلّ المفاتيح المكررة وفق قاعدة "الأخير يفوز" أثناء التحليل، فيراها diff النصي بينما لا يراها diff البنيوي.
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json
يناسب هذا التدفق انحراف الإعداد جيدًا. مرّر بيئة staging والإنتاج عبر التطبيع نفسه، وكل ما يتبقى هو الاختلاف الذي يحتاج فعلًا إلى تفسير. أبقِ قواعد التصفية في سكربت مُدار بالإصدارات بدلًا من إعادة كتابتها من الذاكرة. للجانب المتعلق بالتحرير والسكربتات في سلسلة الأدوات، راجع المحررات وسطر الأوامر والأتمتة.
5. المصفوفات: محاذاة بالفهرس أم بالمفتاح؟
المصفوفات هي أكثر أجزاء مقارنة JSON عرضة للخطأ. مع المحاذاة الافتراضية بالفهرس، يؤدي إدخال سجل واحد في البداية إلى إزاحة كل عنصر لاحق، فتعلّم الأداة كلها كمتغيرة. الأداة ليست معطوبة؛ استراتيجية المحاذاة هي الخاطئة.
| المحاذاة | تصلح عندما | القوة | الخطر |
|---|---|---|---|
| بالفهرس | ترتيب المصفوفة يحمل معنى تجاريًا | بسيطة، لا تحتاج حقولًا إضافية | إدخال واحد يسبب نتائج زائفة متسلسلة |
| بالمفتاح الأساسي | العناصر مجموعة ذات معرّف فريد | تكشف الإدخال والحذف والنقل | تتطلب مفتاحًا فريدًا مستقرًا |
| أطول تتابع مشترك | الترتيب مهم لكن الإدخالات مسموحة | أكثر متانة من الفهرسة المحضة | أصعب شرحًا وأعلى كلفة |
يستخدم jsondiffpatch الخاصية objectHash للإعلان عن مفتاح أساسي، فيفهرس عناصر المصفوفة قبل المقارنة:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
مع jq العادي، يمكنك الترتيب أولًا ثم استخدام diff نصي:
jq -S 'sort_by(.id)' items.json > items.sorted.json
المقايضة في المحاذاة بالمعرّف هي أن التغيرات الموضعية لم تعد تُحتسب اختلافات. إن كان العمل يحتاج إلى معرفة أن سجلًا انتقل من الموضع 2 إلى الموضع 5، فقارن حقل ترتيب صريحًا أيضًا؛ لا تعتمد على محتوى المجموعة وحده.
6. diff البصري: الطبقة المخصصة للبشر
صُممت مخرجات diff البنيوي للآلات لا للاجتماعات. عندما تحتاج إلى شرح تغيير لشخص آخر، يكون التمثيل البصري أكثر فعالية:
- محرر diff في VS Code مناسب لمراجعة ملف بملف، بلوحتين متجاورتين وتظليل داخلي.
- عارضات الشجرة مثل JSON Crack وJSON Hero تُظهر الطبقة التي ينتمي إليها التغيير.
- يأتي
jsondiffpatchبمُنسّق HTML يعرض delta كعرض غني قابل للطي.
قيمة التمثيل البصري هي السياق. يقول diff النصي إن السطر 88 تغيّر؛ بينما يقول عرض الشجرة إن السطر 88 ينتمي إلى data.items[3].price. لكن التمثيل البصري يحمّل عادة المستند كاملًا في متصفح أو تطبيق سطح مكتب. قبل فتح حمولة إنتاج، تأكد مما إذا كانت الأداة تحسب محليًا أم ترفع إلى خادم.
7. ثلاث ممارسات لإدخال diff في CI
أولًا، طبّع قبل المقارنة. ثبّت الإزاحة البادئة، ورتّب المفاتيح تكراريًا، ورشّح الحقول المتقلبة قبل تشغيل diff. وإلا فسيفشل CI مرارًا بسبب الضجيج.
ثانيًا، اختر المحاذاة وفق الدلالات. إن كانت المصفوفة مجموعة، حاذِ بالمفتاح الأساسي. وإن كانت تسلسلًا، فأبقِ المحاذاة بالفهرس. لا تطبق قاعدة واحدة على كل API.
ثالثًا، تحقق من القيم لا من النص. مقارنة الكائنات المحللة في الاختبارات أكثر استقرارًا بكثير من مقارنة مخرجات JSON.stringify. عندما يفشل فحص، اطبع مسار الاختلاف والقيم قبل وبعد بدلًا من إغراق السجل بالحمولة كاملة.
# اطبع المسارات والقيم المختلفة فقط، لا المستند كله
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80
إن احتاج اختلاف إلى تتبع طويل الأمد، أودع سكربت التطبيع والملف المتوقع معًا. وإن كان تحقيقًا لمرة واحدة، فاحذف السكربت المؤقت لاحقًا حتى لا يصبح بهدوء تنفيذًا ثانيًا لمنطق العمل.
8. جدول اختيار الأداة
| السياق | الخيار الأول | البديل | تذكير أساسي |
|---|---|---|---|
| مقارنة ملفات عابرة | diff في VS Code | diff -u |
نسّق أولًا لإزالة الضجيج |
| انحراف الإعداد | jq -S + diff |
delta |
رشّح الحقول المتقلبة |
| انحدار استجابة API | jsondiffpatch |
deepdiff |
حاذِ المصفوفات بالمفتاح الأساسي |
| شرح تغيير للفريق | JSON Crack / JSON Hero | مُنسّق HTML | تحقق مما إذا كانت البيانات تُرفع |
| ملفات كبيرة جدًا | استخراج شجرة فرعية بـ jq |
تصفح بـ jless |
لا تحمّل الشجرة كاملة في الذاكرة |
| فحص قبل الإيداع | git diff --no-index |
pre-commit | تعامل مع رموز الخروج صراحة |
لا توجد أداة diff لـ JSON هي الأفضل مطلقًا؛ بل الأداة الصحيحة للطبقة الحالية. يكتشف diff النصي التغيير، ويحكم diff البنيوي على التكافؤ، ويشرح التمثيل البصري التغيير. إبقاء الطبقات الثلاث منفصلة أكثر موثوقية بكثير من البحث عن أداة واحدة تفعل كل شيء. لمعالجة التعارضات بين الفروع، تابع JSON والدمج في Git.
الأسئلة الشائعة
هل يغيّر jq -S ترتيب المصفوفات أيضًا؟
لا. يرتب -S مفاتيح الكائنات فقط ويترك المصفوفات بترتيبها الأصلي. المصفوفات مرتبة في JSON، لذا لن تحوّل الأداة [1,2] إلى [2,1]. إن كانت المصفوفة مفهوميًا مجموعة غير مرتبة، فرتبها صراحة بـ sort_by.
هل 1 و1.0 القيمة نفسها في diff بنيوي؟
تحوّل معظم المحللات كليهما إلى العدد نفسه، لذا يعدهما diff البنيوي متساويين عادة، بينما يعلّمهما diff النصي مختلفين. استخدم diff البنيوي للحكم على التكافؤ، وdiff النصي للحكم على ما إذا كانت الملفات متطابقة بايتًا ببايت.
هل يمكن استخدام مخرجات diff مباشرة كـ JSON Patch؟
ليس مباشرة. فأداة diff تبلّغ عما يختلف، بينما يصف JSON Patch قائمة عمليات مرتبة؛ تختلف الصيغ والدلالات. كما تميل التصحيحات المولّدة تلقائيًا إلى الإشارة إلى فهارس مصفوفات تصبح قديمة عند التزامن. أضف عملية test قبل تطبيقها.
كيف أقارن ملفات JSON بحجم عشرات الميغابايت؟
استخرج الشجرة الفرعية التي تهمك أولًا، مثلًا بـ jq '.data.items'، ثم رتّب بالمفتاح الأساسي وقارن. لا تحمّل الشجرة كاملة في الذاكرة من أجل diff تكراري. الملفات الكبيرة جدًا أفضل معالجةً بالبث أو المقارنة المجزأة، وتحتاج كلفة الذاكرة إلى تقييم منفصل.
هل يمكن لأدوات diff عبر الإنترنت تسريب بياناتي؟
يعتمد ذلك على التنفيذ. أدوات الواجهة الأمامية المحضة تحسب في المتصفح ولا ترفع البيانات؛ أما الأدوات المقدَّمة من الخادم فترسل المحتوى إلى خلفية قد يدخل إليها السجلات. قبل مقارنة إعدادات إنتاج أو حمولات عملاء، تأكد من أن الصفحة تحسب محليًا، واختبر مرة واحدة مع قطع الشبكة إن لزم.
كيف أختار بين diff نصي وdiff بنيوي؟
لنظرة سريعة على مواضع اختلاف ملفين، رتّب المفاتيح بـ jq -S وشغّل diff -u. وللحكم على ما إذا كان مستندان متكافئين دلاليًا، استخدم diff بنيويًا مثل jsondiffpatch وحاذِ المصفوفات بالمفتاح الأساسي. diff النصي بلا تبعيات ويتكامل مع Git، بينما لا يتأثر diff البنيوي بالإزاحة البادئة أو ترتيب المفاتيح أو صياغة الأعداد.
لماذا يجب ألا يعامل CI كل رمز خروج diff غير صفري كاختلاف؟
يخرج diff بالرمز 1 عند إيجاد اختلافات، لكن ملفًا مفقودًا أو خطأ صلاحيات يعيد أيضًا رمزًا غير صفري. معاملة كل رمز غير صفري على أنه "تغيّر المحتوى" تُبلّغ عن مسار مكتوب خطأً كتغير في البيانات. النمط الأكثر أمانًا يتحقق من الملفات أولًا ثم يتعامل مع الاختلاف منفصلًا.
هل أقارن ملفات بحجم عشرات الميغابايت في المتصفح؟
لا. استخرج الشجرة الفرعية التي تهمك بـ jq أولًا، ثم رتّب بالمفتاح الأساسي وقارن. تحميل الشجرة كاملة في الذاكرة من أجل diff تكراري يكلف ذاكرة ووقتًا أكبر بكثير. الملفات الكبيرة جدًا أفضل معالجةً بالبث أو المقارنة المجزأة.