كيفية قراءة ملف CSV في إطار بيانات PySpark (3 أمثلة)

دليل أكاديمي شامل يشرح كيفية قراءة ملفات CSV وتحويلها إلى إطار بيانات PySpark DataFrame عبر ثلاثة أمثلة عملية مع تحسين الأداء وضبط المخططات.

Mohammed looti أكاديمي وباحث متخصص في علم النفس
تاريخ النشر
تمت المراجعة العلمية · د. مروة عبد العظيم · 24 أكتوبر، 2026
مراجعة وتدقيق علمي معتمد تاريخ التدقيق: 24 أكتوبر، 2026
د. مروة عبد العظيم دكتوراه في علم النفس
عضو هيئة التدريس • جامعة كربلاء
معايير التدقيق والاعتماد السريري

يخضع هذا المحتوى لمعايير ضبط الجودة والتدقيق العلمي والأكاديمي الصارمة في شبكة علم النفس العربي، لضمان صحة المعلومات ودقتها السريرية ومطابقتها لأحدث الأدلة والبراهين الصادرة عن الجمعيات النفسية والطبية المعتمدة (APA / WHO).

تُمثّل معالجة البيانات الضخمة (Big Data) إحدى الركائز التقنية المحورية في عصر التحول الرقمي الراهن، حيث أضحى تدفق المعلومات بكميات هائلة وسرعات فائقة تحدياً حقيقياً للبنى التحتية الحاسوبية التقليدية. في هذا السياق، برز إطار العمل الموزع Apache Spark بوصفه المحرك الحسابي الأكثر نضجاً وكفاءة للتعامل مع أعباء العمل التحليلية المتوازية، متجاوزاً القيود الهيكلية التي عانت منها أطر المعالجة التتابعية، ونماذج الجيل الأول مثل Apache Hadoop MapReduce. وبفضل واجهته البرمجية الشهيرة بلغة بايثون، والمعروفة باسم PySpark، تمكّن مهندسو وعلماء البيانات من الدمج المتقن بين مرونة وبساطة بناء الجمل البرمجية في بايثون، وبين القوة التدميرية لمحرك التنفيذ المكتوب بلغة سكالا والذي يُدير آلاف المعالجات عبر بيئات الحوسبة السحابية والعناقيد الفيزيائية المتعددة بكفاءة تشغيلية منقطعة النظير.

وعلى الرغم من التطور المتسارع في ظهور صيغ التخزين العمودية الحديثة والمحسنة، لا تزال ملفات القيم المفصولة بفواصل (Comma-Separated Values – CSV) تحتل مكانة راسخة في بيئات تبادل البيانات التجارية والأكاديمية، نظراً لبساطتها وسهولة استيعابها بواسطة البشر وقابليتها للتصدير من مختلف قواعد البيانات العلائقية والتطبيقات المكتبية. بيد أن إدخال هذه الملفات النصية غير المهيكلة هيكلةً صارمة إلى بيئة حوسبة موزعة مثل PySpark يفرض جملة من التحديات التقنية الجوهرية، بدءاً من غياب التحديد الصريح لأنماط البيانات، ومروراً بتعقيدات التوزيع غير المتوازن للكتل التخزينية، وانتهاءً بآليات تفسير الفواصل وعلامات الاقتباس والحقول المشوهة. من هنا، تصبح دراسة وفهم كيفية قراءة وتحميل ملفات CSV إلى أطر بيانات PySpark (DataFrames) مسألة لا غنى عنها لبناء خطوط أنابيب بيانات (Data Pipelines) متينة، قادرة على التوسع والصمود في بيئات الإنتاج الحقيقية.

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

1. مقدمة شاملة حول بيئة Apache PySpark وأهمية إطارات البيانات

1.1 مفهوم الحوسبة الموزعة ودور PySpark في معالجة البيانات الضخمة

تقوم فلسفة الحوسبة الموزعة على فكرة تجزئة المهام الحسابية المعقدة ومجموعات البيانات الهائلة إلى أجزاء صغيرة مستقلة، يتم توزيعها ومعالجتها بالتوازي عبر شبكة من الأجهزة الحاسوبية المستقلة التي تُعرف مجتمعة باسم “العنقود” (Cluster). في البيئات الحاسوبية التقليدية، كان المحللون يعتمدون على أجهزة خادمة مفردة ذات مواصفات عتادية ضخمة (Vertical Scaling)، إلا أن هذا النهج سرعان ما يواجه قيوداً فيزيائية واقتصادية حتمية تتعلق بسعة الذاكرة العشوائية وسرعة النواقل الداخلية للمعالجات. جاء إطار عمل Apache Spark، الذي تم تطويره في الأصل داخل مختبرات AMPLab في جامعة كاليفورنيا، بيركلي بواسطة ماتي زاهاريا وفريقه البحثي، ليُحدث ثورة حقيقية عبر تطبيق مبدأ التوسع الأفقي (Horizontal Scaling) المقترن بالحوسبة داخل الذاكرة (In-Memory Computing).

يتميز Spark بالقدرة على الاحتفاظ بالبيانات الوسيطة في الذاكرة العشوائية (RAM) عبر العقد العاملة وتجنب عمليات الكتابة والقراءة المكلفة والمتكررة من الأقراص الصلبة، وهي المشكلة التي كانت تشل حركة المعالجة في إطار MapReduce القديم. ولتمكين مجتمع علماء البيانات ومطوري البرمجيات من استثمار هذه القوة الفائقة دون الحاجة إلى التخصص العميق في لغات المنظومة البيئية لجافا وسكالا، ظهرت مكتبة PySpark كواجهة برمجية متقدمة تترجم تعليمات بايثون إلى خطط تنفيذية منطقية وفيزيائية يفهمها المحرك الأساسي. تستخدم هذه الواجهة تقنية المآخذ الشبكية ومسارات العمليات البينية لتبادل البيانات وتوجيه الأوامر، ما يتيح الجمع بين مرونة التعبير البرمجي لبايثون والأداء الفائق للمحرك الموزع المكتوب بلغة سكالا.

يبرز التجريد الحسابي في PySpark كطبقة عازلة تُخفي تعقيدات التزامن، ومزامنة الخيوط، وتوزيع الذاكرة، والتعافي من الأخطاء (Fault Tolerance). فعندما يكتب المهندس شيفرة برمجية لقراءة ملف أو تطبيق استعلام، لا يتعامل مع الخوادم بصورة منفردة، بل يوجه العمليات نحو كائن تجريدي موحد يقوم تلقائياً بمهام التنسيق وجدولة الوظائف وإرسال حزم المهام الحسابية إلى العقد التنفيذية (Executors). هذا التجريد لا يُسهم فقط في تسريع زمن التطوير، بل يضمن أيضاً استمرارية عمل المسارات حتى في حال فشل أو سقوط بعض العقد الفيزيائية أثناء دورة المعالجة.

1.2 تعريف إطار بيانات PySpark ومقارنته بهياكل البيانات التقليدية

يُعرَّف إطار بيانات PySpark (DataFrame) بأنه مجموعة بيانات موزعة غير متزامنة، منظمة هيكلياً في هيئة أعمدة ذات مسميات محددة، ومبنية فوق التجريد البرمجي الأساسي لسبارك المعروف باسم مجموعات البيانات الموزعة المرنة (Resilient Distributed Datasets – RDDs). من الناحية المفاهيمية، يماثل إطار البيانات الجدول في قواعد البيانات العلائقية أو إطار البيانات في مكتبة Pandas الشهيرة، ولكنه يختلف عنهما جذرياً من حيث البنية الهندسية الداخلية؛ فبينما يعيش إطار بيانات Pandas في النطاق التخزيني والذاكرة العشوائية لجهاز حاسوبي واحد ويُعالج عبر خيط معالجة مفرد أو مكتبات محدودة التوازي، يتوزع إطار بيانات PySpark عبر مئات أو آلاف العقد الحاسوبية، ما يسمح بمعالجة مجموعات بيانات تصل أحجامها إلى التيرابايت والبيتابايت بسلاسة فائقة.

