كيفية تحليل ملفات PDF على نطاق واسع في NodeJS: ما يجب القيام به وما لا يجب القيام به

اتخذ خطوة في بنية البرنامج ، وتعلم كيفية صنع حل عملي لمشكلة تجارية حقيقية مع NodeJS Streams مع هذه المقالة.

صاحب المصلحة الخاص بك ، بعد حفظهم ساعات لا تحصى من البحث عن ملفات PDF للحصول على البيانات الخاصة بهم. (المصدر: GIPHY)

التفاف: ميكانيكا الموائع

واحدة من أعظم نقاط القوة في البرنامج هو أنه يمكننا تطوير أعمال تجريدية تتيح لنا التفكير في الكود ومعالجة البيانات بطرق يمكننا فهمها. تيارات هي واحدة من هذه الفئة من التجريد.

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

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

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

خادم server.log | grep 400 | أقل

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

(أيضًا ، يبدو كأنبوب).

إذا كنت مثلي وتساءلت في هذه المرحلة عن سبب ضرورة ذلك ، اسأل نفسك لماذا نستخدم خطوط الأنابيب في الحياة الواقعية. في الأساس ، إنه هيكل يلغي التخزين بين نقاط المعالجة. لا داعي للقلق بشأن تخزين براميل النفط إذا تم ضخها.

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

لا أريد إعادة اختراع العجلة هنا. والآن بعد أن غطيت استعارة للتيارات والأساس المنطقي لاستخدامها ، لدى Flavio Copes منشورًا رائعًا على المدونة يغطي كيفية تنفيذها في Node. خذ ما دامت تحتاج إلى تغطية الأساسيات هناك ، وعندما تكون مستعدًا ، سنعود إلى حالة الاستخدام.

الوضع

والآن ، بعد أن أصبحت لديك هذه الأداة في شريط الأدوات الخاص بك ، قم بتصوير هذا:

أنت في الوظيفة ، وقد قام مديرك / القانوني / الموارد البشرية / عميلك / (أدخل صاحب المصلحة هنا) بمواجهتك. إنهم يمضون وقتًا طويلاً في البحث عن ملفات PDF منظمة. بالطبع ، الناس عادة لن يخبروك بهذا الشيء. ستسمع ، "أقضي 4 ساعات في إدخال البيانات". أو "أنظر من خلال جداول الأسعار". أو ، "أملأ النماذج الصحيحة حتى نحصل على أقلام رصاص ذات علامات تجارية لشركتنا كل ثلاثة أشهر".

أيا كان ، إذا كان عملهم ينطوي على كل من (أ) قراءة مستندات PDF المنظمة و (ب) الاستخدام الشامل لتلك المعلومات المنظمة. بعد ذلك يمكنك التدخل والقول ، "مهلا ، قد نكون قادرين على أتمتة ذلك وإتاحة وقتك للعمل على أشياء أخرى".

سرعة الجهد. الكود الخاص بك هو عطر ، والآن قل مرحباً بك على إعلاناتك التليفزيونية. (المصدر: كريس بيترز)

من أجل هذا المقال ، دعونا نأتي بشركة وهمية. من أين أتيت ، يشير مصطلح "دمية" إما إلى أحمق أو مصاصة طفل. لذلك دعونا نتخيل هذه الشركة المزيفة التي تصنع اللهايات. بينما نحن في هذا المكان ، فلنقفز على سمكة القرش ونقول إنها مطبوعة ثلاثية الأبعاد. تعمل الشركة كمزود أخلاقي للهدمات للمحتاجين الذين لا يستطيعون تحمل أقساط التأمين بأنفسهم.

(أنا أعرف كيف يبدو غبية ، وتعليق الكفر من فضلك.)

تود مصادر مواد الطباعة التي تدخل في منتجات DummEth ، ويجب أن تضمن أنها تلبي ثلاثة معايير رئيسية:

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

المشروع

لذلك ، من الأسهل متابعة ذلك ، لقد قمت بإعداد ريبو GitLab يمكنك استنساخه واستخدامه. تأكد من تثبيتات Node و NPM محدثة أيضًا.

العمارة الأساسية: القيود

الآن ، ما الذي نحاول القيام به؟ لنفترض أن Todd يعمل بشكل جيد في جداول البيانات ، مثل الكثير من موظفي المكاتب. لكي يقوم Todd بفرز القمح المثلث ثلاثي الأبعاد من القشر ، من الأسهل بالنسبة له قياس المواد حسب درجة الغذاء والسعر للكيلوغرام والموقع. حان الوقت لضبط بعض قيود المشروع.

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

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

الموقع ، سنبسط ونفترض أنها مسافة نسبية بسيطة ، حيث يطير الغراب. في الطرف المقابل من الطيف ، يوجد حل مُعد هندسيًا: استخدام بعض واجهات برمجة التطبيقات (مثل: خرائط Google) لتمييز مسافة السفر القاسية التي ستسافر بها مادة معينة للوصول إلى مركز (مراكز) توزيع Todd. في كلتا الحالتين ، دعنا نقول إننا قد منحناها قيمة (كيلومترات إلى تود) في ملفات PDF الخاصة بـ Todd.

أيضًا ، دعنا نفكر في السياق الذي نعمل فيه. يعمل Todd بفاعلية كمجمع معلومات في سوق ديناميكي. المنتجات تدخل وتخرج ، ويمكن أن تتغير تفاصيلها. هذا يعني أن لدينا عددًا عشوائيًا من ملفات PDF التي يمكن تغييرها - أو بشكل أكثر ملاءمة - يتم تحديثها - في أي وقت.

بناءً على هذه القيود ، يمكننا أخيرًا معرفة ما نريد أن ننجزه كودنا. إذا كنت ترغب في اختبار قدرة التصميم لديك ، فتوقف هنا مؤقتًا وفكر في كيفية تصميم حلك. قد لا يبدو مثل ما أصفه. هذا جيد ، طالما أنك تقدم حلاً قابلاً للتطبيق عاقلًا بالنسبة إلى Todd ، وهو شيء لن تمزق شعرك فيما بعد تحاول الحفاظ عليه.

العمارة الأساسية: حلول

لذلك لدينا عدد تعسفي من ملفات PDF ، وبعض القواعد لكيفية تحليلها. إليك كيف يمكننا القيام بذلك:

  1. قم بإعداد كائن دفق يمكنه القراءة من بعض المدخلات. مثل عميل HTTP يطلب تنزيلات PDF. أو وحدة نمطية كتبناها تقرأ ملفات PDF من دليل في نظام الملفات.
  2. إعداد وسيط العازلة. هذا يشبه النادل في مطعم يقدم الطبق النهائي للعملاء المقصودين. في كل مرة يتم فيها تمرير ملف PDF كامل إلى ساحة البث ، نقوم بدفق هذه القطع في المخزن المؤقت حتى يمكن نقلها.
  3. يقدم النادل (المخزن المؤقت) الطعام (بيانات PDF) للعميل (وظيفة التحليل). يفعل العميل ما يحلو له (تحويل إلى بعض تنسيق جدول البيانات) معه.
  4. عند انتهاء العميل (المحلل اللغوي) ، دع النادل (المخزن المؤقت) يعرف أنه مجاني ويمكنه العمل على طلبات جديدة (ملفات PDF).

ستلاحظ أنه لا توجد نهاية واضحة لهذه العملية. كمطعم ، لا ينتهي أبدًا التحرير والسرد الخاص بـ Stream-Buffer-Parser ، حتى بالطبع لا يوجد المزيد من البيانات - لا توجد طلبات أخرى - قادمة.

الآن أعلم أنه ليس هناك لعق الشفرة حتى الآن. هذا أمر بالغ الأهمية. من المهم أن تكون قادرًا على التفكير في أنظمتنا قبل كتابتها. الآن ، لن نحصل على كل شيء بشكل صحيح في المرة الأولى حتى مع التفكير المسبق. الأشياء دائما كسر في البرية. البق تحتاج إلى إصلاح.

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

لذلك في المخطط الكبير للأشياء ، يبدو مثل هذا:

المفهوم الأولي لبرنامجنا. لن تبدو هكذا عندما تتم ، لكن عملية حل المشكلات لا تقل أهمية عن النتيجة. محاط بدائرة باللون الأخضر: ما نحن عليه حتى اليوم

تقديم التبعيات

الآن كإخلاء مسؤولية ، أود أن أضيف أن هناك عالمًا كاملًا من التفكير حول إدخال التبعيات في التعليمات البرمجية الخاصة بك. أحب أن أغطي هذا المفهوم في منشور آخر. في غضون ذلك ، أود فقط أن أقول إن أحد النزاعات الأساسية في اللعب هو الصراع بين رغبتنا في إنجاز عملنا بسرعة (أي: تجنب متلازمة NIH) ، ورغبتنا في تجنب مخاطر الطرف الثالث.

بتطبيق هذا على مشروعنا ، اخترت إلغاء تحميل الجزء الأكبر من معالجة PDF إلى وحدة pdfreader. فيما يلي بعض الأسباب وراء:

  • تم نشره مؤخرًا ، مما يعد علامة جيدة على أن الريبو مُحدَّث.
  • لديها تبعية واحدة - أي أنها مجرد فكرة مجردة عن وحدة أخرى - يتم الاحتفاظ بها بانتظام على GitHub. هذا وحده هو علامة كبيرة. علاوة على ذلك ، فإن التبعية ، وهي وحدة تسمى pdf2json ، بها مئات النجوم ، و 22 مشاركًا ، والكثير من مقل العيون تراقبها عن كثب.
  • يقوم المشرف ، أدريان جولي ، بعمل مسك دفاتر جيد في متتبع القضايا في جيثب ويميل بنشاط إلى أسئلة المستخدمين والمطورين.
  • عند التدقيق عبر NPM (6.4.1) ، لا توجد نقاط ضعف.

