← كل المقالات

مراقبة المناقصات: الإشعارات التي لم يرها الـ Scraper الخاص بك أبداً

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

216,000 يورو. دون هذا الحد، لا يلتزم أي مجلس بلدي أوروبي يشتري خدمات بنشر مناقصته على TED، السجل المركزي للاتحاد الأوروبي.

وهذا يعني أن العقود التي يمكن لمقدم عطاءات صغير الفوز بها واقعيا توجد في مكان آخر: على بوابة المجلس أو المنطقة أو مرفق الخدمات أو الوزارة التي تتولى عملية الشراء.

التحدي

تنفق أكثر من 250,000 جهة عامة في الاتحاد الأوروبي نحو 2.6 تريليون يورو سنويا على الأشغال والتوريدات والخدمات، وفق إحصاءات المفوضية الأوروبية. تحكم توجيهات الاتحاد الأوروبي فقط العقود التي تتجاوز العتبات المالية، والتي تبلغ اعتبارا من 1 يناير 2026 نحو 140,000 يورو لتوريدات وخدمات الحكومة المركزية، و216,000 يورو للسلطات المحلية والإقليمية، و5,404,000 يورو للأشغال. ودون هذه الحدود، تُطبق القواعد الوطنية ويكون النشر على TED طوعيا.

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

وصفت BidPrime، التي تبيع بيانات عطاءات القطاع العام في أمريكا الشمالية، النسخة الأمريكية من هذا الأمر بأنها "آلاف من تدفقات العمل" في مايو 2026: بوابات تخنق الوصول المؤتمت عبر rate limit، وأنظمة مشتريات تتطلب حسابا أو جلسة، ومرفقات مخزنة بمعزل عن الإشعار، وعناوين URL تتغير دون سابق إنذار. ولا يوجد شيء في هذه القائمة يقتصر على الولايات المتحدة وحدها.

لكن الفشل المؤلم في مراقبة المناقصات ليس البوابة التي ترفض وصولك. فهذه تظهر في سجلاتك logs. بل هو عملية التشغيل التي تنتهي باللون الأخضر دون أن تجلب أي شيء.

يمكن لبوابة مجلس بلدي أن ترد على جلسة منتهية بصفحة عادية بحالة HTTP 200. ويمكن أن يظهر إشعار صيانة مكان جدول النتائج. والبحث الذي أرسلته بالأمس قد يعيد إليك الصفحة الأولى مجددا، لأن الصفحة الثانية كانت موجودة فقط لملف cookie الذي طلبها. يجد الـ parser الخاص بك صفوفا فارغة ويكتب "لا توجد مناقصات جديدة اليوم". وخلافا للسعر الفائت، فإن المناقصة الفائتة لا تعود: فبحلول الوقت الذي يلاحظ فيه أي شخص هذه الفجوة، يكون الموعد النهائي للتقديم قد انقضى ولا تعود للإشعار أي قيمة لمقدم العطاء.

النهج: اجعل كل عملية fetch تثبت ما شاهدته

ابدأ بما لا يتطلب scraping. توفر API الخاصة بـ TED إمكانية الوصول المجهول إلى الإشعارات المنشورة دون الحاجة إلى مفتاح، لذا يمكنك سحب البيانات التي تتجاوز الحد الأدنى من هناك. في المملكة المتحدة، أصبحت منصة Find a Tender هي المنصة المركزية التي تنشر فيها جميع جهات التعاقد معلومات المشتريات منذ 24 فبراير 2025، وفقًا لموقع GOV.UK. تحقق مما يغطيه السجل المركزي بالفعل قبل كتابة parser لبوابة يكرر محتواها.

بالنسبة لكل ما تبقى، هناك ثلاث قواعد تؤدي معظم الغرض.

1. حدد إجابة حقيقية، بما في ذلك الإجابة الفارغة. تحتوي صفحة النتائج على ترويسة العمود الخاصة بها ("تاريخ الإغلاق"، بالصياغة المستخدمة في البوابة نفسها). بينما تحتوي الصفحة الفارغة الصريحة على نص الحالة الفارغة الخاص بالبوابة. أي شيء غير ذلك يعد فجوة وليس صفرًا. أضف كلا النصين إلى قائمة validate.data.accept في الـ request، وستؤدي صفحة انتهاء صلاحية الجلسة إلى فشل الـ request بدلاً من اعتباره يومًا بلا نشاط. ترفض محركاتنا بالفعل اعتبار صفحة التحقق التي تتعرف عليها بمثابة نجاح، ولكن لا يوجد محرك يعرف كيف تصيغ كل جهة محلية صفحة انتهاء مهلة الاتصال الخاصة بها (هذا الجزء يجب أن تحدده بنفسك). عملية المطابقة هي مطابقة بسيطة ومتحسسة لحالة الأحرف، لذا انسخ النص من الصفحة بدلاً من إعادة كتابته يدويًا. لقد شرحنا الآلية التقنية عندما بدأت قواعد validate بتحديد حالة النجاح، ونفس المشكلة من جانب السلاسل الزمنية في مقال عندما يبدو الحظر كنقطة بيانات.