تكمن القوة الجوهرية لإطارات بيانات PySpark في محرك التحسين العلائقي المتقدم المسمى Catalyst. يقوم هذا المحرك، الذي تم تقديمه في ورقة بحثية رائدة قادها مايكل أرمبروست وزملاؤه، بتحليل الاستعلامات البرمجية والتحويلات الهيكلية التي يُطبقها المطور، ثم يُعيد صياغتها وترتيبها تلقائياً بهدف تقليل حجم البيانات المنقولة عبر الشبكة (Data Shuffling) وتسريع زمن الوصول. يمر الاستعلام عبر Catalyst بأربع مراحل رئيسية: تحليل الاستعلام وبناء شجرة التجريد النحوي، والتحسين المنطقي لدفع عوامل التصفية وإسقاط الأعمدة غير الضرورية، والتخطيط الفيزيائي للمقارنة بين استراتيجيات التنفيذ المتاحة، وأخيراً توليد الشيفرة البرمجية المجمعة عبر مشروع Tungsten.

يُكمل مشروع Tungsten هذا الأداء الاستثنائي من خلال تجاوز القيود التقليدية لبيئة تشغيل جافا (JVM). يركز Tungsten على إدارة الذاكرة بشكل صريح خارج نطاق جمع القمامة التلقائي (Off-Heap Memory Management)، مما يلغي كلفة تحويل كائنات جافا إلى نصوص والعكس، ويوفر ضغطاً فائقاً للبيانات الثنائية في الذاكرة. بالإضافة إلى ذلك، يستخدم Tungsten توليد الشيفرة البرمجية في وقت التشغيل (Whole-Stage Code Generation) لتجميع مسار المعالجة بأكمله في دالة وحدة معالجة مركزية (CPU) مدمجة واحدة، مما يجعل أداء إطارات البيانات في PySpark متطابقاً تقريباً مع نظيرتها المكتوبة بلغة سكالا الصافية، ويتفوق بأشواط هائلة على أطر البيانات الفردية غير الموزعة.

1.3 أهمية تنسيق CSV في هندسة البيانات وتطبيقاتها التحليلية

يُعد تنسيق ملفات القيم المفصولة بفواصل (Comma-Separated Values)، المعروف اختصاراً بـ CSV، أحد أقدم وأوسع التنسيقات انتشاراً في تاريخ الحوسبة ومعالجة البيانات. تعود جذور هذا التنسيق النصي البسيط إلى بدايات عصر الحوسبة الجدولية، حيث تتحدد بنية الملف من خلال تقسيم الأسطر إلى سجلات عبر فواصل الأسطر، وتقسيم الحقول داخل كل سجل عبر فاصلة محددة. يرجع انتشاره الكاسح إلى كونه تنسيقاً مفتوحاً ومقروءاً بصورة بشرية مباشرة (Human-Readable)، ولا يتطلب برمجيات احتكارية لفتحه، كما يسهل تصديره واستيراده من شتى أنواع التطبيقات، بدءاً من برامج الجداول الإلكترونية البسيطة وصولاً إلى مستودعات البيانات المؤسسية العملاقة.

ومع ذلك، يفرض تنسيق CSV تحديات تقنية وهندسية بالغة التعقيد عند التعامل معه داخل بيئات الحوسبة الموزعة مثل Spark. أول هذه التحديات يتمثل في غياب البيانات الوصفية (Metadata) والمخطط الصريح للأنواع؛ فالملف هو في جوهره نص نقي خالٍ من أي دلالة تشير إلى ما إذا كان الحقل يحتوي على قيمة عددية صحيحة، أو كسرية، أو تاريخ زمني، أو سلسلة نصية عادية. هذا الغياب يضطر المحركات المعالجة إما إلى اعتبار كافة الحقول نصوصاً اعتباطية، أو القيام بجولة فحص مسبقة ومكلفة لقراءة أجزاء من الملف وتخمين نوع البيانات، وهو ما يستنزف موارد المعالجة وقتاً وجهداً في البيانات الضخمة.

أما التحدي الثاني فيتعلق بكفاءة التخزين وعمليات الإدخال والإخراج (I/O Bottlenecks). لا يدعم تنسيق CSV الضغط المنطقي للمحارف على مستوى الأعمدة، ولا يتيح استخراج أعمدة منتقاة دون قراءة السطر بأكمله، مما يعني استهلاك نطاق ترددي ضخم لنقل حقول قد لا يحتاجها الاستعلام التحليلي. أضف إلى ذلك مشكلات السجلات التالفة الناتجة عن إدراج علامات الفواصل داخل نصوص الحقول نفسها دون إحاطتها بعلامات اقتباس دقيقة، أو وجود نهايات أسطر غير نمطية تعيق عملية التجزئة المتوازية. من هنا، تنبع الحاجة الأكاديمية والمهنية الملحة إلى إتقان تقنيات قراءة هذا التنسيق في PySpark، لضبط التوازن بين كلفة الإدخال ودقة التفسير وسلامة خطوط الإنتاج البرمجية.

2. الإعداد الأولي والتهيئة التقنية لبيئة PySpark

2.1 تثبيت الحزم والمتطلبات الأساسية للنظام

تتطلب بيئة PySpark تكاملاً دقيقاً بين منظومة لغة بايثون والمنظومة التحتية لآلة جافا الافتراضية (JVM)، حيث يعتمد محرك Spark الأساسي على جافا وسكالا في تنفيذ عملياته الداخلية وإدارة ذاكرة العقد. الخطوة الأولى والأكثر حرجاً في تهيئة البيئة هي ضمان تثبيت إصدار متوافق من حزمة أدوات تطوير جافا (Java Development Kit – JDK). يتطلب Apache Spark عادةً الإصدار 8 أو 11 أو 17 من OpenJDK، بينما قد تواجه الإصدارات الأحدث مشكلات في التوافق مع بروتوكولات التسلسل الداخلي أو إدارة الذاكرة في Spark. يمكن التحقق من تثبيت جافا عبر تنفيذ الأمر التالي في سطر الأوامر:

java -version

بعد التحقق من جاهزية بيئة جافا، يتم الانتقال إلى بيئة بايثون، ويُفضل دوماً عزل المشروع ضمن بيئة افتراضية مستقلة (Virtual Environment) باستخدام أدوات مثل venv أو conda، وذلك لتفادي تضارب الحزم والمكتبات. يُنصح باستخدام إصدار حديث ومستقر من بايثون (مثل بايثون 3.9 أو 3.10 أو 3.11). يتم تثبيت حزمة PySpark الرسمية باستخدام أداة إدارة حزم بايثون pip عبر تنفيذ الأمر المباشر:

pip install pyspark

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

2.2 إنشاء وضبط جلسة عمل SparkSession

مع إطلاق الإصدار 2.0 من Apache Spark، أصبحت جلسة العمل SparkSession هي نقطة الدخول الموحدة والمركزية لكافة الوظائف البرمجية المتعلقة بإطارات البيانات وتحليلات لغة SQL ومعالجة التدفقات الهيكلية. قبل هذا الإصدار، كان المطور مطالباً بتهيئة كائنات متعددة ومتباينة مثل SparkContext للعمليات الأساسية، وSQLContext للعمليات الجدولية، وHiveContext للاتصال بمستودعات البيانات، مما أدى إلى تشتت الهيكل البرمجي وتعقيد صيانة الشيفرات. تجمع SparkSession كل هذه الوظائف ضمن واجهة واحدة أنيقة تتبع نمط التصميم البنائي (Builder Pattern).

لتهيئة جلسة العمل برمجياً، نقوم باستيراد الفئة المطلوبة من وحدة SQL في PySpark وتطبيق التكوين التدريجي للجلسة، كما يتضح في الشيفرة التالية:

from pyspark.sql import SparkSession

spark = SparkSession.builder
    .appName("CSV_Processing_Engine")
    .master("local[*]")
    .config("spark.driver.memory", "4g")
    .getOrCreate()

في هذا النموذج البرمجي، تُحدد المعلمة appName اسماً تعريفياً صريحاً للتطبيق، يظهر بوضوح في واجهات المراقبة وسجلات النظام لتسهيل تتبع مسار العمليات. أما الوسيط master، فيُعد المعلمة الجوهرية المسؤولة عن تحديد بيئة التنفيذ؛ حيث يشير الخيار "local[*]" إلى تشغيل Spark في الوضع المحلي على جهاز المستخدم، مع استغلال كافة نوى المعالجة (CPU Cores) المتاحة في الجهاز لإنشاء خيوط معالجة متوازية. وفي بيئات الإنتاج الحقيقية، يُستبدل هذا الخيار بروابط مديري الموارد مثل YARN أو Kubernetes أو Apache Mesos. وتسمح الدالة config بضبط المعايير المتقدمة مثل تخصيص الذاكرة العشوائية لبرنامج المشغل (Driver Memory)، في حين تضمن الدالة getOrCreate عدم إنشاء جلسة جديدة إذا كانت هناك جلسة نشطة بالفعل في الذاكرة.

2.3 التحقق من صحة البيئة التشغيلية واختبار الاتصال

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

print("Spark Version:", spark.version)
test_df = spark.createDataFrame([(1, "Validation_OK")], ["id", "status"])
test_df.show()

يفتح تشغيل Spark تلقائياً خادماً مصغراً لواجهة المستخدم الرسومية يُعرف باسم Spark UI، ويمكن الوصول إليه افتراضياً عبر المتصفح من خلال الرابط المحلي http://localhost:4040 طوال فترة نشاط جلسة العمل. تُعد هذه الواجهة أداة حيوية لمراقبة الأداء؛ إذ تعرض تحليلاً تفصيلياً للمراحل الحاسوبية (Stages)، وحزم المهام (Tasks)، ومخططات الرسوم الموجهة غير الدورية (DAGs)، فضلاً عن استهلاك الذاكرة وتوزيع البيانات عبر الأقسام المختلفة. يتيح فحص هذه الواجهة التأكد من أن جميع العقد وخيوط المعالجة تعمل بالتوافق المتوقع وبدون تراكم للعمليات العالقة.

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

spark.stop()

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

3. البنية التركيبية لدالة spark.read.csv ومحدداتها البرمجية

3.1 البناء النحوي للدالة والمعلمات الأساسية

توفر واجهة برمجة تطبيقات PySpark طريقة متخصصة ومبسطة لقراءة الملفات النصية المفصولة بفواصل من خلال استدعاء دالة spark.read.csv، والتي تمثل في حقيقتها واجهة التفافية (Wrapper) أنيقة حول قارئ أطر البيانات العام المتمثل في كائن DataFrameReader. يتيح هذا القارئ التعبير عن عمليات الإدخال بصيغتين متطابقتين وظيفياً: إما باستخدام الدالة المباشرة ذات الوسائط المسماة، أو باتباع نمط الربط المتسلسل (Fluent API Pattern) باستخدام دالة spark.read.format("csv") المتبوعة بعدة استدعاءات لدالة option()، وصولاً إلى استدعاء دالة load().

يتضمن البناء النحوي لدالة القراءة قائمة ثرية من المعلمات المصممة لتغطية مختلف الحالات المعقدة للملفات النصية. تشمل هذه المعلمات: مسار الملف أو المجلد (path)، ومحدد الفصل بين الأعمدة (sep)، وخيار قراءة السطر الأول كترويسة (header)، وخيار الاستدلال التلقائي على المخطط (inferSchema)، ومحددات علامات الاقتباس (quote)، ومحارف الهروب (escape)، وصيغ التعامل مع التواريخ والأوقات، ومحددات القيم الفارغة (nullValue)، بالإضافة إلى تحديد سلوك التعامل مع الصفوف المشوهة عبر معلمة mode.

تدعم دالة القراءة طيفاً واسعاً من أنظمة الملفات وبروتوكولات التخزين دون الحاجة لتعديل المنطق التحليلي الداخلي. فبإمكان المطور تمرير مسار نسبي أو مطلق لنظام الملفات المحلي، أو استهداف أنظمة الملفات الموزعة مثل نظام ملفات هادوب Hadoop Distributed File System عبر بادئة البروتوكول المخصصة، أو استهداف منصات التخزين السحابي الكبرى مثل أمازون S3 عبر مسارات تبدأ بالبادئة s3a://، أو مسارات مايكروسوفت أزور Data Lake Storage عبر بروتوكولات abfss://، ومستودعات جوجل السحابية gs://. يقوم القارئ تلقائياً بالاعتماد على المشغلات التحتية المناسبة لإدارة الاتصال واسترجاع الكتل وتفكيك الضغط المطبق على مستوى الملفات أثناء القراءة.

3.2 آلية عمل المحرك التحليلي لـ Spark عند استدعاء الدالة

عند تنفيذ أمر قراءة ملف CSV، لا يبدأ Spark بنسخ الملف كاملاً إلى الذاكرة كما تفعل الأدوات المكتبية البسيطة، بل يخضع الملف لمنطق تقسيم الكتل التخزينية الموزعة. يعتمد Spark SQL منذ نسخته الثانية على محلل نصوص مفتوح المصدر وفائق السرعة ومكتوب بلغة جافا يُعرف باسم UniVocity CSV Parser. يتميز هذا المحلل بقدرته على المعالجة المباشرة لدفق البيانات النقي (Byte Stream) على مستوى منخفض جداً، متجنباً إنشاء كائنات نصية مؤقتة غير ضرورية في ذاكرة جافا، مما يخفف الضغط الواقع على آليات جمع القمامة ويضاعف من سرعة الفحص والتحليل.

يقوم محرك Spark، بالتنسيق مع نظام التخزين الموزع، بتقسيم الملف الضخم إلى كتل فيزيائية مستقلة تسمى أقسام التجزئة (Splits أو Partitions)، ويبلغ الحجم الافتراضي لكل كتلة عادة 128 ميجابايت في أنظمة HDFS أو وفق ما تحدده الإعدادات البيئية لـ Spark. يتم تخصيص كل قسم لكتلة قراءة معينة تُمنح لعامل حاسوبي (Task Executor) يقوم بفحص الأسطر النصية وتحويلها إلى كائنات أسطر مهيكلة (InternalRow) داخل محرك Spark SQL. يواجه المحرك هنا تحدياً دقيقاً في التعامل مع نهايات الأسطر المتداخلة عبر حدود الكتل الفيزيائية؛ إذ يقوم بموازنة الإشارات لضمان عدم قراءة سطر واحد مقسوماً بين معالجين مختلفين، مما يحافظ على تكامل البيانات المحملة.

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

3.3 الفروق الجوهرية بين التقييم الكسول والعمليات الفورية

يقوم النموذج الحسابي لـ Apache Spark على مبدأ تقني ركيزي يُعرف باسم “التقييم الكسول” (Lazy Evaluation). وفقاً لهذا المبدأ، تنقسم العمليات في Spark إلى صنفين أساسيين: التحويلات (Transformations) والإجراءات الفورية (Actions). التحويلات هي العمليات التي تُنشئ إطار بيانات جديداً من إطار بيانات قائم دون تعديل المصدر الأصلي، مثل عمليات التصفية filter، وتحديد الأعمدة select، والدمج join. لا يتم تنفيذ هذه التحويلات فعلياً لحظة استدعائها في الشيفرة، بل يقتصر عمل Spark على تسجيلها كعقد داخل رسم بياني موجه غير دوري يُعرف بسلسلة الأنساب أو التتبع البرمجي (Lineage Graph).

