💡 مثال كامل يعمل متوفر على GitHub:
office-metadata-pii-cleanup-nodejs

المقدمة

نقطة تحميل تستقبل ملف DOCX من أحد الموظفين وتخزنه مع تذكرة العميل. النص لا مشكلة فيه. الخصائص ليست كذلك: الملف يحتوي على اسم الشخص الذي أنشأه، والزميل الذي حفظه آخر مرة، ومدير القسم من القالب المؤسسي، وبما أنه جاء من SharePoint، فإن المُعتمد الذي وقع عليه موجود أيضًا.

منظف البيانات الوصفية هو سكريبت صغير يحذف تلك الخصائص قبل تخزين الملف ثم يتحقق من عمله. يبني هذا الدرس سكريبت في Node.js باستخدام GroupDocs.Metadata، في أربع خطوات: اختيار الخصائص حسب الوسم، اختيارها حسب الاسم، مسح كل شيء عندما تتوقف الانتقائية عن الفائدة، والتحقق مما تبقى. كل خطوة تتكون من بضع أسطر، والسكريبت النهائي أقل من مئة سطر.

لماذا تُعد تنقية البيانات الوصفية مهمة

تتراكم البيانات دون أن يختارها أحد. Word يكتب Author و LastSavedBy من حساب نظام التشغيل في كل حفظ، يحتفظ بعداد مراجعات، يتتبع TotalEditingTime، ويسجل LastPrinted. خوادم المستندات تضيف مسارات سير العمل، معرفات المُعتمد، وعناوين URI لنوع المحتوى عند الفحص. لا يظهر أي من هذا عند قراءة أو طباعة المستند، لذا لا يلتقطه المراجعة اللغوية.

الهدف من القيام بذلك في Node.js بدلاً من يدويًا هو أن السكريبت يُعيد أرقامًا: كل استدعاء حذف يُبلغ عن عدد الخصائص التي حُذفت، ويمكن كتابة هذا العدد إلى سجل، أو التحقق منه في اختبار، أو إرفاقه بالسجل الذي ينتمي إليه المستند.

هناك سبب ثانٍ، أقل وضوحًا حتى يبدأ تشغيل مهمة دفعة. التنظيف اليدوي هو قرار يُتخذ مرة واحدة لكل ملف من قبل من يتعامل معه، لذا قد ينتج عن شخصين ينظفان نفس نوع المستند نتائج مختلفة. السكريبت يثبت القاعدة في مكان واحد: نفس الوسوم الأربعة، نفس قوائم السلاسل الجزئية، تُطبق بنفس الطريقة سواء كان الطابور يحتوي على ثلاثة ملفات أو ثلاثة آلاف.

المتطلبات المسبقة

الحزمة هي Node.js عبر Java، لذا يحتاج الجهاز إلى بيئة تشغيل Java إلى جانب Node.

التثبيت

npm install @groupdocs/groupdocs.metadata

يحدد مشروع العينة الإصدار 26.7 ويضيف إدخال overrides يضبط nan إلى ^2.22.0، مما يحافظ على بناء الربط الأصلي على إصدارات Node الحالية. بدون ملف ترخيص تعمل المكتبة في وضع التقييم، وهو كافٍ لتتبع كل خطوة هنا.

الخطوة 1 – اختيار الخصائص حسب ما تعنيه

تختلف أسماء الخصائص بين الصيغ والحزم، لذا القاعدة الأولى تطابق على الوسوم بدلاً من ذلك. ContainsTagSpecification يأخذ وسمًا ويطابق أي خاصية تحمله؛ .or() يدمج المواصفات في واحدة.

const T = groupdocs.Tags;
const spec = new groupdocs.ContainsTagSpecification(T.getPerson().getCreator())
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getEditor()))
  .or(new groupdocs.ContainsTagSpecification(T.getPerson().getManager()))
  .or(new groupdocs.ContainsTagSpecification(T.getCorporate().getCompany()));
const affected = metadata.removeProperties(spec);
metadata.save(outputPath);

