تحليل السجلات
درس 4 من 11
4تحليل السجلات
أول مرة تقرأ فيها تيّارًا لا نهائيًا بدل ملف: سطرًا سطرًا، تتجاهل الفاسد، وتطبع إحصاءات كل عشرة أسطر وعند المقاطعة.
ماذا تشاهد في الرسم: كل سطر يُقسم، ويُفحص، ثم يضيف إلى الحجم الكلّي وعدّاد حالته، والإحصاء يُطبع كل عشرة أسطر.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
تخيّل شريطًا لا ينتهي من الإيصالات يخرج من آلة. لا تستطيع انتظار نهايته، بل تقرأ كل إيصال حين يصل، وتهمل الممزّق منها، وكل عشرة إيصالات تكتب ملخصًا: كم بعت، وكم مرة ظهر كل رمز.
تقرأ sys.stdin سطرًا سطرًا، وتطابق كل سطر بتعبير نمطي regex، وتتخطى غير المطابق، وتحدّث عدّادات في قاموس. تطبع الإحصاءات كل عشرة أسطر وعند المقاطعة بـKeyboardInterrupt، في ذاكرة ثابتة مهما طال التيار.
المتطلبات قبل البداية
المدخل القياسي sys.stdin
كائن تقرأ منه كما تقرأ من ملف، لكن مصدره ما يكتبه برنامج آخر أو المستخدم. وfor line in sys.stdin يقرأ سطرًا سطرًا بلا تحميل الكل في الذاكرة.
القاموس كعدّاد
counts.get(k, 0) + 1 هي الصيغة الآمنة: ترجع صفرًا إذا كان المفتاح جديدًا بدل أن ترفع KeyError.
try/except المتداخلة
واحدة للتحويل الفاشل داخل السطر (ValueError)، وواحدة أكبر للمقاطعة (KeyboardInterrupt). لكل خطأ موضعه.
حارس __main__
النص يطلب صريحًا ألا يُنفَّذ الكود عند الاستيراد. بدون الحارس، مجرد import يبدأ قراءة المدخل.
المفاهيم الأساسية
المطلوب: اقرأ أسطرًا من المدخل القياسي، وبعد كل عشرة أسطر (أو عند Ctrl+C) اطبع مجموع الأحجام وعدد الأسطر لكل رمز حالة. الأسطر غير المطابقة للصيغة تُتجاهل.
1تيّار، لا ملف
هذا أول اختلاف جوهري عن كل ما سبق. البرنامج ما يعرف متى ينتهي المدخل، قد يكون ملفًا من عشرة أسطر، أو مولّدًا يكتب أبدًا. لذلك:
- لا نستخدم
sys.stdin.read()، لأنه يحاول تحميل كل شيء ويعلّق مع تيّار لا ينتهي. - نستخدم
for line in sys.stdin، وهو يقرأ سطرًا ويعالجه ثم يطلب الذي بعده. - الذاكرة تبقى ثابتة مهما كان حجم المدخل.
2تشريح السطر: لماذا الفهرسة من النهاية؟
1.2.3.4، عنوان IP-، فاصل ثابت[2026-10-22 12:00:00.000000]، التاريخ (حقلان لأنه فيه مسافة!)"GET /projects/260 HTTP/1.1"، الطلب (ثلاثة حقول)200، رمز الحالة ← parts[-2]128، حجم الملف ← parts[-1]التاريخ والطلب فيهما مسافات، فعدد الحقول من البداية غير ثابت. لكن القيمتين المطلوبتين هما دائمًا الأخيرة وما قبلها. parts[-1] وparts[-2] تعملان مهما تغيّر ما قبلهما، وهذه ليست حيلة، بل قراءة صحيحة للصيغة.
split وليس تعبيرًا نمطيًا؟التعبير النمطي (regex) يعمل، لكنه أطول وأصعب في الصيانة وأبطأ لكل سطر. النص يقول «السطر غير المطابق يُتجاهل»، وsplit() مع try/except ValueError تنفّذ هذا بسطرين. اختر أبسط أداة تحقّق المطلوب.3نمط المُجمِّع (accumulator)
| المتغيّر | نوعه | دوره |
|---|---|---|
total_size | int | مجموع تراكمي لكل الأحجام الصحيحة |
counts | dict | رمز الحالة ← عدد مراته |
lines | int | عدد الأسطر الصحيحة فقط، وهو ما يحدّد وقت الطباعة |
لاحظ الدقّة في الثالث: الأسطر الفاسدة لا تُحسب، فالطباعة تأتي بعد كل عشرة أسطر مقبولة، لا عشرة أسطر مقروءة.
4رموز الحالة: الترتيب والتجاهل
قاعدتان في النص يسهل نسيانهما:
- الرمز غير الموجود في القائمة لا يُطبع له شيء، ولذلك نستخدم
status in CODESونضيف للقاموس فقط إذا كان فيCODES. - الرموز تُطبع تصاعديًا، و
sorted(counts)تتولّى هذا. لاحظ في المخرج الحقيقي أن 200 قبل 301 قبل 404، مع أن 301 أُدخلت أولًا.
ولاحظ شيئًا دقيقًا: حجم الملف يُجمع دائمًا حتى لو كان الرمز خارج القائمة، لأن النص يفصل بين «مجموع الأحجام» و«عدد الأسطر لكل رمز».
5المقاطعة: except لا finally
عند Ctrl+C يرفع بايثون KeyboardInterrupt. نحتاج نطبع الإحصاءات ثم نترك الاستثناء يستمر (raise) حتى يخرج البرنامج بالرمز الصحيح.
finally بدل except KeyboardInterrupt. finally ينفّذ في كل الحالات، فعند النهاية الطبيعية يطبع مرة من داخله ومرة من شرط النهاية، فتطلع إحصاءاتك مكرّرة. finally للتنظيف دائمًا، except لحالة محدّدة.كيف يعمل؟
ثلاثة أشكال: خط الأنابيب بين البرنامجين، ثم قرار السطر الواحد، ثم الفرق بين النهاية الطبيعية والمقاطعة.
شكل 9، خط الأنابيب: المولّد يكتب والمحلّل يقرأ سطرًا سطرًا، الاثنان يعملان معًا ولا أحد ينتظر الآخر لينتهي.
شكل 10، قرار السطر الواحد: التجاهل لا يزيد العدّاد، والرمز خارج القائمة يُجمع حجمه ولا يُعدّ، والطباعة مشروطة بمضاعفات العشرة.
شكل 11، النهايتان: الطبيعية تطبع مرة أخيرة فقط إذا لم تكن آخر طباعة عند السطر العاشر، والمقاطعة تطبع ثم تُعيد رفع الاستثناء.
الحل خطوة بخطوة
نكتب الحل، نشغّله على مدخل ثابت نعرف جوابه، ثم على مولّد عشوائي، ثم نكسره قصدًا. كل مخرج تحت حقيقي.
الخطوة 1، الحل
#!/usr/bin/python3
"""0-stats"""
import sys
CODES = (200, 301, 400, 401, 403, 404, 405, 500)
def report(total_size, counts):
"""Print the metrics gathered so far."""
print("File size: {:d}".format(total_size))
for code in sorted(counts):
print("{:d}: {:d}".format(code, counts[code]))
def main():
total_size, counts, lines = 0, {}, 0
try:
for line in sys.stdin:
parts = line.split()
if len(parts) < 2:
continue
try:
size = int(parts[-1])
status = int(parts[-2])
except ValueError:
continue
total_size += size
if status in CODES:
counts[status] = counts.get(status, 0) + 1
lines += 1
if lines % 10 == 0:
report(total_size, counts)
except KeyboardInterrupt:
report(total_size, counts)
raise
if lines % 10 != 0:
report(total_size, counts)
if __name__ == "__main__":
main()الخطوة 2، مدخل ثابت نحسبه بأيدينا
$ cat fixed.txt 1.2.3.4 - [2026-10-22 12:00:00.000000] "GET /projects/260 HTTP/1.1" 200 128 5.6.7.8 - [2026-10-22 12:00:01.000000] "GET /projects/260 HTTP/1.1" 404 512 9.9.9.9 - [2026-10-22 12:00:02.000000] "GET /projects/260 HTTP/1.1" 200 64 oops this line is garbage 1.1.1.1 - [2026-10-22 12:00:03.000000] "GET /projects/260 HTTP/1.1" 301 32 2.2.2.2 - [2026-10-22 12:00:04.000000] "GET /projects/260 HTTP/1.1" 500 1024 3.3.3.3 - [2026-10-22 12:00:05.000000] "GET /projects/260 HTTP/1.1" 200 16 $ ./0-stats.py < fixed.txt File size: 1776 200: 3 301: 1 404: 1 500: 1
تحقّق يدوي: 128+512+64+32+1024+16 = 1776 ✓ والسطر الفاسد لم يُحسب في المجموع ولا في العدّ.
الخطوة 3، عشرة أسطر من مولّد (الطباعة عند العاشر)
$ ./0-generator.py | ./0-stats.py File size: 4039 301: 4 200: 1 404: 1 405: 1 500: 3
هنا بالضبط lines = 10، فالطباعة جاءت من داخل الحلقة، وشرط النهاية lines % 10 != 0 لم يُطبّق، فلا تكرار.
الخطوة 4، حالات لا تطبع شيئًا
$ printf '' | ./0-stats.py # empty input: nothing to report $ printf 'hello\nworld\n' | ./0-stats.py # all lines malformed: nothing to report $ echo 'x - [d] "GET /" 200 abc' | ./0-stats.py # size not a number: line skipped
الخطوة 5، المقاطعة بـ Ctrl+C
^C
File size: 300
200: 3
Traceback (most recent call last):
File "./0-stats.py", line 41, in <module>
main()
File "./0-stats.py", line 18, in main
for line in sys.stdin:
KeyboardInterruptالإحصاءات تخرج على stdout والتتبّع على stderr، والبرنامج يخرج بالرمز 130 كما يتوقّع الـshell من برنامج قُوطع.
الخطوة 6، امشِ على المدخل الثابت سطرًا سطرًا
تتبّع ستة أسطر، أحدها فاسد
السطر 1. … "GET /projects/260 HTTP/1.1" 200 128. parts[-2:] = ['200','128']، فنقبله. total_size = 128 وcounts = {200: 1}. lines = 1، فما فيه طباعة.
السطر 2. رمزه 404 وحجمه 512. total_size = 640 وcounts = {200:1, 404:1}.
السطر 3. 200 و64. total_size = 704 وcounts = {200:2, 404:1}. lines = 3.
السطر 4: oops this line is garbage. parts = ['oops','this','line','is','garbage']، فطوله 5 ≥ 2، فنتجاوز الفحص الأول. لكن int('garbage') يرفع ValueError، فنتخطّى السطر، وlines تبقى 3.
الأسطر 5 إلى 7. 301/32 و500/1024 و200/16. المجموع النهائي 1776 وcounts = {200:3, 301:1, 404:1, 500:1}. lines = 6، و6 % 10 != 0، فنطبع مرة أخيرة.
الطباعة. sorted(counts) تعطي [200, 301, 404, 500]، تصاعديًا، لا بترتيب الظهور. لاحظ أن 301 ظهرت قبل 404 في المدخل، لكنها تُطبع بعدها... لا، قبلها: الترتيب العددي 200 < 301 < 404 < 500.
اعتقاد شائع × الحقيقة
| الاعتقاد | الحقيقة |
|---|---|
| «أقرأ كل المدخل ثم أحلّله.» | sys.stdin.read() يعلّق مع تيّار لا ينتهي، ويستهلك ذاكرة بحجم المدخل. التيّار يُقرأ سطرًا سطرًا. |
«finally أنظف من except للتنظيف.» | finally للتنظيف الذي يجب أن يحدث دائمًا. هنا الطباعة ليست تنظيفًا بل سلوكًا لحالة واحدة، وfinally يسبّب طباعة مزدوجة عند النهاية الطبيعية. |
| «العدّاد يزيد مع كل سطر مقروء.» | يزيد مع كل سطر صحيح فقط. الفاسد لا يُحسب، فالطباعة عند كل عشرة أسطر مقبولة. |
| «الرمز غير المعروف أطبعه بصفر.» | لا تطبع له شيئًا أبدًا. النص صريح: «إذا لم يظهر الرمز أو لم يكن عددًا صحيحًا فلا تطبع له شيئًا». |
| «حجم السطر الذي رمزه غير معروف لا يُجمع.» | يُجمع. المجموع لكل الأسطر الصحيحة الصيغة، وتصنيف الرموز شيء آخر منفصل. |
| «القاموس في بايثون غير مرتّب فالترتيب عشوائي.» | القواميس تحفظ ترتيب الإدخال منذ بايثون 3.7، لكن المطلوب ترتيب عددي تصاعدي، فلا بد من sorted() في كل الأحوال. |
أين نستخدمه فعليًا؟
مراقبة الخوادم
كل أداة مراقبة تفعل هذا بالضبط على نطاق أكبر: تقرأ سجلات متدفقة، تجمّع رموز الحالة، وتنبّه لما ترتفع نسبة 5xx.
أدوات سطر الأوامر
برنامجك صار حلقة في خط أنابيب: cat log | ./0-stats.py. هذا نموذج يونكس كله، برامج صغيرة تتكلم عبر stdin/stdout.
التحليلات في الزمن الحقيقي
عدّ الأحداث على نافذة متحرّكة (كل عشرة أسطر هنا) هو جوهر معالجة التيّارات، ونفس المنطق يعمل في Kafka أو Flink بحجم أكبر.
تدقيق الأمن
ارتفاع مفاجئ في 401 أو 403 في سجلّات الدخول هو أول إشارة لمحاولة اختراق، وهذا عدّاد مثل الذي كتبته.
جرّب بنفسك
بدّلت except KeyboardInterrupt بـ finally. ماذا يصير عند نهاية مدخل فيه 7 أسطر صحيحة؟
finally ينفّذ عند الخروج من try بأي طريقة، بما في ذلك النهاية الطبيعية. فتُطبع الإحصاءات من داخله، ثم يُنفَّذ if lines % 10 != 0 بعده فتُطبع ثانية. except KeyboardInterrupt ينفّذ فقط عند المقاطعة فعلًا.
بطاقات مراجعة سريعة
for line in sys.stdin لا sys.stdin.read()؟parts[-1] وparts[-2]؟continue بلا زيادة.except أم finally للمقاطعة؟except KeyboardInterrupt. finally يطبع مرة زائدة عند النهاية الطبيعية.__main__ هنا؟import يبدأ قراءة المدخل فورًا.تحقّق من فهمك
1. لماذا نفحص len(parts) < 2 قبل التحويل؟
لأن parts[-2] على قائمة فيها عنصر واحد يرفع IndexError، وهو خطأ لا يلتقطه except ValueError. الفحص يحمي التحويل قبل أن يحدث.
2. مدخل فيه 10 أسطر صحيحة. كم مرة تُطبع الإحصاءات؟
مرة واحدة فقط، من داخل الحلقة عند السطر العاشر. شرط النهاية lines % 10 != 0 لا يُطبّق لأن 10 % 10 == 0. لو طُبعت مرتين فعندك خطأ في الشرط.
3. (اكتشف الخطأ) برنامجك يطبع File size: 0 عند مدخل فاضٍ تمامًا. مقبول؟
يعتمد على تفسيرك: النص يقول «اطبع بعد كل عشرة أسطر»، فمع صفر أسطر لا يوجد شيء يُطبع، والحل أعلاه لا يطبع شيئًا. الأهم أن تكون واعيًا بالقرار وتوثّقه، لا أن تتركه مصادفة.
4. أحدهم استخدم collections.Counter بدل القاموس. أفضل أم أسوأ؟
أنظف فعلًا: counts[status] += 1 يكفي. لكن النص يمنع المكتبات غير القياسية فقط، وcollections قياسية فهي مسموحة. القاموس البسيط يبقى أوضح لمن يتعلّم، لأنه يكشف المنطق بدل أن يخفيه.
5. (قرار) تريد نافذة متحرّكة: «الإحصاءات لآخر 100 سطر» لا «من البداية». ماذا يتغيّر؟
تحتاج تحفظ الأسطر نفسها لا المجاميع فقط، لأن المجموع التراكمي لا يمكن التراجع عنه. الحل المعتاد collections.deque بطول 100: تضيف من الأمام وتطرح ما يخرج من الخلف. وهذا يوضّح فرقًا مهمًا: المجاميع التراكمية رخيصة، والنوافذ المتحرّكة تحتاج ذاكرة بحجم النافذة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي3
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.