إذاً الكل في الكل ، يبدو أنه يشتمل على تبعية آمنة.

الآن ، تعمل الوحدة بطريقة مباشرة إلى حد ما ، على الرغم من أن README لا يصف بشكل واضح هيكل ناتجها. تلاحظ الهاوية:

  1. الكشف عن فئة PdfReader المراد إنشاء مثيل لها
  2. هذا المثال له طريقتان لتحليل ملف PDF. يقومون بإرجاع نفس الإخراج ويختلفون فقط في الإدخال: PdfReader.parseFileItems لاسم ملف ، و PdfReader.parseBuffer من البيانات التي لا نريد الرجوع إليها من نظام الملفات.
  3. تطلب الأساليب رد اتصال ، يتم الاتصال به في كل مرة يجد فيها PdfReader ما يشير إليه على أنه عنصر PDF. هناك ثلاثة أنواع. أولاً ، بيانات تعريف الملف ، والتي هي دائمًا العنصر الأول. الثاني هو بيانات الصفحة. إنه بمثابة حرف إرجاع لإحداثيات عناصر النص المراد معالجتها. الأخير هو العناصر النصية التي يمكننا اعتبارها كائنات / بنيات بسيطة لها خاصية نص ، وإحداثيات ADB ثنائية الأبعاد عائمة على الصفحة.
  4. يعود الأمر إلى رد الاتصال لدينا لمعالجة هذه العناصر في بنية بيانات من اختيارنا وأيضًا لمعالجة أي أخطاء يتم إلقاؤها عليها.

إليك مقتطف الشفرة كمثال:

const {PdfReader} = requ ('pdfreader')؛
// تهيئة القارئ
قارئ const = جديد PdfReader () ؛
// قراءة بعض المخزن المؤقت المعرفة بشكل تعسفي
reader.parseBuffer (buffer، (err، item) => {
  إذا (يخطئ)
    console.error (يخطئ)؛
  آخر إذا (! البند)
    / * pdfreader يصطف العناصر الموجودة في PDF وينقلها إلى
     * رد الاتصال. عندما لا يتم تمرير أي عنصر ، فهذا يشير إلى ذلك
     * لقد انتهينا من قراءة ملف PDF. * /
    console.log ( 'تم.')؛
  آخر إذا (item.file)
    تشير عناصر الملف إلى مسار ملف PDF فقط.
    console.log (`تحليل $ {item.file && item.file.path || 'a buffer'}`)
  آخر إذا (item.page)
    / / تحتوي عناصر الصفحة ببساطة على رقم صفحتها.
    console.log (`Reached page $ {item.page}`)؛
  آخر إذا (item.text) {
    تحتوي عناصر النص على عدد قليل من الخصائص:
    const itemAsString = [
      item.text،
      'x:' + item.x ،
      'y:' + item.y ،
      'w:' + item.width ،
      "ح:" + عنصر. الارتفاع ،
    ] .join ( '\ ن \ ر')؛
    console.log ('Text Item:'، itemAsString)؛
  }
})؛

تود ملفات PDF

دعنا نعود إلى وضع تود ، فقط لتوفير بعض السياق. نريد تخزين تخزين البيانات بناءً على ثلاثة معايير رئيسية:

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

لقد تم ترميز نص برمجي بسيط يفرز بعض المنتجات الوهمية بشكل عشوائي ، ويمكنك العثور عليه في دليل / data الخاص بـ repo companion لهذا المشروع. يكتب هذا البرنامج النصي البيانات العشوائية إلى ملفات JSON.

يوجد أيضًا مستند قالب هناك. إذا كنت على دراية بمحركات templating مثل Handlebars ، فستفهم ذلك. هناك خدمات عبر الإنترنت - أو إذا كنت تشعر بالمغامرة ، فيمكنك استرجاع خدماتك الخاصة - التي تأخذ بيانات JSON وتعبئة القالب ، وتعيدها لك بصيغة PDF. ربما من أجل الاكتمال ، يمكننا تجربة ذلك في مشروع آخر. على أي حال: لقد استخدمت هذه الخدمة لإنشاء ملفات PDF الوهمية التي سنقوم بتحليلها.

إليك الشكل الذي يبدو عليه المرء (تم قطع مسافة بيضاء إضافية):

نود أن نستنتج من هذا PDF بعض JSON التي تعطينا:

  • معرف الطلب والتاريخ ، لأغراض مسك الدفاتر ،
  • SKU من مصاصة ، لتحديد فريد ، و
  • خصائص المصاصة (الاسم ودرجة الغذاء وسعر الوحدة والمسافة) ، حتى يتمكن تود بالفعل من استخدامها في عمله.