نقاط رئيسية:

  • تغطي الوسوم الأربعة مجموعة الهوية: المنشئ، المُحرر، المدير، وحقل الشركة المؤسسية.
  • العنوان (Title) والموضوع (Subject) والكلمات المفتاحية (Keywords) تبقى دون تعديل، لذا يظل فهرس السجلات الذي يعتمد عليها يعمل.
  • removeProperties يُعيد عدد العناصر المتأثرة بدلاً من قيمة منطقية.

احطِ كل ذلك بـ try/finally مع metadata.close() داخل الـ finally. الربط يبقي الملف مفتوحًا حتى ذلك الحين، وحلقة بدون إغلاق ستنفد المقابض.

الخطوة 2 – اختيار الخصائص حسب الاسم

سلاسل التعليقات، عدادات المراجعات، وحقول الخادم لا تحمل وسمًا. بالنسبة لها، WithNameSpecification(needle, false) يطابق أي خاصية يحتوي اسمها على needle، ومُنشئ من أربع أسطر يربط واحدًا لكل سلسلة جزئية:

let spec = null;
for (const needle of needles) {
  const s = new groupdocs.WithNameSpecification(needle, false /* fullMatch */);
  spec = spec ? spec.or(s) : s;
}
return spec;

ثلاث تمريرات تعيد استخدام هذا المُنشئ بقوائم مختلفة. التعليقات تُعالج أولًا:

const affected = metadata.removeProperties(
  nameContainsSpec(['Comment', 'Reviewer', 'Reviewed']));
metadata.save(outputPath);

خط الزمن التحريري هو المجموعة التي تُنسى غالبًا، وهو ما يصف كيف تم إنتاج المستند:

const affected = metadata.removeProperties(nameContainsSpec([
  'Revision', 'TrackedChange', 'LastPrinted', 'TotalEditingTime', 'EditTime',
]));
metadata.save(outputPath);

تمرير SharePoint هو نفس الاستدعاء مع Server، Workflow، Approver، ContentType، و Template. المطابقة الجزئية مقصودة: فهي تلتقط CommentsCount إلى جانب Comment دون الحاجة إلى قائمة أسماء دقيقة لكل صيغة.

الخطوة 3 – مسح كل شيء عندما تتوقف الانتقائية عن الفائدة

للنسخة التي تُرسل خارج المؤسسة، استدعاء واحد يحل محل الأربع تمريرات:

const affected = metadata.sanitize();
metadata.save(outputPath);

sanitize() يمسح كل حزمة بيانات وصفية تُكتشفها المكتبة، بما في ذلك أجزاء OOXML المخصصة، وعادةً ما يكون عددها أكبر من مجموع التمريرات المستهدفة. كما أنه يأخذ العنوان والموضوع، لذا يُستَخدم في الحد الفاصل بدلاً من حلقة مراجعة.

الخطوة 4 – التحقق، لأن الفشل الصامت يبدو نجاحًا

يعيد الفحص استخدام نفس المواصفات عبر findProperties، الذي يقرأ دون كتابة. النتيجة مجموعة Java، لذا تُستعرض بالفهرس:

const props = metadata.findProperties(tagSpec.or(nameSpec));
for (let i = 0; i < props.getCount(); i++) {
  const p = props.get_Item(i);
  const val = p.getValue && p.getValue();
  const value = val ? String(val.getRawValue ? val.getRawValue() : val) : '';
  if (!value || value === '0' || value === '0.0') continue;
  leaks.push(`${p.getName()}=${value}`);
}

مرشح الفراغ والصفر يستحق مكانه. أضفته بعد أن فشلت تجربة بسبب عداد مراجعة تم مسحه إلى 0، وكان الفحص يُبلغ بأمانة عن الخاصية المتبقية.

مثال عملي كامل

المستودع يربط الدوال الستة في index.js، الذي يطبق الترخيص، يشغّل كل تمريرة على resources/pii-sample.docx، يتحقق من وجود كل ملف ناتج، وينتهي بالتحقق من أن قائمة التسريبات فارغة. فشل أي تحقق يخرج برمز غير صفري، لذا يعمل كله كفحص في CI بدلاً من عرض توضيحي تقرأه.

