العودة إلى المدونة

سحب بيانات خرائط جوجل ببايثون: ما الذي ينكسر وما الذي يصمد

Ilyas Yıldırım
Ilyas Yıldırım
قراءة 7 دقيقة

اكتب في محرك البحث "سحب بيانات خرائط جوجل ببايثون" وستحصل على مئة شرح بالشكل نفسه تقريبا. استورد requests، واستورد BeautifulSoup، وجد العنصر، ودُر في حلقة، واكتب ملف CSV. تنسخ الكود، وتشغّله، وتحصل على قائمة فارغة.

الكود ليس خاطئا تماما. لقد كُتب لويب لم تعد خرائط جوجل جزءا منه منذ سنوات. هذا المقال يمر على الطرق الثلاثة التي يجربها الناس فعلا، ويشرح أين ينتهي كل منها، وينتهي بالنسخة التي تستمر في العمل بعد أن تتوقف أنت عن مراقبتها.

الطريق الأول: requests وBeautifulSoup

هذا هو طريق معظم الشروحات، ويفشل فورا.

الدالة requests.get() تنزّل كود HTML الذي أرسله الخادم. وخرائط جوجل ترسل هيكلا صغيرا ثم تبني قائمة النتائج كاملة داخل المتصفح بجافاسكربت. الأنشطة التجارية ليست في المستند الذي نزّلته. هي لا توجد أصلا حتى يشغّل محرك متصفح تلك الصفحة.

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

إن رأيت شرحا يعرض هذا وهو يعمل، فانظر إلى تاريخه، ثم انظر هل المخرجات في الصورة حقيقية.

الطريق الثاني: Selenium أو Playwright

هذا الطريق يعمل. وهنا الفخ.

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

وهكذا تبدو الأشهر التالية.

تتحرك المحددات. تغيّر جوجل واجهتها باستمرار، وأسماء الأصناف المولّدة ليست التزاما تجاه أحد. وحين تتغير لا يرمي سكربتك خطأ. بل يعيد صفر صف، أو أسوأ من ذلك، يعيد صفوفا بعمود فارغ في صمت.

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

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

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

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

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

الطريق الثالث: واجهة Places

واجهة Places من جوجل نفسها مستقرة وموثقة ولا تنكسر حين تتغير الواجهة. ولأجل دليل فروع أو إكمال عنوان داخل منتجك فهي الجواب الصحيح ولا شيء يقاربها.

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

عملنا على فئات الحقول والحسابات في أسعار Google Places API، ومسألة الترخيص في Places API أم السحب المباشر. باختصار: هي واجهة لعرض أماكن على مستخدم، لا لملء نظام إدارة عملاء.

ما الذي يصمد فعلا

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

كل ما يلي يعمل مقابل واجهة BasedOnBusiness البرمجية عبر مكتبة requests. أنشئ مفتاحا من الإعدادات، تحت قسم الواجهة البرمجية وويب هوك. تبدأ المفاتيح بـ bdb_live_.

أرسل المهمة

import os
import requests

BASE = "https://www.basedonb.com/api/v1"
KEY = os.environ["BDB_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

response = requests.post(
    f"{BASE}/scrapes",
    headers={**HEADERS, "Idempotency-Key": "dentist-dubai-2026-08"},
    json={
        "query": "dentist",
        "country": "AE",
        "city": "Dubai",
        "target_leads": 200,
    },
    timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])

تفصيلان يستحقان المعرفة. الحقل state يستخدم صيغة GeoNames المنقوطة، أي US.CA وDE.BE وTR.34. والدول التي لا تقسيم فرعي لها تقبل country وحده. وإن لم تكن متأكدا مما تتوقعه دولة ما، فإن نقاط النهاية /geodata/countries و/geodata/states و/geodata/cities تعيد القيم المقبولة.

وترويسة Idempotency-Key أهم مما تبدو. فإذا انتهت مهلة طلبك وأعدت المحاولة، يمنع هذا المفتاح إنشاء مهمة ثانية وإنفاق أرصدتك مرتين.

انتظر انتهاءها

import time

def wait_for(job_id, poll_seconds=5, timeout_seconds=1800):
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        job = requests.get(f"{BASE}/scrapes/{job_id}", headers=HEADERS, timeout=30).json()
        if job["status"] == "done":
            return job
        if job["status"] in ("failed", "cancelled"):
            raise RuntimeError(f"Job ended as {job['status']}")
        print(f"{job['status']} {job.get('progress', 0):.0%} · {job['leads_found']} found")
        time.sleep(poll_seconds)
    raise TimeoutError("Job did not finish in time")

wait_for(job["id"])

خمس ثوان فاصل مهذب ويبقى مريحا داخل الحد، وهو 100 طلب في الدقيقة لكل مفتاح. لا تسأل كل 200 جزء من الثانية لمجرد أن الحلقة تسمح بذلك.

تصفح النتائج

تصل النتائج صفحة صفحة مع مؤشر. وكائن page يخبرك هل تتابع.

def fetch_all(job_id, page_size=500):
    rows, cursor = [], None
    while True:
        params = {"limit": page_size}
        if cursor:
            params["cursor"] = cursor
        page = requests.get(
            f"{BASE}/scrapes/{job_id}/results",
            headers=HEADERS,
            params=params,
            timeout=60,
        ).json()
        rows.extend(page["results"])
        if not page["page"]["has_more"]:
            return rows
        cursor = page["page"]["next_cursor"]

الحقل limit يقبل حتى 500. وكل صف يحمل place_id وtitle وcategory وaddress وphone وwebsite وrating وreviews_count وlatitude وlongitude وprice_level وbusiness_status، إضافة إلى enrich_status وemail_enrich_status اللذين يخبرانك هل اكتمل الإثراء من الموقع لذلك السجل.

اكتب الملف

import pandas as pd

frame = pd.DataFrame(fetch_all(job["id"]))
with_site = frame[frame["website"].notna()]
print(f"{len(frame)} records, {len(with_site)} with a website")
frame.to_csv("dentist-dubai.csv", index=False)

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

توقف عن السؤال المتكرر واستخدم ويب هوك

الحلقة مقبولة في سكربت تشغّله بنفسك. أما داخل خط عمل آلي فتريد أن تخبرك المهمة هي.

سجّل نقطة نهاية لويب هوك، فيصلك حدث scrape.done عند انتهاء المهمة، ويحمل ترويسة X-Webhook-Signature تتحقق منها مقابل سر التوقيع الذي يُعرض مرة واحدة عند الإنشاء. وتُعاد المحاولة للتسليمات الفاشلة بعد دقيقة، ثم 5 دقائق، ثم 30 دقيقة، ثم ساعتين. احفظ قيمة X-Webhook-Event-Id وتجاهل الأحداث التي عالجتها من قبل، لأن إعادة المحاولة قد تصل بعد معالجتك للتسليم الأول.

ملاحظة عن الجانب القانوني

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

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

أيهما تبني

إن كنت تتعلم فاكتب نسخة Selenium. هي تمرين ممتاز فعلا، وستفهم بعدها كل أداة تشتريها بشكل أفضل.

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

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

جرّبه على بيانات حقيقية

الحسابات الجديدة تحصل على 50 رصيد تصدير لمرة واحدة بلا بطاقة، حيث يساوي رصيد واحد سجل نشاط واحد. وهذا يغطي السكربتات أعلاه من أولها إلى آخرها: أرسل مهمة صغيرة، واسأل عن حالتها، وتصفح النتائج، وافتح ملف CSV.

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