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

ملفات CSV مفيدة جدًا لاستيراد مجموعات البيانات الكبيرة، ونستخدمها بانتظام في مهام مختلفة في OOZOU. وتوفر Ruby مكتبة CSV رائعة لمعالجة ملفات CSV وإنشائها. لن أشرح هنا ما هو ملف القيم المفصولة بفواصل، فأفترض أنكم تعرفونه ما دمتم تقرؤون هذا المقال. وإن لم تكونوا كذلك، فاقرؤوا رابط ويكيبيديا أعلاه. هناك بعض أخطاء التنسيق التي نصادفها كثيرًا في OOZOU. فكثيرًا ما يرسل إلينا العملاء ملفات CSV أُنشئت في Microsoft Office وفيها مشكلات في التنسيق، مثل الأسطر الفارغة أو القيم التي لم تُهرَّب (escape) بشكل صحيح، على سبيل المثال:
name,description,product_code
Bookcase,This bookcase is unique, stylish, and great value for money,CD0018394
Computer Desk,This desk is 50"x180" and made from pine, aluminum, and plastic,CD0190049
هذان مثالان على بيانات تالفة. في السطر الثاني فواصل أكثر من اللازم، فلن يعرف المعالج أي حقل يقابل أي قيمة. ويمكننا حل ذلك بوضع القيمة بين علامتي تنصيص. لكن السطر الثالث يحتوي أصلًا على علامات تنصيص، فعلينا مضاعفتها لتهريبها، على سبيل المثال:
name,description,product_code
Bookcase,"This bookcase is unique, stylish, and great value for money",CD0018394
Computer Desk,"This desk is 50""x180"" and made from pine, aluminum, and plastic",CD0190049
ويمكنكم أيضًا تحديد فاصل آخر وترك الفواصل العادية للقيم. وهذا يطرح أول أمر يجب الاتفاق عليه عند التعامل مع ملفات CSV.
احرصوا على أن يكون تنسيق ملفات CSV ثابتًا
العناوين في ملفات CSV اختيارية. فيمكننا أن نخبر المعالج بما نتوقعه، أو نخبره بأن السطر الأول يحتوي على العناوين.
CSV.parse('path/to/csv.csv', headers: true)
CSV.parse('path/to/csv.csv', headers: :first_row)
سيقرأ الكود أعلاه العناوين من السطر الأول، لذلك نتوقع ملفًا مثل:
name,description,product_code
Bookcase,"This bookcase is unique, stylish, and great value for money",CD0018394
Computer Desk,"This desk is 50""x180"" and made from pine, aluminum, and plastic",CD0190049
لكن إذا قرر مزوّد الملف حذف العناوين، فسيحصل المحلّل على أسماء عناوين غريبة لن تفهمها السكربتات. وقد واجهنا أيضًا حالة غيّر فيها المزوّد العناوين تغييرًا طفيفًا، فلم تتعرف السكربتات على القيم. ويمكننا التعامل مع ذلك بطريقة أخرى أيضًا.
CSV.parse('path/to/csv.csv', headers: [:name, :description, :product_code])
هنا أخبرنا المحلّل بأن الملف لا يحتوي على عناوين، وبأننا نتوقع القيم بهذا الترتيب. لكننا الآن أمام المشكلة المعاكسة: الملف الذي يحتوي على عناوين ستكون في بدايته قيمة غير صالحة. كما أصبح الترتيب مهمًا، فإذا بدأ المزوّد فجأة بوضع الاسم في الخانة الأخيرة، فسيفشل السكربت مرة أخرى. وحل هذه المشكلات سهل: اتفقوا على تنسيق محدد مع مزوّد الملفات. نظريًا هذا بسيط ومنطقي، لكنني مررت بأكثر من موقف تغيّر فيه التنسيق دون أي إشعار. لذلك من المفيد دائمًا التحقق من القيم المتوقعة وإطلاق أخطاء (raise errors) عندما تكون غير صالحة.
نسّقوا العناوين والقيم
عند تحليل ملفات CSV يمكننا أن نطلب من Ruby معالجة العناوين والقيم معًا. وإذا كان بإمكاني طلب أسماء عناوين محددة، فأفضّل تمرير خيار header_converters لتحويل المفاتيح النصية إلى symbols، فيصبح جلب القيم أقرب إلى أسلوب Ruby.
csv = CSV.parse('path/to/csv.csv', headers: true, header_converters: :symbol, converters: [:integer, :float, :date_time]).to_h
csv.each do |row|
row[:name]
end
تفحص هذه الـ converters القيم لمعرفة ما إذا كان يمكن تحويلها إلى integers أو floats أو date_times. وهناك converters مدمجة أخرى أيضًا.
لكنكم ستحتاجون في أحيان كثيرة إلى تحويلات أكثر تقدمًا. ويمكنكم بسهولة إنشاء converter مخصص وتحويل كل حقل بطريقة مختلفة:
my_converter = -> { |value, field|
case field.header
when :name
value.to_s.capitalize
when :contact_number
PhonyRails.normalize_number(value, country_code: 'TH')
when :created_at_utc
Time.zone.parse(value)
else
fail("Unknown field name #{field.inspect}=#{value}")
end
}
csv = CSV.parse('path/to/csv.csv', headers: true, header_converters: :symbol, converters: [my_converter]).to_h
سجّلوا كل شيء
عند استيراد البيانات من المهم أن تسجّلوا أكبر قدر ممكن من المعلومات، لأنكم على الأرجح ستنسون تسجيل معلومة مهمة لن تتضح أهميتها إلا لاحقًا.
عادةً أشغّل سكربتات الاستيراد إما كـ rake tasks أو عبر rails runner. أنشئوا logger يكتب إلى ملف في مكان ما على نظامكم، وضعوا التاريخ في اسم الملف! فهذا يمنع الكتابة فوق الملف عن طريق الخطأ إذا اضطررتم إلى تشغيل السكربت أكثر من مرة، كما يمنحكم مرجعًا لوقت تشغيل السكربت فعليًا. وإذا كان السكربت سيسجّل غيغابايتات من البيانات، ففكروا في استخدام عدة ملفات سجل لخطوات السكربت المختلفة، أو إعداد logrotate لتدوير الملف كل ساعة.
اجعلوا السكربتات idempotent
يعني ذلك أن تشغيل السكربت أكثر من مرة لا يغيّر النتيجة. ومن أفضل الطرق لتحقيق ذلك أن تستوردوا بيانات CSV أولًا إلى جدول وسيط، ثم تعالجوا صفوفه في مهمة منفصلة. وهذا يتيح لكم الاستيراد المجمّع باستخدام gems مثل activerecord-import، ويمكن أن يحتوي كل صف في الجدول على حقل منطقي (boolean) باسم processed يوضح ما إذا كان قد عولج أم لا. ويمكنكم أيضًا إنشاء جدول في قاعدة البيانات لتسجيل الخطوات المنفذة على كل صف، على غرار gems مثل statesman التي تستخدم transition associations لتسجيل الانتقالات مع بيانات وصفية عن كل إجراء.
اعرضوا التقدّم وإلا فقدتم أعصابكم
إذا كنتم تستوردون مئات الآلاف من الصفوف، فقد يستغرق الأمر ساعات. ومن المريح جدًا أن تحصلوا على تقرير بالتقدم. فإذا لم تروا التقدم، فقد تتساءلون إن كان السكربت لا يزال يعمل وتفكرون في إيقافه، وأنا أعرف ذلك من التجربة. وأفضل حل في الوقت الفعلي توصلت إليه هو شريط تقدم في الـ terminal. ويمكنكم استخدام أدوات مثل tty-progressbar لعرض التقدم بطرق متنوعة، بل يمكنها أيضًا توقع المدة المتبقية لاكتمال السكربت بناءً على معدل معالجة الصفوف الحالي.
هل تحتاجون إلى استيراد بيانات إلى تطبيقكم أو خدمتكم؟
نحن في OOZOU خبراء في هذا المجال، ونستورد البيانات منذ سنوات من واجهات API وخوادم FTP وحتى رسائل البريد الإلكتروني. وإذا كنتم تبحثون عن فريق قادر على بناء نظام متين لكم، فتواصلوا معنا أدناه


