التعامل مع الـ Pagination: كيف تجلب آلاف البيانات دون انهيار السكربت.

دقائق القراءة: 6

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

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

ما المقصود بالـ Pagination عملياً؟

المقصود هو أن يعيد Endpoint النتائج على دفعات صغيرة بدلاً من تسليم كل شيء دفعة واحدة. بدلاً من طلب 50 ألف عنصر في استجابة واحدة، تطلب 100 عنصر في كل مرة، ثم تنتقل إلى الصفحة التالية حتى تنتهي.

هذا التصميم يحقق عدة فوائد مهمة في الأتمتة:

الأنواع الشائعة للـ Pagination

1) Pagination بالـ Offset و Limit

هذا النوع هو الأبسط والأكثر انتشاراً في واجهات REST API. ترسل عادة متغيرين مثل limit=100 وoffset=200 للحصول على الدفعة المطلوبة.

مثال توثيقي:
GET /users?limit=100&offset=200
يعني: أرسل 100 سجل بدءاً من السجل رقم 201.

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

2) Pagination بالـ Page Number

بعض الأنظمة تستخدم متغيراً مثل page=3 مع per_page=100. هذا الأسلوب مناسب للواجهات الإدارية ولوحات العرض، لكنه يحمل القيود نفسها تقريباً من ناحية تغير البيانات أثناء عملية السحب الطويلة.

3) Cursor Pagination

هذا هو الأسلوب الأكثر موثوقية عند التعامل مع كميات كبيرة. بدلاً من حساب الصفحة التالية رقمياً، يعيد الخادم مؤشراً مثل next_cursor أو next_page_token. في الطلب التالي، تعيد إرسال هذا المؤشر.

هذا يضمن غالباً تنقلاً أكثر استقراراً بين الدفعات، خصوصاً إذا كانت البيانات تُرتب حسب تاريخ الإنشاء أو المعرف الداخلي. لذلك تفضله كثير من الخدمات الحديثة، كما ستجده في بعض منصات استخدام Pipedream للمبرمجين: دمج Node.js مع الأتمتة أو التكاملات كثيفة البيانات.

لماذا تنهار السكربتات عند التعامل الخاطئ مع Pagination؟

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

النهج الأفضل هو المعالجة التدريجية Batch Processing: اجلب دفعة، عالجها، خزّن الناتج أو أرسله، ثم انتقل للدفعة التالية. بهذه الطريقة تبقى الذاكرة مستقرة تقريباً مهما زاد حجم البيانات.

بناء سكربت JavaScript آمن لجلب آلاف السجلات

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

const API_URL = 'https://api.example.com/orders';
const API_TOKEN = process.env.API_TOKEN;

async function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function fetchAllOrders() {
  let nextCursor = null;
  let hasMore = true;
  let processedCount = 0;

  while (hasMore) {
    const url = new URL(API_URL);
    url.searchParams.set('limit', '100');

    if (nextCursor) {
      url.searchParams.set('cursor', nextCursor);
    }

    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`,
        'Accept': 'application/json'
      }
    });

    if (response.status === 429) {
      await sleep(2000);
      continue;
    }

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    const data = await response.json();

    for (const order of data.items) {
      await processOrder(order);
      processedCount++;
    }

    nextCursor = data.next_cursor;
    hasMore = Boolean(nextCursor);

    await sleep(300);
  }

  console.log(`Processed ${processedCount} orders`);
}

async function processOrder(order) {
  console.log(`Processing order ${order.id}`);
}

fetchAllOrders().catch(console.error);

هذا النمط البسيط يتفوق على محاولات السحب العشوائي لأنه يركز على الاستقرار. ويمكنك تطويره بربطه مع قاعدة بيانات أو صف انتظار أو حتى مع الجدولة الزمنية (CRON Jobs): كيف تجعل السكربت يعمل وأنت نائم لتنفيذ المزامنة تلقائياً كل ساعة أو كل ليلة.

كيف توثق نقطة الاتصال قبل كتابة الكود؟

قبل أن تكتب أول سطر، راجع توثيق Endpoint بدقة. فهمك للشكل المتوقع للاستجابة سيحدد بنية الحلقة البرمجية، وطريقة اكتشاف الصفحة التالية، وآلية التعامل مع الأخطاء، وحتى طريقة الحفظ المرحلي للبيانات.

نقاط يجب التأكد منها في توثيق الـ API:
هل تستخدم الواجهة offset أم cursor؟
ما الحد الأقصى لقيمة limit؟
هل يوجد حقل مثل has_more أو next_page؟
ما رموز الأخطاء المتوقعة؟ وهل يعيد الخادم رأس Retry-After؟
هل يتطلب الوصول Bearer Token أو مفتاحاً ثابتاً؟

ولفهم قراءة هذا النوع من المستندات بسرعة، يفيدك الرجوع إلى توثيق الـ API: كيفية قراءة مستندات Swagger و Redoc، وكذلك تشريح طلب الـ API: الـ Endpoint، الـ Headers، والـ Body.

أفضل الممارسات في مشاريع الأتمتة الواقعية

احفظ حالة التنفيذ

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

لا تخلط بين السحب الكامل والتحديث اللحظي

السحب الكامل Full Sync مناسب للبداية أو للمراجعات الدورية. أما بعد ذلك، فالأفضل الانتقال إلى تحديثات تفاضلية باستخدام حقل تاريخ مثل updated_at أو الاعتماد على الفرق بين الـ API والـ Webhook: “لا تتصل بنا، نحن سنتصل بك” عندما يكون متاحاً. بهذه الطريقة تقلل عدد الطلبات بشكل كبير.

أمّن المفاتيح ولا تكتبها داخل الكود

كثير من سكربتات الجلب تفشل أمنياً قبل أن تفشل تقنياً. إذا كان سكربتك يستخدم API Key أو Bearer Token، فاحتفظ به في متغيرات بيئة كما شرحنا في أمن البيانات: كيفية تخزين المفاتيح السرية في ملفات .env.، وراجع أيضاً التعامل مع الـ Bearer Tokens وتجديد الصلاحيات آلياً إذا كانت الجلسات قصيرة العمر.

متى لا يكون Pagination وحده كافياً؟

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

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *