مهندسی ربات هوشمند ردیاب رتبه گوگل با Python و Selenium: از اسکرچ تا تحلیل داده‌های SEO

Sara majidiSara majidi
38 min read

Table of contents


مقدمه: چرا خودکارسازی ردیابی رتبه یک ضرورت است، نه یک انتخاب؟

درود بر شما همکاران و علاقه‌مندان به دنیای برنامه‌نویسی و بهینه‌سازی وب! سجاد اکبری هستم و در این مقاله قصد دارم تجربه خودم را در ساخت یک ابزار قدرتمند و حیاتی برای هر متخصص سئو یا توسعه‌دهنده وب با شما به اشتراک بگذارم: یک ربات هوشمند برای ردیابی رتبه‌های گوگل.

همه ما می‌دانیم که جایگاه در نتایج جستجوی گوگل (SERP) حکم مرگ و زندگی را برای یک کسب‌وکار آنلاین دارد. اما بررسی دستی این رتبه‌ها، به خصوص برای تعداد زیادی کلمه کلیدی و به صورت مداوم، نه تنها خسته‌کننده و زمان‌بر است، بلکه مستعد خطای انسانی و فاقد کارایی لازم برای تصمیم‌گیری‌های داده‌محور (Data-Driven) است.

در این آموزش، ما فراتر از یک اسکریپت ساده خواهیم رفت. هدف ما مهندسی یک ربات پایدار، قابل اعتماد و توسعه‌پذیر با استفاده از پایتون – زبانی که به خاطر خوانایی و اکوسیستم غنی‌اش به آن علاقه‌مندم – و کتابخانه Selenium، استاندارد صنعتی برای اتوماسیون مرورگر، است. ما نه تنها "چگونه" کد بزنیم را یاد می‌گیریم، بلکه "چرا" پشت هر تصمیم طراحی را نیز درک خواهیم کرد. این مقاله یک نقشه راه دقیق برای شما خواهد بود تا بتوانید چنین ابزاری را از پایه ساخته و برای تحلیل‌های عمیق سئو آماده کنید. پس آستین‌ها را بالا بزنید و با من همراه شوید!


فاز ۱: بنیان‌گذاری پروژه و برپایی آزمایشگاه کدنویسی حرفه‌ای

قبل از نوشتن اولین خط کد، باید یک محیط توسعه استاندارد و ایزوله را برپا کنیم. این کار تضمین می‌کند که پروژه ما با سایر پروژه‌ها و کتابخانه‌های سیستمی تداخلی نداشته باشد و قابلیت بازتولید (Reproducibility) آن حفظ شود.

  1. چرا پایتون و Selenium؟ انتخاب‌های یک معمار نرم‌افزار:

    • پایتون: سادگی سینتکس، کتابخانه‌های استاندارد قدرتمند (مانند datetime, csv, json)، و اکوسیستم بی‌نظیر برای داده‌کاوی (Pandas, NumPy, Matplotlib) و وب (Requests, Scrapy, Flask/Django). برای اسکرپینگ و اتوماسیون، پایتون به دلیل خوانایی و سرعت توسعه، گزینه‌ای ایده‌آل است.

    • Selenium: ابزاری توانمند برای کنترل مرورگرهای واقعی (Chrome, Firefox, Edge و ...). این امکان را به ما می‌دهد تا با صفحات وبی که به شدت به جاوا اسکریپت برای بارگذاری محتوا متکی هستند (مانند نتایج جستجوی گوگل) تعامل داشته باشیم، کاری که با کتابخانه‌های ساده‌تری مثل Requests و BeautifulSoup به تنهایی دشوار یا غیرممکن است.

  2. ایجاد محیط مجازی (Virtual Environment): سنگ بنای یک پروژه تمیز: همیشه، و تاکید می‌کنم همیشه، پروژه‌های پایتون خود را در یک محیط مجازی ایجاد کنید. این کار از تداخل نسخه‌های مختلف کتابخانه‌ها جلوگیری می‌کند.

     # در ترمینال یا خط فرمان
     python -m venv google_rank_tracker_env
     # فعال سازی محیط در ویندوز
     google_rank_tracker_env\Scripts\activate
     # فعال سازی محیط در مک/لینوکس
     source google_rank_tracker_env/bin/activate
    

    از این پس، تمام دستورات pip install ما فقط کتابخانه‌ها را در این محیط نصب خواهند کرد.

  3. نصب کتابخانه‌های ضروری و مدیریت هوشمند وابستگی‌ها:

     pip install selenium pandas openpyxl # openpyxl برای کار با فایل اکسل در صورت نیاز
    

    نکته حرفه‌ای: بلافاصله پس از نصب کتابخانه‌های اصلی، یک فایل requirements.txt ایجاد کنید. این فایل لیست تمام وابستگی‌های پروژه شما و نسخه‌های دقیق آنها را نگهداری می‌کند، که برای اشتراک‌گذاری پروژه یا راه‌اندازی مجدد آن در محیط دیگر حیاتی است.

     pip freeze > requirements.txt
    

    برای نصب از روی این فایل در آینده: pip install -r requirements.txt

  4. WebDriver: پل ارتباطی با مرورگر – انتخاب و تنظیم دقیق: Selenium برای کنترل مرورگر به یک واسط به نام WebDriver نیاز دارد. برای هر مرورگر، WebDriver مخصوص به خود وجود دارد (مثلاً ChromeDriver برای گوگل کروم).

    • دانلود: WebDriver را متناسب با نسخه مرورگری که روی سیستم خود دارید، از وب‌سایت رسمی آن دانلود کنید (مثلاً https://chromedriver.chromium.org/downloads برای کروم).

    • مدیریت مسیر:

      • روش ساده (اما نه همیشه بهترین برای پروژه‌های بزرگ): فایل اجرایی WebDriver (مثلاً chromedriver.exe یا chromedriver) را در کنار اسکریپت پایتون خود قرار دهید.

      • روش استاندارد: مسیر پوشه‌ای که WebDriver در آن قرار دارد را به متغیر محیطی PATH سیستم خود اضافه کنید.

      • روش ترجیحی در کد (برای قابلیت حمل بیشتر پروژه): مسیر WebDriver را مستقیماً در کد مشخص کنید. این روش را در ادامه پیاده‌سازی خواهیم کرد.


فاز ۲: معماری ربات: طراحی یک سیستم ماژولار و قابل نگهداری

یک کدنویس حرفه‌ای تنها به کار کردن کد فکر نمی‌کند، بلکه به خوانایی، قابلیت نگهداری و توسعه‌پذیری آن نیز اهمیت می‌دهد. ما ربات خود را به صورت ماژولار طراحی خواهیم کرد.

  1. طراحی مبتنی بر کلاس (Object-Oriented Design): ما منطق اصلی ربات را در یک کلاس به نام GoogleRankTracker کپسوله خواهیم کرد. این کلاس مسئولیت‌های زیر را بر عهده خواهد داشت:

    • راه‌اندازی و مدیریت درایور Selenium.

    • اجرای جستجو برای یک کلمه کلیدی.

    • پیمایش صفحات نتایج.

    • استخراج لینک‌ها و عناوین.

    • یافتن رتبه دامنه هدف.

    • بستن مرورگر.

  2. مدیریت تنظیمات (Configuration): جداسازی کد از داده‌های پیکربندی: کلمات کلیدی، دامنه هدف، تعداد صفحات مورد بررسی و مسیر WebDriver نباید مستقیماً در کد نوشته (Hardcode) شوند. از یک فایل config.ini یا config.json برای این منظور استفاده خواهیم کرد. برای سادگی در این آموزش، می‌توانیم آن‌ها را به عنوان پارامتر به کلاس پاس دهیم یا در ابتدای اسکریپت تعریف کنیم، اما در پروژه‌های واقعی، استفاده از فایل کانفیگ ارجحیت دارد.

    مثال ساده برای تعریف در ابتدای اسکریپت (در آموزش کامل‌تر می‌توان فایل JSON را نشان داد):

     TARGET_DOMAIN = "example.com" # دامنه وب‌سایت شما
     KEYWORDS_TO_TRACK = ["کلمه کلیدی اول", "بهترین قیمت محصول ایکس", "آموزش پایتون پیشرفته"]
     MAX_PAGES_TO_CHECK = 5 # حداکثر تعداد صفحات نتایج برای بررسی
     CHROME_DRIVER_PATH = "مسیر/کامل/به/chromedriver.exe" # یا فقط 'chromedriver' اگر در PATH است
    
  3. استراتژی‌های هوشمند برای شناسایی المان‌های وب (Locating Elements): فراتر از ID و Name: ساختار صفحات وب، به خصوص گوگل، می‌تواند تغییر کند. بنابراین، باید از روش‌های مقاوم‌تری برای یافتن المان‌ها استفاده کنیم.

    • By.ID، By.NAME: سریع و ساده، اما همیشه در دسترس نیستند یا ممکن است تغییر کنند.

    • By.XPATH: بسیار قدرتمند و انعطاف‌پذیر. امکان نوشتن مسیرهای پیچیده بر اساس ساختار DOM را می‌دهد. مثال: //div[@class='g']//a/h3 (برای یافتن عنوان‌های نتایج). باید XPath هایی بنویسیم که کمتر به تغییرات جزئی حساس باشند.

    • By.CSS_SELECTOR: معمولاً خواناتر از XPath و با عملکرد خوب. مثال: div.g a > h3.

    • By.CLASS_NAME، By.TAG_NAME، By.LINK_TEXT، By.PARTIAL_LINK_TEXT: برای موارد خاص مفید هستند.

نکته حرفه‌ای از سجاد اکبری: همیشه سعی کنید سلکتورهایی بنویسید که تا حد امکان عمومی باشند اما همچنان یکتا المان مورد نظر شما را انتخاب کنند. از وابستگی به کلاس‌هایی که به نظر می‌رسد به صورت خودکار تولید شده‌اند (مانند class="xyz123abc") پرهیز کنید. به ساختار سلسله‌مراتبی (والد-فرزند) و المان‌های لنگر (Anchor elements) قابل اتکاتر توجه کنید.


فاز ۳: پیاده‌سازی نبض تپنده ربات – تعامل دقیق با گوگل

حالا زمان کدنویسی کلاس GoogleRankTracker است.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
import urllib.parse # برای نرمال سازی URL ها

# تنظیمات اولیه (در پروژه‌های واقعی از فایل کانفیگ خوانده شود)
TARGET_DOMAIN = "example.com"
CHROME_DRIVER_PATH = "chromedriver" # فرض بر اینکه در PATH است یا در کنار اسکریپت
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" # یک User-Agent معقول

class GoogleRankTracker:
    def __init__(self, driver_path, target_domain, user_agent=None):
        self.driver_path = driver_path
        self.target_domain = target_domain.lower() # برای مقایسه بهتر، به حروف کوچک تبدیل می کنیم

        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument(f"user-agent={user_agent if user_agent else USER_AGENT}")
        # chrome_options.add_argument("--headless")  # اجرای بدون نمایش مرورگر (برای سرور یا اجرای سریعتر)
        # chrome_options.add_argument("--disable-gpu") # گاهی برای headless لازم است
        # chrome_options.add_argument("--no-sandbox") # برای اجرای روی لینوکس به عنوان روت (با احتیاط)
        # chrome_options.add_argument("--disable-dev-shm-usage") # برای رفع مشکلات حافظه اشتراکی در لینوکس

        # اگر مسیر درایور را به صورت مستقیم می‌دهیم:
        # self.driver = webdriver.Chrome(executable_path=self.driver_path, options=chrome_options)
        # اگر درایور در PATH است یا می‌خواهیم Selenium خودش مدیریت کند (با Selenium Manager از نسخه 4.6.0 به بعد):
        try:
            self.driver = webdriver.Chrome(options=chrome_options)
        except Exception as e:
            print(f"خطا در راه‌اندازی ChromeDriver: {e}")
            print("مطمئن شوید ChromeDriver نصب و در PATH سیستم قرار دارد یا مسیر آن به درستی مشخص شده.")
            raise # خطا را مجددا پرتاب می‌کنیم تا برنامه متوقف شود

        self.driver.implicitly_wait(5) # یک انتظار ضمنی کلی برای یافتن المان‌ها

    def _normalize_url(self, url):
        """ URL را برای مقایسه بهتر، نرمال می کند (حذف www و http/https) """
        parsed_url = urllib.parse.urlparse(url)
        domain = parsed_url.netloc
        if domain.startswith("www."):
            domain = domain[4:]
        return domain.lower()

    def search_keyword(self, keyword, max_pages=3):
        print(f"\n🔍 در حال جستجوی کلمه کلیدی: '{keyword}' برای دامنه '{self.target_domain}'...")
        try:
            self.driver.get("https://www.google.com/")

            # مدیریت کوکی‌ها (در صورت نمایش پاپ‌آپ اولیه)
            try:
                # این سلکتور ممکن است بر اساس زبان یا منطقه گوگل تغییر کند
                # باید با inspect element دقیق ترین سلکتور را پیدا کرد
                consent_button_selectors = [
                    "//button[.//div[contains(text(),'Accept all')]]", # English
                    "//button[.//div[contains(text(),'Alles akzeptieren')]]", # German
                    "//button[.//div[contains(text(),'Tout accepter')]]", # French
                    "//button[.//div[contains(text(),'Accetta tutto')]]", # Italian
                    "//button[.//div[contains(text(),'Aceptar todo')]]", # Spanish
                    "//button[.//div[contains(text(),'پذیرفتن همه')]]", # Persian example (ممکن است دقیق نباشد)
                    "//div[text()='I agree']", # Alternative
                    "//button[@id='L2AGLb']" # یک id که گاهی دیده شده
                ]
                consent_button = None
                for selector in consent_button_selectors:
                    try:
                        # انتظار کوتاه برای دکمه کوکی
                        consent_button = WebDriverWait(self.driver, 5).until(
                            EC.element_to_be_clickable((By.XPATH, selector))
                        )
                        if consent_button:
                            consent_button.click()
                            print("پیام کوکی پذیرفته شد.")
                            break
                    except TimeoutException:
                        continue # اگر با این سلکتور پیدا نشد، بعدی را امتحان کن
                if not consent_button:
                     print("دکمه پذیرش کوکی پیدا نشد یا نیازی به کلیک نبود.")
            except Exception as e:
                print(f"خطا در مدیریت پاپ آپ کوکی: {e}")


            search_box = WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.NAME, "q"))
            )
            search_box.clear()
            search_box.send_keys(keyword)
            search_box.send_keys(Keys.RETURN)

            time.sleep(2) # کمی صبر برای بارگذاری نتایج اولیه

            current_rank = 0
            for page_num in range(1, max_pages + 1):
                print(f"---- بررسی صفحه {page_num} نتایج ----")
                WebDriverWait(self.driver, 10).until(
                    EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.g, div.hlcw0c, div.Gx5Zad")) # سلکتورهای رایج برای بلاک نتایج
                )

                # گوگل ساختارهای متنوعی برای نمایش نتایج دارد. باید انعطاف پذیر باشیم.
                # معمولا نتایج اصلی دارای یک تگ <a> با یک تگ <h3> درون آن هستند.
                # سلکتورهای پیشنهادی:
                # "div.g a:not([class]) h3" (پیچیده تر، سعی در حذف لینک های داخلی مثل People Also Ask)
                # "div.g div[role='link']"
                # "div.g .yuRUbf > a" (ساختار جدیدتر گوگل)
                # "div.Gx5Zad.fP1Qef.xpd.ETM_NB .kCrYT a" (ساختار دیگر)

                search_results_elements = self.driver.find_elements(By.CSS_SELECTOR, "div.g .yuRUbf > a, div.hlcw0c .yuRUbf > a, div.Gx5Zad.fP1Qef.xpd.ETM_NB .kCrYT a")

                if not search_results_elements:
                    # گاهی گوگل نتایج را در ساختار کمی متفاوت‌تری قرار می‌دهد
                    # یا ممکن است به دلیل CAPTCHA یا تغییرات ساختاری، نتیجه‌ای پیدا نشود.
                    print("هشدار: هیچ نتیجه‌ای با سلکتور اصلی در این صفحه یافت نشد. تلاش با سلکتور جایگزین...")
                    search_results_elements = self.driver.find_elements(By.XPATH, "//div[@class='g' or @class='hlcw0c' or @class='Gx5Zad fP1Qef xpd ETM_NB']//a[@href and count(h3)>0]") # XPath انعطاف‌پذیرتر

                if not search_results_elements:
                    print("هیچ نتیجه معتبری در این صفحه یافت نشد. احتمالاً به CAPTCHA برخورد کرده‌اید یا ساختار SERP تغییر کرده است.")
                    # اینجا می‌توان منطقی برای تشخیص CAPTCHA اضافه کرد (مثلاً بررسی وجود المان‌های خاص CAPTCHA)
                    # یا یک اسکرین‌شات گرفت: self.driver.save_screenshot(f"captcha_or_empty_page_{keyword.replace(' ', '_')}_page{page_num}.png")
                    break # ادامه بررسی این کلمه کلیدی فایده ندارد

                found_in_page = False
                for elem in search_results_elements:
                    try:
                        link = elem.get_attribute("href")
                        if link:
                            current_rank += 1
                            normalized_link_domain = self._normalize_url(link)
                            # print(f"رتبه {current_rank}: {link}") # برای دیباگ
                            if self.target_domain in normalized_link_domain:
                                title_element = elem.find_element(By.XPATH, ".//h3")
                                title = title_element.text if title_element else "عنوان یافت نشد"
                                print(f"🎉 دامنه '{self.target_domain}' پیدا شد!")
                                print(f"رتبه: {current_rank}, عنوان: '{title}', URL: {link}")
                                return {"keyword": keyword, "rank": current_rank, "url": link, "title": title, "page": page_num, "status": "Found"}
                    except Exception as e: # هر خطای غیرمنتظره ای حین پردازش یک نتیجه
                        # print(f"خطای جزئی در پردازش یک نتیجه: {e}")
                        continue # برو به نتیجه بعدی

                # رفتن به صفحه بعد (اگر هنوز دامنه پیدا نشده)
                if page_num < max_pages:
                    try:
                        # سلکتور دکمه "بعدی" (Next) می‌تواند بسیار متغیر باشد
                        next_button_selectors = [
                            "//a[@id='pnnext']", # ID قدیمی
                            "//a[@aria-label='Page suivante']", # فرانسوی
                            "//a[@aria-label='Next page']", # انگلیسی (جدیدتر)
                            "//a[@aria-label='Seite weiter']", # آلمانی
                            "//span[text()='Next']", # گاهی اوقات
                            "//span[text()='بعدی']" # فارسی (اگر زبان گوگل فارسی باشد)
                        ]
                        next_button = None
                        for selector in next_button_selectors:
                            try:
                                next_button = self.driver.find_element(By.XPATH, selector)
                                if next_button and next_button.is_displayed() and next_button.is_enabled():
                                    break
                            except NoSuchElementException:
                                continue

                        if next_button:
                            # self.driver.execute_script("arguments[0].scrollIntoView(true);", next_button) # اسکرول به دکمه
                            # time.sleep(0.5)
                            next_button.click()
                            time.sleep(2) # صبر برای بارگذاری صفحه جدید
                        else:
                            print("دکمه 'صفحه بعد' پیدا نشد. پایان جستجو برای این کلمه کلیدی.")
                            break 
                    except Exception as e:
                        print(f"خطا در کلیک روی دکمه 'صفحه بعد': {e}")
                        break # اگر نتوانستیم به صفحه بعد برویم، جستجو برای این کلمه کلیدی را متوقف می‌کنیم
                else:
                    print("به حداکثر تعداد صفحات بررسی رسیدیم.")

            print(f"دامنه '{self.target_domain}' برای کلمه کلیدی '{keyword}' در {max_pages} صفحه اول یافت نشد.")
            return {"keyword": keyword, "rank": "Not Found", "url": "", "title": "", "page": max_pages, "status": "Not Found"}

        except TimeoutException:
            print(f"Timeout: بارگذاری صفحه گوگل یا یافتن المان جستجو بیش از حد طول کشید برای کلمه کلیدی: {keyword}")
            self.driver.save_screenshot(f"timeout_error_{keyword.replace(' ', '_')}.png")
            return {"keyword": keyword, "rank": "Error - Timeout", "url": "", "title": "", "page": 0, "status": "Error"}
        except Exception as e:
            print(f"یک خطای غیرمنتظره رخ داد برای کلمه کلیدی '{keyword}': {e}")
            # گرفتن اسکرین‌شات می‌تواند در دیباگ کردن بسیار مفید باشد
            self.driver.save_screenshot(f"unexpected_error_{keyword.replace(' ', '_')}.png")
            return {"keyword": keyword, "rank": "Error - Unexpected", "url": "", "title": "", "page": 0, "status": "Error"}

    def close_browser(self):
        print("\n🔴 در حال بستن مرورگر...")
        if self.driver:
            self.driver.quit()