من الناحية النظرية، تُعد عملية استدعاء قراءة الملف spark.read.csv() تحويلاً كسولاً؛ فبمجرد كتابة السطر البرمجي للقراءة، لا يقوم Spark بفحص الملفات بالكامل أو تحميل محتوياتها إلى الذاكرة، بل يكتفي بالتحقق المبدئي من المسارات وتثبيت مؤشر الإدخال وبناء خطة استعلام منطقية غير محلولة. يستمر هذا السلوك الكسول طالما أن العمليات اللاحقة تقتصر على التحويلات. ولكن بمجرد استدعاء إجراء فوري صريح (Action) — مثل الدالة show() لعرض عينة من البيانات، أو count() لحساب إجمالي عدد الصفوف، أو collect() لجلب النتائج إلى المشغل، أو write() لحفظ المخرجات — يستيقظ المحرك ويقوم بتشغيل خطة التنفيذ المادية الفعلية عبر العقد العاملة.

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

4. المثال العملي الأول: قراءة ملف CSV بالحالة الافتراضية البسيطة

4.1 تجهيز ملف البيانات الافتراضي وهيكله البنائي

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

team,points,assists
Rockets,115,24
Warriors,128,31
Lakers,102,19
Celtics,110,26
Bulls,98,18

يتميز هذا الملف بأن سطره الأول يحتوي على الكلمات المفتاحية المعبرة عن هوية الأعمدة (team، points، assists)، بينما تمثل الأسطر الخمسة اللاحقة البيانات الرقمية والنصية الفعلية للسجلات. الفاصل المستخدم بين كل قيمة وأخرى هو الفاصلة اللاتينية التقليدية (,)، ونهايات الأسطر مبنية وفق معيار محرف الانتقال لسطر جديد الموحد. يتم حفظ هذا الملف محلياً في المسار التابع لدليل العمل الحالي للمشروع ليكون جاهزاً للاختبار البرمجي المباشر.

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

4.2 تنفيذ الشيفرة البرمجية لقراءة الملف دون وسائط إضافية

سنقوم الآن بكتابة الشيفرة البرمجية المباشرة لقراءة الملف الرياضي السابق بالاعتماد كلياً على الحالة الافتراضية لقارئ CSV، ودون تمرير أي معاملات اختيارية تخص الترويسة أو أنواع البيانات. تقتصر الشيفرة على استيراد بيئة الجلسة، وقراءة المسار، ثم استدعاء دالة العرض الجدولية:

from pyspark.sql import SparkSession

spark = SparkSession.builder
    .appName("Example1_Default_Read")
    .master("local[*]")
    .getOrCreate()

# قراءة ملف CSV بالحالة الافتراضية المطلقة
df_default = spark.read.csv("sports_data.csv")

# استدعاء إجراء العرض لفحص الهيكل والمحتوى
df_default.show()
df_default.printSchema()

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

4.3 تحليل المخرجات وفهم السلوك الافتراضي لتسمية الأعمدة

عند فحص مخرجات دالتي show() و printSchema() للشيفرة السابقة، تبرز ثلاث نتائج حاسمة تُفسر السلوك الافتراضي لمحرك PySpark:

أولاً، يتعامل القارئ افتراضياً مع المعلمة header على أنها False. وبما أن المطور لم يخبر Spark صراحة بأن السطر الأول يحتوي على أسماء الحقول، يفترض المحرك أن كل سطر في الملف دون استثناء هو سطر بيانات عادي. نتيجة لذلك، يتحول السطر team,points,assists إلى الصف الأول الفعلي داخل إطار البيانات، جنباً إلى جنب مع صفوف اللاعبين والفرق اللاحقة، مما يخل بالتحليلات الإحصائية إذا ما حاول المحلل إجراء عمليات تجميع حسابية على هذه الأعمدة لاحقاً.

ثانياً، لتجاوز مشكلة غياب أسماء الأعمدة، يقوم Spark بتوليد أسماء نظامية تلقائية بالصيغة القياسية: _c0 للعمود الأول، و _c1 للعمود الثاني، و _c2 للعمود الثالث (حيث الحرف c يشير اختصاراً إلى كلمة Column متبوعاً بمؤشر رقمي يبدأ من الصفر). يصبح إطار البيانات محتوياً على ثلاثة أعمدة بهذه المسميات غير المعبرة دلالياً.

ثالثاً، فيما يخص المخطط الهيكلي للبيانات (Schema)، يتعامل Spark افتراضياً مع المعلمة inferSchema على أنها False. وفي هذه الحالة، يلتزم المحرك بالقاعدة الآمنة قطيعاً: اعتبار كافة الأعمدة المستوردة من نوع السلاسل النصية StringType. هذا السلوك يضمن نجاح القراءة دون انهيار المحرك حتى لو تباينت مدخلات السجلات، ولكنه يجرّد الأرقام (مثل النقاط 115 و 128) من طبيعتها الحسابية، ويمنع تطبيق العمليات الرياضية مثل الجمع وحساب المتوسط ما لم يتم إجراء تحويل قسري لاحق لنوع البيانات (Type Casting).

5. المثال العملي الثاني: قراءة ملف CSV مع تضمين سطر الترويسة

5.1 أهمية وسيط الترويسة في الحفاظ على دلالة البيانات

يُعد سطر الترويسة (Header) الرابط الدلالي الأهم الذي يمنح البيانات الرقمية والنصية المسطحة معناها السياقي. فبدون الترويسة، تفقد الأرقام قيمتها التفسيرية، إذ لا يمكن للمحلل التمييز تلقائياً بين ما إذا كان الرقم 115 يعبر عن عدد النقاط، أو دقائق اللعب، أو عدد التذاكر المباعة. في خطوط أنابيب معالجة البيانات الحديثة، يشكل الاعتماد على أسماء الأعمدة الدلالية ركيزة لا غنى عنها لبناء استعلامات واضحة وقابلة للصيانة والتطوير باستخدام تعابير SQL أو عمليات DataFrame المباشرة.

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

5.2 التطبيق البرمجي لقراءة الملف مع تفعيل خيار الترويسة

لقراءة ملف sports_data.csv مع استثمار السطر الأول كأسماء للأعمدة، نستخدم الوسيط المسمى header=True مباشرة داخل دالة القراءة، أو نستخدم الصيغة البديلة عبر واجهة DataFrameReader التتابعية. توضح الشيفرة التالية كلا الأسلوبين:

from pyspark.sql import SparkSession

spark = SparkSession.builder
    .appName("Example2_Header_Read")
    .master("local[*]")
    .getOrCreate()

# الأسلوب المباشر بتمرير المعلمة المسماة
df_with_header = spark.read.csv("sports_data.csv", header=True)

# الأسلوب البديل باستخدام نمط التوصيل المتسلسل (Fluent API)
df_with_header_alt = spark.read.format("csv")
    .option("header", "true")
    .load("sports_data.csv")

# فحص النتائج
df_with_header.show()
df_with_header.printSchema()

عند فحص الجدول المطبوع بواسطة الدالة show()، يتبين فوراً التغير الهيكلي الإيجابي؛ فقد اختفت المسميات الافتراضية _c0 و _c1، وتصدرت الكلمات team و points و assists واجهة الأعمدة. كما انخفض عدد الصفوف بمقدار صف واحد، حيث تم نقل السطر الأول من مصفوفة البيانات التشغيلية إلى الطبقة التعريفية الوصفية (Metadata) لإطار البيانات.

5.3 التحقق من صحة أسماء الأعمدة وتوافقها مع المعايير