كيف نفعل ذلك؟

قراءة البيانات

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

تقوم دالة readPDFPages في /parser/index.js بمعالجة هذا ، على غرار رمز المثال المكتوب أعلاه:

/ * يقبل مخزن مؤقت (على سبيل المثال: من fs.readFile) ، ويوزع
 * كملف PDF ، مع إعادة بنية البيانات القابلة للاستخدام لـ
 * تطبيق معين ، تحليل المستوى الثاني.
 * /
وظيفة readPDFPages (المخزن المؤقت) {
  قارئ const = جديد PdfReader () ؛
  / / نحن نعود وعد هنا ، وقراءة PDF
  // العملية غير متزامنة.
  إرجاع وعد جديد ((حل ، رفض) => {
    // يمثل كل عنصر في هذه المجموعة صفحة في PDF
    دع الصفحات = [] ؛
    reader.parseBuffer (buffer، (err، item) => {
      إذا (يخطئ)
        / / إذا واجهنا مشكلة ، أخرج!
        رفض (يخطئ)
      آخر إذا (! البند)
        // إذا كنا خارج العناصر ، فقم بالعزم باستخدام بنية البيانات
        تصميم (صفحات).
      آخر إذا (item.page)
        إذا وصل المحلل إلى صفحة جديدة ، فقد حان الوقت
        // العمل على كائن الصفحة التالية في صفيف صفحاتنا.
        pages.push ({})؛
      آخر إذا (item.text) {
        // إذا لم يكن لدينا عنصر صفحة جديد ، فنحن نحتاج
        // لاسترداد أو إنشاء صفيف "صف" جديد
        // لتمثيل مجموعة من العناصر النصية في موقعنا
        / / الموقف الحالي Y ، والتي ستكون Y لهذا العنصر
        // موضع.
        // وبالتالي ، يقرأ هذا السطر كـ ،
        // "إما استرداد صفيف صفحتنا الحالية ،
        // في موقعنا الحالي Y ، أو قم بعمل جديد "
        const صف = الصفحات [pages.length-1] [item.y] || []؛
        // أضف العنصر إلى الحاوية المرجعية (مثل: الصف)
        row.push (item.text)؛
        // تضمين الحاوية في الصفحة الحالية
        pages [pages.length-1] [item.y] = row؛
      }
    })؛
  })؛
}

إذاً الآن ، بتمرير مخزن PDF مؤقت إلى هذه الوظيفة ، سنحصل على بعض البيانات المنظمة. إليك ما حصلت عليه من اختبار التشغيل ، وطباعته إلى JSON:

[{'3.473': ['تفاصيل تفاصيل المنتج'] ،
    "4.329": ["التاريخ: 23/05/2019"] ،
    "5.185": ['معرِّف إعادة الطلب: 298831'] ،
    "6.898": ["Pacifier Tech" ، "Todd Lerr"] ،
    "7.754": ["123 Example Blvd" ، "DummEth Pty. Ltd." ]،
    "8.61": ["تمبكتو" ، "1337 ليت سانت" ،
    "12. 235": ["SKU" ، "6308005"] ،
    "13. 466": ["اسم المنتج" ، "مصاصة مربعة الليمون مربع"] ،
    '14. 698 ': [' درجة الغذاء '،' 3 '] ،
    "15 .928999999999998": ['$ / kg' ، '1.29'] ،
    "17 .16": ['الموقع' ، '55']}]

إذا نظرت بعناية ، ستلاحظ وجود خطأ إملائي في PDF الأصلي. "الطلب" خطأ إملائي كـ "Requsition". جمال المحلل اللغوي لدينا هو أننا لا نهتم بشكل خاص بالأخطاء المشابهة في مستندات الإدخال الخاصة بنا. طالما تم تنظيمها بشكل صحيح ، يمكننا استخراج البيانات منها بدقة.

الآن نحتاج فقط إلى تنظيم هذا الأمر في شيء أكثر قابلية للاستخدام (كما لو كنا نعرضه عبر واجهة برمجة التطبيقات). الهيكل الذي نبحث عنه هو شيء على غرار هذا:

{
  reqID: '000000' ،
  التاريخ: "DD / MM / YYYY" ، // أو أي شيء آخر يعتمد على الجغرافيا
  sku: '000000' ،
  اسم: "بعض سلسلة قلصنا" ،
  foodGrade: 'X' ،
  unitPrice: 'DCC' ، // D for Dollars ، C for Cents
  الموقع: "XX" ،
}

جانبا: سلامة البيانات

لماذا نحن بما في ذلك الأرقام كسلسلة؟ يعتمد على خطر التحليل. دعنا نقول فقط أننا أجبرنا جميع أرقامنا على الجمل:

سيكون سعر الوحدة وموقعها على ما يرام - من المفترض أن تكون أرقامًا قابلة للعد بعد كل شيء.

درجة الغذاء ، لهذا المشروع المحدود للغاية ، من الناحية الفنية آمنة. لن يتم فقد أية بيانات عندما نكرهها - ولكن إذا كان مصنفًا فعالًا ، مثل Enum ، فمن الأفضل الاحتفاظ بها كسلسلة.

ومع ذلك ، فإن معرف الطلب ورقم SKU ، إذا تم الإكراه على الجمل ، قد يفقد بيانات مهمة. إذا كان معرّف طلب معين يبدأ بثلاثة أصفار وقمنا بالإكراه على رقم ، حسنًا ، فقد فقدنا هذه الأصفار وقمنا بتشويه البيانات.

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

إعادة هيكلة البيانات

الآن لدينا معلومات تود ، نحتاج فقط إلى تنظيمها بطريقة قابلة للاستخدام. يمكننا استخدام مجموعة متنوعة من وظائف معالجة الصفيف والكائن ، وهنا MDN هو صديقك.

هذه هي الخطوة التي يكون فيها لكل شخص تفضيلاته الخاصة. البعض يفضل الطريقة التي تنجز المهمة وتقلل من وقت التطوير. يفضل الآخرون الكشف عن أفضل خوارزمية للمهمة (على سبيل المثال: تقليل وقت التكرار). إنه تمرين جيد لمعرفة ما إذا كان يمكنك التوصل إلى طريقة للقيام بذلك ومقارنته بما حصلت عليه. أحب أن أرى طرقًا أفضل وأبسط وأسرع أو حتى مجرد طرق مختلفة لتحقيق الهدف نفسه.

على أي حال ، إليك كيفية القيام بذلك: وظيفة parseToddPDF في /parser/index.js.

وظيفة parseToddPDF (الصفحات) {
  const page = pages [0]؛ / / نعلم أنه ستكون هناك صفحة واحدة فقط
  // الخريطة التعريفية لبيانات PDF التي نتوقعها ، بناءً على هيكل تود
  الحقول const = {
    // "نتوقع أن يكون الحقل reqID في الصف عند 5.185 ، و
    // العنصر الأول في هذه المجموعة "
    reqID: {row: '5.185' ، الفهرس: 0} ،
    التاريخ: {row: '4.329' ، الفهرس: 0} ،
    sku: {row: '12 .235 '، الفهرس: 1} ،
    الاسم: {row: '13 .466 '، الفهرس: 1} ،
    foodGrade: {row: '14 .698 '، الفهرس: 1} ،
    unitPrice: {row: '15 .928999999999998 '، الفهرس: 1} ،
    الموقع: {row: '17 .16 '، الفهرس: 1} ،
  }؛
  بيانات const = {}؛
  / / قم بتعيين بيانات الصفحة إلى كائن يمكننا إرجاعه ، وفقًا لـ
  // حقولنا المواصفات
  Object.keys (الحقول)
    .forEach ((key) => {
      حقل المجال = الحقول [مفتاح] ؛
      const val = page [field.row] [field.index]؛
      // نحن لا نريد أن نفقد الأصفار الرائدة هنا ، ويمكن أن نثق
      // أي تطبيق / معالجة البيانات للقلق بشأن ذلك. هذا هو
      // لماذا لا نجبر على الرقم.
      البيانات [مفتاح] = فال ؛
    })؛
  // إصلاح بعض حقول النص يدويًا حتى تكون قابلة للاستخدام
  data.reqID = data.reqID.slice ('Requsition ID:' .length)؛
  data.date = data.date.slice ('Date:' .length)؛
  إرجاع البيانات ؛
}

اللحوم والبطاطس هنا في الحلقة الأمامية ، وكيف نستخدمها. بعد استرداد المواضع Y لكل عنصر نص سابقًا ، من السهل تحديد كل حقل نريده كموضع في كائن صفحاتنا. توفير فعال خريطة لمتابعة.

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

بعد قليل من الخطوط المفردة لترتيب بعض حقول السلسلة ، يمكننا إرجاع كائن البيانات وسنعود إلى السباقات. إليك ما يبدو عليه:

{reqID: '298831' ،
  التاريخ: '23 / 05/2019 '،
  sku: '6308005' ،
  اسم: 'ساحة الليمون Qartz مصاصة' ،
  الغذاء الصف: '3' ،
  سعر الوحدة: "1.29" ،
  الموقع: '55'}

ضع كل شيء معا

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

تافهة ، وأنا أعلم ، وبالطبع يمكن تعميم الكتب المدرسية بشكل صريح بالنسبة لنا لاستخدامها عمليًا ، ولكن مهلا ، من المفاهيم الأساسية إضفاء الطابع الرسمي عليها.

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

  • هل سيكون تطبيق سطر أوامر؟
  • هل سيكون خادمًا متسقًا مع مجموعة من نقاط النهاية لواجهة برمجة التطبيقات؟ هذا له مجموعة الأسئلة الخاصة به - REST أو GraphQL ، على سبيل المثال؟
  • ربما كانت مجرد وحدة نمطية في هيكل قاعدة بيانات أوسع - على سبيل المثال ، ماذا لو قمنا بتعميم تحليلنا عبر مجموعة من المستندات الثنائية وأردنا فصل نموذج التزامن عن نوع الملف المصدر المعين وتطبيق التحليل؟

من أجل البساطة ، سألتف منطق التحليل في أداة مساعدة لسطر الأوامر. هذا يعني أن الوقت قد حان لتقديم مجموعة من الافتراضات:

  • هل تتوقع مسارات الملفات كمدخلات ، وهل هي نسبية أم مطلقة؟
  • أو بدلاً من ذلك ، هل تتوقع أن يتم توصيل بيانات PDF المتسلسلة ،؟
  • هل ستقوم بإخراج البيانات إلى ملف؟ لأنه إذا كان الأمر كذلك ، فسوف يتعين علينا توفير هذا الخيار كحجة للمستخدم لتحديد ...

معالجة إدخال سطر الأوامر

مرة أخرى ، الحفاظ على الأمور بسيطة قدر الإمكان: لقد اخترت أن يتوقع البرنامج قائمة من مسارات الملفات ، إما كوسائط سطر أوامر فردية:

ملف فهرس العقدة - 1.pdf - ملف 2.pdf ... - ملف - n.pdf

أو مد الأنابيب إلى الإدخال القياسي كقائمة مسارات ملف مفصولة في السطر الجديد:

# قراءة الأسطر من ملف نصي بكل مساراتنا
cat files-to-parse.txt | مؤشر العقدة
# أو ربما قم بسردها من دليل
العثور على. / البيانات - اسم "* .pdf" | مؤشر العقدة

يسمح هذا لعملية Node بمعالجة ترتيب تلك المسارات بأي طريقة تراها مناسبة ، مما يسمح لنا بتوسيع نطاق رمز المعالجة لاحقًا. للقيام بذلك ، سنقوم بقراءة قائمة مسارات الملفات ، بغض النظر عن الطريقة التي تم توفيرها بها ، ونقسمها ببعض الأرقام التعسفية إلى قوائم فرعية. إليك الكود ، طريقة getTerminalInput في ./input/index.js:

وظيفة getTerminalInput (المصفوفات الفرعية) {
  إرجاع وعد جديد ((حل ، رفض) => {
    الناتج const = [] ؛
  
    if (process.stdin.isTTY) {
      const input = process.argv.slice (2) ؛
      const len ​​= Math.min (subArrays، Math.ceil (input.length / subArrays))؛
      بينما (طول الإدخال)
        output.push (input.splice (0، len))؛
      }
      تصميم (المخرجات).
    } آخر {
    
      دع الإدخال = '' ؛
      process.stdin.setEncoding ( 'UTF-8')؛
      process.stdin.on ('مقروء' ، () => {
        دع القطعة
        بينما (القطعة = process.stdin.read ())
          المدخلات + = قطعة.
      })؛
      process.stdin.on ('end'، () => {
        input = input.trim (). split ('\ n')؛
        const len ​​= Math.min (input.length، Math.ceil (input.length / subArrays))؛
        بينما (طول الإدخال)
          output.push (input.splice (0، len))؛
        }
        تصميم (المخرجات).
      })
    
    }
    
  })؛
}

