تحويل دفتر Jupyter إلى Markdown: نُبقي السرد ونرمي الغلاف
الدفتر مستندان يتقاسمان اسم ملف واحد. هناك ما كتبته أنت — عناوين وشرح وكود والنتيجة المطبوعة التي أثبتت الفكرة — وهناك غلاف JSON الذي تحفظه الصيغة داخله. افتح ملف .ipynb في محرّر نصوص وسترى الغلاف: كل سطر من النثر مقطوعًا في سلسلة نصية مستقلة، و"execution_count": 7 فوق كل خلية، وكائنات "metadata": {} فارغة في كل مكان، وفي مكان ما بالمنتصف ربع ميغابايت من base64 لا يظهر إلا مخطط انتشار صغيرًا واحدًا.
التحويل إلى Markdown يرمي الغلاف ويُبقي المستند. خلايا markdown لديك كانت Markdown أصلًا، فتمرّ كما هي. وخلايا الكود تصير كتلًا محاطة بأسيجة موسومة بلغة النواة، وهي الصيغة التي يفهمها أصلًا كل نموذج وكل ملف README وكل مولّد مواقع ساكنة. أفلِت ملف .ipynb أعلاه وسيجري التحويل داخل تبويب المتصفح هذا — لا يُرفع الملف إلى أي مكان.
أين يقع الوزن فعلًا
يفترض الناس أن الدفتر الضخم ضخم بسبب الكود. ونادرًا ما يكون كذلك. الوزن يجلس في أربعة مواضع، مرتّبة تنازليًا:
- مخرجات الصور بترميز base64. كل استدعاء
plt.show()يكتب صورة PNG المرسومة داخل الملف نصًّا بترميز base64. والشكل الواحد يتراوح عادة بين 200 و400 كيلوبايت من المحارف. ودفتر فيه اثنا عشر رسمًا هو في معظمه رسوم. ولا شيء من ذلك يعني شيئًا لمن يقرأ النص. - تمثيلات HTML لإطارات البيانات. يُخرج Pandas للنتيجة الواحدة
text/plainوtext/htmlمعًا. ونسخة HTML تحمل أنماطًا مضمّنة و<table>بوسم لكل خلية، فتبلغ عشرين ضعف حجم النسخة النصية وهي تقول الشيء نفسه بالضبط. - لقطات أشرطة التقدّم. حلقة tqdm تكتب سطرًا جديدًا عند كل تحديث، مفصولًا بمحارف إرجاع السطر. فينتهي الملف وفيه مئات النسخ من شريط لم يُعرض على الشاشة سوى مرة واحدة.
- بيانات وصفية لكل خلية. تافهة منفردة، غير تافهة مجتمعة: معرّفات الخلايا وعدّادات التنفيذ وأعلام الطيّ وكائنات بيانات وصفية فارغة، عبر بضع مئات من الخلايا، تتراكم إلى حجم ملموس.
التحويل أعلاه يعالج كلًّا من هذه. تُحصى حمولات الوسائط وتُستبدل بسطر نائب واحد كي يبقى موضع الشكل مرئيًا. وحين تُقدَّم النتيجة نصًّا عاديًا وHTML معًا، يفوز النص العادي. وتتقلّص سلاسل إرجاع السطر إلى الحالة الأخيرة للسطر. وتُحذف البيانات الوصفية بالكامل. وتُبلّغك الأداة بما أزالته، فيكون التوفير مرئيًا لا مزعومًا.
لماذا تهمّ الكتل المسيّجة أكثر مما تبدو
أنفع ما يفعله Markdown للدفتر هو أن يعلّم الحدّ بين النثر والكود تعليمًا صريحًا. ففي JSON الخام لا يوجد ذلك الحدّ إلا حقلًا اسمه "cell_type" يبعد أسطرًا عن المحتوى. وإن سُطّح الملف بلا عناية التصق الشرحُ بالكود الذي يصفه، واضطرّ القارئ — إنسانًا كان أم نموذجًا — إلى الاستدلال على الفرق من الصياغة وحدها.
السياج الموسوم باللغة يزيل هذا التخمين. وهو ينقل لغة النواة معه: يقرأ المحوّل language_info من البيانات الوصفية للدفتر، ويرجع إلى kernelspec عند غيابه، فيُوسم دفتر R أو Julia بـ R أو Julia بدل أن يُصنَّف Python بصمت. وتحصل المخرجات على سياج خاص بها بلا وسم أسفل الخلية التي أنتجتها، يسبقه سطر Output: بسيط، فتبقى علاقة السبب بالنتيجة ظاهرة دون اختراع صياغة لا يملكها Markdown.
آثار الأخطاء تستحقّ البقاء
يغري المرءَ أن يحذف مخرجات الأخطاء مع ما يحذفه. وهذا في العادة قرار خاطئ. فإن سلّمت دفترًا إلى نموذج وسألته لماذا تفشل خلية، كان أثر الخطأ هو السؤال كلّه. وما يجعل هذه الآثار كريهة في صورتها الخام ليس محتواها بل رموز الألوان ANSI التي يلفّها بها IPython — محارف هروب تظهر لونًا مقروءًا في الطرفية وخربشة مثل ESC[0;31m في أي مكان آخر.
تُنزع تلك السلاسل ويبقى نصّ الأثر. والطويل منها يُختصر من الوسط لا من النهاية، لأن الإطارات المفيدة في المكدّس العميق هي الأولى والأخيرة، أما المئتا سطر من دواخل المكتبات بينهما فهي بالضبط ما كنت ستتخطّاه.
الدفاتر داخل مخرجات مستودع
التحويل نفسه يعمل داخل أدوات GitHub وGitLab والمجلد المحلي في هذا الموقع. اختر ملف .ipynb في مستودع، وسيصل إلى المخرجات خلايا مقروءة لا جدارًا من JSON. وهذا أهمّ مما يبدو: ففي مستودعات علم البيانات يسكن التفكير الفعلي في الدفاتر غالبًا، وقبل ذلك كانت هي الملفات التي تضطر إلى إلغاء تحديدها كي تبقى المخرجات صالحة.
وفي هذا السياق يكون حدّ المخرجات أضيق مما هو عليه في هذه الصفحة، لأن مخرجات المستودع سياق لنموذج، ومخرجات الخلايا أقلّ أجزائه قيمة لكل token. وتسجّل ملاحظة في النص المحوَّل مقدار ما أُزيل، فلا يختفي شيء بصمت.
الدفاتر القديمة تعمل أيضًا
الإصدار الرابع من الصيغة يضع الخلايا في مصفوفة cells على المستوى الأعلى. أما الإصدار الثالث، الذي ما زال منثورًا في المستودعات العامة من مطلع العقد الثاني من الألفية، فيُعشّشها مستوى أعمق داخل worksheets ويسمّي حقل المصدر input بدل source. وكلا الشكلين يُقرأ. وكذلك نوع المخرجات pyout الذي استعمله الإصدار الثالث حيث يقول الرابع execute_result.
وأمران لا يُحاولان عمدًا. أدوات الدفتر التفاعلية — المزالق والرسوم المدعومة بـipywidgets — تخزّن حالتها في كتلة بيانات وصفية منفصلة ولا تُنتج نصًّا مفيدًا؛ والنائب يخبرك أن واحدة كانت هنا. وترتيب تنفيذ الخلايا يُحفظ كما يظهر في الملف، لا مرتّبًا بعدّاد التنفيذ، لأن الترتيب الذي تقرأ به الدفتر هو الترتيب الذي كُتب به.
لا شيء يُرفع
تحمل الدفاتر أشياء ينساها الناس: مفتاح API لُصق في خلية أثناء تصحيح خطأ، وسلسلة اتصال بقاعدة بيانات، وشريحة من بيانات الإنتاج طُبعت للتأكد من عملية دمج. والتحويل يجري بلغة JavaScript داخل هذا التبويب. لا خطوة رفع، ولا نسخة على خادم، ولا حذف تطلبه لاحقًا. أغلق التبويب وقد انتهى الأمر.
وإن أردت نصًّا عاديًا بدل Markdown — بلا أسيجة ولا بنية، النثر والكود فقط — فإن محوّل الدفاتر إلى نص يفعل ذلك، والملف الذي اخترته ينتقل معك.