يتيح كائن إطار البيانات في PySpark فحص أسماء الأعمدة برمجياً من خلال الخاصية df.columns، والتي تعيد مصفوفة (List) نصية تحتوي على التسميات بالترتيب الفيزيائي لظهورها. يمكن الاستفادة من هذه الخاصية للتحقق الآلي من مطابقة الهيكل لمتطلبات خط الإنتاج البرمجي:

column_names = df_with_header.columns
print("قائمة الأعمدة المستخرجة:", column_names)
assert "team" in column_names, "خطأ: عمود الفريق غير متوفر في الملف!"

ومع ذلك، تواجه هذه الطريقة تحدياً شائعاً في البيانات المؤسسية الواقعية: احتواء أسماء الأعمدة على مسافات بيضاء زائدة في البداية أو النهاية (مثل " team ")، أو اشتمالها على حروف خاصة مثل الأقواس والرموز النقطية وعلامات الاستفهام، أو تشابه المسميات مع كلمات محجوزة في محرك الاستعلامات SQL. هذه التسميات المشوهة قد تتسبب في أخطاء فادحة عند محاولة الوصول إلى الأعمدة باستخدام تعبير النقطة (Dot Notation) مثل df.points أو داخل تعابير Spark SQL.

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

cleaned_df = df_with_header
for col_name in df_with_header.columns:
    clean_name = col_name.strip().lower().replace(" ", "_").replace(".", "_")
    cleaned_df = cleaned_df.withColumnRenamed(col_name, clean_name)

cleaned_df.printSchema()

6. المثال العملي الثالث: معالجة ملفات CSV ذات الفواصل المخصصة

6.1 مفهوم الفواصل المخصصة وسياقات استخدامها الشائعة

على الرغم من أن الفاصلة التقليدية (,) هي المحدد المعياري الأكثر شهرة، إلا أن البيئات التطبيقية غالباً ما تفرض استخدام محددات فصل بديلة (Custom Delimiters). السبب الأكثر شيوعاً لذلك يرجع إلى طبيعة البيانات اللغوية والنصية ذاتها؛ فالنصوص الوصفية، مثل التعليقات، أو العناوين البريدية، أو المسميات الطبية، غالباً ما تحتوي في أصلها اللغوي على فواصل عادية كعلامات ترقيم، مما يؤدي إلى تكسر الصفوف وتبعثر الحقول عند استخدام الفاصلة التقليدية كفاصل بنائي للأعمدة ما لم يتم ضبط الاقتباس بشكل صارم.

علاوة على ذلك، تُملي بعض المعايير الجغرافية الإقليمية استخدام محددات أخرى؛ ففي العديد من البلدان الأوروبية، تُستخدم الفاصلة التقليدية كفاصلة عشرية للأرقام الرياضية (مثل كتابة 10,5 بدلاً من 10.5)، مما يدفع أدوات معالجة البيانات وقواعد البيانات في تلك المناطق إلى اعتماد الفاصلة المنقوطة (Semicolon ;) كمحدد افتراضي لتجنب تضارب التفسير الرقمي. كما تشيع في بيئات النظم الموروثة والمستودعات الضخمة ملفات مفصولة بعلامات الجدولة (Tab-Separated Values – TSV باستخدام t)، أو ملفات تستخدم الشريط الرأسي (Pipe |) بوصفه محرفاً نادراً ونظيفاً يندر وجوده داخل النصوص الحرة.

6.2 استخدام الوسيط sep أو delimiter لضبط محدد الفصل

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

يمكن تمرير المعلمة sep مباشرة داخل دالة spark.read.csv()، أو تطبيقها كخيار option("delimiter", "|") داخل النمط المتسلسل. ومن المهم الإشارة إلى أن Spark يدعم محددات الفصل أحادية المحرف ومحددة الترميز، كما يمتلك قدرة في الإصدارات الحديثة على التعامل مع محددات الفصل متعددة المحارف (Multi-char Delimiters مثل || أو ::)، وإن كان استخدام المحددات المتعددة يفرض كلفة حسابية إضافية ضئيلة في تجزئة النصوص المجدولة.

6.3 التطبيق العملي ومقارنة دقة الفصل بين الرموز المختلفة

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

team;points;assists
Nets;108;22
Bucks;120;27
Heat;95;20
Suns;112;25

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

from pyspark.sql import SparkSession

spark = SparkSession.builder
    .appName("Example3_Custom_Delimiter")
    .master("local[*]")
    .getOrCreate()

# 1. القراءة الخاطئة (افتراض الفاصلة العادية)
df_broken = spark.read.csv("sports_semicolon.csv", header=True)
print("--- شكل المخرجات عند استخدام القارئ الافتراضي الخاطئ ---")
df_broken.show()
print("عدد الأعمدة:", len(df_broken.columns))

# 2. القراءة الصحيحة باستخدام وسيط الفصل المخصص
df_corrected = spark.read.csv("sports_semicolon.csv", header=True, sep=";")
print("--- شكل المخرجات عند تصحيح محدد الفصل إلى الفاصلة المنقوطة ---")
df_corrected.show()
print("عدد الأعمدة بعد التصحيح:", len(df_corrected.columns))

عند معاينة مخرجات المحاولة الأولى الخاطئة، نكتشف أن Spark، نظراً لعدم عثوره على أي فاصلة تقليدية (,) في السطر الأول، اعتبر السطر كاملاً عبارة عن عمود واحد ضخم يحمل الاسم team;points;assists، ودمج كافة البيانات اللاحقة داخل هذا العمود الفردي ككتلة نصية مصمتة. أما في المحاولة الثانية، وعند تمرير sep=";"، نجح المحرك في تفكيك الحقول بدقة، وأنتج إطار بيانات متزناً يحتوي على ثلاثة أعمدة مستقلة وأربعة صفوف نقية، مما يُبرز الأهمية البالغة لمطابقة معلمات القراءة مع الخصائص الفيزيائية للملف المصدري.

7. التحكم في أنماط البيانات ومخطط الجدول

7.1 دور المعلمة inferSchema في الاستدلال التلقائي على الأنواع

كما اتضح في الأمثلة السابقة، يُبقي Spark جميع الأعمدة كنصوص عامة (StringType) ما لم يتم توجيهه لعكس ذلك. لتخفيف عبء التحويل اليدوي، يوفر المحرك المعلمة المنطقية inferSchema. عند ضبط هذه المعلمة على True، كما في الشيفرة:

df_inferred = spark.read.csv("sports_data.csv", header=True, inferSchema=True)
df_inferred.printSchema()

يقوم Spark بتفعيل خوارزمية استدلال ذكية تفحص المحتوى النصي لكل عمود. إذا وجد المحرك أن جميع قيم عمود points تمثل قيماً عددية صحيحة تقع ضمن نطاق أعداد 32 بت، فإنه يُرقي نوع العمود تلقائياً من StringType إلى IntegerType. وإذا تضمنت القيم كسوراً، يُرقيها إلى DoubleType، كما يمتلك القدرة على التعرف على صيغ التواريخ القياسية وترقيتها إلى TimestampType أو DateType.

على الرغم من جاذبية هذا الخيار وسهولة تطبيقه في مراحل التحليل الاستكشافي السريع (Ad-hoc Analysis)، إلا أنه ينطوي على كلفة معمارية خفية باهظة في بيئات الإنتاج الكبيرة. لكي يستنتج Spark الأنواع بشكل دقيق، يضطر المحرك إلى تنفيذ مرور مسحي كامل وإضافي على الملفات عبر وظيفة حاسوبية مستقلة (Distributed Job). فإذا كان الملف بحجم مئات الجيجابايت، يعني هذا قراءة مئات الملايين من السطور مرتين: مرة لاستنتاج الأنواع، ومرة أخرى لتحميل البيانات الفعلية، مما يضاعف زمن عمليات الإدخال والإخراج ويهدر موارد العقد الحسابية بلا طائل.