لماذا تقسيم القائمة؟ دعنا نقول أن لديك وحدة المعالجة المركزية 8 النواة على الأجهزة المستهلك ، و 500 PDFs لتحليل.

لسوء الحظ لـ Node ، على الرغم من أنه يتعامل مع التعليمات البرمجية غير المتزامنة بشكل خيالي بفضل حلقة الأحداث الخاصة به ، فإنه يعمل فقط على مؤشر ترابط واحد. لمعالجة ملفات PDF 500 ، إذا كنت لا تستخدم شفرة ذات مؤشرات ترابط متعددة (مثل: عملية متعددة) ، فأنت تستخدم فقط ثُمن سعة المعالجة. على افتراض أن كفاءة الذاكرة ليست مشكلة ، يمكنك معالجة البيانات بمعدل يصل إلى ثمانية أضعاف من خلال الاستفادة من وحدات التوازي المدمجة في Node.

تقسيم مدخلاتنا إلى أجزاء تسمح لنا بذلك.

جانبا ، هذا هو أساسا موازن التحميل البدائي ويفترض بوضوح أن أعباء العمل المقدمة عن طريق تحليل كل PDF قابلة للتبادل. أي أن ملفات PDF بنفس الحجم وتمسك بنفس البنية.

من الواضح أن هذه حالة تافهة ، خاصة وأننا لا نأخذ في الاعتبار معالجة الأخطاء في العمليات المنفذة والعامل المتاح حاليًا للتعامل مع الأحمال الجديدة. في حالة قيامنا بإعداد خادم API للتعامل مع طلبات التحليل الواردة ، يتعين علينا مراعاة هذه الاحتياجات الإضافية.