# ---- نحوه استفاده از کلاس ----
if __name__ == "__main__":
    # این لیست کلمات کلیدی و دامنه را مطابق نیاز خود تغییر دهید
    MY_TARGET_DOMAIN = "wikipedia.org" # برای تست می‌توانید از دامنه‌های معروف استفاده کنید
    KEYWORDS_TO_TRACK = ["پایتون (زبان برنامه‌نویسی)", "یادگیری ماشین", "گوگل"]
    MAX_PAGES = 2 # برای تست سریع، تعداد صفحات را کم بگذارید

    tracker = None # برای اینکه در finally قابل دسترسی باشد
    results_data = []
    try:
        tracker = GoogleRankTracker(driver_path=CHROME_DRIVER_PATH, target_domain=MY_TARGET_DOMAIN)
        for kw in KEYWORDS_TO_TRACK:
            result = tracker.search_keyword(kw, max_pages=MAX_PAGES)
            results_data.append(result)
            # تاخیر هوشمند بین هر کلمه کلیدی برای جلوگیری از بلاک شدن
            # در دنیای واقعی، این تاخیر باید بیشتر و شاید تصادفی باشد
            time.sleep(5) # 5 تا 15 ثانیه می‌تواند مناسب باشد
    except Exception as e:
        print(f"خطای کلی در اجرای برنامه: {e}")
    finally:
        if tracker:
            tracker.close_browser()

    print("\n--- نتایج نهایی ---")
    for item in results_data:
        print(item)

    # در فاز بعدی، این نتایج را در یک فایل CSV ذخیره خواهیم کرد

توضیحات کد بالا (بسیار مهم برای مقاله):

  • __init__: مرورگر را با ChromeOptions پیکربندی می‌کند. امکان اجرای headless (بدون نمایش پنجره مرورگر، مناسب برای سرور و افزایش سرعت) و تنظیم user-agent (برای جلوگیری از شناسایی شدن به عنوان ربات ساده) در آن دیده شده. مدیریت خطای اولیه راه‌اندازی درایور نیز مهم است.

  • _normalize_url: یک متد کمکی خصوصی برای یکسان‌سازی URLها قبل از مقایسه (حذف http/https و www.). این دقت مقایسه را افزایش می‌دهد.

  • search_keyword:

    • ابتدا به صفحه اصلی گوگل می‌رود.

    • مدیریت کوکی: تلاش برای کلیک روی دکمه پذیرش کوکی‌ها (اگر نمایش داده شود). این بخش بسیار حساس به تغییرات گوگل و زبان/منطقه کاربر است. از چندین سلکتور رایج استفاده شده.

    • از WebDriverWait و EC.presence_of_element_located برای انتظار هوشمند تا زمانی که باکس جستجو (q) آماده شود، استفاده می‌کند. این بسیار بهتر از time.sleep() ثابت است.

    • کلمه کلیدی را وارد و جستجو را انجام می‌دهد.

    • در یک حلقه، صفحات نتایج را بررسی می‌کند.

    • سلکتور نتایج: div.g .yuRUbf > a و چند سلکتور دیگر برای پوشش ساختارهای مختلفی که گوگل برای نمایش نتایج ارگانیک استفاده می‌کند، در نظر گرفته شده‌اند. این بخش نیاز به بیشترین توجه و به‌روزرسانی در طول زمان دارد زیرا گوگل دائماً در حال تغییر کلاس‌ها و ساختار HTML خود است.

    • استخراج لینک: ویژگی href از تگ <a> استخراج می‌شود.

    • یافتن دامنه: دامنه استخراج شده از لینک با target_domain مقایسه می‌شود.

    • مدیریت صفحات (Pagination): تلاش برای یافتن دکمه "بعدی" (Next) با استفاده از سلکتورهای مختلف XPath (بسیار حساس به زبان). اگر پیدا شود، کلیک کرده و به صفحه بعد می‌رود.

    • مدیریت خطا: از try-except برای مدیریت TimeoutException (اگر صفحه یا المان در زمان مقرر بارگذاری نشود) و سایر خطاهای احتمالی استفاده شده. گرفتن اسکرین‌شات در زمان خطا برای دیباگ بسیار مفید است.

  • close_browser: مرورگر را می‌بندد و منابع را آزاد می‌کند.

  • بلاک if __name__ == "__main__":: نحوه استفاده از کلاس و اجرای یک جستجوی نمونه را نشان می‌دهد. نتایج در یک لیست جمع‌آوری می‌شوند. استفاده از finally تضمین می‌کند که مرورگر حتی در صورت بروز خطا بسته شود.

  • تاخیرها (time.sleep): استفاده از تاخیرهای کوتاه بین درخواست‌ها و پیمایش صفحات برای کاهش احتمال شناسایی شدن به عنوان ربات و جلوگیری از فشار بیش از حد به سرورهای گوگل حیاتی است.

عالی! حالا که اسکلت اصلی رباتمان را با موفقیت بنا کرده‌ایم و می‌تواند جستجوهای اولیه را انجام دهد، وقت آن است که به سراغ چالش‌های دنیای واقعی برویم. اینترنت، و به خصوص گوگل، یک محیط پویا و گاهی خصمانه برای ربات‌هاست. در این فاز، یاد می‌گیریم چگونه رباتمان را هوشمندتر و مقاوم‌تر کنیم.


فاز ۴: رام کردن غول خفته – مدیریت پیشرفته خطا، دور زدن هوشمندانه موانع و استراتژی‌های ضد شناسایی

یک ربات ساده در اولین مواجهه با تغییرات غیرمنتظره یا مکانیزم‌های دفاعی وب‌سایت‌ها از کار می‌افتد. اما ما به دنبال ساخت یک ابزار حرفه‌ای هستیم. سجاد اکبری به شما نشان خواهد داد که چگونه پایداری ربات خود را به سطح بالاتری ارتقا دهید.

  1. بازبینی و تقویت مدیریت خطا (Robust Error Handling):

    • در کد قبلی، از try-except های عمومی استفاده کردیم. اکنون باید دقیق‌تر شویم. Selenium استثناهای خاص خود را دارد (مانند NoSuchElementException, TimeoutException, WebDriverException, StaleElementReferenceException). شناسایی و مدیریت جداگانه این خطاها به ما امکان می‌دهد واکنش‌های مناسب‌تری نشان دهیم.

    • مثال: اگر NoSuchElementException رخ دهد، شاید بخواهیم با یک سلکتور دیگر تلاش کنیم یا گزارش دقیق‌تری ثبت کنیم، به جای اینکه کل فرآیند را متوقف کنیم.

    • لاگ‌گیری (Logging) پیشرفته: به جای print های ساده، از کتابخانه logging پایتون استفاده کنید. این امکان را می‌دهد تا سطوح مختلفی از اطلاعات (DEBUG, INFO, WARNING, ERROR, CRITICAL) را ثبت کرده و خروجی را به فایل یا کنسول هدایت کنید. این برای دیباگ کردن ربات در محیط عملیاتی (production) حیاتی است.

    # در ابتدای اسکریپت اضافه شود:
    import logging

    logging.basicConfig(level=logging.INFO, 
                        format='%(asctime)s - %(levelname)s - %(message)s',
                        # filename='rank_tracker.log', # برای ذخیره لاگ در فایل
                        # filemode='a' # Append mode
                        )
    # سپس به جای print از logger استفاده کنید:
    # print("پیام شما") -> logging.info("پیام شما") یا logging.error("خطای شما")

در کد اصلی کلاس، print ها را با logging.info, logging.warning, logging.error جایگزین کنید.

  1. مقابله با CAPTCHA ها: نبرد هوش و الگوریتم (بخش تئوری و عملی محدود): CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) بزرگترین دشمن ربات‌های اسکرپر است.

    • چرا گوگل CAPTCHA نمایش می‌دهد؟ ترافیک بیش از حد از یک IP، الگوهای رفتاری غیرطبیعی (کلیک‌های خیلی سریع، پیمایش غیرانسانی)، User-Agent مشکوک.

    • راهکارهای مستقیم (بسیار دشوار و ناپایدار با Selenium به تنهایی برای reCAPTCHA v2/v3):

      • تشخیص CAPTCHA: می‌توانید وجود المان‌های iframe مربوط به CAPTCHA یا متن‌های خاص را بررسی کنید.

          # درون متد search_keyword، پس از بارگذاری نتایج و قبل از استخراج
          try:
              captcha_iframe = self.driver.find_element(By.XPATH, "//iframe[contains(@src, 'recaptcha')]")
              if captcha_iframe:
                  logging.warning(f"کپچا شناسایی شد برای کلمه کلیدی: {keyword}!")
                  self.driver.save_screenshot(f"captcha_detected_{keyword.replace(' ', '_')}.png")
                  # در این نقطه، ربات نمی‌تواند ادامه دهد مگر اینکه راهکاری برای حل کپچا داشته باشید.
                  return {"keyword": keyword, "rank": "CAPTCHA", "url": "", "title": "", "page": 0, "status": "CAPTCHA"}
          except NoSuchElementException:
              pass # کپچایی پیدا نشد، خوب است!
        
      • راهکارهای انسانی یا نیمه‌خودکار: در صورت تشخیص، به کاربر اطلاع داده شود تا دستی حل کند (اگر ربات به صورت تعاملی اجرا می‌شود).

    • سرویس‌های حل CAPTCHA (راهکار عملی‌تر اما هزینه‌بر):

      • سرویس‌هایی مانند 2Captcha، Anti-CAPTCHA، DeathByCaptcha از طریق API، کپچاها را برای شما حل می‌کنند (معمولاً توسط انسان‌ها در آن سوی سرویس).

      • پیاده‌سازی این APIها در کد پایتون امکان‌پذیر است اما فراتر از حوصله این آموزش پایه است. (می‌توان به عنوان "مسیر پیشرفته" به آن اشاره کرد).

    • بهترین استراتژی با Selenium: پیشگیری! با رعایت نکات زیر، احتمال مواجهه با کپچا را کاهش دهید.

  2. کاهش احتمال شناسایی شدن (Stealth Techniques):

    • User-Agent های معقول و چرخشی:

      • User-Agent پیش‌فرض Selenium گاهی توسط وب‌سایت‌ها شناسایی می‌شود. همیشه یک User-Agent واقعی و رایج تنظیم کنید (مانند مثالی که در کد قبلی داشتیم).

      • برای حرفه‌ای‌تر شدن، لیستی از User-Agent های مختلف مرورگرهای رایج (کروم، فایرفاکس، اج در نسخه‌های مختلف) تهیه کرده و در هر بار اجرای ربات یا حتی برای هر درخواست، یکی را به صورت تصادفی انتخاب کنید.

        import random
        USER_AGENTS = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
            # ... (لیست بلندتری اضافه کنید)
        ]
        # در __init__ کلاس، user_agent را به صورت تصادفی انتخاب کنید:
        # chrome_options.add_argument(f"user-agent={random.choice(USER_AGENTS)}")
  • تأخیرهای هوشمند و تصادفی (Smart and Randomized Delays):

    • به جای time.sleep( ثابت )، از تأخیرهای تصادفی در یک بازه مشخص استفاده کنید تا رفتار ربات کمتر قابل پیش‌بینی باشد.
        time.sleep(random.uniform(3, 7)) # مثلاً تاخیری بین ۳ تا ۷ ثانیه