7.2 بناء وتطبيق مخطط صريح باستخدام StructType و StructField

يتمثل المعيار الهندسي الأمثل للتعامل مع ملفات CSV في بيئات الإنتاج في تجاوز آلية الاستدلال التلقائي كلياً، والاعتماد بدلاً من ذلك على المخطط الهيكلي الصريح والمُعرف مسبقاً (Explicit Schema Enforcement). يُعرف المخطط في PySpark برمجياً عبر استخدام كائن StructType، والذي يمثل وعاءً حاوياً لمجموعة من الكائنات الجزئية من نوع StructField.

يتطلب تعريف كل حقل StructField تمرير أربع معلمات أساسية: اسم الحقل كنص، ونوع البيانات الفعلي المستورد من وحدة الأنواع pyspark.sql.types، وقيمة منطقية تُحدد ما إذا كان الحقل يقبل قيماً فارغة (nullable)، وحقل اختياري للبيانات الوصفية (metadata). توضح الشيفرة التالية كيفية بناء وتطبيق مخطط صريح واحترافي لملفنا الرياضي:

from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, StringType, IntegerType

spark = SparkSession.builder
    .appName("Explicit_Schema_Example")
    .master("local[*]")
    .getOrCreate()

# بناء المخطط الهيكلي الصريح بدقة رياضية
custom_schema = StructType([
    StructField("team", StringType(), nullable=False),
    StructField("points", IntegerType(), nullable=True),
    StructField("assists", IntegerType(), nullable=True)
])

# تطبيق المخطط الصريح أثناء القراءة وتجاوز الاستدلال التلقائي
df_strict = spark.read.csv(
    "sports_data.csv",
    header=True,
    schema=custom_schema
)

df_strict.printSchema()
df_strict.show()

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

7.3 قياس الأثر الأدائي لتحديد المخطط يدوياً مقابل الاستدلال التلقائي

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

أولاً، من حيث زمن الاستجابة وعمليات الإدخال والإخراج (I/O): يؤدي تحديد المخطط يدوياً إلى تخفيض زمن قراءة الملفات إلى النصف تقريباً، بفضل إلغاء مرحلة المسح الاستطلاعي المزدوج. في الملفات التي تتجاوز أحجامها 100 جيجابايت، يترجم هذا الإلغاء إلى توفير عشرات الدقائق من زمن المعالجة وتقليل استهلاك النطاق الترددي للشبكة الموزعة بنسبة تصل إلى 50%.

ثانياً، منع أخطاء تحويل الأنواع المفاجئة (Runtime Type Poisoning): عند استخدام الاستدلال التلقائي، قد يعتمد Spark على عينة أولية تبلغ ألف صف لتحديد النوع؛ فإذا كانت جميع هذه الصفوف تحتوي على أرقام صحيحة، سيصنف العمود كـ IntegerType، ولكن إذا ظهرت قيمة نصية غير متوقعة في الصف رقم مليون، فقد تنهار الوظيفة بأكملها أو تتحول تلك القيمة إلى قيمة فارغة (Null) بصورة غير منضبطة. أما المخطط الصريح فيحدد مسبقاً كيفية التعامل مع هذه الحالات وفق قواعد نمطية موحدة.

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

8. الخيارات المتقدمة ومعالجة الاستثناءات أثناء قراءة ملفات CSV

8.1 معالجة القيم المفقودة والبيانات المشوهة

نادراً ما تأتي البيانات في الواقع العملي بنقاء تام؛ إذ تشيع ملفات CSV المليئة بالقيم المفقودة، أو الحقول التي تستخدم رموزاً اعتباطية للدلالة على غياب البيانات، مثل نصوص "NA" أو "N/A" أو "null" أو حتى علامات الاستفهام "?". يتعامل Spark افتراضياً مع الحقول الفارغة فقط (الفواصل المتتالية مثل ,,) كقيم خالية (Null)، ولكنه سيعتبر النص "NA" قيمة نصية فعلية، مما يمنع تحويل العمود إلى نوع رقمي.