2. تحقق من العدد الذي تعرضه البوابة. توضح معظم قوائم النتائج عدد الإشعارات المتطابقة ("1-20 من 143"). استخرج هذا الرقم وقارنه بعدد الصفوف التي قمت بتخزينها. إذا اختلفا، فإن عملية التشغيل لم تنته بعد، بغض النظر عما تقوله رموز الحالة status codes.

3. اعثر على المسار مرة واحدة، ثم أعد استخدامه. تميل البوابات المقسمة إلى صفحات والمرتبطة بجلسات محددة إلى توقع طلبات الصفحات التالية من نفس العنوان ونفس المتصفح. باستخدام منصة مثل FourA، يكتشف خيار Auto مسارًا يعمل مع الصفحة الأولى ويعيد بيانات جلسته: معرف الـ proxy و User-Agent وملفات تعريف الارتباط cookies من المحاولة الناجحة. تمر كل صفحة بعد ذلك عبر Single على نقطة الخروج تلك، وهو مسار بيئة الإنتاج الفعلي. يُعد Auto مجرد أداة لاكتشاف المسار فقط.

import math, re, requests

API = "https://api.foura.ai/api"
H = {"X-API-Key": "pk_live_..."}
LIST = "https://tenders.example-council.gov/open?page={}"
PER_PAGE = 20

# A real answer is the results table or the portal's own empty-state line.
# An expired session or a maintenance notice served at 200 is neither.
CHECK = {"data": {"accept": ["Closing date", "No open tenders"]}}

def fetch(endpoint, body):
    r = requests.post(f"{API}/{endpoint}", headers=H, json=body).json()
    if r.get("error"):
        raise RuntimeError(r["error"])  # a gap to refill, not a quiet day
    return r

# Auto finds a route that works and hands back its session...
first = fetch("auto", {"url": LIST.format(1), "validate": CHECK})
proxy, ua = first["session"]["proxy"], first["session"]["userAgent"]
pages = [first["data"]]

# ...and Single replays it on the same exit for every other page.
m = re.search(r"\d+-\d+ of (\d+)", pages[0])
total = int(m.group(1)) if m else 0
for n in range(2, math.ceil(total / PER_PAGE) + 1):
    pages.append(fetch("single", {
        "method": "GET", "url": LIST.format(n), "proxy": proxy,
        "headers": [["User-Agent", ua]], "validate": CHECK,
    })["data"])

إذا كانت البوابة تحتفظ بحالة البحث الخاصة بك على خادمها، فأرسل cookie الجلسة الخاص بها في قائمة headers نفسها؛ يعيد Single كل header لـ Set-Cookie لكل قفزة إعادة توجيه (redirect). عادة ما تكون وثائق المناقصات خلف رابط تنزيل كملفات PDF، وتوفر ميزة التحكم في redirect ووضع الـ buffer الخام في Single استعادتها كـ bytes بدلا من نصوص مشوهة. عادة إضافية: قم بالاستعلام الدوري (poll) لكل بوابة وفقا لجدولها الزمني الخاص. هذه خوادم صغيرة تديرها مجالس محلية صغيرة، وإرسال عشرين request في دفعة واحدة يبدو أسوأ بكثير بالنسبة لها مقارنة بعشرين موزعة على مدار فترة ما بعد الظهر.

النتائج

  • التقاط مناقصات أكثر بنسبة 25%. GroupBWT، وهي شركة خدمات scraping، تذكر هذا لعميل يراقب أكثر من 50 بوابة مشتريات مقارنة بخط أساس يدوي، "ليس بسبب وجود مناقصات أكثر، ولكن لأنهم توقفوا عن تفويتها."
  • لا حاجة لـ scraper للجزء الذي يتجاوز الحد الأدنى. تأتي إشعارات TED من API لا يتطلب مفتاحا، وبالتالي فإن كل ساعة عمل على الـ parser تذهب إلى البوابات التي ليس لديها API.
  • تظهر الفجوات بينما لا يزال الإشعار مفتوحا. الصفحة التي تفشل في تلبية قاعدة القبول تثير خطأ في يوم حدوثه، وتعيد المهمة محاولتها بدلا من تخزين يوم فارغ.
  • اكتشاف المسار يتم مرة واحدة لكل بوابة، وليس لكل صفحة. يتم بحث Auto مرة واحدة؛ وتكون كل صفحة متابعة عبارة عن استدعاء Single على المخرج الذي وجده.

النقطة الأولى فقط هي نتيجة مقاسة؛ وتصف النقاط الثلاث الأخرى ما يغيره هذا الإعداد.

النتيجة الرئيسية

في مراقبة المناقصات، أغلى صف يمكنك تخزينه هو "لا شيء جديد اليوم"، لأنه الصف الوحيد الذي لا يمكنك تصحيحه لاحقا. تعود الأسعار غدا. لكن المناقصة التي أغلقت بينما كان الـ scraper يقرأ صفحة جلسة منتهية الصلاحية لن تعود.

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