مقدمة: عندما يؤدي file_get_contents إلى تعطيل الخادم
\nتخيل الموقف: أنت تكتب محللاً لسجلات الخادم، أو أداة استيراد لملف CSV يحتوي على ملايين الأسطر، أو تقوم بمعالجة تفريغ قاعدة بيانات. تكتب السطر $data = file_get_contents('bigfile.log'); — وفجأة يتعطل سكريبت PHP الخاص بك مع الخطأ Fatal error: Allowed memory size exhausted. هل هذا مألوف؟ إذا لم يكن كذلك، فأنت محظوظ. لكن عاجلاً أم آجلاً، سيواجه كل مطور هذا التحدي: "كيف أقرأ ملفاً كبيراً سطراً سطراً دون أن أُعطل الخادم؟".
على Stack Overflow، حصد سؤال "How to read a large file line by line?" مئات الآلاف من المشاهدات. وهذا ليس من قبيل الصدفة. في هذه المقالة، سنستعرض ليس طريقة واحدة فقط، بل عدة طرق — من الأساسية إلى المتقدمة. ستتعلم كيفية قراءة ملفات بحجم عشرات الجيجابايت باستخدام بضعة ميغابايت فقط من الذاكرة العشوائية. هيا بنا!
\n\nلماذا file_get_contents() و file() هما الخيار الخاطئ للملفات الكبيرة
\nقبل أن نتعمق في التحسين، دعنا نفهم لماذا الدوال القياسية غير مناسبة.
\n- \n
- file_get_contents() — تقوم بتحميل الملف بأكمله في سلسلة نصية واحدة. إذا كان وزن الملف 2 جيجابايت، سيحاول PHP تخصيص 2 جيجابايت من الذاكرة العشوائية. على استضافة بحد أقصى للذاكرة memory_limit يبلغ 128 ميجابايت، هذا خطأ فادح مضمون. \n
- file() — تقرأ الملف بأكمله أيضاً، ولكنها تقوم أيضاً بتقسيمه إلى مصفوفة من الأسطر. استهلاك الذاكرة يزداد أكثر (بالإضافة إلى التكاليف الإضافية للمصفوفة). \n
الخلاصة: إذا كان حجم الملف أكبر من 10-20% من حد الذاكرة الخاص بك — انسَ هذه الدوال. نحن بحاجة إلى نهج تدفقي.
\n\nالطريقة الأولى: الطريقة الكلاسيكية — fgets() ومؤشر الملف
\nالطريقة الأساسية والأكثر موثوقية هي فتح الملف باستخدام fopen() وقراءته سطراً سطراً باستخدام fgets(). يستخدم هذا الأسلوب المخزن المؤقت الداخلي لـ PHP، والذي يبلغ عادةً 8 كيلوبايت، ولا يقوم بتحميل الملف بأكمله في الذاكرة.
<?php\n$handle = fopen('bigfile.log', 'r');\nif ($handle) {\n while (($line = fgets($handle)) !== false) {\n // معالجة السطر\n echo $line; // أو الكتابة في قاعدة البيانات، أو حساب شيء ما\n }\n fclose($handle);\n} else {\n // خطأ في فتح الملف\n echo "تعذر فتح الملف.";\n}\n?>\n\nالمزايا: استهلاك ضئيل للذاكرة، يعمل على أي إصدار PHP.
\nالعيوب: تحتاج إلى إغلاق الملف يدوياً، لا توجد طرق ملائمة لتخطي الرؤوس أو التكرار عبر الأسطر مع العد.
\nهذا الكود سيتحمل ملفاً بأي حجم، طالما أنك لا تحاول حفظ جميع الأسطر في مصفوفة داخل الحلقة.
\n\nالطريقة الثانية: SplFileObject — نهج كائني التوجه
\nبدءاً من PHP 5.1، لدينا الفئة الرائعة SplFileObject. وهي تطبق واجهات Iterator و SeekableIterator، مما يجعلها مثالية للقراءة سطراً سطراً.
<?php\n$file = new SplFileObject('bigfile.log');\nwhile (!$file->eof()) {\n $line = $file->fgets();\n // معالجة السطر\n echo $line;\n}\n?>\n\nلكن هناك ملاحظة! SplFileObject افتراضياً قد يقوم بتحميل الأسطر في الذاكرة إذا استخدمت current() داخل foreach. لضمان استهلاك منخفض للذاكرة، استخدم fgets() في حلقة while، كما هو موضح أعلاه.
نسخة أكثر تقدماً — إعادة تعيين العلامات لتحسين الأداء:
\n\n<?php\n$file = new SplFileObject('bigfile.log');\n$file->setFlags(SplFileObject::DROP_NEW_LINE); // إزالة \\n من السطر\n\nforeach ($file as $lineNum => $line) {\n // $lineNum — رقم السطر (بدءاً من 0)\n echo "السطر $lineNum: $line\\n";\n}\n?>\n\nهام: عند استخدام foreach مع SplFileObject، لا يزال PHP يقرأ سطراً واحداً في كل مرة. الذاكرة لا تتأثر. ولكن إذا حاولت استدعاء $file->seek(1000000) ثم القفز في الملف — كن حذراً: seek يقوم بالتكرار خلال