لتوحيد وتصحيح هذه المعالجة، توفر دالة القراءة المعلمة nullValue. عند تمرير رمز معين إلى هذه المعلمة، يقوم القارئ بتحويله فوراً إلى القيمة المنطقية null البرمجية في محرك Spark. كما يمكن استخدام المعلمة nanValue لتحديد كيفية تفسير القيم غير الرقمية (Not a Number)، بالإضافة إلى التحكم في علامات الاقتباس عبر المعلمة quote (التي تضبط افتراضياً على علامة الاقتباس المزدوجة ") والمعلمة escape لتحديد محرف الهروب (مثل الشرطة المائلة العكسية ) عند ورود علامات الاقتباس داخل النصوص الفعلية، كما هو موضح أدناه:

df_clean = spark.read.csv(
    "sports_data.csv",
    header=True,
    schema=custom_schema,
    nullValue="NA",
    quote=""",
    escape="\"
)

8.2 قراءة ملفات CSV المتعددة أو الموزعة داخل مسارات الدلائل

في معماريات بحيرات البيانات (Data Lakes)، غالباً ما تُخزن البيانات مقسمة عبر مئات الملفات الصغيرة الموزعة داخل هياكل مجلدات شجرية، وذلك كنتيجة لعمليات التصدير الدورية المتوازية. يتميز قارئ PySpark بقدرته الفائقة على استيعاب هذه الهياكل المتعددة بمرونة وسلاسة تامة دون الحاجة لكتابة حلقات تكرار خارجية في بايثون.

يتيح Spark تمرير قائمة من المسارات المنفصلة إلى دالة القراءة، أو تمرير مسار مجلد كامل ليقوم المحرك بقراءة كافة ملفات CSV المتواجدة داخله ودمجها تلقائياً في إطار بيانات موحد بشرط تطابق هياكلها. كما يدعم استخدام محارف البدل التعبيرية (Wildcards) مثل النجمة *؛ فعلى سبيل المثال، يؤدي المسار "data/2023/*/*.csv" إلى قراءة جميع ملفات CSV المتواجدة في أي مجلد فرعي تابع لعام 2023.

علاوة على ذلك، يستفيد Spark تلقائياً من تقنية “اكتشاف تقسيم المسارات” (Partition Discovery). إذا كانت الملفات محفوظة في أدلة تتبع نمط المفتاح والقيمة القياسي (مثل data/year=2023/month=10/data.csv)، فإن القارئ يستخلص قيمتي year و month تلقائياً من بنية المجلدات، ويُضيفهما كأعمدة نظامية جديدة ومفهرسة داخل إطار البيانات، مما يتيح تصفية البيانات بسرعة فائقة دون فحص الملفات غير المطلوبة.

8.3 أوضاع المعالجة: PERMISSIVE و DROPMALFORMED و FAILFAST

لإدارة مخاطر تلف البيانات والأسطر التي تنتهك المخطط الهيكلي (كأن يحتوي سطر معين على سبعة أعمدة بدلاً من ثلاثة نتيجة وجود فواصل شاردة)، يوفر Spark معياراً حاسماً يُضبط عبر المعلمة mode، ويشمل ثلاثة أوضاع تشغيلية أساسية:

1. الوضع التسامحي (PERMISSIVE): وهو الوضع الافتراضي والأكثر مرونة. في هذا الوضع، لا يُسقط Spark التطبيق عند مواجهة سطر مشوه، بل يقوم بتعيين كافة الحقول التالفة كقيم خالية (Null). والأهم من ذلك، يتيح هذا الوضع إنشاء عمود مخصص باسم _corrupt_record داخل المخطط الصريح؛ حيث يقوم Spark تلقائياً بحفظ السطر النصي المشوه بالكامل داخل هذا العمود، مما يمكن المهندسين من عزل السجلات التالفة وفحصها لاحقاً دون إيقاف خط الإنتاج.

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

3. وضع الفشل السريع (FAILFAST): وهو الوضع الأكثر صرامة والأعلى موثوقية في الأنظمة المالية والمصرفية. بمجرد أن يكتشف المحلل سطرًا واحداً تالفاً أو منتهكاً للمخطط، يُوقف Spark المعالجة فورياً، ويُطلق استثناءً برمجياً حاداً (RuntimeException)، مما يمنع تمرير أي بيانات مشبوهة إلى المستودعات النهائية. يتم ضبط هذا الخيار برمجياً كما يلي:

df_failfast = spark.read.csv(
    "sports_data.csv",
    header=True,
    schema=custom_schema,
    mode="FAILFAST"
)

9. تحسين الأداء وإدارة الموارد عند قراءة الملفات الضخمة

9.1 استراتيجيات تقسيم البيانات وإعادة التوزيع

ترتبط كفاءة المعالجة الموزعة في Spark ارتباطاً وثيقاً بعدد وحجم التقسيمات البرمجية (Partitions) التي يتم تفتيت إطار البيانات إليها عند قراءة ملفات CSV. تنشأ هذه الأقسام الأولية بناءً على حجم الكتل الفيزيائية للملفات، فإذا كان لدينا ملف بحجم 1.2 جيجابايت وكان حجم الكتلة التخزينية المعتمد 128 ميجابايت، سينشئ Spark تلقائياً حوالي 10 أقسام، وسيقوم بتخصيص مهمة حاسوبية (Task) لكل قسم.

يظهر الخلل الأدائي عندما تكون الملفات المصدرية مقسمة بشكل بالغ السوء؛ كأن تتكون البيانات من آلاف الملفات فائقة الصغر (Small Files Problem – بحجم عدة كيلوبايتات لكل ملف)، مما يؤدي إلى استنزاف موارد المشغل في جدولة آلاف المهام وتنسيقها. لعلاج ذلك وضبط عدد الأقسام بما يتلاءم مع إجمالي عدد الأنوية المتاحة في العنقود، توفر PySpark دالتين جوهريتين لإعادة التقسيم: repartition() و coalesce().

تُستخدم الدالة repartition(n) لزيادة أو تقليص عدد الأقسام بالتساوي عبر تطبيق إعادة خلط كاملة للبيانات عبر الشبكة (Full Shuffle)، وهي مثالية لتوزيع الأعباء وتجنب ظاهرة انحراف البيانات (Data Skew). أما الدالة coalesce(n)، فتُستخدم حصرياً لتقليص عدد الأقسام عبر دمج الأقسام المتجاورة على نفس العقد دون إجراء خلط شبكي كامل، مما يجعلها خياراً فائق الكفاءة والسرعة لتقليص آلاف الأقسام الصغيرة الناتجة عن قراءة مجلدات CSV الضخمة.

9.2 تقنيات التخزين المؤقت لتحسين سرعة الاستعلامات المتكررة

نظراً لطبيعة التقييم الكسول في Spark، فإن تكرار استدعاء إجراءات فورية متعددة (مثل استدعاء df.count() متبوعة بـ df.groupBy().agg()) على نفس إطار البيانات المستورد من CSV سيؤدي إلى إعادة قراءة ملفات CSV وتفكيك نصوصها مراراً وتكراراً من القرص الصلب عند كل إجراء، ما لم يتم تثبيتها في الذاكرة. لحل هذه المعضلة الأدائية، يوفر Spark آليات التخزين المؤقت عبر الدالتين cache() و persist().

تقوم الدالة cache() بتخزين إطار البيانات المحسوب في الذاكرة العشوائية للعقد العاملة وفق مستوى التخزين الافتراضي للمحرك العلائقي، وهو الاحتفاظ بالبيانات ككائنات غير متسلسلة في الذاكرة، مع إسقاط الفائض إلى القرص الصلب عند امتلاء الذاكرة (MEMORY_AND_DISK). أما الدالة persist()، فتمنح المهندس تحكماً مطلقاً في اختيار مستوى التخزين من فئة StorageLevel، مثل التخزين بالذاكرة فقط (MEMORY_ONLY)، أو التخزين المتسلسل ثنائياً لضغط المساحة (MEMORY_ONLY_SER)، أو النسخ الاحتياطي للأقسام عبر عقدتين لضمان استمرارية التشغيل دون إعادة الحساب (MEMORY_AND_DISK_2).

وللحفاظ على الصحة المعمارية للنظام ومنع اختناقات الذاكرة، يجب الالتزام بقاعدة تحرير الموارد الصارمة؛ فعند اكتمال التحليلات المتكررة على إطار البيانات المخزن مؤقتاً، يتعين على المطور استدعاء دالة الإلغاء فورياً:

df_clean.unpersist()

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

9.3 التعامل مع مشكلات الذاكرة واختناقات الإدخال والإخراج

أثناء قراءة ومعالجة ملفات CSV العملاقة، تواجه بيئات PySpark نوعين شهيرين من أخطاء الذاكرة القاتلة: خطأ نفاد ذاكرة المشغل (Driver Out of Memory Error)، وخطأ نفاد ذاكرة المنفذ (Executor Out of Memory Error). يحدث الخطأ الأول في الغالب عندما يستدعي المطور أوامر جلب غير منضبطة مثل collect() أو toPandas() على ملفات ضخمة، مما يجبر المشغل على محاولة حشر مئات الجيجابايتات داخل ذاكرته المفردة، والحل هنا هو تجنب جلب البيانات الخام واستخدام دوال الحفظ المباشر أو التجميع.

أما خطأ نفاد ذاكرة المنفذين فيحدث عادة عند قيام القارئ بمحاولة فك ضغط ملفات CSV ضخمة غير قابلة للتجزئة (Non-splittable Compressed Files مثل الملفات المضغوطة بصيغة .gz العادية). لا يمكن لمحرك Spark تجزئة ملف Gzip مفرد على عدة معالجات، مما يضطر عقدة عاملة واحدة لمحاولة قراءة وضغط الملف بأكمله، فتمتلئ الذاكرة وتنهار المهمة. للتغلب على هذه المعضلة، يُنصح بضغط ملفات CSV باستخدام خوارزميات قابلة للتجزئة مثل Bzip2 أو Snappy المهيأ للتجزئة، أو تجزئة الملفات فيزيائياً قبل وضعها في بحيرة البيانات.

بالإضافة إلى ذلك، يمكن تحسين سعة كتل القراءة عبر تعديل المعلمة المتقدمة spark.sql.files.maxPartitionBytes في إعدادات الجلسة. تتيح هذه المعلمة رفع أو خفض الحجم الأقصى للبيانات التي يتم تعيينها لقسم قراءة واحد (الافتراضي 134,217,728 بايت أو 128 ميجابايت)، مما يساعد في موازنة التوازي وتقليل التنافس الشبكي عند قراءة البيانات عبر البنى السحابية الموزعة.

10. أفضل الممارسات الأكاديمية والمقارنات المنهجية

10.1 مقارنة أدائية بين Spark DataFrame ومكتبة Pandas عند التعامل مع CSV

تُعد المقارنة المنهجية بين إطار بيانات PySpark وإطار بيانات مكتبة Pandas من أكثر المقارنات إثارة للجدل بين مهندسي البيانات. يعتمد الاختيار بينهما على طبيعة الحمل الحسابي، ومقاييس حجم البيانات، وحدود العتاد المتاح، كما يوضح التحليل المقارن التالي:

تعمل مكتبة Pandas في بيئة حوسبة أحادية النواة والذاكرة (Single-node In-memory Architecture)، وتتطلب عادة مساحة ذاكرة عشوائية حرة تعادل من 5 إلى 10 أضعاف الحجم الفيزيائي لملف CSV على القرص لضمان قراءته وتطبيق العمليات عليه دون انهيار. بالتالي، تتفوق Pandas في السرعة وزمن الوصول اللحظي (Low-latency) عند التعامل مع مجموعات البيانات الصغيرة والمتوسطة التي لا تتعدى بضعة جيجابايتات، نظراً لغياب كلفة التنسيق والاتصال الشبكي وتوليد المهام الموزعة.

في المقابل، تم تصميم PySpark للتعامل مع البيانات التي تتجاوز سعة الذاكرة الفيزيائية للجهاز الواحد (Out-of-Core Processing) وحتى مقاييس البيتابايت عبر بنية موزعة ذات تعافي تلقائي من الأخطاء. تتميز PySpark بتفوقها الكاسح في أحجام البيانات الكبيرة، وبفضل محرك التحسين Catalyst ومشروع Tungsten، تصبح عمليات التصفية والتجميع على ملفات CSV الضخمة أكثر كفاءة بمراحل من محاولات تقطيع البيانات عبر وسائط Pandas الجزئية (Chunking).

وعند الرغبة في التوفيق بين البيئتين، تبرز مكتبة Apache Arrow كجسر تقني فائق الكفاءة. من خلال ضبط المعلمة spark.sql.execution.arrow.pyspark.enabled = true، يتم تفعيل تبادل البيانات بصيغة عمودية موحدة بين PySpark و Pandas دون الحاجة لإجراء عمليات تحويل متسلسلة ثقيلة للبيانات، مما يخفض زمن التحويل بين البيئتين بنسبة تصل إلى 80% في الحالات المعتمدة.

10.2 متى ينبغي تفضيل صيغ التخزين العمودية على صيغة CSV؟

على الرغم من الانتشار الواسع لملفات CSV، إلا أنها من الناحية الهندسية تُمثل أسوأ خيار ممكن لتخزين البيانات الضخمة المخصصة للتحليلات الدورية ومسارات الإنتاج المستمرة. يجب اعتبار ملف CSV مجرد صيغة تبادلية أولية عند نقطة الاستيعاب (Data Ingestion Layer)، حيث تفرض المنهجيات الهندسية المتقدمة تحويلها الفوري والمباشر إلى صيغ تخزين عمودية حديثة مثل Apache Parquet أو Apache ORC.

تتفوق صيغة Parquet على صيغ CSV في عدة جوانب حاسمة:

أولاً، كفاءة التخزين والضغط: نظراً لتخزين البيانات عمودياً حسب كل حقل على حدة بدلاً من تخزينها كصفوف، تتشابه أنماط البيانات المتجاورة مما يسمح بتطبيق خوارزميات ضغط فائقة الفعالية مثل Snappy و Gzip و Dictionary Encoding. يؤدي هذا عادة إلى تقليص حجم تخزين ملف CSV الأصلي بنسبة تتراوح بين 75% و 90% على القرص الصلب.

ثانياً، ميزة دفع عوامل التصفية للأسفل (Predicate Pushdown): تحتوي ملفات Parquet في ترويستها وبياناتها الوصفية على إحصائيات دقيقة لأدنى وأعلى قيمة (Min/Max Statistics) لكل عمود داخل كل كتلة تخزينية. عند تنفيذ استعلام تصفية مثل points > 120، يستطيع محرك Spark قراءة هذه البيانات الوصفية وتخطي قراءة الكتل التي لا تطابق الشرط تماماً دون مسحها، وهو ما يستحيل تطبيقه على ملفات CSV التي تلزم المحرك بمسح كل حرف من بداية الملف إلى نهايته.

ثالثاً، صرامة المخطط الهيكلي وتطوره: تتضمن ملفات Parquet مخطط البيانات وأنواعها بصورة ثنائية مدمجة وثابتة، مما يلغي كلفة الاستدلال التلقائي ويمنع تماماً أخطاء تشوه السجلات المرتبطة بالفواصل وعلامات الاقتباس في ملفات CSV.

10.3 دليل إرشادي شامل لكتابة شيفرات برمجية قابلة للتوسع والصيانة

لبناء وظائف ومسارات قراءة ملفات CSV تكون قادرة على الصمود والتوسع في بيئات الإنتاج الفعلية، يُوصى باتباع القواعد والممارسات الهندسية الصارمة التالية:

1. الامتناع القاطع عن استخدام inferSchema=True في خطوط الإنتاج: يجب تحديد المخطط دائماً عبر كائنات StructType الصريحة لضمان موثوقية الأداء ومنع مسح البيانات مرتين وتثبيت عقود البيانات (Data Contracts).

2. بناء دوال استيعاب معيارية ومغلفة (Encapsulated Ingestion Functions): يُفضل تجنب كتابة استدعاءات القراءة المتناثرة في الأكواد، وتغليف منطق القراءة داخل دوال تعيد أطر بيانات موحدة وتقوم تلقائياً بتنظيف أسماء الأعمدة وضبط محددات التعامل مع الأخطاء والقيم المفقودة.

3. التوثيق الصريح لكافة المعلمات الحساسة: ينبغي دوماً تحديد معلمات sep و header و quote و nullValue و mode بشكل صريح وواضح في الشيفرة، وعدم الركون إلى السلوكيات الافتراضية لقارئ Spark؛ إذ قد تخضع الإعدادات الافتراضية للتغيير في الإصدارات المستقبلية أو تختلف باختلاف إعدادات البيئة السحابية المنفذة.

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

خاتمة

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

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

المراجع

  • Armbrust, M., Xin, R. S., Lian, C., Huai, Y., Liu, D., Bradley, J. K., Meng, X., Kaftan, T., Franklin, M. J., Ghodsi, A., & Zaharia, M. (2015). Spark SQL: Relational data processing in Spark. In Proceedings of the 2015 ACM SIGMOD International Conference on Management of Data (pp. 1383–1394). Association for Computing Machinery. https://doi.org/10.1145/2723372.2742797
  • Chambers, B., & Zaharia, M. (2018). Spark: The definitive guide: Big data processing made simple. O’Reilly Media.
  • Zaharia, M., Chowdhury, M., Franklin, M. J., Shenker, S., & Stoica, I. (2010). Spark: Cluster computing with working sets. In Proceedings of the 2nd USENIX Conference on Hot Topics in Cloud Computing (HotCloud ’10) (pp. 10–17). USENIX Association.
  • Zaharia, M., Chowdhury, M., Das, T., Dave, A., Ma, J., McCauley, M., Franklin, M. J., Shenker, S., & Stoica, I. (2012). Resilient distributed datasets: A fault-tolerant abstraction for in-memory cluster computing. In Proceedings of the 9th USENIX Conference on Networked Systems Design and Implementation (NSDI ’12) (pp. 15–28). USENIX Association.
  • The Apache Software Foundation. (2023). PySpark Documentation: pyspark.sql.DataFrameReader.csv. Apache Spark Official Project Documentation. https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrameReader.csv.html

تقييم هذا المحتوى

كن أول من يقيّم هذا المحتوى

اقتباس هذا المقال

looti, M. (2026, أكتوبر 24). كيفية قراءة ملف CSV في إطار بيانات PySpark (3 أمثلة). عرب سايكلوجي. https://arabpsychology.com/statistics/read-csv-into-pyspark-dataframe-examples/
looti, Mohammed. “كيفية قراءة ملف CSV في إطار بيانات PySpark (3 أمثلة).” عرب سايكلوجي, 24 أكتوبر 2026, https://arabpsychology.com/statistics/read-csv-into-pyspark-dataframe-examples/.
looti, Mohammed. “كيفية قراءة ملف CSV في إطار بيانات PySpark (3 أمثلة).” عرب سايكلوجي. أكتوبر 24, 2026. https://arabpsychology.com/statistics/read-csv-into-pyspark-dataframe-examples/.