تجميع الكود الخاص بنا

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

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

تم بناء هذا الجزء العلوي من وحدة الأطفال_العملية ، والتي تكون أقل ارتباطًا بإحكام مع برامج Node المتوازية وتسمح لك بتفرخ العمليات الأخرى ، مثل برامج shell أو ثنائيات أخرى قابلة للتنفيذ ، والتفاعل معهم باستخدام المدخلات والمخرجات القياسية وغير ذلك.

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

لذلك دعونا نمشي عبر الكود. هنا بكميات كبيرة:

const const = require ('cluster') ؛
const numCPUs = تتطلب ('os'). cpus ().
const {getTerminalInput} = require ('./ input') ؛
(وظيفة المتزامن الرئيسي () {
  if (cluster.isMaster) {
    const workerData = ننتظر getTerminalInput (numCPUs) ؛
    لـ (اسمح i = 0 ؛ i 
      العامل المنفصل = cluster.fork () ؛
      const params = {filenames: workerData [i]}؛
      worker.send (بارامس)؛
    }
  } آخر {
    تتطلب ( './ عامل')؛
  }
}) ()؛

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

الآن الطريقة الرئيسية هي في الواقع بسيطة إلى حد ما. في الواقع ، يمكننا تقسيمها إلى خطوات:

  1. إذا كانت العملية الرئيسية لدينا ، فقم بتقسيم المدخلات المرسلة إلينا بالتساوي حسب عدد مراكز وحدة المعالجة المركزية لهذا الجهاز
  2. بالنسبة إلى كل عامل يتم تحميله ، عليك أن تفرخ عاملًا حسب الكتلة ، وقم بإعداد كائن يمكننا إرساله إليه بواسطة قناة رسائل RPC المشتركة بين الوحدات النمطية [الكتلة] ، وأرسل الشيء الملعون إليها.
  3. إذا لم نكن في الواقع الوحدة الرئيسية ، فعندئذ يجب أن نكون عاملًا - فقط قم بتشغيل الكود في ملف العامل لدينا واتصل به يوميًا.

لا شيء يحدث هنا ، وهذا يسمح لنا بالتركيز على الرفع الحقيقي ، والذي يكتشف كيفية استخدام العامل لقائمة أسماء الملفات التي نقدمها له.

المراسلة ، المتزامن ، والجداول ، جميع عناصر النظام الغذائي المغذي

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

const Bufferer = require ('../ buffer') ؛
const Parser = require ('../ parser') ؛
const {createReadStream} = require ('fs') ؛
process.on ('message' ، المتزامن (خيارات) => {
  const {filenames} = خيارات ؛
  محلل const = محلل جديد () ؛
  const parseAndLog = async (buf) => console.log (انتظار parser.parse (buf) + '،')؛
  const parsingQueue = filenames.reduce (async (result، filename) => {
    في انتظار النتيجة ؛
    إرجاع وعد جديد ((حل ، رفض) => {
      قارئ القارئ = createReadStream (اسم الملف) ؛
      const buffer = new Bufferer ({onEnd: parseAndLog}) ؛
      قارئ
        .pipe (bufferer)
        .فقط ("إنهاء" ، حل)
        .once ('خطأ' ، رفض)
    
    })؛
  
  }، صحيح)؛
  محاولة {
    ننتظر parsingQueue؛
    process.exit (0)؛
  } catch (err) {
    console.error (يخطئ)؛
    process.exit (1)؛
  }
})؛

الآن هناك بعض الاختراقات القذرة هنا ، لذا كن حذرًا إذا كنت من غير المبتدئين (المزاح فقط). لنلقِ نظرة على ما يحدث أولاً:

الخطوة الأولى هي أن تطلب جميع المكونات الضرورية. ضع في اعتبارك أن هذا يعتمد على ما يفعله الكود نفسه. لذلك اسمحوا لي أن أقول إننا سنستخدم دفقًا قابل للكتابة مخصصًا ، لقد أطلقنا عليه اسم "المخزن المؤقت" ، وهو عبارة عن غلاف لمنطقنا المعرب منذ آخر مرة ، وأيضًا باسم "محلل" و "محلل" و "createReadStream" القديم الموثوق به من وحدة fs.

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

لذلك يمكننا أولاً أن نرى أنه غير متزامن. أولاً ، نستخرج أسماء الملفات من الرسالة نفسها - إذا كانت هذه عبارة عن رمز إنتاج ، فسأقوم بالتحقق منها هنا. في الحقيقة ، جحيم ، سأقوم بالتحقق منها في كود معالجة الإدخال الخاص بنا في وقت سابق بعد ذلك ، نبدأ في إنشاء كائن التحليل اللغوي الخاص بنا - واحد فقط للعملية بأكملها - بحيث يمكننا تحليل عدة مخازن مؤقتة بمجموعة واحدة من الطرق. من بين اهتماماتي أنه يدير الذاكرة داخليًا ، وعند التفكير ، يعد هذا أمرًا جيدًا للمراجعة لاحقًا.

ثم هناك مجموعة بسيطة ، parseAndLog تدور حول تحليل يسجّل JSON - ified PDF المخزن المؤقت مع فاصلة ملحقة به ، فقط لجعل الحياة أسهل لسَلسَلة نتائج تحليل ملفات PDF متعددة.

عامل الخاص بك ، تستعد وجاهزة للتاريخ مع القدر.

وأخيرا لحم الأمر ، طابور غير متزامن. دعني أشرح:

تلقى هذا العامل قائمة أسماء الملفات. لكل اسم ملف (أو مسار ، حقًا) ، نحتاج إلى فتح دفق قابل للقراءة من خلال نظام الملفات حتى نتمكن من الحصول على بيانات PDF. بعد ذلك ، نحتاج إلى إنتاج "المخزن المؤقت" الخاص بنا ، (النادل لدينا ، متابعًا من قياس المطعم سابقًا) ، حتى نتمكن من نقل البيانات إلى المحلل اللغوي لدينا.

المخزن المؤقت هو مخصص المدرفلة. كل ما تفعله حقًا هو قبول وظيفة للاتصال عندما تتلقى جميع البيانات التي تحتاجها - هنا نطلب منها فقط تحليل تلك البيانات وتسجيلها.

لذلك ، لدينا الآن جميع القطع ، نحن فقط نجمعها معًا:

  1. الدفق المقروء - ملف PDF ، ينتقل إلى المخزن المؤقت
  2. ينتهي المخزن المؤقت ويدعو طريقة parseAndLog على مستوى العمال

يتم التفاف هذه العملية برمتها في وعد ، والتي يتم إرجاعها هي نفسها إلى وظيفة الاختزال الموجودة داخلها. عندما يتم حلها ، تستمر عملية التقليل.

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

على أي حال ، فإن باقي الكود ينهي العملية بناءً على معالجة الأخطاء. مرة أخرى ، إذا كان هذا رمزًا للإنتاج ، فيمكنك المراهنة على وجود تسجيل أشد ومعالجة للخطأ هنا ، ولكن كدليل على المفهوم ، يبدو هذا جيدًا.

لذلك يعمل ، ولكن هل هو مفيد؟

لذلك هناك لديك. لقد كانت رحلة قليلاً ، وهي بالتأكيد ناجحة ، ولكن مثل أي رمز ، من المهم مراجعة نقاط قوتها وضعفها. خارج قمة رأسي:

  • تيارات يجب أن تتراكم في المخازن المؤقتة. هذا ، للأسف ، يهزم الغرض من استخدام التدفقات ، وكفاءة الذاكرة تعاني تبعا لذلك. هذا شريط لاصق مناسب للعمل مع وحدة pdfreader. يسعدني معرفة ما إذا كانت هناك طريقة لدفق بيانات PDF وتحليلها على مستوى دقيق. خاصةً إذا كان منطق التحليل الوظيفي المعياري يمكن تطبيقه عليه.
  • في هذه المرحلة الصغيرة ، يكون منطق التحليل هشًا بشكل مزعج. مجرد التفكير ، ماذا لو كان لدي مستند أطول من صفحة؟ مجموعة من الافتراضات تطير خارج النافذة وتجعل الحاجة إلى تدفق بيانات PDF أقوى.
  • أخيرًا ، سيكون من الرائع أن نرى كيف يمكننا بناء هذه الوظيفة بنقاط التسجيل ونقاط النهاية لواجهة برمجة التطبيقات لتقديمها إلى الجمهور - مقابل سعر أو مجانًا ، وفقًا للسياقات التي يتم استخدامها.

إذا واجهت أي انتقادات أو مخاوف محددة ، أحب أن أسمعها أيضًا ، لأن اكتشاف نقاط الضعف في الكود هو الخطوة الأولى لإصلاحها. وإذا كنت على دراية بأي طريقة أفضل لدفق ملفات PDF وتحليلها بشكل متزامن ، فأعلمني حتى أتمكن من تركها هنا لأي شخص يقرأ هذا المنشور للحصول على إجابة. في كلتا الحالتين - أو لأي غرض آخر - أرسل لي رسالة عبر البريد الإلكتروني أو اتصل على رديت.