این تأخیرها را بین جستجوی کلمات کلیدی، بین پیمایش صفحات، و حتی قبل از برخی کلیک‌ها اعمال کنید.

  • شبیه‌سازی رفتار انسانی (در حد امکان):

    • Selenium به صورت پیش‌فرض کلیک‌ها و ورود متن را بسیار سریع انجام می‌دهد. می‌توان با جاوا اسکریپت سرعت تایپ را کنترل کرد یا حرکات موس را شبیه‌سازی کرد (این موارد پیشرفته‌تر هستند و ممکن است پیچیدگی را زیاد کنند).

    • از بارگذاری تمامی منابع صفحه (تصاویر، CSS های سنگین) در صورت عدم نیاز، جلوگیری کنید. این کار را می‌توان با تنظیمات ChromeOptions انجام داد، که هم سرعت را بالا می‌برد و هم مصرف منابع را کم می‌کند (اما ممکن است بر روی شناسایی المان‌ها تاثیر بگذارد اگر برای نمایش به این منابع وابسته باشند).

        # chrome_options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2}) # غیرفعال کردن تصاویر
  • پروکسی‌ها (Proxies):

    • اگر ربات را به طور مکرر اجرا می‌کنید، گوگل ممکن است IP شما را شناسایی و مسدود (یا با کپچاهای بیشتر مواجه) کند.

    • پروکسی‌های چرخشی (Rotating Proxies): استفاده از سرویس‌های پروکسی که در هر درخواست یا به صورت دوره‌ای IP شما را تغییر می‌دهند، بهترین راهکار است. این سرویس‌ها معمولاً پولی هستند.

    • تنظیم پروکسی در Selenium:

        PROXY = "ip_address:port" # مثال: "123.45.67.89:8080"
        # chrome_options.add_argument(f'--proxy-server={PROXY}')
  • مدیریت کوکی‌ها و سشن‌ها (Cookies and Sessions):

    • ربات می‌تواند کوکی‌ها را ذخیره و در اجراهای بعدی بارگذاری کند تا رفتار طبیعی‌تری داشته باشد (گوگل فکر کند کاربر بازگشته است). اما برای ردیابی رتبه، معمولاً بهتر است هر بار با یک سشن "تمیز" شروع کنیم تا نتایج شخصی‌سازی شده به حداقل برسد، مگر اینکه هدف خاصی از شبیه‌سازی کاربر خاصی داشته باشید. پاک کردن کوکی‌ها قبل از هر جستجوی کلیدواژه اصلی: self.driver.delete_all_cookies()
  1. استراتژی بازیابی (Retry Mechanism): برای خطاهای موقتی (مانند مشکلات شبکه یا بارگذاری ناقص صفحه)، یک مکانیزم تلاش مجدد پیاده‌سازی کنید.

     # مثال برای تلاش مجدد در search_keyword
     def search_keyword_with_retry(self, keyword, max_pages=3, retries=2):
         for attempt in range(retries + 1):
             try:
                 # اینجا همان منطق search_keyword قبلی قرار می‌گیرد
                 result = self._perform_search(keyword, max_pages) # متد اصلی جستجو را جدا کنید
                 if result.get("status") not in ["Error - Timeout", "CAPTCHA"]: # اگر خطای جدی نبود
                     return result
                 elif result.get("status") == "CAPTCHA": # اگر کپچا بود، تلاش مجدد فایده ندارد
                     logging.warning(f"کپچا در تلاش {attempt + 1} برای {keyword}. توقف تلاش.")
                     return result
    
                 logging.warning(f"تلاش {attempt + 1} برای {keyword} ناموفق بود (status: {result.get('status')}). تلاش مجدد پس از تاخیر...")
                 if attempt < retries:
                     time.sleep(random.uniform(10, 20) * (attempt + 1)) # تاخیر بیشتر در هر تلاش مجدد
             except Exception as e:
                 logging.error(f"خطای بحرانی در تلاش {attempt + 1} برای {keyword}: {e}")
                 if attempt < retries:
                     time.sleep(random.uniform(10, 20) * (attempt + 1))
                 else: # اگر آخرین تلاش هم ناموفق بود
                     self.driver.save_screenshot(f"critical_failure_{keyword.replace(' ', '_')}.png")
                     return {"keyword": keyword, "rank": "Error - Critical", "url": "", "title": "", "page": 0, "status": f"Critical Failure after {retries+1} attempts"}
         return {"keyword": keyword, "rank": "Error - Max Retries", "url": "", "title": "", "page": 0, "status": f"Max Retries Reached for {keyword}"} # اگر همه تلاش ها ناموفق بودند
    
     # سپس متد search_keyword قبلی را به _perform_search تغییر نام داده و از search_keyword_with_retry استفاده کنید.
    

    نکته مهم: ربات حرفه‌ای، رباتی است که در برابر ناملایمات خم به ابرو نمی‌آورد! با پیاده‌سازی این تکنیک‌ها، شما یک قدم بزرگ به سمت ساخت چنین ابزاری برداشته‌اید. به یاد داشته باشید که اسکرپینگ یک بازی موش و گربه است. کدی که امروز عالی کار می‌کند، ممکن است فردا نیاز به تنظیم داشته باشد. بنابراین، لاگ‌گیری و آمادگی برای به‌روزرسانی، کلید موفقیت بلندمدت است.


فاز ۵: سازماندهی گنجینه اطلاعات – ذخیره‌سازی و ساختاردهی داده‌های رتبه