تفصيل يستحق النسخ إلى نسختك الخاصة: كل تمريرة تقرأ نفس ملف المصدر وتكتب مخرجًا منفصلًا، بدلاً من ربط ملف مُنظف بآخر. هذا يبقي عدد العناصر المتأثرة مستقلاً، لذا سطر السجل لتمرير التعليقات يُظهر ما وجده قاعدة التعليقات وليس ما تبقى بعد قاعدة الهوية.

متى يجب تشغيل تمريرة مستهدفة بدلاً من sanitize()؟

كلما كان المستند لا يزال قيد الاستخدام. الملفات المتداولة بين المراجعين تعتمد على العنوان، الموضوع، والكلمات المفتاحية للبحث والتصنيف، وsanitize() يزيل الثلاثة مع البيانات الشخصية. شغّل تمرير الهوية والتعليقات أثناء التعاون، احتفظ بالحقول الوصفية، واستخدم المسح الكامل للنسخة التي تُرسل فعليًا خارج المؤسسة.

تطبيقات واقعية

معالج التحميل

مسار Express ينظف المرفق قبل كتابته إلى التخزين، يسجل عدد العناصر المتأثرة على التذكرة، ويرفض التحميل عندما تكون قائمة التسريبات غير فارغة.

مهمة تصدير ليلية

عامل يتجول في مجلد التصدير، يطبق تمرير الهوية وتمرير الخادم، ويفشل المهمة بدلاً من تسجيل تحذير عندما لا يزال المستند يُظهر بيانات شخصية متبقية.

بوابة ما قبل النشر

خطوة بناء تنظف مرفقات الوثائق قبل الإصدار، باستخدام sanitize() لأن لا شيء في تلك الملفات يحتاج إلى الحفاظ على بياناته الوصفية.

أفضل الممارسات والنصائح

  • دائمًا اكتب إلى مسار جديد حتى يبقى الأصلي متاحًا لحل النزاعات.
  • أغلق كائن البيانات الوصفية في كتلة finally، خاصةً داخل الحلقات.
  • دمج المواصفات بـ .or() في وظائف الدُفعات؛ فتح واحد وحفظ واحد أفضل من أربعة.
  • سجّل عدد العناصر المتأثرة لكل تمريرة، بما في ذلك الصفر، حتى يكون الصيغة غير المعروفة مرئية.

استكشاف المشكلات الشائعة

عدد العناصر المتأثرة صفر على مستند تعرف أنه يحتوي على بيانات
تحقق من أن صيغة الإدخال مُعترف بها قبل الاستنتاج بأن الملف نظيف؛ ملف غير قابل للقراءة وملف نظيف ينتجان نفس الصفر.

فحص التسريبات يُبلغ عن خصائص قمت بإزالتها للتو
وجّه الفحص إلى مسار الإخراج المحفوظ، وليس إلى مسار الإدخال. الفحص يقرأ أي ملف يُعطى له.

بالونات التعليقات لا تزال مرئية في Word
نص التعليق يعيش في جسم المستند، وليس في حزمة بيانات وصفية. GroupDocs.Metadata يمسح الخصائص المتعلقة بالتعليقات؛ إزالة البالونات نفسها تحتاج إلى مكتبة تحرير محتوى مثل Aspose.Words.

الخلاصة

أربع خطوات، ست دوال، سكريبت واحد يُبلغ عما فعل. مواصفات الوسوم تتعامل مع مجموعة الهوية عبر الصيغ، مواصفات الأسماء تغطي العائلات التي لا تصنفها الوسوم، sanitize() يتعامل مع الحد الفاصل، وفحص التسريبات يحول كل ذلك إلى فحص. استنسخ المستودع، شغّله على مستند مر بمرحلة مراجعة حقيقية، وانظر إلى الأعداد قبل أن تقرر أي تمريرات يحتاجها خط أنابيبك.

موارد إضافية