پس از اینکه ربات ما با موفقیت داده‌های رتبه‌بندی را استخراج کرد، باید این اطلاعات ارزشمند را به شکلی ذخیره کنیم که هم قابل خواندن برای انسان باشد و هم به راحتی توسط برنامه‌های دیگر (برای تحلیل یا نمایش) قابل استفاده باشد. کتابخانه Pandas دوست صمیمی ما در این مرحله خواهد بود.

  1. چرا Pandas برای مدیریت داده‌ها؟

    • Pandas ساختارهای داده‌ای قدرتمند و انعطاف‌پذیری مانند DataFrame را ارائه می‌دهد که برای داده‌های جدولی (مثل نتایج ما) ایده‌آل است.

    • توابع داخلی متعددی برای خواندن و نوشتن فرمت‌های مختلف فایل (CSV, Excel, JSON, SQL و ...) دارد.

    • امکانات گسترده‌ای برای پاک‌سازی، تبدیل، و تحلیل داده‌ها فراهم می‌کند که در فاز بعدی به آن خواهیم پرداخت.

  2. ساختاردهی داده‌ها در DataFrame: ما لیستی از دیکشنری‌ها (results_data در کد قبلی) را داریم که هر دیکشنری نتیجه جستجوی یک کلمه کلیدی است. این ساختار به راحتی به یک DataFrame تبدیل می‌شود.

     # در انتهای اسکریپت، پس از اتمام حلقه جستجوی کلمات کلیدی
     import pandas as pd
     from datetime import datetime
    
     # ... (کد قبلی و results_data)
    
     if results_data: # فقط اگر داده ای برای ذخیره وجود دارد
         df = pd.DataFrame(results_data)
    
         # اضافه کردن ستون تاریخ و زمان برای هر رکورد (مهم برای پیگیری روند)
         df['timestamp_executed'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
         # تنظیم ترتیب ستون ها برای خوانایی بهتر (اختیاری)
         column_order = ['timestamp_executed', 'keyword', 'rank', 'url', 'title', 'page', 'status', 'target_domain']
         # اضافه کردن دامنه هدف به هر ردیف برای وضوح بیشتر اگر در فایل ذخیره شود
         df['target_domain'] = MY_TARGET_DOMAIN # یا از tracker.target_domain
    
         # فیلتر کردن و اطمینان از وجود همه ستون‌ها
         df = df.reindex(columns=column_order, fill_value='') # اگر ستونی نبود، با خالی پر شود
    
         logging.info("\n--- DataFrame نهایی نتایج ---")
         # برای نمایش بهتر در لاگ یا کنسول اگر DataFrame بزرگ است:
         # pd.set_option('display.max_rows', None)
         # pd.set_option('display.max_colwidth', None)
         # pd.set_option('display.width', 1000)
         logging.info(f"\n{df.to_string(index=False)}") # نمایش کل DataFrame بدون ایندکس
    
         # ۱. ذخیره به صورت فایل CSV (Comma Separated Values)
         # CSV یک فرمت رایج، ساده و قابل استفاده در اکثر نرم‌افزارهای صفحه‌گسترده و تحلیلی است.
         csv_filename = f"google_rank_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
         try:
             df.to_csv(csv_filename, index=False, encoding='utf-8-sig') 
             # index=False: ایندکس DataFrame را در فایل CSV ذخیره نکن
             # encoding='utf-8-sig': برای پشتیبانی صحیح از کاراکترهای فارسی در Excel
             logging.info(f"نتایج با موفقیت در فایل CSV ذخیره شد: {csv_filename}")
         except Exception as e:
             logging.error(f"خطا در ذخیره فایل CSV: {e}")
    
         # ۲. ذخیره به صورت فایل Excel (اختیاری، نیاز به openpyxl)
         # Excel برای گزارش‌دهی و اشتراک‌گذاری با کاربرانی که با CSV راحت نیستند، مناسب است.
         # اطمینان حاصل کنید که openpyxl نصب است: pip install openpyxl
         excel_filename = f"google_rank_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
         try:
             df.to_excel(excel_filename, index=False, sheet_name='Rankings')
             logging.info(f"نتایج با موفقیت در فایل Excel ذخیره شد: {excel_filename}")
         except Exception as e:
             logging.error(f"خطا در ذخیره فایل Excel: {e}")
             logging.warning("برای ذخیره در اکسل، کتابخانه 'openpyxl' باید نصب باشد: pip install openpyxl")
    
     else:
         logging.info("هیچ داده‌ای برای ذخیره‌سازی جمع‌آوری نشد.")
    
     # (بخش finally و tracker.close_browser() مثل قبل باقی می‌ماند)
    
  3. شرح کد ذخیره‌سازی:

    • ابتدا یک DataFrame از لیست results_data ساخته می‌شود.

    • یک ستون timestamp_executed اضافه می‌کنیم که زمان دقیق اجرای اسکریپت و جمع‌آوری داده را ثبت می‌کند. این برای تحلیل‌های روندی در آینده بسیار مهم است.

    • ستون target_domain نیز برای وضوح بیشتر به DataFrame اضافه می‌شود، خصوصاً اگر قصد دارید نتایج چندین دامنه را در یک فایل یا دیتابیس агрегировать کنید.

    • df.reindex(columns=column_order) ترتیب ستون‌ها را برای خوانایی بهتر مرتب می‌کند.

    • df.to_csv(): داده‌ها را به فایل CSV ذخیره می‌کند. index=False از نوشتن ایندکس DataFrame در فایل جلوگیری می‌کند. encoding='utf-8-sig' تضمین می‌کند که کاراکترهای فارسی (و سایر زبان‌ها) به درستی در برنامه‌هایی مانند Excel نمایش داده شوند.

    • df.to_excel(): داده‌ها را در یک شیت از فایل Excel ذخیره می‌کند. این نیاز به نصب کتابخانه openpyxl دارد.

    • استفاده از تاریخ و زمان در نام فایل خروجی، به شما کمک می‌کند تا اجراهای مختلف ربات را از هم تفکیک کرده و تاریخچه‌ای از نتایج داشته باشید.

  4. تفکر برای آینده: پایگاه داده (Databases)

    • برای پروژه‌های بزرگ‌تر یا زمانی که نیاز به اجرای مکرر ربات و ذخیره حجم زیادی از داده‌های تاریخی دارید، استفاده از یک پایگاه داده (مانند SQLite برای سادگی، یا PostgreSQL/MySQL برای مقیاس‌پذیری بیشتر) راه‌حل بهتری است.

    • Pandas می‌تواند به راحتی با پایگاه‌های داده SQL از طریق کتابخانه‌هایی مانند sqlite3 (برای SQLite) یا SQLAlchemy (برای انواع پایگاه‌های داده) تعامل داشته باشد (df.to_sql() و pd.read_sql()).

    • مزایای پایگاه داده: جستجوی سریع‌تر، جلوگیری از افزونگی داده، امکان ایجاد روابط بین جداول، مدیریت آسان‌تر حجم زیاد داده.

    • (اشاره به این موضوع کافی است، پیاده‌سازی کامل آن می‌تواند یک مقاله جداگانه باشد.)

سجاد اکبری تاکید می‌کند: "داده‌ها تا زمانی که به درستی ذخیره و سازماندهی نشوند، بی‌فایده‌اند. این مرحله شاید به اندازه کدنویسی ربات هیجان‌انگیز نباشد، اما سنگ بنای تمام تحلیل‌ها و تصمیم‌گیری‌های آینده شماست. دقت در این مرحله، در زمان تحلیل داده‌ها، ساعت‌ها در وقت شما صرفه‌جویی خواهد کرد."


حالا که داده‌هایمان را به شکلی قابل استفاده ذخیره کرده‌ایم، در فاز بعدی به سراغ شیرین‌ترین بخش ماجرا خواهیم رفت: استخراج دانش از این داده‌ها با استفاده از تحلیل‌های پایه سئو. آماده‌اید که معنای این رتبه‌ها را کشف کنیم؟

پس از اینکه با موفقیت داده‌های رتبه‌بندی را جمع‌آوری و به شکلی ساختاریافته ذخیره کردیم، زمان آن رسیده است که از این داده‌ها برای به دست آوردن بینش‌های ارزشمند در مورد عملکرد سئو استفاده کنیم. اینجاست که قدرت واقعی داده‌کاوی نمایان می‌شود.


فاز ۶: رمزگشایی از اعداد – تحلیل‌های بنیادین SEO و بصری‌سازی داده‌های رتبه

داده‌های خام رتبه‌بندی به تنهایی داستان کاملی را روایت نمی‌کنند. ما باید این داده‌ها را پردازش، تحلیل و به شکلی بصری ارائه کنیم تا بتوانیم الگوها، روندها و نقاط قوت و ضعف استراتژی سئوی خود را شناسایی کنیم. سجاد اکبری در این فاز، شما را با تکنیک‌های پایه اما قدرتمند تحلیل داده آشنا می‌کند.

  1. بارگذاری و آماده‌سازی داده‌ها برای تحلیل: فرض می‌کنیم داده‌های ما در یک فایل CSV (مثلاً google_rank_results_YYYYMMDD_HHMMSS.csv) ذخیره شده‌اند. اولین قدم، بارگذاری این داده‌ها در یک DataFrame پانداس است. اگر چندین فایل از اجراهای مختلف دارید، می‌توانید آن‌ها را با هم ادغام کنید تا یک دید تاریخی به دست آورید.

     import pandas as pd
     import matplotlib.pyplot as plt
     import seaborn as sns # برای نمودارهای زیباتر (اختیاری)
     from datetime import datetime
     import glob # برای یافتن فایل‌ها با یک الگو
    
     # تنظیمات اولیه برای نمودارها (اختیاری)
     sns.set_theme(style="whitegrid") # تم برای نمودارهای seaborn
     plt.rcParams['font.family'] = 'Tahoma' # یا هر فونت فارسی دیگری که نصب دارید
     plt.rcParams['figure.figsize'] = (12, 6) # اندازه پیش‌فرض نمودارها
    
     def load_and_prepare_data(file_pattern="google_rank_results_*.csv"):
         """
         تمام فایل‌های CSV که با الگوی داده شده مطابقت دارند را بارگذاری کرده،
         آنها را ادغام می‌کند و برای تحلیل آماده می‌سازد.
         """
         all_files = glob.glob(file_pattern)
         if not all_files:
             logging.warning(f"هیچ فایلی با الگوی '{file_pattern}' پیدا نشد.")
             return pd.DataFrame() # برگرداندن DataFrame خالی
    
         df_list = []
         for filename in sorted(all_files): # مرتب سازی بر اساس نام فایل (معمولا تاریخ را مرتب می‌کند)
             try:
                 df_temp = pd.read_csv(filename)
                 # اطمینان از وجود ستون‌های کلیدی و نوع داده مناسب
                 if 'timestamp_executed' in df_temp.columns:
                     df_temp['timestamp_executed'] = pd.to_datetime(df_temp['timestamp_executed'])
                 else: # اگر فایل قدیمی‌تر فاقد این ستون بود
                     # سعی در استخراج تاریخ از نام فایل (این بخش نیاز به تطبیق با فرمت نام فایل دارد)
                     try:
                         date_str = filename.split('_')[-2] + " " + filename.split('_')[-1].split('.')[0][:6]
                         df_temp['timestamp_executed'] = datetime.strptime(date_str, '%Y%m%d %H%M%S')
                     except:
                          df_temp['timestamp_executed'] = pd.NaT # Not a Time
    
                 if 'rank' in df_temp.columns:
                     # تبدیل رتبه به عددی، "Not Found", "CAPTCHA", "Error" به مقادیر بزرگ (برای مرتب‌سازی و فیلتر)
                     # یا می‌توان آن‌ها را به NaN تبدیل کرد و جداگانه بررسی نمود.
                     # برای سادگی، فعلا مقادیر غیر عددی را به یک عدد بزرگ (مثلا 101 برای رتبه نیافته) تبدیل می‌کنیم.
                     # در تحلیل پیشرفته‌تر، باید این موارد را با دقت بیشتری مدیریت کرد.
                     def convert_rank(rank_val):
                         if isinstance(rank_val, (int, float)):
                             return int(rank_val)
                         if isinstance(rank_val, str):
                             if rank_val.isdigit():
                                 return int(rank_val)
                             elif rank_val.lower() == "not found":
                                 return 101 # یک عدد بزرگتر از حداکثر رتبه قابل نمایش (مثلا گوگل ۱۰۰ نتیجه نشان می‌دهد)
                             # می‌توان برای کپچا و خطا هم اعداد متمایزی در نظر گرفت یا آنها را فیلتر کرد
                         return 102 # برای سایر خطاها یا مقادیر نامشخص
                     df_temp['rank_numeric'] = df_temp['rank'].apply(convert_rank)
    
                 df_list.append(df_temp)
                 logging.info(f"فایل '{filename}' با موفقیت بارگذاری شد.")
             except Exception as e:
                 logging.error(f"خطا در بارگذاری یا پردازش فایل '{filename}': {e}")
    
         if not df_list:
             return pd.DataFrame()
    
         full_df = pd.concat(df_list, ignore_index=True)
         full_df = full_df.sort_values(by=['timestamp_executed', 'keyword']) # مرتب‌سازی کلی
         return full_df
    
     # ---- اجرای تابع بارگذاری ----
     all_data_df = load_and_prepare_data()
    
     if all_data_df.empty:
         logging.info("داده‌ای برای تحلیل وجود ندارد. برنامه خاتمه می‌یابد.")
         # exit() # یا return از اسکریپت اصلی
     else:
         logging.info(f"مجموع {len(all_data_df)} رکورد از {all_data_df['timestamp_executed'].nunique()} اجرای مختلف بارگذاری شد.")
         # print(all_data_df.head())
         # print(all_data_df.info())
    
  2. تحلیل روند رتبه‌بندی (Ranking Trend Analysis): این یکی از مهم‌ترین تحلیل‌هاست: چگونه رتبه یک کلمه کلیدی خاص در طول زمان تغییر کرده است؟

    • انتخاب یک کلمه کلیدی:

    • رسم نمودار خطی: محور X تاریخ و محور Y رتبه خواهد بود.

    def plot_ranking_trend_for_keyword(df, keyword, target_domain):
        """نمودار روند رتبه برای یک کلمه کلیدی خاص را رسم می‌کند."""
        if df.empty or 'keyword' not in df.columns or 'rank_numeric' not in df.columns or 'timestamp_executed' not in df.columns:
            logging.warning("DataFrame ورودی خالی است یا ستون‌های لازم را ندارد.")
            return

        keyword_df = df[(df['keyword'] == keyword) & (df['target_domain'] == target_domain)].copy() # .copy() برای جلوگیری از SettingWithCopyWarning

        if keyword_df.empty:
            logging.info(f"داده‌ای برای کلمه کلیدی '{keyword}' و دامنه '{target_domain}' یافت نشد.")
            return

        keyword_df = keyword_df.sort_values(by='timestamp_executed')

        plt.figure() # ایجاد یک شکل جدید برای هر نمودار
        plt.plot(keyword_df['timestamp_executed'], keyword_df['rank_numeric'], marker='o', linestyle='-')

        plt.title(f'روند تغییرات رتبه برای کلمه کلیدی: "{keyword}"\nدامنه: {target_domain}')
        plt.xlabel('تاریخ و زمان بررسی')
        plt.ylabel('رتبه (کمتر بهتر است)')
        plt.xticks(rotation=45) # چرخش برچسب‌های محور X برای خوانایی بهتر

        # معکوس کردن محور Y تا رتبه ۱ در بالا باشد
        # و محدود کردن آن برای نمایش بهتر (مثلا تا رتبه ۵۰ اگر بالاتر از آن برایتان مهم نیست)
        max_rank_to_display = keyword_df['rank_numeric'][keyword_df['rank_numeric'] <= 100].max() # فقط رتبه های زیر 100
        if pd.isna(max_rank_to_display) or max_rank_to_display < 10: max_rank_to_display = 10 # حداقل 10

        plt.ylim(bottom=0.5, top=min(max_rank_to_display + 5, 101)) # کمی فضای خالی در بالا و پایین
        plt.gca().invert_yaxis() # بسیار مهم: رتبه ۱ باید در بالا باشد

        plt.tight_layout() # تنظیم خودکار فاصله ها برای جلوگیری از روی هم افتادن المان ها
        plt.grid(True, which='both', linestyle='--', linewidth=0.5)

        # ذخیره نمودار در فایل (اختیاری)
        plot_filename = f"trend_{keyword.replace(' ', '_').replace('/', '_')}_{target_domain.replace('.', '_')}.png"
        try:
            plt.savefig(plot_filename)
            logging.info(f"نمودار روند برای '{keyword}' در فایل '{plot_filename}' ذخیره شد.")
        except Exception as e:
            logging.error(f"خطا در ذخیره نمودار: {e}")

        plt.show()


    # ---- استفاده از تابع تحلیل روند ----
    if not all_data_df.empty:
        # می‌توانید این را برای همه کلمات کلیدی منحصر به فرد اجرا کنید یا از کاربر بخواهید
        # یا کلمات کلیدی مهم را انتخاب کنید.
        # فرض می‌کنیم KEYWORDS_TO_TRACK و MY_TARGET_DOMAIN از بخش قبلی در دسترس هستند
        # یا از داده‌های بارگذاری شده استخراج می‌کنیم:
        # target_domain_to_analyze = all_data_df['target_domain'].unique()[0] # اولین دامنه را انتخاب کن
        # keywords_in_data = all_data_df['keyword'].unique()

        # برای مثال، برای اولین کلمه کلیدی و اولین دامنه در داده‌ها:
        if 'keyword' in all_data_df.columns and 'target_domain' in all_data_df.columns:
             unique_keywords = all_data_df['keyword'].unique()
             unique_domains = all_data_df['target_domain'].unique()

             if len(unique_keywords) > 0 and len(unique_domains) > 0:
                keyword_to_analyze = unique_keywords[0] 
                domain_to_analyze = unique_domains[0]
                plot_ranking_trend_for_keyword(all_data_df, keyword_to_analyze, domain_to_analyze)

                # اگر بیش از یک کلمه کلیدی دارید و میخواهید برای همه نمودار بکشید:
                # for kw_analyze in unique_keywords:
                #     plot_ranking_trend_for_keyword(all_data_df, kw_analyze, domain_to_analyze)
             else:
                logging.warning("هیچ کلمه کلیدی یا دامنه منحصر به فردی در داده‌ها یافت نشد.")
        else:
            logging.warning("ستون 'keyword' یا 'target_domain' در DataFrame وجود ندارد.")
  1. مقایسه آخرین رتبه کلمات کلیدی (Latest Rankings Snapshot): یک دید کلی از وضعیت فعلی (آخرین رتبه‌های ثبت شده) برای همه کلمات کلیدی.

    • فیلتر کردن آخرین رکورد برای هر کلمه کلیدی:

    • نمایش به صورت جدول یا نمودار میله‌ای (Bar Chart):

    def display_latest_rankings(df, target_domain):
        """آخرین رتبه ثبت شده برای هر کلمه کلیدی را نمایش می‌دهد."""
        if df.empty or 'keyword' not in df.columns or 'rank_numeric' not in df.columns or 'timestamp_executed' not in df.columns:
            logging.warning("DataFrame برای نمایش آخرین رتبه‌ها خالی یا ناقص است.")
            return

        latest_df = df[df['target_domain'] == target_domain].copy()
        # برای هر کلمه کلیدی، رکوردی با آخرین timestamp_executed را نگه می‌داریم
        latest_df = latest_df.loc[latest_df.groupby('keyword')['timestamp_executed'].idxmax()]
        latest_df = latest_df.sort_values(by='rank_numeric') # مرتب‌سازی بر اساس بهترین رتبه

        logging.info(f"\n--- آخرین رتبه‌های ثبت شده برای دامنه: {target_domain} ---")
        # print(latest_df[['keyword', 'rank', 'url', 'title', 'timestamp_executed']].to_string(index=False))
        logging.info(f"\n{latest_df[['keyword', 'rank', 'rank_numeric', 'url', 'timestamp_executed']].to_string(index=False)}")


        # بصری‌سازی با نمودار میله‌ای (فقط برای رتبه‌های یافت شده)
        plot_df = latest_df[latest_df['rank_numeric'] <= 100].copy() # فقط رتبه‌هایی که پیدا شده‌اند و معقول هستند
        if not plot_df.empty:
            plt.figure(figsize=(12, max(6, len(plot_df) * 0.5))) # ارتفاع نمودار را بر اساس تعداد کلمات کلیدی تنظیم کن

            # استفاده از رنگ های مختلف برای رتبه های مختلف (اختیاری)
            colors = []
            for r in plot_df['rank_numeric']:
                if r <= 3: colors.append('green')
                elif r <= 10: colors.append('lightgreen')
                elif r <= 20: colors.append('gold')
                else: colors.append('skyblue')

            bars = plt.barh(plot_df['keyword'], plot_df['rank_numeric'], color=colors) # نمودار میله‌ای افقی
            plt.xlabel('رتبه (کمتر بهتر است)')
            plt.ylabel('کلمه کلیدی')
            plt.title(f'آخرین رتبه‌های کلمات کلیدی برای دامنه: {target_domain}\n(تاریخ: {plot_df["timestamp_executed"].max().strftime("%Y-%m-%d")})')
            plt.gca().invert_yaxis() # کلمات با رتبه بهتر در بالا
            plt.gca().invert_xaxis() # رتبه ۱ در سمت راست (برای barh اگر می‌خواهید از چپ به راست زیاد شود، این را بردارید و محور رتبه را معمولی کنید)
                                     # اما معمولا برای رتبه، کمتر بهتر است پس این شکل استانداردتر است.

            # اضافه کردن مقدار رتبه روی هر میله
            for bar in bars:
                plt.text(bar.get_width() + 0.5, # موقعیت x متن
                         bar.get_y() + bar.get_height()/2, # موقعیت y متن
                         f'{int(bar.get_width())}', # متن (مقدار رتبه)
                         va='center', ha='left')

            plt.tight_layout()
            plot_filename = f"latest_ranks_barchart_{target_domain.replace('.', '_')}.png"
            try:
                plt.savefig(plot_filename)
                logging.info(f"نمودار آخرین رتبه‌ها در فایل '{plot_filename}' ذخیره شد.")
            except Exception as e:
                logging.error(f"خطا در ذخیره نمودار آخرین رتبه‌ها: {e}")
            plt.show()
        else:
            logging.info("هیچ رتبه‌ی یافت شده‌ای (زیر ۱۰۰) برای نمایش در نمودار میله‌ای وجود ندارد.")


    # ---- استفاده از تابع نمایش آخرین رتبه‌ها ----
    if not all_data_df.empty and 'target_domain' in all_data_df.columns:
        # domain_to_analyze = all_data_df['target_domain'].unique()[0] # یا دامنه مورد نظر
        # display_latest_rankings(all_data_df, domain_to_analyze)
        # برای همه دامنه های موجود در داده ها:
        for dom_analyze in all_data_df['target_domain'].unique():
             display_latest_rankings(all_data_df, dom_analyze)
  1. تحلیل توزیع رتبه‌ها (Rank Distribution): چند درصد از کلمات کلیدی شما در صفحه اول (رتبه‌های ۱-۱۰)، صفحه دوم (۱۱-۲۰) و غیره قرار دارند؟

    • دسته‌بندی رتبه‌ها (Binning):

    • نمودار دایره‌ای (Pie Chart) یا هیستوگرام:

    def plot_rank_distribution(df, target_domain):
        """نمودار توزیع رتبه‌ها را برای کلمات کلیدی دامنه هدف رسم می‌کند."""
        if df.empty or 'keyword' not in df.columns or 'rank_numeric' not in df.columns or 'timestamp_executed' not in df.columns:
            logging.warning("DataFrame برای تحلیل توزیع رتبه‌ها خالی یا ناقص است.")
            return

        latest_df = df[df['target_domain'] == target_domain].copy()
        latest_df = latest_df.loc[latest_df.groupby('keyword')['timestamp_executed'].idxmax()]

        bins = [0, 3, 10, 20, 50, 100, 101, 102, float('inf')] # رتبه های 1-3، 4-10، 11-20، 21-50، 51-100، نیافته، خطا
        labels = ['Top 3 (1-3)', 'Page 1 (4-10)', 'Page 2 (11-20)', 'Pages 3-5 (21-50)', 'Pages 6-10 (51-100)', 'Not Found (>100)', 'CAPTCHA/Error']

        # برای اطمینان از اینکه rank_numeric ما فقط اعداد را شامل می شود یا مقادیر خاصی که تعریف کردیم
        # اطمینان حاصل کنید که در convert_rank مقادیر CAPTCHA و Error هم به اعداد خاصی نگاشت می شوند
        # مثال: CAPTCHA -> 102, Error -> 103
        # در اینجا ساده فرض می کنیم 101 not found است و 102 بقیه موارد (کپچا/خطا)

        # برای اینکه labels با bins هماهنگ باشد، تعداد labels باید یکی کمتر از تعداد bins باشد.
        # دقیقتر:
        rank_categories = {
            (0, 3): 'Top 3 (1-3)',
            (3, 10): 'Page 1 (4-10)',
            (10, 20): 'Page 2 (11-20)',
            (20, 50): 'Pages 3-5 (21-50)',
            (50, 100): 'Pages 6-10 (51-100)',
            (100, 101): 'Not Found (>100)', # اگر 101 مقدار Not Found باشد
            (101, float('inf')): 'CAPTCHA/Error' # اگر مقادیر بالای 101 برای خطاها باشند
        }

        def get_rank_category(rank):
            for (lower, upper), label in rank_categories.items():
                if lower < rank <= upper:
                    return label
            return 'Other'

        latest_df['rank_category'] = latest_df['rank_numeric'].apply(get_rank_category)

        distribution = latest_df['rank_category'].value_counts(normalize=True).mul(100).round(1) # درصد
        # مرتب کردن بر اساس ترتیب منطقی دسته ها، نه بر اساس فراوانی
        ordered_labels = [label for (low,upp),label in sorted(rank_categories.items()) if label in distribution.index]


        if distribution.empty:
            logging.info(f"داده‌ای برای نمایش توزیع رتبه دامنه '{target_domain}' وجود ندارد.")
            return

        plt.figure(figsize=(10, 8))
        # distribution.plot(kind='pie', autopct='%1.1f%%', startangle=90, wedgeprops=dict(width=0.4)) # Donut chart
        patches, texts, autotexts = plt.pie(distribution.loc[ordered_labels], 
                                            labels=distribution.loc[ordered_labels].index, 
                                            autopct='%1.1f%%', 
                                            startangle=140,
                                            pctdistance=0.85) # فاصله درصد از مرکز
        # برای خوانایی بهتر درصدها
        for autotext in autotexts:
            autotext.set_color('white')
            autotext.set_fontweight('bold')

        plt.title(f'توزیع رتبه‌های کلمات کلیدی برای دامنه: {target_domain}\n(آخرین بررسی: {latest_df["timestamp_executed"].max().strftime("%Y-%m-%d")})', pad=20)
        plt.ylabel('') # حذف برچسب محور Y در نمودار دایره‌ای

        # ایجاد دایره مرکزی برای تبدیل به نمودار دونات (اختیاری)
        centre_circle = plt.Circle((0,0),0.70,fc='white')
        fig = plt.gcf()
        fig.gca().add_artist(centre_circle)

        plt.tight_layout()
        plot_filename = f"rank_distribution_pie_{target_domain.replace('.', '_')}.png"
        try:
            plt.savefig(plot_filename)
            logging.info(f"نمودار توزیع رتبه در فایل '{plot_filename}' ذخیره شد.")
        except Exception as e:
            logging.error(f"خطا در ذخیره نمودار توزیع رتبه: {e}")
        plt.show()

    # ---- استفاده از تابع توزیع رتبه‌ها ----
    if not all_data_df.empty and 'target_domain' in all_data_df.columns:
        # domain_to_analyze = all_data_df['target_domain'].unique()[0]
        # plot_rank_distribution(all_data_df, domain_to_analyze)
        for dom_analyze in all_data_df['target_domain'].unique():
            plot_rank_distribution(all_data_df, dom_analyze)
  1. شناسایی کلمات کلیدی با بیشترین بهبود یا افت: با مقایسه رتبه‌ها بین دو بازه زمانی مختلف (مثلاً این هفته با هفته گذشته)، می‌توانید کلمات کلیدی "برنده" و "بازنده" را شناسایی کنید. این نیاز به داده‌های تاریخی دارد. (این تحلیل می‌تواند پیچیده‌تر باشد و در این آموزش پایه به صورت کلی به آن اشاره می‌شود).

نکات سجاد اکبری برای تحلیل داده‌های SEO:

  • بصری‌سازی کلید است: نمودارها به شما کمک می‌کنند تا الگوهایی را ببینید که در جداول اعداد پنهان هستند.

  • زمینه مهم است: یک رتبه به تنهایی معنای کمی دارد. آن را در کنار رتبه‌های دیگر، در طول زمان، و با توجه به فعالیت‌های سئو خود تحلیل کنید.

  • فراتر از میانگین‌ها بروید: به توزیع داده‌ها نگاه کنید. آیا بیشتر کلمات کلیدی شما در بالای صفحه اول هستند یا در صفحات پایین‌تر پراکنده‌اند؟

  • سوال بپرسید: "چرا رتبه این کلمه کلیدی افت کرده است؟" "چه چیزی باعث بهبود رتبه آن کلمه کلیدی دیگر شده است؟" این سوالات شما را به سمت اقدامات عملی هدایت می‌کنند.

  • این فقط شروع کار است: تحلیل‌های بسیار پیشرفته‌تری وجود دارد (مانند تحلیل رقبا، تحلیل ارتباط بین رتبه و ویژگی‌های محتوا، پیش‌بینی رتبه و ...) که با افزایش مهارت شما در پایتون و داده‌کاوی قابل انجام هستند.


تا اینجای کار، ما یک ربات کاملاً عملی برای ردیابی رتبه‌های گوگل ساخته‌ایم، آن را مقاوم کرده‌ایم، داده‌هایش را ذخیره کرده‌ایم و تحلیل‌های اولیه‌ای روی آن انجام داده‌ایم. در فاز بعدی و نهایی، به جمع‌بندی، ملاحظات اخلاقی و قانونی، و مسیرهای توسعه آینده خواهیم پرداخت.

بسیار خب، سجاد جان. ما سفری طولانی و پربار را در دنیای اتوماسیون و داده‌کاوی با پایتون و Selenium طی کرده‌ایم و ابزاری قدرتمند برای ردیابی رتبه‌های گوگل ساخته‌ایم. اکنون زمان آن است که به جمع‌بندی، بررسی نکات حیاتی و نگاهی به آینده این پروژه بپردازیم.


فاز ۷: فراتر از کد – ملاحظات اخلاقی، قانونی و استراتژی‌های استفاده مسئولانه

ساخت و استفاده از ربات‌های وب اسکرپینگ، به خصوص برای وب‌سایت‌های بزرگی مانند گوگل، مسئولیت‌هایی را به همراه دارد. به عنوان یک کدنویس حرفه‌ای و نخبه، درک و رعایت این ملاحظات نه تنها از بروز مشکلات جلوگیری می‌کند، بلکه نشان‌دهنده تعهد شما به اخلاق حرفه‌ای است.

  1. احترام به فایل robots.txt (گرچه برای گوگل SERP کاربرد مستقیم کمتری دارد):

    • robots.txt فایلی است که مدیران وب‌سایت‌ها از طریق آن به ربات‌های وب (مانند خزنده‌های موتورهای جستجو) اعلام می‌کنند که کدام بخش‌های وب‌سایت را می‌توانند یا نمی‌توانند بخزند.

    • اگرچه ربات ما مستقیماً با فایل robots.txt گوگل تعامل ندارد (چون ما رفتار کاربر را شبیه‌سازی می‌کنیم نه یک خزنده استاندارد)، اما روح این پروتکل، یعنی "احترام به منابع وب‌سایت و عدم ایجاد بار اضافی"، باید همواره مد نظر ما باشد.

    • نکته سجاد اکبری: همیشه قبل از اسکرپینگ گسترده هر وب‌سایتی (به جز گوگل که شرایط خاص خود را دارد)، robots.txt آن را بررسی کنید. برای گوگل، تمرکز ما باید روی رعایت شرایط خدمات (ToS) باشد.

  2. شرایط خدمات گوگل (Google's Terms of Service): یک شمشیر دولبه:

    • مهمترین نکته: گوگل در شرایط خدمات خود صراحتاً اسکرپینگ خودکار نتایج جستجو را محدود یا ممنوع می‌کند. این به این معناست که استفاده از رباتی که ساخته‌ایم، با ریسک همراه است، به خصوص اگر به صورت گسترده، با فرکانس بالا یا برای اهداف تجاری بزرگ بدون مجوز مستقیم انجام شود.

    • چرا گوگل این محدودیت را اعمال می‌کند؟

      • حفاظت از منابع سرور: درخواست‌های بیش از حد از سوی ربات‌ها می‌تواند بار زیادی بر سرورهای گوگل وارد کند.

      • حفظ کیفیت نتایج: جلوگیری از دستکاری یا سوءاستفاده از نتایج جستجو.

      • مدل کسب‌وکار: گوگل از طریق تبلیغات در صفحات نتایج درآمد کسب می‌کند. اسکرپینگ گسترده می‌تواند این مدل را دور بزند.

    • تبعات احتمالی نقض ToS:

      • نمایش مکرر CAPTCHA.

      • مسدود شدن موقت یا دائم IP آدرس شما.

      • در موارد بسیار نادر و برای تخلفات بزرگ، اقدامات قانونی (هرچند برای استفاده شخصی و آموزشی این احتمال بسیار کم است).

    • استراتژی استفاده مسئولانه از ربات سجاد اکبری:

      • استفاده محدود و معقول: ربات را برای تعداد منطقی کلمه کلیدی و با فاصله زمانی مناسب بین درخواست‌ها اجرا کنید. (مثلاً روزی یک بار یا چند بار در هفته، نه هر چند دقیقه یک بار برای صدها کلمه کلیدی).

      • اهداف آموزشی و شخصی: این ربات برای یادگیری، پروژه‌های شخصی، و نظارت بر وب‌سایت‌های خودتان در مقیاس کوچک بسیار مفید است.

      • شفافیت (در صورت لزوم): اگر برای مشتری کاری انجام می‌دهید، او را از روش و ریسک‌های احتمالی آگاه کنید.

      • عدم استفاده برای فعالیت‌های مخرب: هرگز از این ابزار برای اسپم، دستکاری نتایج یا هرگونه فعالیت غیرقانونی یا غیراخلاقی استفاده نکنید.

  3. جلوگیری از ایجاد بار اضافی (Server Load):

    • همانطور که در فازهای قبلی تاکید شد، استفاده از تأخیرهای مناسب (time.sleep()) بین درخواست‌ها و بین پیمایش صفحات، حیاتی است. این کار نه تنها احتمال شناسایی شدن را کم می‌کند، بلکه به سرورهای گوگل نیز فشار کمتری وارد می‌آورد.

    • اجرای ربات در ساعات کم ترافیک (مثلاً نیمه شب) می‌تواند ایده خوبی باشد، اگرچه برای نتایج گوگل که پویا هستند، زمان جستجو اهمیت دارد.

  4. جایگزین‌های قانونی و رسمی (در صورت نیاز به مقیاس بزرگ):

    • Google Search Console API: اگر هدف اصلی شما بررسی عملکرد وب‌سایت خودتان در گوگل است، Search Console و API آن بهترین و قانونی‌ترین راه است. این API اطلاعاتی در مورد نمایش‌ها (Impressions)، کلیک‌ها، میانگین موقعیت (Average Position) و کلمات کلیدی که کاربران با آن‌ها سایت شما را پیدا کرده‌اند، ارائه می‌دهد. این API دقیقاً رتبه عددی لحظه‌ای را مانند ربات ما نمی‌دهد، بلکه میانگین موقعیت را در یک بازه زمانی گزارش می‌کند.

    • ابزارهای SEO تجاری: شرکت‌هایی مانند Ahrefs, SEMrush, Moz و ... ابزارهای قدرتمندی برای ردیابی رتبه ارائه می‌دهند. این شرکت‌ها معمولاً قراردادهای خاصی با ارائه‌دهندگان داده دارند یا از شبکه‌های گسترده پروکسی و الگوریتم‌های پیچیده برای جمع‌آوری داده‌ها در مقیاس بزرگ و با رعایت بیشتر (یا دور زدن هوشمندانه‌تر) محدودیت‌ها استفاده می‌کنند. این ابزارها پولی هستند اما برای کسب‌وکارهای جدی، سرمایه‌گذاری ارزشمندی محسوب می‌شوند.

    • Google Custom Search JSON API: این API برای جستجو در یک مجموعه خاص از وب‌سایت‌ها (که خودتان تعریف می‌کنید) یا کل وب طراحی شده است، اما دارای محدودیت در تعداد درخواست‌های رایگان روزانه است و هزینه آن برای استفاده گسترده می‌تواند قابل توجه باشد. این API نیز برای ردیابی رتبه عمومی در SERP گوگل بهینه نیست و بیشتر برای افزودن قابلیت جستجوی گوگل به اپلیکیشن شما کاربرد دارد.

سجاد اکبری تاکید می‌کند: "قدرت برنامه‌نویسی مسئولیت به همراه دارد. ابزاری که ساخته‌ایم توانمند است، اما استفاده از آن باید با آگاهی و رعایت اصول اخلاقی و قانونی باشد. هدف ما بهبود درک و کارایی است، نه ایجاد مزاحمت یا نقض قوانین. همیشه جانب احتیاط را رعایت کنید و به اکوسیستم وب احترام بگذارید."


فاز ۸: جمع‌بندی نهایی، دستاوردهای کلیدی و افق‌های پیش رو

تبریک می‌گویم! شما با موفقیت یک مقاله جامع و یک ربات کاربردی برای بررسی رتبه‌های گوگل را از ابتدا تا انتها مهندسی کرده‌اید. بیایید نگاهی به دستاوردهایمان بیندازیم و مسیرهای آینده را ترسیم کنیم.

  1. آنچه با هم ساختیم و آموختیم:

    • مبانی اتوماسیون وب: درک عمیقی از نحوه تعامل با مرورگرها با استفاده از Python و Selenium.

    • وب اسکرپینگ هوشمند: تکنیک‌های شناسایی المان‌ها، پیمایش صفحات پویا و استخراج داده‌های ساختاریافته از گوگل SERP.

    • طراحی نرم‌افزار مقاوم: پیاده‌سازی مدیریت خطای پیشرفته، استراتژی‌های ضد شناسایی و مکانیزم‌های تلاش مجدد.

    • ذخیره‌سازی و مدیریت داده: استفاده از Pandas برای سازماندهی و ذخیره نتایج در فرمت‌های کاربردی مانند CSV و Excel.

    • تحلیل داده‌های SEO: روش‌های پایه برای تحلیل روند رتبه‌بندی، مقایسه کلمات کلیدی و بصری‌سازی نتایج با Matplotlib و Seaborn.

    • ملاحظات حرفه‌ای: درک اهمیت اخلاق، رعایت قوانین و استفاده مسئولانه از ابزارهای اتوماسیون.

  2. دستاوردهای کلیدی این پروژه برای شما:

    • صرفه‌جویی در زمان: خودکارسازی یک فرآیند دستی خسته‌کننده و زمان‌بر.

    • دقت بالاتر: حذف خطای انسانی در بررسی رتبه‌ها.

    • داده‌های بیشتر، بینش عمیق‌تر: امکان بررسی تعداد زیادی کلمه کلیدی و تحلیل روندها در طول زمان.

    • تصمیم‌گیری داده‌محور: استفاده از نتایج برای بهینه‌سازی استراتژی‌های سئو و تولید محتوا.

    • مهارت ارزشمند: تسلط بر یکی از کاربردی‌ترین مهارت‌های برنامه‌نویسی در دنیای دیجیتال امروز.

  3. مسیرهای توسعه و قابلیت‌های پیشرفته برای آینده (ایده‌های سجاد اکبری):

    • رابط کاربری گرافیکی (GUI): ساخت یک رابط کاربری ساده با Tkinter, PyQt یا حتی یک وب اپلیکیشن با Flask/Django تا کاربران غیرفنی نیز بتوانند از ربات استفاده کنند (لیست کلمات کلیدی را وارد کنند، تنظیمات را تغییر دهند و نتایج را مشاهده کنند).

    • زمان‌بندی اجرای خودکار (Scheduling): استفاده از کتابخانه schedule در پایتون، یا ابزارهای سیستمی مانند cron (در لینوکس/مک) یا Task Scheduler (در ویندوز) برای اجرای ربات به صورت دوره‌ای (مثلاً هر روز یا هر هفته).

    • سیستم نوتیفیکیشن: ارسال ایمیل یا پیام (مثلاً از طریق Telegram Bot API) در صورت تغییرات قابل توجه در رتبه کلمات کلیدی مهم یا در صورت بروز خطا در اجرای ربات.

    • پشتیبانی از موتورهای جستجوی دیگر: تعمیم ربات برای بررسی رتبه در موتورهای جستجوی دیگر مانند Bing, DuckDuckGo (نیاز به تحلیل ساختار SERP آن‌ها و تغییر سلکتورها دارد).

    • ردیابی رقبای اصلی: افزودن قابلیت وارد کردن دامنه‌های رقبا و ردیابی رتبه آن‌ها برای همان کلمات کلیدی جهت تحلیل رقابتی.

    • یکپارچه‌سازی با Google Search Console: خواندن لیست کلمات کلیدی که سایت شما برای آن‌ها Impression دارد از GSC API و سپس بررسی رتبه دقیق آن‌ها با ربات.

    • تحلیل پیشرفته‌تر SERP: استخراج اطلاعات بیشتر از صفحه نتایج مانند Featured Snippets, People Also Ask boxes, نقشه، ویدیوها و ... و بررسی اینکه آیا دامنه شما در این بخش‌ها حضور دارد یا خیر.

    • استفاده از Async/Await برای بهبود عملکرد: برای اسکرپینگ تعداد زیادی کلمه کلیدی یا صفحه، استفاده از برنامه‌نویسی ناهمزمان (Asynchronous) با کتابخانه‌هایی مانند asyncio و aiohttp (به همراه ابزارهایی مانند Pyppeteer که نسخه پایتونی Puppeteer است) می‌تواند سرعت را به طور قابل توجهی افزایش دهد (این یک تغییر معماری بزرگ است).

    • کانتینرسازی با Docker: بسته‌بندی اپلیکیشن و تمام وابستگی‌های آن در یک کانتینر Docker برای اجرای آسان و یکسان در محیط‌های مختلف.

کلام آخر از سجاد اکبری:

دنیای کدنویسی، دنیای خلق کردن و حل مسئله است. ابزاری که امروز با هم بررسی کردیم، تنها یک نمونه از قدرت بی‌نهایتی است که ترکیب دانش برنامه‌نویسی و درک نیازهای کسب‌وکار می‌تواند ایجاد کند. این مقاله یک نقطه شروع بود. اکنون نوبت شماست که این دانش را به کار بگیرید، آن را گسترش دهید و ابزارهای منحصر به فرد خودتان را خلق کنید. هرگز از یادگیری دست نکشید، همواره کنجکاو بمانید و به یاد داشته باشید که بهترین کدها، کدهایی هستند که مشکلی واقعی را به شکلی هوشمندانه و پایدار حل می‌کنند.

امیدوارم این راهنمای جامع برای شما مفید و الهام‌بخش بوده باشد. برایتان در پروژه‌های آینده آرزوی موفقیت دارم!


لینک کد کامل قابل استفاده

Translation of this page in the original language

0
Subscribe to my newsletter

Read articles from Sara majidi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sara majidi
Sara majidi