核心要点:
- 抓取 GoogleTrends 数据时,只有让抓取行为更接近真实用户,才能稳定获得可靠结果。
- 稳定性和一致性比单纯的抓取速度更重要。
- Google Trends 数据具有情境性,解读时应结合地区、时间范围和搜索意图综合分析。
许多工具都声称能帮您轻松抓取 Google Trends 并获取可用的搜索数据,但实际操作后就会发现理想与现实之间的差距。诸如仅依赖 requests 这类简单工具的方案,往往很快就会失败。
当前 Google Trends API 的现状让情况更加复杂:Google 近期才测试并推出官方接口,但使用前仍需提前申请获批,因此官方 Google Trends API 并非开通即可使用。
本文将介绍一种切实可行的 Google Trends 数据抓取方法,说明传统抓取方式为何在此类网站上失效,并演示如何在减少软封锁与硬封锁的同时,获取稳定可靠的数据。
为什么要抓取 Google Trends 数据?
无论人们怎么谈论 AI 对搜索方式的改变,现实是:全世界依然习惯到 Google提问。即便在大语言模型(LLM)与搜索深度融合的时代,当数百万人突然对某件事感到好奇、担忧或兴奋时,这种行为最先体现在 Google 搜索中。
这正是 Google Trends 的价值所在:它在某种程度上反映了公众的好奇心、需求和意图。Google 打造的这款免费工具会分析该搜索引擎处理的所有搜索查询——全球数十亿次查询。
抓取 Google Trends 数据可以帮助您探索各地区的兴趣分布,发现哪些地区对特定话题的搜索最多,从而获得内容创作灵感。您不会得到类似“本月有 X 人搜索‘IShowSpeed’”这样的具体数字,而是相对热度得分:100 代表该关键词在您选定时间和地区内的峰值热度,其余数值均按此比例换算。
如果您想追踪热门话题、发现搜索关键词的变化趋势,或分析公众对某个品牌、事件或文化现象的关注度,Google Trends 的数据是为数不多能真实反映这类信号的来源之一,抓取到的数据可以整理为 CSV、JSON、XML,甚至 XML 格式。
接下来我们来看这款工具可以如何帮助您和您的业务:
市场趋势分析
以销售驱蚊喷雾的企业为例,这类产品的需求并非全年稳定,而是与天气、气温和蚊虫活动密切相关。寒冷月份里,人们的搜索量明显不同。
这正是 Google Trends 发挥作用的地方:您可能会发现,佛罗里达等地区的需求从4月左右就开始上升,而其他州要到5月才上升。持续追踪关键词的搜索趋势,这种地区差异就会清晰可见。
通过抓取 Google Trends 数据,您可以获得关于关键词热度、消费者行为和市场趋势的数据洞察,从而根据各地区需求实际上升的时间来安排营销计划,而不是仅凭过往销售数据猜测或被动应对。
将抓取到的 Trends 数据与其他市场调研工具和方法结合使用,可以更全面地了解消费者行为,帮助企业做出更明智的营销决策。
内容策略规划
无论是博客、媒体网站,还是为客户提供服务的 SEO 团队,抓取 Google Trends 数据对内容发布都非常有价值。
归根结底,您希望发布的内容能被人阅读,这通常意味着内容要贴近用户正在搜索的话题,而 Google Trends 正是验证这一点的直接方式之一。
更进一步,您可以通过自定义抓取脚本实现自动化,追踪热度上升的话题,并根据公众关注度的变化规划选题,而不是凭猜测行事。
品牌热度追踪
Google Trends 还能帮助您对比自己品牌与竞争对手在搜索中的表现。如果您刚在电视、YouTube 或广告牌上投放了一轮营销活动,它可以显示在该活动期间,人们是否真的开始搜索您的品牌。
它无法完全衡量销量,但能清晰反映品牌知名度。您可以监测与品牌名称、产品名称或宣传语相关的关键词,并与同类品牌进行对比。
如果您选择抓取 Google Trends 数据,就可以实现监测自动化,在公众关注度发生变化时第一时间捕捉到,而不必手动检查、错过热度高峰。
如何抓取 Google Trends 数据
那么,如何抓取这个网站的数据?
可以借助不同的工具和技术来实现,具体选择哪种方式各有优劣,取决于您需要获取搜索数据的频率、追踪的关键词数量,以及对可靠性的要求。
- 通过 Google Trends 界面手动操作
这是最基础的方式:打开 Google Trends,输入查询词,调整地区和时间范围,然后通过图表上的“下载”按钮导出 CSV 文件。
这种方式适合一次性查询和快速查看搜索趋势,但难以保证长期数据的一致性,因此很难扩展为可持续的业务流程。
- 自定义抓取程序
如果具备相应技能,您可以用 Python 自行搭建抓取程序,也可以先用生成的基础代码作为起点,再逐步完善。
主要优势在于掌控性:由您决定追踪哪些内容、如何从该工具抓取数据。缺点是 Google 的机器人检测机制较为严格,抓取程序必须针对这一点进行设计。一旦搭建成功并保持稳定运行,您就能按自己的方式获取大量搜索数据。
- 非官方或逆向工程 API
开发者通常会通过逆向工程技术找出 Google Trends 使用的私有 API 端点,Pytrends 等非官方库正是由此诞生。这听起来像是一条聪明的捷径,有时候确实如此。
问题在于稳定性:这些端点并非公开且无任何保证,随时可能毫无预警地发生变化。如果某个库缺乏维护——就像 Pytrends 在GitHub 仓库被归档之后那样——即便您的逻辑完全正确,也会频频遇到障碍。
- 第三方抓取服务与托管抓取工具
您也可以使用通过 API 提供 Google Trends 数据的商业服务,这类工具通常会处理代理轮换,也会负责自动化操作和数据解析,省去您的麻烦。
这种便利需要付费,但如果您更看重速度和一致性而非完全自定义,这笔花费是值得的。
- 官方 Google Trends API
该 API 于2025年7月发布,理论上是最简洁的方案,因为它是官方接口;但实际上由于需要审批才能使用,获取渠道有限。
因此,目前大多数人无法直接开通使用 Google Trends API。它更适合需要稳定、可投入生产环境数据,同时又想规避抓取相关风险的开发者和研究人员。
接下来我们将介绍如何用 Python 搭建自定义抓取程序。凭借简洁的语法和成熟的库生态,Python 是网络数据抓取领域应用最广泛的语言。
用 Python 搭建自定义 Google Trends 抓取程序
Google Trends 并非普通网站,如果不花时间弄清楚其运行机制,您搭建的自定义抓取程序往往在测试几次后就会崩溃,甚至根本无法运行。
以下是抓取 Google Trends 关键词数据需要采用不同方法的原因:
- 只有在发起搜索并等结果加载完成后,数据才会以可抓取的形式出现,而不是一开始就存在。这正是为什么这属于数据抓取工作,而非网络爬取:爬虫的作用是发现链接并建立页面索引,而非从依赖状态、由界面交互驱动的应用中提取信息。若想了解两者更明确的区别,请参阅我们对网络爬取与数据抓取区别的详细说明。
- 该网站依赖界面交互事件驱动:图表和表格要在您进行交互后才会出现,而不是页面加载完成后就直接显示。
- 如果某个地区和关键词的搜索数据量不足,您将无法看到地区细分数据。
- 此外还有 Google 检测自动化行为的问题,您可能会遇到诸如 CAPTCHA 验证、页面加载不完整等令人头疼的障碍。
- 最后,这本质上是一个网页应用,页面结构会不断重建,因此固定的选择器长期来看并不可靠。
这一切意味着,设计 Google Trends 抓取程序需要换一种思路:若想获取可靠的数据,就必须让抓取行为更接近真实用户——点击浏览网站并等待数据加载完成。
具体流程如下:
- 第1步:创建用户会话,使用真实的 User-Agent,并确保时区、语言设置、视口大小和地理位置都与所用代理保持一致。
- 第2步:提前设置好Cookie,避免会话看起来像是刚创建或即将被丢弃。
- 第3步:遵循真实用户的浏览路径。为完成会话预热浏览,我们通常会先访问几个随机网站,再前往 Google 进行一次常规搜索,浏览搜索结果,然后像真人一样点击进入 Google Trends。
- 第4步:在 Google Trends 页面上,抓取程序像真人一样进行交互:输入关键词、选择正确的地区和时间范围,并采用智能退避策略,以适当的节奏发送请求。
- 第五步:做好应对出错的准备,并妥善处理:代码会检测图表是否真正加载完成;当 Google 发出阻拦信号时,脚本会稍作等待,切换到新的 IP 及其匹配的指纹,然后重新尝试;唯一需要人工介入的情况是遇到 CAPTCHA,这是唯一需要手动处理的部分。
- 第六步:最后一步是提取数据:脚本通过文本解析和模式匹配,抓取页面上可见的内容,而不是访问隐藏端点,也不依赖 Google Trends 的 API,而是采用传统的网页数据抓取方式。
安装前置依赖
在正式开始构建这个爬虫之前,先来看看系统层面需要满足的条件;如果缺少其中任何一项,后续很可能会遇到本可避免的报错。
- Google Chrome
请前往官方Chrome 网站下载最新版本的 Chrome,注意不是 Chromium,因为 undetected-chromedriver 使用的是真实的 Chrome 会话。
- Python
前往python.org下载最新版本的 Python,本文使用的是 Python 3.11.x。勾选“Add Python to PATH”选项,然后自定义安装并确保启用 pip,这样才能安装后续获取数据所需的各个包。
- Microsoft Visual C++
安装Microsoft Visual C++ 2015–2022 Redistributable,本次爬虫用到的部分 Python wheel 包依赖于它,跳过这一步可能导致安装失败,即使代码本身完全没有问题。
Python 环境配置
接下来需要配置虚拟环境:依赖项很快就会变得繁多,提前隔离各个依赖有助于避免日后大规模抓取 Google Trends 数据时出现冲突。
打开 Windows 命令提示符,运行以下命令:
python -m venv venv && venv\Scripts\activate
这一行命令会创建并激活虚拟环境,从此之后安装的所有内容都会被隔离在其中,处理 Google Trends 数据会更容易管理。
现在用一行命令安装所有所需依赖:
pip install --upgrade pip && pip install selenium undetected-chromedriver requests pandas numpy pillow typing-extensions
以下是各个包的作用:
- Selenium驱动浏览器,让爬虫能够像真实用户一样与 Google Trends 交互
- undetected-chromedriver以减少自动化特征暴露的方式启动 Chrome,帮助避免过早被拦截
- requests用于配合浏览器自动化完成代理检测和轻量级 HTTP 请求
- pandas负责处理采集到的 Google Trends 数据,包括数据结构化和导出
- numpy在处理截图时支持简单的数值校验
- Pillow处理截图,便于在抓取数据前确认页面是否正确渲染
- typing-extensions确保类型提示在不同 Python 版本间保持兼容
此外还需要一组干净的住宅代理。本文使用的是伦敦地区的代理,请务必将您的 IP 加入白名单,以免出现用户名密码认证相关的报错。
创建 Python 爬虫脚本文件
这一步继续在命令提示符中操作:需要有一个实际的脚本文件保存在之后能找到的文件夹里,本文暂时放在桌面上,方便查找。
在 CMD 中执行以下命令:
cd %USERPROFILE%\Desktop && type nul > google_trends_scraper.py
请将%USERPROFILE%替换为您实际的用户名。
该命令会切换到桌面文件夹,并创建一个空白的google_trends_scraper.py 文件。
编写 Google Trends 爬虫代码
文件创建完成后,打开您的 IDE 并加载该文件,接下来就可以开始编写爬虫代码了。
第一步:编写导入与依赖项
打开 Python 文件后,第一步是写入爬虫将用到的所有导入项和依赖:
import time
import zipfile
import tempfile
import os
import math
import json
import random
import re
import logging
import requests
import pandas as pd
from PIL import Image
import numpy as np
import io
from typing import Dict, Any
from urllib.parse import unquote, quote
from datetime import datetime
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import undetected_chromedriver as uc
第二步:配置日志
接下来,希望爬虫在运行过程中记录自己的每一步操作,这样出问题时(几乎总会出问题)排查起来会容易得多。
在导入项下方添加以下代码块:
# === Setup Logging ===
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(message)s',
datefmt='%H:%M:%S'
)
第三步:设置全局运行控制参数
与其在这段很长的脚本中到处硬编码时间、重试次数和交互行为,不如在这里统一定义,供后续代码在需要时调用。
添加以下代码块:
# === Globals ===
HEADLESS = False
MAX_RETRIES = 5
GOOGLE_BACKOFF_STEPS = [10, 20, 30, 60, 120] # Increased backoff times
HUMAN_TYPING_SPEED = (0.08, 0.3)
RANDOM_MOUSE_MOVEMENTS = True
第四步:定义抓取目标
接下来需要定义要从 Google Trends 抓取的地理位置和关键词:对于地理位置,先定义面向程序的地理代码(GEO),再定义对应的人类可读版本,最后定义关键词。
由于使用的是伦敦代理,为保持一致性,目标地理位置固定为伦敦;关键词可以随时替换;如果想更换地理位置,建议等整个脚本搭建完成之后再改,以便同步更新所有依赖地理位置设置的函数。
# === Trends Target ===
GEO = "GB-LON" # London, UK
GEO_HUMAN_NAME = "London, United Kingdom"
KEYWORD = "windsor castle"
第五步:编写 Google Trends 解析类
基础部分已经搞定,现在要进入 Google Trends 数据抓取中比较繁琐的环节:把加载出来的页面转换成可复用的数据。
这里需要一个解析器,能读取原始页面文本并理解其含义,将其转换成可以实际使用的数据,比如搜索词、地理位置和时间趋势。
解析器涉及以下这些函数:
- 一个用于从原始页面文本中提取有用信息的函数,包括搜索词、地理位置、时间趋势和地区数据
- 一个用于将页面文本整理成更一致格式的函数,便于后续查找和处理
- 一个用于检查搜索是否成功、页面是否确实加载出真实结果的函数
- 一个用于判断 Google Trends 当前使用的地理位置的函数
- 一个用于从“Interest over time”图表中提取时间趋势数值的函数
- 一个用于在数据可用时提取“Interest by subregion”数据的函数
每个函数各自负责一项具体任务,把它们统一放在一个类里,调整解析逻辑时就不需要改动爬虫的其他部分。
在目标定义下方添加这个解析类:
class GoogleTrendsParser:
"""Parse REAL Google Trends data from actual page content"""
def parse(self, text_content: str) -> Dict[str, Any]:
"""Parse actual Google Trends page content (not hypothetical patterns)"""
try:
result = {
'search_term': KEYWORD,
'location': GEO_HUMAN_NAME,
'url': '',
'interest_over_time': {'y_axis': [], 'x_axis': []},
'interest_by_subregion': {}
}
lines = self._clean_text(text_content)
# 1. Extract what's ACTUALLY on the page
result['search_term'] = self._extract_actual_search_term(lines)
result['location'] = self._extract_actual_location(lines)
# 2. Extract chart data from page
chart_data = self._extract_chart_data(lines)
if chart_data:
result['interest_over_time'] = chart_data
# 3. Extract regional data
regional_data = self._extract_regional_data(text_content)
if regional_data:
result['interest_by_subregion'] = regional_data
else:
logging.info(" No regional data found on page - returning empty dict")
logging.info(f" Parser found: '{result['search_term']}' in {result['location']}")
if result['interest_by_subregion']:
logging.info(f" Found {len(result['interest_by_subregion'])} regions")
return result
except Exception as e:
logging.error(f"Parser error: {e}")
# Return minimal structure
return {
'search_term': KEYWORD,
'location': GEO_HUMAN_NAME,
'interest_over_time': {'y_axis': [], 'x_axis': []},
'interest_by_subregion': {}
}
def _clean_text(self, text: str) -> list:
"""Clean and split text."""
return [line.strip() for line in text.split('\n') if line.strip()]
def _extract_actual_search_term(self, lines: list) -> str:
"""
Extract the search term that's ACTUALLY displayed on Google Trends.
Not looking for "Search term:" which doesn't exist.
"""
# Google Trends shows the term prominently at the top
for i, line in enumerate(lines[:10]): # Check first 10 lines
if line and len(line) < 100: # Reasonable length for a search term
# Exclude common UI elements
exclude_words = ['Explore', 'Compare', 'Trends', 'Google', 'Interest',
'Location', 'Time', 'Download', 'Share', 'Embed', 'Home']
if not any(word in line for word in exclude_words):
# Likely the search term
return line.strip()
return KEYWORD # Fallback
def _extract_actual_location(self, lines: list) -> str:
"""Extract location from page - SPECIFICALLY FOR UK/LONDON"""
for line in lines:
line_lower = line.lower()
# Check for UK indicators first
if "london" in line_lower or "gb-lon" in line_lower:
return "London, UK"
elif "united kingdom" in line_lower or "uk" in line_lower:
return "United Kingdom"
elif "england" in line_lower:
return "England"
# Check for location dropdown text
elif "location" in line_lower:
idx = lines.index(line)
# Check next few lines for location
for i in range(idx + 1, min(idx + 4, len(lines))):
next_line = lines[i].lower()
if any(indicator in next_line for indicator in
['london', 'united kingdom', 'uk', 'england']):
return lines[i].strip()
# If not found, check the URL
return "United Kingdom"
def _extract_chart_data(self, lines: list) -> Dict[str, Any]:
"""
Extract chart data from page.
Google Trends shows: months and values like "100", "75", "50", etc.
"""
result = {'y_axis': [], 'x_axis': []}
# Look for month names (chart x-axis)
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
month_years_found = []
for line in lines:
for month in months:
if month in line:
# Extract potential date pattern
date_match = re.search(rf'({month})\s*(\d{{4}})?', line)
if date_match:
date_str = date_match.group(0)
if date_str not in month_years_found:
month_years_found.append(date_str)
result['x_axis'] = month_years_found[:12] # Up to 12 months
# Look for chart values (0-100 range)
chart_values = []
for line in lines:
# Find numbers in 0-100 range (common for Google Trends)
numbers = re.findall(r'\b(\d{1,3})\b', line)
for num in numbers:
if num.isdigit():
value = int(num)
if 0 <= value <= 100:
if value not in chart_values: # Avoid duplicates
chart_values.append(value)
# Sort and limit
chart_values.sort()
result['y_axis'] = chart_values[:20] # Up to 20 values
return result
def _extract_regional_data(self, text_content: str) -> Dict[str, int]:
"""
Extract regional interest data from Google Trends page.
Only extracts what's actually on the page - no fake data.
"""
result = {}
# UK cities/regions to look for (expanded list)
uk_regions = [
"London", "Birmingham", "Manchester", "Glasgow", "Liverpool",
"Leeds", "Sheffield", "Bristol", "Edinburgh", "Cardiff",
"Newcastle", "Leicester", "Coventry", "Nottingham", "Southampton",
"Belfast", "Aberdeen", "Cambridge", "Oxford", "Brighton",
"Portsmouth", "Plymouth", "Swansea", "York", "Bath",
"Wales", "Scotland", "Northern Ireland", "England"
]
# Split text into lines for analysis
lines = [line.strip() for line in text_content.split('\n') if line.strip()]
# Method 1: Look for "Interest by subregion" or "Interest by region" section
interest_section_start = -1
for i, line in enumerate(lines):
line_lower = line.lower()
if ('interest by subregion' in line_lower or
'interest by region' in line_lower or
'interest by city' in line_lower):
interest_section_start = i
logging.info(" Found regional interest section")
break
# If we found the section, extract data from it
if interest_section_start != -1:
# Look for regional data in the next 50 lines after the section header
section_end = min(interest_section_start + 50, len(lines))
for j in range(interest_section_start + 1, section_end):
current_line = lines[j]
current_line_lower = current_line.lower()
# Skip empty lines or lines that are too long (probably not region data)
if not current_line or len(current_line) > 50:
continue
# Check if this line contains a UK region
for region in uk_regions:
if region.lower() in current_line_lower:
# Look for a number in this line
numbers_in_line = re.findall(r'\b(\d{1,3})\b', current_line)
# If no number in this line, check the next line
if not numbers_in_line and j + 1 < len(lines):
next_line = lines[j + 1]
numbers_in_line = re.findall(r'\b(\d{1,3})\b', next_line)
# Process found numbers
for num in numbers_in_line:
if num.isdigit():
value = int(num)
# Validate it's a reasonable interest value (0-100)
if 0 <= value <= 100:
# Don't overwrite if we already have this region
if region not in result:
result[region] = value
logging.info(f" Found {region}: {value}")
break
# Method 2: If no structured section found, scan for region+number patterns
if not result:
logging.info(" Scanning for regional data patterns...")
for i, line in enumerate(lines):
if len(line) > 50: # Skip long lines (not likely region data)
continue
for region in uk_regions:
if region.lower() in line.lower():
# Extract numbers from this line
numbers = re.findall(r'\b(\d{1,3})\b', line)
valid_numbers = [int(n) for n in numbers if n.isdigit() and 0 <= int(n) <= 100]
if valid_numbers:
# Use the largest valid number
best_value = max(valid_numbers)
if region not in result: # Don't overwrite
result[region] = best_value
logging.info(f" Found {region}: {best_value}")
# Method 3: Check for specific regional data in the Related topics/queries section
# Sometimes regional data appears near related topics
if not result:
related_section_found = False
for i, line in enumerate(lines):
if 'related topics' in line.lower() or 'related queries' in line.lower():
related_section_found = True
# Check lines before this section for regional data
for j in range(max(0, i - 10), i):
check_line = lines[j]
for region in uk_regions:
if region.lower() in check_line.lower():
numbers = re.findall(r'\b(\d{1,3})\b', check_line)
valid_numbers = [int(n) for n in numbers if n.isdigit() and 0 <= int(n) <= 100]
if valid_numbers and region not in result:
result[region] = max(valid_numbers)
break
# Sort by value (descending) and limit to top regions
if result:
sorted_result = dict(sorted(result.items(), key=lambda x: x[1], reverse=True))
# Take top regions (max 15)
top_count = min(15, len(sorted_result))
return dict(list(sorted_result.items())[:top_count])
return result
第六步:定义处理 Cookie 的函数
由于每次抓取都运行在独立的会话中,Google 偶尔会弹出 Cookie 同意提示;如果不处理,爬虫可能会卡住,因为它不知道如何关闭这个提示。
接下来定义的函数会用两种方式处理这个问题:第一种是主动检测 Cookie 同意提示并点击相应按钮将其关闭。
第二种是在确认访问的是 Google 域名后,提前注入 Google 的同意 Cookie,尽量避免提示框一开始就弹出来。
将以下代码添加到脚本中:
def handle_cookie_consent(driver, timeout=6):
"""
Handle Google cookie/consent UI if present.
Returns True if clicked something, False otherwise.
"""
logging.info("Checking for cookie consent dialog...")
def try_click(elem):
try:
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", elem)
except:
pass
try:
elem.click()
return True
except:
try:
driver.execute_script("arguments[0].click();", elem)
return True
except:
return False
# Common Google button ids that sometimes exist
id_candidates = ["L2AGLb", "W0wltc"] # L2AGLb often = accept, W0wltc often = reject (varies)
for bid in id_candidates:
try:
elem = WebDriverWait(driver, 1).until(EC.presence_of_element_located((By.ID, bid)))
if elem.is_displayed() and elem.is_enabled():
logging.info(f" Found consent button by id: {bid} text='{elem.text.strip()}'")
if try_click(elem):
time.sleep(1.5)
return True
except:
pass
# XPath candidates (robust text matching)
xpath_candidates = [
"//button[contains(., 'Reject all')]",
"//button[contains(., 'Accept all')]",
"//button[contains(., 'I agree')]",
"//div[@role='dialog']//button[contains(., 'Reject')]",
"//div[@role='dialog']//button[contains(., 'Accept')]",
"//form//button[contains(., 'Reject')]",
"//form//button[contains(., 'Accept')]",
]
# CSS candidates
css_candidates = [
"div[role='dialog'] button",
"form button",
"button[aria-label*='Accept']",
"button[aria-label*='Reject']",
]
# 1) Try on main document
end = time.time() + timeout
while time.time() < end:
for xp in xpath_candidates:
try:
elems = driver.find_elements(By.XPATH, xp)
for e in elems:
if e.is_displayed() and e.is_enabled():
txt = (e.text or "").strip()
if txt:
logging.info(f" Found consent button (xpath): '{txt}'")
if try_click(e):
time.sleep(1.5)
return True
except:
pass
for cs in css_candidates:
try:
elems = driver.find_elements(By.CSS_SELECTOR, cs)
for e in elems:
if e.is_displayed() and e.is_enabled():
txt = (e.text or "").strip().lower()
if any(k in txt for k in ["accept", "reject", "agree"]):
logging.info(f" Found consent button (css): '{e.text.strip()}'")
if try_click(e):
time.sleep(1.5)
return True
except:
pass
time.sleep(0.25)
# 2) Try inside consent iframes
try:
frames = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='consent'], iframe[name*='consent'], iframe[title*='consent']")
for fr in frames:
try:
driver.switch_to.frame(fr)
for xp in xpath_candidates:
elems = driver.find_elements(By.XPATH, xp)
for e in elems:
if e.is_displayed() and e.is_enabled():
logging.info(f" Found consent button in iframe: '{(e.text or '').strip()}'")
if try_click(e):
driver.switch_to.default_content()
time.sleep(1.5)
return True
driver.switch_to.default_content()
except:
try:
driver.switch_to.default_content()
except:
pass
except:
pass
logging.info("No cookie consent dialog found")
return False
def set_google_consent_cookies(driver):
"""
Set Google consent cookies.
Call this AFTER navigating to https://www.google.com (or /ncr).
"""
try:
logging.info("Setting Google consent cookies...")
# Only do this if we are on a google domain
if "google." not in (driver.current_url or ""):
logging.warning(f" Not on a Google domain ({driver.current_url}). Skipping cookie injection.")
return False
consent_cookie = {
"name": "CONSENT",
"value": "YES+GB.en-GB+V9+BX",
"domain": ".google.com",
"path": "/",
"secure": True,
"httpOnly": False,
}
socs_cookie = {
"name": "SOCS",
"value": "CAISHAgCEhJnd3NfMjAyNTAxMjEtMF9SQzIaAmVuIAEaBgiA_LCgBg",
"domain": ".google.com",
"path": "/",
"secure": True,
"httpOnly": False,
}
driver.add_cookie(consent_cookie)
driver.add_cookie(socs_cookie)
logging.info("Injected CONSENT and SOCS cookies")
return True
except Exception as e:
logging.error(f" Failed to set consent cookies: {e}")
return False
第七步:页面级数据校验
目前还不需要考虑页面导航或绕过拦截的问题,这一步只是为之后让爬虫更加稳健打下基础。
这一步的主要目的是检查爬虫在访问页面后立即获取到的内容,不论它是通过什么方式到达这个页面的;浏览器具体是如何导航到这里的细节留到后面步骤再处理,现在只需确认页面确实是预期的页面。
这个函数在浏览器和解析器之间起中介作用,相当于把当前查看的页面状态固定下来,再把原始信息传递给逻辑链中的下一步。
具体来说,这个函数会:
- 对页面进行截图
- 记录数据来源,便于出问题时回头排查
- 保存一些证据,方便在结果不符合预期时用于调试和验证
- 将所有内容交给解析器,让它开始解析数据
def extract_trends_data_from_current_page(driver) -> Dict[str, Any]:
"""
Extract Google Trends data from the CURRENT page.
"""
logging.info("Extracting data from Google Trends page...")
try:
# Get page text
body = driver.find_element(By.TAG_NAME, "body")
page_text = body.text
# Also get URL for context
current_url = driver.current_url
logging.info(f" Current URL: {current_url}")
logging.info(f" Page text length: {len(page_text)} characters")
# Debug: Save page text for analysis
with open("debug_page_text.txt", "w", encoding="utf-8") as f:
f.write(f"URL: {current_url}\n")
f.write(f"Time: {datetime.now()}\n")
f.write("-" * 80 + "\n")
f.write(page_text[:2000]) # First 2000 chars
# Parse with IMPROVED parser
parser = GoogleTrendsParser()
parsed_data = parser.parse(page_text)
# Always add URL
parsed_data['url'] = current_url
# Log what we got
logging.info(f" Extracted data for: '{parsed_data.get('search_term', 'Unknown')}'")
logging.info(f" Location: {parsed_data.get('location', 'Unknown')}")
regions = parsed_data.get('interest_by_subregion', {})
if regions:
logging.info(f" Regional data ({len(regions)} regions):")
for region, value in regions.items():
logging.info(f" • {region}: {value}")
else:
logging.info("No regional data found")
# Take screenshot for visual verification
timestamp = datetime.now().strftime('%H%M%S')
screenshot_name = f"extraction_{timestamp}.png"
driver.save_screenshot(screenshot_name)
logging.info(f"📸 Saved screenshot: {screenshot_name}")
return parsed_data
except Exception as e:
logging.error(f" Failed to extract page data: {e}")
# Emergency debug
try:
timestamp = datetime.now().strftime('%H%M%S')
driver.save_screenshot(f"error_extraction_{timestamp}.png")
# Save error page
with open(f"error_page_{timestamp}.txt", "w", encoding="utf-8") as f:
f.write(f"Error: {e}\n")
f.write(f"URL: {driver.current_url}\n")
f.write(driver.page_source[:5000])
except:
pass
return {
'search_term': KEYWORD,
'location': GEO_HUMAN_NAME,
'url': driver.current_url if 'driver' in locals() else '',
'interest_over_time': {'y_axis': [], 'x_axis': []},
'interest_by_subregion': {}
}
第八步:为 Google Trends 爬虫打造独特的身份与行为模式
接下来,希望爬虫的行为更接近真实、持续活跃的用户,而不是一个用完即弃的机器人;这一部分要让浏览器看起来像是来自一个真实的地点,拥有独特的身份和一点“个性”,具体通过以下三个关键方式实现:
- 首先是代理服务器和完整浏览器身份指纹,它们为浏览器提供一个固定的“据点”,以及向所访问网站标识自身的独特方式,从而建立起稳定的网络存在感和设备指纹,就像真实用户一样。
- 其次是 Cookie 管理,它能保留完整的会话历史,让重复访问看起来很自然,就像回到一个熟悉的地方。
- 最后是智能退避机制,加入贴近真实的时间间隔和犹豫停顿,用更接近真人在操作前思考的节奏,取代那种固定不变的等待时间。
将以下代码块添加到脚本中:
# === London Proxy Pool ===
PROXIES = [
"ultra.marsproxies.com:44443",
]
# === London Identity Pool (3 Profiles) ===
IDENTITY_POOL = [
# 1) Windows + Chrome (Desktop) - ENHANCED
{
"timezone": "Europe/London",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"viewport": (1366, 768),
"platform": "Win32",
"hardware_concurrency": 8,
"max_touch_points": 0,
"language": "en-GB,en;q=0.9",
# Add these for better stealth:
"webgl_vendor": "Google Inc. (Intel)", # Common for Chrome on Intel PCs
"webgl_renderer": "ANGLE (Intel, Intel(R) UHD Graphics...)", # Example
"device_memory": 8, # In GB
"do_not_track": 0, # 0 = DNT not enabled (common)
"canvas_hash": "needs_calculated_value", # You'll need to generate this
"audio_hash": "needs_calculated_value", # You'll need to generate this
},
# 2) macOS + Safari (Desktop) - ENHANCED
{
"timezone": "Europe/London",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15",
"viewport": (1440, 900),
"platform": "MacIntel",
"hardware_concurrency": 4, # Consider 8 for Apple Silicon
"max_touch_points": 0,
"language": "en-GB,en;q=0.9",
# Add these for better stealth:
"webgl_vendor": "Apple Inc.",
"webgl_renderer": "Apple M1 Pro", # Match your hardware choice
"device_memory": 8, # In GB
"do_not_track": 1, # Safari more commonly has DNT enabled
"canvas_hash": "needs_calculated_value",
"audio_hash": "needs_calculated_value",
},
]
# === Cookie Management ===
def save_cookies(driver, filename="cookies.json"):
"""Save cookies to file"""
try:
cookies = driver.get_cookies()
with open(filename, 'w') as f:
json.dump(cookies, f, indent=2)
logging.info(f" Saved cookies to {filename}")
return True
except Exception as e:
logging.error(f" Failed to save cookies: {e}")
return False
def load_cookies(driver, filename="cookies.json"):
"""Load cookies from file"""
try:
with open(filename, 'r') as f:
cookies = json.load(f)
# Navigate to domain first
driver.get("https://trends.google.com")
time.sleep(2)
for cookie in cookies:
try:
driver.add_cookie(cookie)
except:
pass
logging.info(f" Loaded {len(cookies)} cookies from {filename}")
return True
except Exception as e:
logging.debug(f"Could not load cookies: {e}")
return False
# === Smart Backoff Logic ===
def smart_backoff(min_wait=1, max_wait=5, reason="", driver=None):
"""
Intelligent waiting with exponential backoff and jitter
"""
# Add more randomness - sometimes wait much longer
if random.random() < 0.15: # 15% chance of longer wait
min_wait = min_wait * 1.5
max_wait = max_wait * 2
wait_time = random.uniform(min_wait, max_wait)
# Add micro-variations during wait
if wait_time > 1.5 and driver and random.random() < 0.4:
# Break long waits into smaller chunks with micro-movements
chunks = max(2, int(wait_time / 0.7))
chunk_time = wait_time / chunks
for i in range(chunks):
time.sleep(chunk_time)
# Occasionally trigger tiny mouse movements during long waits
if random.random() < 0.3:
try:
driver.execute_script("""
var evt = new MouseEvent('mousemove', {
clientX: Math.random() * window.innerWidth,
clientY: Math.random() * window.innerHeight,
bubbles: true
});
document.dispatchEvent(evt);
""")
except:
pass
else:
time.sleep(wait_time)
if reason:
logging.info(f" {reason} (waiting {wait_time:.1f}s)")
return wait_time
第九步:让交互操作更接近真人
要让爬虫像真人一样与页面交互,需要参考我们自己操作时的方式:在输入框中仔细打字、在需要时把元素滚动到可见区域、并确认输入内容确实和预期一致。
这一步需要引入一个名为human_type_with_smart_correction的工具函数,以及一个名为scroll_element_into_view的定位辅助函数,二者能让整个过程稳定得多。
这两个辅助函数配合使用,可以让爬虫滚动到需要输入的位置、清空已有内容、在打字过程中加入小停顿和偶尔的打字错误,最后检查最终输入是否与预期一致,如果不一致,函数会先纠正再继续往下走。
之后只要爬虫需要在浏览器中输入内容——无论是Google 搜索、Trends 关键词,还是地理位置字段——都会调用这些辅助函数,确保整个交互看起来像是由真人完成的。
需要添加的函数如下:
def human_type_with_smart_correction(element, text, context="search", driver=None):
"""
Human-ish typing that ALWAYS ends with the exact desired text in the input.
If driver is provided, we can use JS as a last-resort clear.
"""
logging.info(f" Typing '{text}' ({context})")
def hard_clear():
element.click()
smart_backoff(0.15, 0.35)
# Try select-all + backspace (both control and command)
for combo in (Keys.CONTROL + "a", Keys.COMMAND + "a"):
try:
element.send_keys(combo)
smart_backoff(0.05, 0.15)
element.send_keys(Keys.BACKSPACE)
smart_backoff(0.1, 0.25)
if not (element.get_attribute("value") or "").strip():
return True
except:
pass
# Try element.clear()
try:
element.clear()
smart_backoff(0.1, 0.25)
if not (element.get_attribute("value") or "").strip():
return True
except:
pass
# JS last resort if driver is available
if driver is not None:
try:
driver.execute_script("arguments[0].value = '';", element)
smart_backoff(0.1, 0.2)
if not (element.get_attribute("value") or "").strip():
return True
except:
pass
return False
# 1) Clear reliably
if not hard_clear():
logging.warning("Could not fully clear field, proceeding anyway")
# 2) Type with small human errors, but keep it controlled
error_chance = 0.03 if context == "search" else 0.02
for ch in text:
# occasional hesitation
if random.random() < 0.05:
smart_backoff(0.08, 0.25)
if random.random() < error_chance and ch.isalpha():
err_type = random.choice(["typo", "extra"])
if err_type == "typo":
wrong = random.choice("abcdefghijklmnopqrstuvwxyz")
element.send_keys(wrong)
smart_backoff(0.06, 0.18)
element.send_keys(Keys.BACKSPACE)
smart_backoff(0.05, 0.12)
elif err_type == "extra":
extra = random.choice("aeious")
element.send_keys(extra)
smart_backoff(0.05, 0.12)
element.send_keys(Keys.BACKSPACE)
smart_backoff(0.05, 0.12)
element.send_keys(ch)
smart_backoff(0.03, 0.12)
# 3) Final verification + hard correction (guarantee)
smart_backoff(0.25, 0.6, "Final verification")
final_text = element.get_attribute("value") or ""
logging.info(f" Typed: '{final_text}'")
logging.info(f" Expected: '{text}'")
if final_text.strip() != text.strip():
logging.warning("Text mismatch. Hard-correcting...")
hard_clear()
for ch in text:
element.send_keys(ch)
smart_backoff(0.02, 0.06)
final_text2 = element.get_attribute("value") or ""
if final_text2.strip() != text.strip():
raise RuntimeError(f"Typing correction failed. Got '{final_text2}' expected '{text}'")
logging.info("Text corrected")
else:
logging.info("Text typed correctly")
smart_backoff(0.2, 0.6)
return True
# === Helper: Safe Element Scroll ===
def scroll_element_into_view(driver, element):
"""Safely scroll an element into view."""
try:
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)
time.sleep(random.uniform(0.2, 0.5))
return True
except:
return False
第十步:定义时间范围控制函数
假设爬虫已经到达目标关键词的 Google Trends 结果页面,这些数据到底应该反映哪个时间段呢?
Google 在这里提供了多种选项,比如过去 24 小时、一周、一个月、12 个月,也可以自定义时间范围。
这时候就需要一个专门的辅助函数,直接从界面中获取所需的时间范围。
默认情况下,系统会将时间范围设为过去12个月,但您可以轻松切换为适合您使用场景的任意时间段。将以下函数添加到脚本中:
def set_timeframe(driver, timeframe="Past 12 months"):
"""
More flexible version of setting the timeframe on Google Trends
"""
logging.info(f" Setting timeframe: {timeframe}")
try:
# STEP 1: Open the dropdown
dropdown = WebDriverWait(driver, 15).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "md-select[aria-label^='Select time period']"))
)
logging.info(" Found timeframe dropdown")
random_mouse_movement(driver, dropdown)
smart_backoff(0.5, 1.0, "Hovering over timeframe dropdown", driver)
dropdown.click()
smart_backoff(1.5, 2.5, "Dropdown menu opening", driver)
# STEP 2: Find all potential options (md-option and _md-text)
options = driver.find_elements(By.CSS_SELECTOR, "md-option, div._md-text")
logging.info(f" Found {len(options)} dropdown items")
for option in options:
try:
if option.is_displayed() and timeframe.lower() in option.text.lower():
logging.info(f" Matching timeframe found: {option.text.strip()}")
random_mouse_movement(driver, option)
smart_backoff(0.5, 1.0, "Hovering over timeframe option", driver)
try:
option.click()
except Exception:
driver.execute_script("arguments[0].click();", option)
logging.info(f" Timeframe '{timeframe}' selected")
smart_backoff(2.5, 4.0, "Waiting for charts to reload", driver)
return True
except Exception as e:
logging.debug(f" Option check error: {e}")
continue
logging.warning(f" Timeframe option not found: {timeframe}")
return False
except Exception as e:
logging.error(f" Failed to set timeframe: {e}")
return False
第11步:确保您的爬虫定位正确
即使您已正确设置GEO参数并使用了与地区匹配的代理,Google仍可能默认定位到错误的位置。
下一个函数正是为此提供一道保障:它首先会从URL和页面内容中查找线索,判断您当前所处的位置。
如果无法确认正确的位置,它会介入,打开位置选择器,搜索您想要的目标位置并应用,然后才开始提取数据。
这套逻辑同样适用于任何目标位置,只需替换传入函数的目标位置参数即可。
def verify_and_set_location(driver, target_location="London"):
"""
Verify we're in the right location, set if not
"""
logging.info(f" Verifying location: {target_location}")
try:
# Check current URL for geo parameter
current_url = driver.current_url
if "geo=" in current_url:
geo_match = re.search(r'geo=([A-Za-z]{2}-[A-Za-z]{2,})', current_url)
if geo_match:
logging.info(f" Location already set in URL: {geo_match.group(1)}")
return True
# Check page text for location indicators
page_text = driver.find_element(By.TAG_NAME, "body").text.lower()
location_indicators = ["london", "united kingdom", "gb", "england", "uk"]
for indicator in location_indicators:
if indicator in page_text:
logging.info(f" Location detected in page: {indicator}")
return True
# If location not detected, try to set it
logging.info(" Location not detected, attempting to set...")
# Look for location selector
location_selectors = [
"div[aria-label*='Location']",
"div[aria-label*='location']",
"button[aria-label*='Location']",
"md-select[aria-label*='Location']",
"//div[contains(text(), 'Worldwide') or contains(text(), 'Location')]",
".hierarchy-select",
"div.custom-location-picker-select",
"//div[@role='button' and contains(@aria-label, 'Location')]",
]
location_button = None
for selector in location_selectors:
try:
if selector.startswith("//"):
elements = driver.find_elements(By.XPATH, selector)
else:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
for element in elements:
try:
if element.is_displayed():
text = element.text.lower()
if any(word in text for word in ['location', 'worldwide', 'country', 'region']):
location_button = element
logging.info(f" Found location selector")
break
except:
continue
if location_button:
break
except:
continue
if location_button:
# Click location selector
random_mouse_movement(driver, location_button)
smart_backoff(0.8, 1.5)
try:
location_button.click()
except:
driver.execute_script("arguments[0].click();", location_button)
logging.info("Opened location dropdown")
smart_backoff(2, 4, "Waiting for location dropdown")
# Search for Dubai
try:
location_search = driver.find_element(By.CSS_SELECTOR, "input[placeholder*='location'], input[placeholder*='Location'], input[aria-label*='search'], input[type='search']")
location_search.clear()
human_type_with_smart_correction(location_search, "London", context="location")
smart_backoff(2, 4, "Waiting for location results")
# Select London from results
london_options = [
"//div[contains(text(), 'London')]",
"//md-option[contains(., 'London')]",
"//div[contains(@class, '_md-text') and contains(text(), 'London')]",
"//div[contains(text(), 'London, United Kingdom')]",
]
for option_selector in london_options:
try:
elements = driver.find_elements(By.XPATH, option_selector)
for element in elements:
if element.is_displayed() and "london" in element.text.lower():
element.click()
logging.info("Selected London location")
smart_backoff(3, 6, "Waiting for location to apply")
return True
except:
continue
except Exception as e:
logging.warning(f" Could not search for location: {e}")
logging.warning(" Could not set location via UI, might already be correct")
return False
except Exception as e:
logging.error(f" Location verification failed: {e}")
return False
第12步:让会话表现得像真实用户
回到浏览器会话:在尝试获取任何Google Trends数据之前,我们希望整个会话看起来很自然,就像普通用户在使用浏览器一样。下面这段代码会加入一些模拟真人的会话行为,让会话显得更像是被真实使用过:
- 启动并运行会话
我们的pre_warm_browser()函数会在一些中立网站上转几圈、滚动一下页面,甚至点击一个安全链接,让会话显得更有真实感。您可以根据目标位置自由替换这些网站。
- 让浏览器看起来正在被使用
我们的random_window_resize()函数会偶尔改变窗口大小,避免每次运行时都显得像是同一个脚本在重复执行。
- 增加一些空闲时间
我们的simulate_human_scanning()和random_mouse_movement()函数会尝试加入一些普通用户会有的空闲动作,比如移动一下鼠标或轻轻滚动页面,让会话看起来不是一直静止不动。
- 像真人一样结束操作
最后,我们的clean_exit()函数也会让退出过程显得更自然:先短暂暂停,保存Cookie,稍作整理,然后再关闭浏览器,而不是直接消失不见。
将以下代码块添加到脚本中:
# === Pre-warm Browser ===
def pre_warm_browser(driver):
"""Pre-warm the browser with human-like activity before actual scraping"""
logging.info("Pre-warming browser with human-like activity...")
# Visit a few neutral sites first
neutral_sites = [
"https://www.theguardian.com/uk",
"https://en.wikipedia.org/wiki/London",
"https://www.reddit.com/r/london",
]
sites_to_visit = random.sample(neutral_sites, min(2, len(neutral_sites)))
for site in sites_to_visit:
try:
site_name = site.split('//')[1].split('/')[0]
logging.info(f" Visiting {site_name}")
driver.get(site)
smart_backoff(4, 8, f"Browsing {site_name}", driver)
# Do some human-like scrolling
scrolls = random.randint(2, 4)
for scroll_num in range(scrolls):
scroll_amount = random.randint(300, 700)
direction = random.choice([-1, 1])
driver.execute_script(f"window.scrollBy(0, {scroll_amount * direction});")
smart_backoff(1, 3, f"Reading content (scroll {scroll_num+1}/{scrolls})", driver)
# Occasionally click on a random safe link
if random.random() < 0.4:
try:
links = driver.find_elements(By.TAG_NAME, "a")
safe_links = []
for link in links:
try:
if link.is_displayed() and link.is_enabled():
href = link.get_attribute("href") or ""
text = link.text.lower()
# Avoid problematic links
if href and not any(x in href for x in ["javascript:", "mailto:", "#"]) and len(text) > 2:
safe_links.append(link)
except:
continue
if safe_links:
link = random.choice(safe_links[:15])
logging.info(f"🔗 Clicking on: {link.text[:50]}...")
driver.execute_script("arguments[0].click();", link)
smart_backoff(3, 7, "Following link", driver)
driver.back()
smart_backoff(2, 4, "Going back", driver)
except:
pass
except Exception as e:
logging.debug(f"Pre-warm site {site} failed: {e}")
continue
logging.info("Browser pre-warmed")
# === Random Window Resize ===
def random_window_resize(driver):
"""Randomly resize browser window to appear more human"""
if random.random() < 0.25: # 25% chance
try:
sizes = [
(1366, 768),
(1440, 900),
(1536, 864),
(1600, 900),
(1920, 1080),
(1280, 720),
]
width, height = random.choice(sizes)
driver.set_window_size(width, height)
logging.debug(f" Resized window to {width}x{height}")
smart_backoff(0.8, 2, "Window resized", driver)
except:
pass
# === Random Human Interactions ===
def simulate_human_scanning(driver, duration=2, intensity="light"):
"""
Simulate human scanning/reading behavior WITH SAFETY CHECKS
"""
logging.info(" Simulating human scanning...")
intensities = {
"very_light": {"scrolls": 0, "moves": 1, "clicks": 0},
"light": {"scrolls": 1, "moves": 1, "clicks": 0},
"medium": {"scrolls": 1, "moves": 2, "clicks": 0},
"heavy": {"scrolls": 2, "moves": 3, "clicks": 1}
}
config = intensities.get(intensity, intensities["very_light"])
# Random mouse movements - MORE NATURAL
for i in range(config["moves"]):
try:
window_size = driver.get_window_size()
start_x = random.randint(50, window_size['width'] - 50)
start_y = random.randint(50, window_size['height'] - 50)
end_x = random.randint(50, window_size['width'] - 50)
end_y = random.randint(50, window_size['height'] - 50)
# Create more natural mouse movement with curve
driver.execute_script(f"""
var startX = {start_x};
var startY = {start_y};
var endX = {end_x};
var endY = {end_y};
var steps = 15;
var currentStep = 0;
function moveStep() {{
if (currentStep <= steps) {{
var t = currentStep / steps;
// Bezier curve for natural movement
var x = startX + (endX - startX) * t;
var y = startY + (endY - startY) * t;
// Add slight curve
x += Math.sin(t * Math.PI) * 20;
y += Math.cos(t * Math.PI) * 15;
var evt = new MouseEvent('mousemove', {{
clientX: Math.round(x),
clientY: Math.round(y),
view: window,
bubbles: true
}});
document.dispatchEvent(evt);
currentStep++;
setTimeout(moveStep, 20 + Math.random() * 15);
}}
}}
moveStep();
""")
# Wait for movement to complete plus some random time
movement_time = 20 * 15 / 1000 + random.uniform(0.1, 0.4)
time.sleep(movement_time)
smart_backoff(0.2, 0.8, f"Mouse movement {i+1}/{config['moves']}", driver)
except:
pass
# Random scrolling - WITH BOUNDS CHECK
for i in range(config["scrolls"]):
try:
current_scroll = driver.execute_script("return window.pageYOffset;")
window_height = driver.execute_script("return window.innerHeight;")
document_height = driver.execute_script("return document.body.scrollHeight;")
max_scroll = document_height - window_height
direction = random.choice([-1, 1])
if direction == 1 and current_scroll < max_scroll - 100:
amount = random.randint(100, min(400, max_scroll - current_scroll - 50))
elif direction == -1 and current_scroll > 100:
amount = random.randint(100, min(400, current_scroll))
else:
direction = direction * -1
amount = random.randint(100, 300)
# Smooth scrolling
steps = random.randint(3, 6)
step_amount = amount / steps
for step in range(steps):
driver.execute_script(f"window.scrollBy(0, {step_amount * direction});")
time.sleep(random.uniform(0.05, 0.15))
smart_backoff(0.3, 1.2, f"Scrolling {i+1}/{config['scrolls']}", driver)
except:
pass
# Random clicks (on safe elements) - WITH VISIBILITY CHECK (REDUCED)
for i in range(config["clicks"]):
try:
safe_elements = driver.find_elements(By.CSS_SELECTOR, "body, div, span, p, a, button")
visible_elements = []
for element in safe_elements:
try:
if element.is_displayed() and element.is_enabled():
location = element.location
size = element.size
if location and size:
text = element.text.lower()
# Avoid clicking on important elements
if not any(word in text for word in ['submit', 'login', 'sign', 'buy', 'purchase', 'download']):
visible_elements.append(element)
except:
continue
if visible_elements:
element = random.choice(visible_elements[:5]) # Reduced from 10 to 5
scroll_element_into_view(driver, element)
smart_backoff(0.3, 0.8, "Preparing to click", driver)
try:
actions = ActionChains(driver)
actions.move_to_element(element)
actions.click()
actions.perform()
except:
driver.execute_script("arguments[0].click();", element)
smart_backoff(0.3, 0.8, f"Random click {i+1}/{config['clicks']}", driver)
except Exception as e:
pass
# === Random Mouse Movements (Updated) ===
def random_mouse_movement(driver, element=None):
if not RANDOM_MOUSE_MOVEMENTS:
return
try:
window_size = driver.get_window_size()
if element:
try:
scroll_element_into_view(driver, element)
smart_backoff(0.3, 0.7, "Scrolled to element", driver)
element_location = element.location
if element_location:
# Move to element with natural motion
actions = ActionChains(driver)
# Get element center
size = element.size
target_x = element_location['x'] + size['width'] / 2
target_y = element_location['y'] + size['height'] / 2
# Add some offset for more natural movement
offset_x = random.randint(-10, 10)
offset_y = random.randint(-10, 10)
# Move in natural curve
actions.move_to_element_with_offset(element, int(size['width']/2) + offset_x, int(size['height']/2) + offset_y)
actions.perform()
smart_backoff(0.2, 0.5, "Moved to element", driver)
return
except:
pass
# Random movement within viewport bounds
actions = ActionChains(driver)
target_x = random.randint(50, window_size['width'] - 50)
target_y = random.randint(50, window_size['height'] - 50)
steps = random.randint(3, 6)
current_x, current_y = 0, 0
for step in range(steps):
step_x = current_x + (target_x - current_x) * (step + 1) / steps
step_y = current_y + (target_y - current_y) * (step + 1) / steps
# Add natural curve to movement
curve_x = math.sin(step * 0.5) * 30
curve_y = math.cos(step * 0.5) * 25
step_x += curve_x + random.randint(-15, 15)
step_y += curve_y + random.randint(-15, 15)
# Ensure we stay within bounds
step_x = max(10, min(step_x, window_size['width'] - 10))
step_y = max(10, min(step_y, window_size['height'] - 10))
actions.move_by_offset(int(step_x - current_x), int(step_y - current_y))
current_x, current_y = step_x, step_y
time.sleep(random.uniform(0.03, 0.1))
actions.perform()
smart_backoff(0.1, 0.3, "Random mouse movement", driver)
except Exception as e:
pass
# === Clean Exit ===
def clean_exit(driver):
"""Clean exit to look more human with proxy extension cleanup"""
try:
# Scroll to top
driver.execute_script("window.scrollTo({top: 0, behavior: 'smooth'});")
smart_backoff(1, 2, "Scrolling to top", driver)
# Random mouse movement to top-left
driver.execute_script("""
var evt = new MouseEvent('mousemove', {
clientX: 50,
clientY: 50,
view: window,
bubbles: true
});
document.dispatchEvent(evt);
""")
# Wait a bit
smart_backoff(2, 4, "Final pause before exit", driver)
# Save cookies before quitting
try:
identity_hash = hash(str(driver.execute_script("return navigator.userAgent;")))
cookie_file = f"cookies_{identity_hash}.json"
save_cookies(driver, cookie_file)
except:
pass
# Clean up proxy extension files
try:
if hasattr(driver, 'proxy_extension_cleanup'):
cleanup = driver.proxy_extension_cleanup
if os.path.exists(cleanup.get('extension_path', '')):
os.remove(cleanup['extension_path'])
if os.path.exists(cleanup.get('temp_dir', '')):
os.rmdir(cleanup['temp_dir'])
logging.info(" Cleaned up proxy extension files")
except:
pass
# Close
driver.quit()
logging.info("Browser closed cleanly")
except:
try:
driver.quit()
except:
pass
第13步:增强爬虫的恢复能力
现在,让我们的爬虫变得更智能、更稳健。
首先,我们通过设置一个函数来处理可能出现的CAPTCHA:该函数会查找页面中的CAPTCHA元素,或任何提示需要人工验证的文字内容。
一旦发现CAPTCHA,脚本会自动暂停,并发出明确提示,让人工介入处理,待问题解决后再继续运行。这种情况通常只会在运行初期出现,而不会贯穿整个过程。
接下来,我们加入一种检测硬屏蔽和软屏蔽的方法,让爬虫知道何时该适当退避,而不是一味继续执行。最后,我们再添加一个safe_click_with_retry辅助函数,用于处理那些容易出错的脆弱交互。
# === CAPTCHA Handling Functions ===
def detect_and_handle_captcha(driver):
"""
Detect if CAPTCHA is present and handle it appropriately.
Returns True if CAPTCHA was detected, False otherwise.
"""
try:
# Check for CAPTCHA iframes or elements
captcha_selectors = [
"iframe[src*='captcha']",
"iframe[src*='recaptcha']",
"div.g-recaptcha",
"div.captcha",
"div#captcha",
"//div[contains(text(), 'CAPTCHA')]",
"//div[contains(text(), 'captcha')]",
"//div[contains(text(), 'Robot') or contains(text(), 'robot')]",
"//img[contains(@src, 'captcha')]",
]
captcha_detected = False
# Check for CAPTCHA iframes
for selector in captcha_selectors:
try:
if selector.startswith("//"):
elements = driver.find_elements(By.XPATH, selector)
else:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
if elements and len(elements) > 0:
for element in elements:
try:
if element.is_displayed():
logging.warning(f" CAPTCHA detected with selector: {selector}")
captcha_detected = True
break
except:
continue
if captcha_detected:
break
except:
continue
# Check page text for CAPTCHA indicators
if not captcha_detected:
try:
page_text = driver.find_element(By.TAG_NAME, "body").text.lower()
captcha_indicators = [
"captcha",
"verify you are human",
"i'm not a robot",
"prove you're not a robot",
"security check",
"type the text",
"enter the characters",
"select all images with",
"verify you're human"
]
for indicator in captcha_indicators:
if indicator in page_text:
logging.warning(f" CAPTCHA text detected: {indicator}")
captcha_detected = True
break
except:
pass
if captcha_detected:
# Visual CAPTCHA Alert
logging.warning("=" * 60)
logging.warning(" CAPTCHA DETECTED! ")
logging.warning("=" * 60)
logging.warning("Human intervention required!")
logging.warning("The script will wait 60 seconds for you to solve it.")
logging.warning("=" * 60)
# Try to make system notification
try:
import platform
system = platform.system()
if system == "Darwin": # macOS
import os
os.system(f"""
osascript -e 'display notification "CAPTCHA detected! Please solve it in the browser." with title "Google Trends Scraper" sound name "Submarine"'
""")
elif system == "Linux":
import os
os.system('notify-send "Google Trends Scraper" "CAPTCHA detected! Please solve it in the browser."')
elif system == "Windows":
# Windows notification
import ctypes
ctypes.windll.user32.MessageBoxW(0,
"CAPTCHA detected!\n\nPlease solve the CAPTCHA in the browser window.\n\nThe script will wait 60 seconds.",
"Google Trends Scraper",
0x40 | 0x0) # MB_ICONINFORMATION | MB_OK
except:
pass
logging.warning(" CAPTCHA detected - human intervention required!")
logging.info(" Waiting 60 seconds for manual CAPTCHA solving...")
# Play a sound to alert user (if possible)
try:
import sys
if sys.platform == "win32":
import winsound
winsound.Beep(1000, 1000)
elif sys.platform == "darwin": # macOS
import os
os.system('afplay /System/Library/Sounds/Ping.aiff')
except:
pass
# Wait for user to solve CAPTCHA
time.sleep(60)
# Check if CAPTCHA is still there
smart_backoff(5, 10, "Checking if CAPTCHA was solved", driver)
# Verify if we can proceed
try:
page_text = driver.find_element(By.TAG_NAME, "body").text.lower()
if any(indicator in page_text for indicator in ["captcha", "verify", "robot"]):
logging.error("CAPTCHA still present after wait")
return True
else:
logging.info("CAPTCHA appears to be solved")
return False
except:
logging.info("Could not verify CAPTCHA status")
return False
return False
except Exception as e:
logging.error(f" Error in CAPTCHA detection: {e}")
return False
# === Enhanced Block Detection ===
def detect_block(driver):
try:
# Wait for page to load
time.sleep(2)
html = driver.page_source.lower()
page_text = driver.find_element(By.TAG_NAME, "body").text.lower()
# STRICT block indicators only
strict_block_indicators = [
"unusual traffic from your computer network",
"automated queries",
"our systems have detected unusual traffic",
"this page appears when google automatically detects requests",
"we're sorry...",
"we've detected unusual activity",
"distressed search page",
]
for indicator in strict_block_indicators:
if indicator in html or indicator in page_text:
logging.warning(f" STRICT Block detected: {indicator}")
return True
# Check for CAPTCHA separately
if detect_and_handle_captcha(driver):
return "CAPTCHA"
# Check for rate limiting
rate_limit_indicators = [
"rate limit",
"try again in a bit",
"please try again later",
"quota exceeded",
]
for indicator in rate_limit_indicators:
if indicator in page_text:
logging.warning(f" Rate limit detected: {indicator}")
return "RATE_LIMIT"
return False
except Exception as e:
logging.error(f" Error in detect_block: {e}")
return False
# === Safe Click Helper Function ===
def safe_click_with_retry(driver, element_name="Explore button", selector_type="xpath", selector="//span[contains(text(), 'Explore')]"):
"""
Safely click an element with retry logic for stale element references
"""
max_retries = 3
for retry in range(max_retries):
try:
# Find the element fresh each time
if selector_type == "xpath":
element = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.XPATH, selector))
)
elif selector_type == "css":
element = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
)
# Try to click with multiple methods
try:
# Method 1: Normal click
element.click()
logging.info(f" Clicked {element_name} (normal click)")
return True
except:
# Method 2: JavaScript click
driver.execute_script("arguments[0].click();", element)
logging.info(f" Clicked {element_name} (JavaScript)")
return True
except Exception as e:
if retry < max_retries - 1:
logging.debug(f"Retry {retry + 1}/{max_retries} for {element_name}: {str(e)[:100]}")
smart_backoff(1, 2, f"Retrying {element_name}", driver)
else:
logging.warning(f" Failed to click {element_name} after {max_retries} retries")
return False
return False
第14步:编写一个函数,模拟完整的Google Trends抓取会话
现在,我们定义一个函数,让整个抓取过程呈现为一次连贯的浏览会话:从Google出发,进入Google Trends,最终抵达关键词结果页面。
这个函数本身并不执行实际操作,而是调用我们之前定义的所有其他函数,决定每个函数的调用时机,并处理过程中可能出现的各种异常情况。
具体执行步骤如下:
- 确保浏览器完成预热浏览
它会准备好会话、调整窗口大小,并在一些中立页面上稍作浏览。
- 从真实用户会开始的地方开始操作
接着,它会打开Google、处理Cookie同意弹窗、在搜索框中输入内容、等待搜索结果出现,然后点击官方链接——整个过程都带有延迟和巧妙的退避逻辑,以避免被识别为机器人活动。
- 应对Google在初期的阻拦
如果CAPTCHA突然出现,它会交由人工处理;如果Google对会话实施屏蔽或速率限制,它会先退避再重试,整个过程安全可控,不会导致崩溃或强行闯入页面。
- 以真实用户的方式进入Google Trends
它会处理Trends的Cookie弹窗(如果出现)、滚动页面、验证并设置位置、找到搜索框、谨慎输入关键词,并尽量避免落入常见的界面陷阱。
- 在开始抓取数据之前,使Google Trends进入正确的状态
如果点击操作失败,它会退而求其次改用回车键,确保时间范围设置正确,并确认图表已实际加载出来,之后才会继续后续操作。
- 在提取数据前先稳定页面状态
它会像真实用户一样等待图表加载完成,再次确认位置设置依然正确,然后才从页面中提取可见数据。
以下是需要添加到脚本中的代码块:
def complete_human_journey(driver, keyword):
"""
Complete human-like journey from Google search to Google Trends data
"""
logging.info(" Starting complete human journey...")
# PRE-WARM PHASE
pre_warm_browser(driver)
# Random window resize
random_window_resize(driver)
# PHASE 1: Start at Google homepage
logging.info(" Phase 1: Starting at Google homepage")
driver.get("https://www.google.com")
smart_backoff(4, 8, "Loading Google homepage", driver)
# === CRITICAL: HANDLE COOKIE CONSENT DIALOG ===
cookie_handled = handle_cookie_consent(driver)
if cookie_handled:
logging.info("Cookie consent dialog handled")
smart_backoff(2, 4, "After cookie handling", driver)
else:
logging.info("No cookie consent dialog found or already handled")
# Human scanning of homepage
simulate_human_scanning(driver, duration=2, intensity="very_light")
# Find Google search box
logging.info(" Looking for Google search box...")
google_search_box = None
google_selectors = [
"textarea[name='q']",
"input[name='q']",
"textarea[title='Search']",
"input[title='Search']",
"textarea[aria-label='Search']",
"input[aria-label='Search']",
"textarea.gLFyf",
"input.gLFyf",
]
for selector in google_selectors:
try:
element = WebDriverWait(driver, 15).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
)
if element.is_displayed():
google_search_box = element
logging.info(f" Found Google search: {selector}")
break
except:
continue
if not google_search_box:
raise Exception(" Could not find Google search box")
# Type "Google trends" in Google search
logging.info("⌨️ Typing 'Google trends' in Google search...")
random_mouse_movement(driver, google_search_box)
smart_backoff(0.5, 1.2, "Moving to search box", driver)
google_search_box.click()
smart_backoff(0.3, 0.8, "Clicked search box", driver)
human_type_with_smart_correction(google_search_box, "google trends", context="search")
# Wait for Google suggestions
smart_backoff(2, 4, "Waiting for Google suggestions", driver)
# Press Enter to search
google_search_box.send_keys(Keys.RETURN)
logging.info(" Searching for 'Google trends' on Google")
smart_backoff(3, 6, "Loading Google search results", driver)
# === ADDED: CAPTCHA CHECK RIGHT AFTER SEARCHING ===
# Check for CAPTCHA after Google search
captcha_found = detect_and_handle_captcha(driver)
if captcha_found:
logging.warning(" CAPTCHA encountered, trying alternative approach...")
# Try to go back and search again
try:
driver.back()
smart_backoff(3, 5, "Went back due to CAPTCHA", driver)
# Try a different search approach
google_search_box = driver.find_element(By.CSS_SELECTOR, "textarea[name='q'], input[name='q']")
google_search_box.clear()
smart_backoff(0.5, 1)
# Type differently
google_search_box.send_keys("Google Trends official")
smart_backoff(1, 2)
google_search_box.send_keys(Keys.RETURN)
smart_backoff(4, 7, "Retrying search with different terms", driver)
except:
pass
# PHASE 2: Scan search results
logging.info(" Phase 2: Scanning Google search results")
simulate_human_scanning(driver, duration=3, intensity="light")
# Find and click the Google Trends result
logging.info(" Looking for Google Trends link...")
# Wait for search results to load
smart_backoff(3, 5, "Waiting for search results", driver)
# Look for the official Google Trends link
trends_link = None
trends_selectors = [
"a[href*='trends.google.com']",
"a:has(h3:contains('Google Trends'))",
"//a[contains(@href, 'trends.google.com') and contains(., 'Trends')]",
"a[ping*='/url?sa=t&source=web&rct=j&url=https://trends.google.com']",
"//a[contains(@href, 'trends.google.com')]//h3[contains(text(), 'Trends')]",
]
for selector in trends_selectors:
try:
if selector.startswith("//"):
elements = driver.find_elements(By.XPATH, selector)
else:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
for element in elements:
try:
if element.is_displayed():
# Get link text for verification
link_text = element.text.lower() if element.text else ""
href = element.get_attribute("href") or ""
if "trends" in link_text or "trends.google.com" in href:
trends_link = element
logging.info(f" Found Google Trends link: {link_text[:50]}...")
logging.info(f" URL: {href[:80]}...")
break
except:
continue
if trends_link:
break
except:
continue
if not trends_link:
# Fallback: Look for any link containing "trends"
try:
all_links = driver.find_elements(By.TAG_NAME, "a")
for link in all_links:
try:
if link.is_displayed():
text = link.text.lower()
href = link.get_attribute("href") or ""
if "trends" in text and "google" in text:
trends_link = link
logging.info(f" Found fallback Trends link: {text[:50]}...")
break
except:
continue
except:
pass
if not trends_link:
# Ultimate fallback: Go directly but simulate human behavior
logging.warning(" Could not find Trends link, going directly")
driver.get("https://trends.google.com")
smart_backoff(4, 7, "Loading Google Trends directly", driver)
else:
# Click the link (human-like) - WITH INTERCEPTION HANDLING
random_mouse_movement(driver, trends_link)
smart_backoff(0.8, 1.5, "Hovering over Trends link", driver)
try:
# Try normal click first
trends_link.click()
logging.info("🔗 Clicked Google Trends link (normal click)")
except Exception as e:
if "element click intercepted" in str(e):
logging.info(" Element click intercepted, trying JavaScript click...")
try:
# Try JavaScript click
driver.execute_script("arguments[0].click();", trends_link)
logging.info(" Clicked Google Trends link (JavaScript click)")
except Exception as js_e:
logging.warning(f" JavaScript click failed: {js_e}, trying scroll and click...")
try:
# Scroll element into view and try again
driver.execute_script("arguments[0].scrollIntoView({block: 'center', behavior: 'smooth'});", trends_link)
smart_backoff(0.8, 1.5, "Scrolled to link", driver)
trends_link.click()
logging.info(" Clicked Google Trends link (after scroll)")
except Exception as scroll_e:
logging.warning(f" All clicks failed, going directly: {scroll_e}")
driver.get("https://trends.google.com")
else:
# Some other error
logging.warning(f" Click failed: {e}, going directly")
driver.get("https://trends.google.com")
smart_backoff(4, 8, "Loading Google Trends website", driver)
# PHASE 3: On Google Trends website
logging.info(" Phase 3: On Google Trends website")
# === CHECK FOR COOKIE DIALOG ON GOOGLE TRENDS TOO ===
cookie_handled_trends = handle_cookie_consent(driver)
if cookie_handled_trends:
logging.info(" Cookie consent handled on Google Trends")
smart_backoff(2, 4, "After cookie handling", driver)
# Simulate human exploring the page
simulate_human_scanning(driver, duration=4, intensity="light")
# Verify and set London location
verify_and_set_location(driver, "London")
# PHASE 4: Search for keyword on Google Trends
logging.info(f" Phase 4: Searching for '{keyword}' on Google Trends")
# === DEBUG: Print all search-like inputs first ===
logging.info(" DEBUG: Looking for search inputs...")
try:
all_inputs = driver.find_elements(By.TAG_NAME, "input")
logging.info(f" Found {len(all_inputs)} total input elements")
visible_inputs = []
for i, inp in enumerate(all_inputs[:15]): # Check first 15
try:
if inp.is_displayed():
inp_id = inp.get_attribute("id") or "no-id"
inp_type = inp.get_attribute("type") or "no-type"
inp_class = inp.get_attribute("class") or "no-class"
inp_aria = inp.get_attribute("aria-label") or "no-aria"
inp_placeholder = inp.get_attribute("placeholder") or "no-placeholder"
logging.info(f" Input #{i}: id='{inp_id}', type='{inp_type}', aria='{inp_aria}'")
logging.info(f" class='{inp_class[:50]}...', placeholder='{inp_placeholder}'")
if inp_type == "text" and ("search" in inp_aria.lower() or "search" in inp_placeholder.lower()):
visible_inputs.append(inp)
logging.info(f" This looks like a search box!")
except:
pass
logging.info(f"🔍 Found {len(visible_inputs)} visible text inputs that might be search boxes")
except:
logging.warning(" Could not debug inputs")
# Find the search box on Google Trends
trends_search_box = None
trends_search_selectors = [
"input#i4", # Always the correct one for real keyword search
"input[jsname='YPqjbf'][aria-label='Search']:not([disabled])", # Skip top disabled box
"input[aria-label='Search']:not([disabled])",
"input[placeholder*='earch']:not([disabled])",
"input[type='text']:not([disabled])",
]
for selector in trends_search_selectors:
try:
logging.info(f" Trying selector: {selector}")
elements = driver.find_elements(By.CSS_SELECTOR, selector)
logging.info(f" Found {len(elements)} elements with selector: {selector}")
for element in elements:
try:
if element.is_displayed() and element.is_enabled():
# Additional verification
el_id = element.get_attribute("id") or ""
element_aria = element.get_attribute("aria-label") or ""
el_class = element.get_attribute("class") or ""
el_aria = element.get_attribute("aria-label") or ""
if "explore topics" not in (element.get_attribute("value") or "").lower():
trends_search_box = element
logging.info(f" Selected search box: id={el_id}, aria={el_aria}")
break
except Exception as e:
logging.debug(f" Element check failed: {e}")
continue
if trends_search_box:
break
except Exception as e:
logging.debug(f"Selector {selector} failed: {e}")
continue
if not trends_search_box:
# Last resort: Look for any input that looks like a search box
logging.info(" Trying last resort: scanning all inputs...")
try:
all_inputs = driver.find_elements(By.TAG_NAME, "input")
for inp in all_inputs:
try:
if inp.is_displayed() and inp.get_attribute("type") == "text":
val = inp.get_attribute("value") or ""
aria = inp.get_attribute("aria-label") or ""
if "explore topics" not in val.lower() and "search" in aria.lower():
trends_search_box = inp
logging.info(" Found fallback search box (filtered)")
break
except:
continue
except:
pass
if not trends_search_box:
raise Exception(" Could not find search box on Google Trends")
# Human-like interaction with search box
logging.info(" Moving to search box...")
random_mouse_movement(driver, trends_search_box)
smart_backoff(0.8, 1.5, "Moving to search box", driver)
trends_search_box.click()
smart_backoff(0.5, 1.2, "Clicked search box", driver)
# === ENHANCED: CLEAR THE SEARCH BOX PROPERLY ===
logging.info(" Checking and clearing search box...")
# Check if there's existing text
existing_text = trends_search_box.get_attribute("value") or trends_search_box.text
if existing_text and existing_text.strip():
logging.info(f" Found existing text in search box: '{existing_text[:50]}...'")
# Clear using multiple methods for reliability
clear_methods = [
lambda: trends_search_box.send_keys(Keys.CONTROL + 'a'),
lambda: trends_search_box.send_keys(Keys.COMMAND + 'a'),
lambda: trends_search_box.clear(),
lambda: driver.execute_script("arguments[0].value = '';", trends_search_box)
]
for method_num, clear_method in enumerate(clear_methods):
try:
clear_method()
smart_backoff(0.2, 0.4, f"Clearing method {method_num + 1}")
# Verify clear
current_text = trends_search_box.get_attribute("value") or trends_search_box.text
if not current_text or not current_text.strip():
logging.info(" Search box cleared successfully")
break
except:
continue
# Final verification
trends_search_box.click()
for _ in range(5):
trends_search_box.send_keys(Keys.BACKSPACE)
smart_backoff(0.05, 0.1)
# Send a space then backspace to ensure clear
trends_search_box.send_keys(' ')
smart_backoff(0.1, 0.2)
trends_search_box.send_keys(Keys.BACKSPACE)
smart_backoff(0.2, 0.4)
else:
logging.info(" Search box is already empty")
# Type the keyword with human-like errors and corrections
logging.info(f"⌨️ Typing '{keyword}' on Google Trends...")
human_type_with_smart_correction(trends_search_box, keyword, context="trends_search")
smart_backoff(2, 4, "Waiting for keyword suggestions", driver)
# FIX: Scroll back up to remove 'Explore topics' from view
try:
logging.info(" Scrolling back to top to avoid 'Explore topics' interference...")
driver.execute_script("window.scrollTo({ top: 0, behavior: 'smooth' });")
smart_backoff(1.5, 2.8, "Scrolling to top", driver)
except Exception as e:
logging.warning(f" Could not scroll to top: {e}")
# DEBUG: Take a screenshot to see what the page looks like
try:
driver.save_screenshot("debug_after_typing.png")
logging.info(" Saved debug screenshot: debug_after_typing.png")
except:
pass
# DEBUG: Check page text
try:
page_text = driver.find_element(By.TAG_NAME, "body").text.lower()
if "explore" in page_text:
logging.info(" 'Explore' text found somewhere on page")
else:
logging.info(" 'Explore' text NOT found on page")
# Show first 500 chars of page
logging.info(f" Page text preview: {page_text[:500]}...")
except:
pass
# === ADD THIS: Close dropdown before looking for Explore button ===
try:
# Try to close any open dropdowns by clicking on a neutral area
body = driver.find_element(By.TAG_NAME, "body")
# Click near the top-left corner to avoid clicking on important elements
actions = ActionChains(driver)
actions.move_to_element_with_offset(body, 10, 10)
actions.click()
actions.perform()
logging.info(" Clicked neutral area to close dropdowns")
smart_backoff(1, 2, "Waiting for dropdowns to close")
except:
pass
# ====== FIND AND CLICK CORRECT EXPLORE BUTTON ======
logging.info(" Looking for CORRECT Explore button (not 'Explore topics')...")
explore_clicked = False
explore_button = None
# Wait a bit for the button to appear after typing
smart_backoff(2, 4, "Waiting for Explore button to appear", driver)
# First, let's debug: show all Explore-like elements
try:
explore_elements = driver.find_elements(By.XPATH, "//*[contains(text(), 'Explore')]")
logging.info(f" DEBUG: Found {len(explore_elements)} elements with 'Explore' text")
for i, elem in enumerate(explore_elements[:5]):
try:
if elem.is_displayed():
elem_text = elem.text.strip()
elem_tag = elem.tag_name
elem_loc = elem.location
logging.info(f" Element {i}: '{elem_text}' ({elem_tag}) at {elem_loc}")
except:
continue
except:
pass
# Try specific selectors for the CORRECT Explore button
correct_selectors = [
"button:has(span.UywwFc-vQzf8d)",
"button:has(span[jsname='V67aGc'])",
"//button[.//span[text()='Explore'] and not(contains(., 'topics'))]",
"//button[contains(., 'Explore') and not(contains(., 'topics'))]",
]
for selector in correct_selectors:
try:
if selector.startswith("//"):
elements = driver.find_elements(By.XPATH, selector)
else:
elements = driver.find_elements(By.CSS_SELECTOR, selector)
for element in elements:
try:
if element.is_displayed() and element.is_enabled():
text = element.text.strip().lower()
# CRITICAL: Filter out "Explore topics" - we want JUST "Explore"
if "explore" in text and "topics" not in text:
explore_button = element
logging.info(f" Found CORRECT Explore button: '{element.text}'")
logging.info(f" Using selector: {selector}")
# Check if it's a span, find parent button
if explore_button.tag_name == 'span':
logging.info(" Found Explore span, looking for parent button...")
try:
parent_button = explore_button.find_element(By.XPATH, "./ancestor::button[1]")
if parent_button and parent_button.is_displayed():
logging.info(" Found parent button for Explore span")
explore_button = parent_button # Use the button instead of span
except:
pass # Keep using the span if can't find parent
break
except Exception as e:
logging.debug(f" Element check failed: {e}")
continue
if explore_button:
break
except Exception as e:
logging.debug(f"Selector {selector} failed: {e}")
continue
# If still not found, try scanning all buttons with better filtering
if not explore_button:
logging.warning(" Explore button not found with selectors, checking all buttons...")
try:
all_buttons = driver.find_elements(By.TAG_NAME, "button")
logging.info(f" Found {len(all_buttons)} total buttons")
for i, btn in enumerate(all_buttons[:15]): # Check first 15
try:
if btn.is_displayed() and btn.is_enabled():
btn_text = btn.text.strip().lower()
# CRITICAL FILTER: Must have "explore" but NOT "topics"
if "explore" in btn_text and "topics" not in btn_text:
explore_button = btn
logging.info(f" Found Explore button by scanning: '{btn.text}'")
break
except:
continue
except:
pass
# ====== CLICK THE BUTTON ======
if explore_button:
# Double-check it's the right button
button_text = explore_button.text.strip().lower()
if "topics" in button_text:
logging.error(" OOPS! Found WRONG 'Explore topics' button! Skipping...")
explore_button = None
else:
# Human-like click
logging.info(f" Clicking CORRECT Explore button: '{explore_button.text}'")
random_mouse_movement(driver, explore_button)
smart_backoff(0.5, 1.2, "Hovering over Explore button", driver)
try:
# Try multiple click methods
click_success = False
# Method 1: Normal click
try:
explore_button.click()
click_success = True
logging.info(" Normal click successful")
except:
# Method 2: JavaScript click
try:
driver.execute_script("arguments[0].click();", explore_button)
click_success = True
logging.info(" JavaScript click successful")
except:
# Method 3: ActionChains click
try:
ActionChains(driver).click(explore_button).perform()
click_success = True
logging.info(" ActionChains click successful")
except:
logging.warning(" All click methods failed")
if click_success:
explore_clicked = True
smart_backoff(4, 7, "Waiting after Explore click", driver)
# Check if we're on the right page
try:
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "svg, canvas, div[jsname='hiK3ld']"))
)
logging.info(" Charts detected - success!")
except:
logging.info(" Charts not immediately detected, but continuing...")
except Exception as click_error:
logging.warning(f" Error clicking Explore button: {click_error}")
explore_clicked = False
else:
logging.warning(" CORRECT Explore button not found")
# ====== FALLBACK: PRESS ENTER ======
if not explore_clicked:
logging.info(" Fallback: Pressing Enter on search box...")
try:
# Re-find search box
trends_search_box = driver.find_element(
By.CSS_SELECTOR,
"input[aria-label*='earch'], input[placeholder*='earch'], input[type='text']"
)
trends_search_box.send_keys(Keys.RETURN)
logging.info(" Pressed Enter on search box")
smart_backoff(6, 10, "Waiting for results after Enter", driver)
explore_clicked = True # Consider this success
except Exception as e:
logging.error(f" Could not press Enter: {e}")
# Set the timeframe to "Past 12 months"
set_timeframe(driver, "Past 12 months")
# Check if charts loaded
if not check_charts_loaded(driver):
logging.warning(" Charts didn't load properly, trying recovery...")
# Multiple recovery attempts
for recovery_attempt in range(2):
logging.info(f" Recovery attempt {recovery_attempt + 1}/2")
smart_backoff(10, 15, f"Extended wait attempt {recovery_attempt + 1}", driver)
# Try refreshing
driver.refresh()
smart_backoff(6, 10, "Page refreshed", driver)
# Set timeframe again
set_timeframe(driver, "Past 12 months")
if check_charts_loaded(driver):
logging.info(" Charts loaded after recovery")
break
if recovery_attempt == 0:
# Try going back and searching again
driver.back()
smart_backoff(4, 7, "Went back to search", driver)
# Find search box again and retry
try:
trends_search_box = driver.find_element(By.CSS_SELECTOR, "input[aria-label='Search'][type='text']")
trends_search_box.click()
smart_backoff(1, 2)
trends_search_box.clear()
human_type_with_smart_correction(trends_search_box, keyword, context="trends_search")
smart_backoff(2, 4)
trends_search_box.send_keys(Keys.RETURN)
smart_backoff(8, 12, "Retried search", driver)
except:
pass
# PHASE 5: Wait for results with intelligent backoff
logging.info(" Phase 5: Loading results with smart backoff...")
# Progressive loading with increasing patience
load_stages = [
(random.uniform(3, 6), "Initial page load"),
(random.uniform(2.5, 5), "Content rendering"),
(random.uniform(2, 4), "Chart elements"),
(random.uniform(1, 3), "Final touches"),
]
for wait_time, stage in load_stages:
logging.info(f" {stage}...")
time.sleep(wait_time)
# Simulate human watching the load
if random.random() < 0.3:
simulate_human_scanning(driver, duration=1, intensity="very_light")
# Final scan of the results page
logging.info(" Final scan of results page...")
simulate_human_scanning(driver, duration=3, intensity="light")
# Verify we're in London
page_source = driver.page_source.lower()
if any(indicator in page_source for indicator in ["london", "united kingdom", "uk", "gb"]):
logging.info(" Confirmed: Viewing London trends")
else:
logging.warning(" London location not clearly detected")
# Additional wait for any final loading
smart_backoff(3, 7, "Final page stabilization", driver)
logging.info(" Complete human journey finished successfully!")
# Extract data from the page
trends_data = extract_trends_data_from_current_page(driver)
return driver, trends_data # Return both driver AND data
第15步:为模拟的浏览过程配置真实可信的身份
接下来的函数会进一步强化浏览器所呈现的身份与来源信息。
各部分如何协同运作:
- assign_proxy_and_identity
该函数会将一个代理与匹配的浏览器身份配对,并能轻松处理两种不同的代理格式(IP白名单认证和用户名密码认证),让您可以随意切换,不会遇到麻烦。
- launch_stealth_browser
这个函数承担了主要工作:它会使用配对好的代理与身份,启动一个真实且难以被检测的Chrome会话,并从一开始就应用好所有必要设置。
它会配置代理路由、窗口大小、语言、User-Agent与硬件信号、所有必要的WebDriver隐身补丁、时区与地理位置设置,甚至在会话进入Google之前就处理好Cookie同意弹窗。
为确保一切按预期运行,它会在浏览器内部验证代理,给会话留出一点稳定的时间,再额外短暂等待一下以策稳妥。
将以下代码块添加到您的代码中:
def assign_proxy_and_identity():
proxy_raw = random.choice(PROXIES)
identity = random.choice(IDENTITY_POOL)
# Parse proxy - handle whitelisted case
parts = proxy_raw.split(":")
if len(parts) == 2:
# Format: host:port (whitelisted)
proxy_host_port = proxy_raw
logging.info(f" London Proxy (Whitelisted): {proxy_host_port}")
elif len(parts) == 4:
# Format: host:port:user:pass
proxy_host_port = f"{parts[0]}:{parts[1]}"
logging.info(f" London Proxy (Authenticated): {proxy_host_port}")
else:
raise ValueError(f"Invalid proxy format: {proxy_raw}")
logging.info(f" Identity: {identity['user_agent'][:50]}...")
return proxy_raw, identity # Return full proxy string
# === Launch Stealth Browser (Enhanced) ===
def launch_stealth_browser(proxy_raw: str, identity: dict):
"""
Launch browser with WHITELISTED proxy (NO authentication needed)
Format: ultra.marsproxies.com:44443:ignore:ignore
"""
try:
# === PARSE PROXY ===
parts = proxy_raw.split(":")
if len(parts) < 2:
raise ValueError(f"Invalid proxy format: {proxy_raw}")
# Extract just host and port
host, port = parts[0], parts[1]
logging.info(f" Using WHITELISTED proxy: {host}:{port}")
logging.info(" No authentication needed (IP whitelisted)")
# === CHROME OPTIONS ===
options = uc.ChromeOptions()
# === CRITICAL: PROXY SETTING FOR WHITELISTED ===
options.add_argument(f"--proxy-server=http://{host}:{port}")
# === ESSENTIAL ARGUMENTS ===
options.add_argument(f"--window-size={identity['viewport'][0]},{identity['viewport'][1]}")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument(f"--lang={identity['language'].split(',')[0]}")
options.add_argument(f'--user-agent={identity["user_agent"]}')
# === PROXY-SPECIFIC ARGUMENTS ===
options.add_argument("--ignore-certificate-errors")
options.add_argument("--allow-running-insecure-content")
options.add_argument("--disable-web-security")
# === ADDITIONAL STEALTH ARGUMENTS ===
options.add_argument("--disable-popup-blocking")
options.add_argument("--disable-notifications")
options.add_argument("--disable-background-timer-throttling")
options.add_argument("--disable-backgrounding-occluded-windows")
options.add_argument("--disable-breakpad")
options.add_argument("--disable-component-update")
options.add_argument("--disable-domain-reliability")
options.add_argument("--disable-features=AudioServiceOutOfProcess")
options.add_argument("--disable-hang-monitor")
options.add_argument("--disable-ipc-flooding-protection")
options.add_argument("--disable-renderer-backgrounding")
# === HEADLESS MODE ===
if HEADLESS:
options.add_argument("--headless=new")
# === LAUNCH CHROME ===
logging.info(" Launching Chrome with whitelisted proxy...")
driver = uc.Chrome(
options=options,
version_main=144,
headless=HEADLESS,
suppress_welcome=True,
use_subprocess=True,
)
# === STORE PROXY INFO ===
driver.proxy_info = {
'host': host,
'port': port,
'type': 'whitelisted_no_auth'
}
# === COMPREHENSIVE STEALTH SCRIPTS ===
stealth_scripts = [
"""
// Overwrite the navigator.webdriver property
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""",
"""
// Overwrite the navigator.plugins property
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
""",
"""
// Overwrite the navigator.languages property
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
});
""",
"""
// Overwrite the navigator.connection property
Object.defineProperty(navigator, 'connection', {
get: () => ({
downlink: 10,
effectiveType: '4g',
rtt: 50,
saveData: false
})
});
""",
"""
// Spoof Chrome runtime
window.chrome = {
runtime: {},
loadTimes: function() {},
csi: function() {},
app: {}
};
""",
"""
// Add hairline to canvas
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function() {
const context = originalGetContext.apply(this, arguments);
if (context && context.constructor.name === 'CanvasRenderingContext2D') {
const originalFillText = context.fillText;
context.fillText = function() {
if (arguments[0] === '') {
arguments[0] = ' ';
}
return originalFillText.apply(this, arguments);
};
}
return context;
};
""",
f"""
// Set user agent
Object.defineProperty(navigator, 'userAgent', {{
get: () => '{identity['user_agent']}'
}});
// Set platform
Object.defineProperty(navigator, 'platform', {{
get: () => '{identity['platform']}'
}});
// Set hardware concurrency
Object.defineProperty(navigator, 'hardwareConcurrency', {{
get: () => {identity['hardware_concurrency']}
}});
// Set max touch points
Object.defineProperty(navigator, 'maxTouchPoints', {{
get: () => {identity['max_touch_points']}
}});
// Set language
Object.defineProperty(navigator, 'language', {{
get: () => '{identity['language'].split(',')[0]}'
}});
// Set languages
Object.defineProperty(navigator, 'languages', {{
get: () => ['en-US', 'en']
}});
// Set device memory
Object.defineProperty(navigator, 'deviceMemory', {{
get: () => {identity.get('device_memory', 8)}
}});
"""
]
# Execute all stealth scripts
for script in stealth_scripts:
try:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": script
})
except Exception as e:
logging.debug(f"Could not execute stealth script: {e}")
# === SET LONDON TIMEZONE AND GEOLOCATION ===
try:
# Set London timezone
driver.execute_cdp_cmd("Emulation.setTimezoneOverride", {
"timezoneId": identity["timezone"]
})
# Set London geolocation
driver.execute_cdp_cmd("Emulation.setGeolocationOverride", {
"latitude": 51.5074,
"longitude": -0.1278,
"accuracy": 100
})
logging.info(" Set London timezone and geolocation")
except Exception as e:
logging.warning(f"Could not set location/timezone: {e}")
# === SET USER AGENT OVERRIDE ===
try:
driver.execute_cdp_cmd("Emulation.setUserAgentOverride", {
"userAgent": identity["user_agent"],
"acceptLanguage": identity["language"],
"platform": identity["platform"]
})
except Exception as e:
logging.debug(f"Could not set user agent override: {e}")
# ====== CRITICAL: SET COOKIES BEFORE FIRST NAVIGATION ======
# This must happen BEFORE any navigation to Google domains
logging.info(" Setting Google consent cookies...")
# Navigate to about:blank first (clean state)
driver.get("https://www.google.com/ncr")
time.sleep(2)
# First try the UI dialog (most reliable)
handle_cookie_consent(driver, timeout=8)
# Then set cookies (optional but ok as reinforcement)
set_google_consent_cookies(driver)
# Refresh so cookies apply to the session
driver.refresh()
time.sleep(2)
# Set the consent cookies
set_google_consent_cookies(driver)
# === VERIFY PROXY IN BROWSER ===
logging.info(" Verifying proxy in browser...")
try:
driver.get("https://api.ipify.org?format=json")
time.sleep(3)
body = driver.find_element(By.TAG_NAME, "body").text
browser_ip_data = json.loads(body)
browser_ip = browser_ip_data.get('ip', 'Unknown')
logging.info(f" Browser IP through proxy: {browser_ip}")
# Try to get proxy IP for comparison
proxy_ip = None
try:
proxy_url = f"http://{host}:{port}"
proxies = {'http': proxy_url, 'https': proxy_url}
response = requests.get(
'https://api.ipify.org?format=json',
proxies=proxies,
timeout=15,
headers={'User-Agent': identity['user_agent']}
)
if response.status_code == 200:
proxy_ip = response.json()['ip']
# Try to get location info
try:
loc_response = requests.get(
f'https://ipapi.co/{proxy_ip}/json/',
timeout=5,
headers={'User-Agent': identity['user_agent']}
)
if loc_response.status_code == 200:
loc_data = loc_response.json()
proxy_country = loc_data.get('country_code', 'Unknown')
proxy_city = loc_data.get('city', 'Unknown')
logging.info(f" Proxy location: {proxy_city}, {proxy_country}")
driver.proxy_info['country'] = proxy_country
driver.proxy_info['city'] = proxy_city
except:
pass
except:
pass
if proxy_ip and browser_ip == proxy_ip:
logging.info(" Proxy confirmed working in browser!")
elif browser_ip != 'Unknown':
logging.info(f" Browser using proxy (IP: {browser_ip})")
else:
logging.warning(" Could not get browser IP")
# Navigate back to blank page
driver.get("about:blank")
time.sleep(1)
except Exception as e:
logging.warning(f" Could not verify proxy in browser: {e}")
# Navigate to blank page anyway
try:
driver.get("about:blank")
except:
pass
# === WAIT FOR BROWSER TO STABILIZE ===
smart_backoff(2, 4, "Browser stabilizing", driver)
logging.info(" Browser launched successfully with whitelisted proxy and pre-set cookies!")
return driver
except Exception as e:
logging.error(f" FAILED to launch browser with proxy: {e}")
logging.error(f" Proxy: {proxy_raw}")
logging.error(f" Error details: {type(e).__name__}")
# Try to provide helpful error messages
if "chrome not reachable" in str(e).lower():
logging.error(" Tip: Check if Chrome is installed and up to date")
elif "proxy" in str(e).lower():
logging.error(" Tip: Proxy might be blocked or not whitelisted correctly")
elif "timeout" in str(e).lower():
logging.error(" Tip: Proxy might be too slow or not responding")
raise Exception(f"Browser launch failed: {e}")
# === Convert Selenium Cookies to Dict ===
def get_cookies_as_dict(driver):
cookies = driver.get_cookies()
cookie_dict = {}
for c in cookies:
cookie_dict[c['name']] = c['value']
return cookie_dict
第16步:编写Google Trends数据抓取逻辑
信不信由您,我们目前为止写下的2000行代码,全都是为了确保接下来这一小段代码能够顺利运行。我们在这里不依赖DOM抓取的方式,因为Google Trends基于Angular构建,而Google可能会频繁且毫无预警地更改类名。
因此,我们不依赖随时可能发生变化的后端元素,而是依赖Google实际展示给用户的内容——毕竟我们已经让Google有充分理由相信我们是真实用户。
在此基础上,我们使用正则表达式通过模式识别,从可见的页面内容中提取所需数据。
将这段简短的代码添加到脚本中:
# === Main Scraping Function ===
def scrape_google_trends():
"""
Main function that:
1. Does the full human journey (Google → Google Trends → search gym membership)
2. Extracts data using regex from the page
3. Saves the data
"""
for attempt in range(MAX_RETRIES):
try:
logging.info(f" Attempt {attempt+1}/{MAX_RETRIES}")
# Assign proxy and identity
proxy_raw, identity = assign_proxy_and_identity()
# Launch browser
smart_backoff(2, 5, "Starting browser")
driver = launch_stealth_browser(proxy_raw, identity)
logging.info(" Stealth browser launched")
# Try to load previous cookies if they exist
try:
identity_hash = hash(str(identity))
cookie_file = f"cookies_{identity_hash}.json"
load_cookies(driver, cookie_file)
except:
pass
# Complete human journey - NOW RETURNS DATA TOO
driver, trends_data = complete_human_journey(driver, KEYWORD)
# Check for blocks - with CAPTCHA-specific handling
block_status = detect_block(driver)
if block_status:
if block_status == "CAPTCHA":
logging.error(" CAPTCHA encountered, cleaning up and trying new identity...")
else:
logging.error(" Block detected, cleaning up...")
# Try to save screenshot for debugging
try:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
block_screenshot = f"blocked_{timestamp}.png"
driver.save_screenshot(block_screenshot)
logging.info(f" Saved block screenshot: {block_screenshot}")
except:
pass
clean_exit(driver)
# Different backoff for CAPTCHA vs other blocks
if block_status == "CAPTCHA":
# Longer backoff for CAPTCHA
captcha_backoff = 120 + random.uniform(0, 60) # 2-3 minutes
logging.info(f" CAPTCHA encountered, waiting {captcha_backoff:.1f}s...")
time.sleep(captcha_backoff)
else:
# Normal backoff for other blocks
if attempt < len(GOOGLE_BACKOFF_STEPS):
backoff_time = GOOGLE_BACKOFF_STEPS[attempt]
else:
backoff_time = GOOGLE_BACKOFF_STEPS[-1]
jitter = backoff_time * random.uniform(-0.2, 0.2)
total_wait = max(10, backoff_time + jitter)
logging.info(f" Backing off for {total_wait:.1f}s...")
time.sleep(total_wait)
continue # Try again with new attempt
# ====== REGEX DATA EXTRACTION ======
# (Data already extracted in complete_human_journey, but verify)
if not trends_data or not trends_data.get('interest_by_subregion'):
logging.warning(" Could not extract data from page, trying again...")
trends_data = extract_trends_data_from_current_page(driver)
# Take screenshot of the results
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
screenshot_path = f"trends_{KEYWORD.replace(' ', '_')}_london_{timestamp}.png"
driver.save_screenshot(screenshot_path)
logging.info(f" Saved screenshot: {screenshot_path}")
# Save cookies for next time
try:
identity_hash = hash(str(identity))
cookie_file = f"cookies_{identity_hash}.json"
save_cookies(driver, cookie_file)
except:
pass
# Clean exit
clean_exit(driver)
# ====== SAVE THE DATA ======
if trends_data:
# Create enhanced data structure
final_data = {
"extraction_info": {
"keyword": KEYWORD,
"human_journey": "Google → Google Trends → Search",
"location_target": GEO_HUMAN_NAME,
"extracted_at": datetime.now().isoformat(),
"method": "regex_text_parsing",
"screenshot": screenshot_path
},
"google_trends_data": trends_data,
"summary": {
"subregions_count": len(trends_data.get('interest_by_subregion', {})),
"timeline_points": len(trends_data.get('interest_over_time', {}).get('x_axis', [])),
"regional_data": trends_data.get('interest_by_subregion', {})
}
}
# Save JSON
json_filename = f"trends_{KEYWORD.replace(' ', '_')}_london_{timestamp}.json"
with open(json_filename, 'w', encoding='utf-8') as f:
json.dump(final_data, f, indent=2, ensure_ascii=False)
logging.info(f" Saved data to: {json_filename}")
# Print summary
print("\n" + "="*60)
print(" HUMAN JOURNEY COMPLETE - DATA EXTRACTED")
print("="*60)
print(f"Journey: Google Search → Google Trends → '{KEYWORD}'")
print(f"Location: {GEO_HUMAN_NAME}")
regions = trends_data.get('interest_by_subregion', {})
if regions:
print(f"\n REGIONAL INTEREST DATA:")
for region, value in regions.items():
print(f" {region}: {value}")
dates = trends_data.get('interest_over_time', {}).get('x_axis', [])
if dates:
print(f"\n TIMELE: {dates[0]} to {dates[-1]}")
print(f"\n Files saved:")
print(f" • Data (JSON): {json_filename}")
print(f" • Screenshot: {screenshot_path}")
print("="*60)
return final_data
else:
raise Exception("Failed to extract any data from page")
except Exception as e:
logging.error(f"Attempt {attempt+1} failed: {e}")
try:
clean_exit(driver)
except:
pass
# Exponential backoff
if attempt < len(GOOGLE_BACKOFF_STEPS):
backoff_time = GOOGLE_BACKOFF_STEPS[attempt]
else:
backoff_time = GOOGLE_BACKOFF_STEPS[-1]
jitter = backoff_time * random.uniform(-0.2, 0.2)
total_wait = max(10, backoff_time + jitter) # Minimum 10 seconds
logging.info(f" Backing off for {total_wait:.1f}s...")
time.sleep(total_wait)
logging.critical(" All attempts failed.")
return None
第17步:执行您的脚本
现在,我们添加真正让整个流程运转起来的部分——主执行代码块。这部分只有在您直接运行脚本时才会被触发。
我们会先添加一个短暂延迟,避免每次运行都在同一时间启动。延迟结束后,脚本会调用scrape_google_trends()来运行完整的抓取函数。这个函数整合了我们目前为止构建的所有内容。
完成后,它会先确认返回的结果确实可用,然后再开始解析返回的JSON数据。接着,它会记录一份简要总结:排名最高的地区、完整的地区分布、时间范围、成功抓取到的数据点,以及截图的保存位置。
如果所有重试都未能成功,运行仍然失败,我们会记录一条简洁的失败提示,然后程序退出。以下是需要添加到脚本中的执行代码块:
# ===== MAIN EXECUTION BLOCK =====
# This part runs when you execute: python your_script.py
if __name__ == "__main__":
startup_delay = random.uniform(0, 15)
logging.info(f" Starting in {startup_delay:.1f}s...")
time.sleep(startup_delay)
# Now returns the parsed data
result_data = scrape_google_trends()
if result_data is not None:
logging.info(" HUMAN JOURNEY COMPLETED SUCCESSFULLY!")
logging.info(f" Extracted data for '{KEYWORD}' in {GEO_HUMAN_NAME}")
# Show quick summary
trends_data = result_data.get('google_trends_data', {})
regions = trends_data.get('interest_by_subregion', {})
if regions:
top_region = max(regions.items(), key=lambda x: x[1]) if regions else ("None", 0)
logging.info(f" Highest interest: {top_region[0]} ({top_region[1]})")
# Log all regions
logging.info(" Regional breakdown:")
for region, value in sorted(regions.items(), key=lambda x: x[1], reverse=True):
logging.info(f" • {region}: {value}")
# Log timeline info
timeline = trends_data.get('interest_over_time', {})
dates = timeline.get('x_axis', [])
if dates:
logging.info(f" Timeline range: {dates[0]} to {dates[-1]}")
logging.info(f" Data points: {len(dates)}")
# Log file info
if result_data.get('extraction_info', {}).get('screenshot'):
logging.info(f" Screenshot saved: {result_data['extraction_info']['screenshot']}")
# Success message
print("\n" + "="*60)
print(" MISSION ACCOMPLISHED!")
print("="*60)
print(f"Your human journey successfully extracted Google Trends data.")
print(f"Keyword: '{KEYWORD}'")
print(f"Location: {GEO_HUMAN_NAME}")
print(f"Method: Regex parsing from page text")
print("="*60)
else:
logging.error(" Scraping failed - all attempts exhausted.")
这就是最终结果。如果一切顺利,脚本会将输出文件保存在项目运行所在的文件夹中。您最终会得到一个JSON文件(以及一张截图),该JSON文件就是整个模拟浏览过程最终整理好的结果。
它记录了本次运行的上下文信息、提取到的结构化Trends数据,以及一份简要摘要,方便您快速核验抓取结果是否正确。
以下是带占位符的JSON文件示例:
{
"extraction_info": {
"keyword": "<KEYWORD_STRING>",
"human_journey": "Google → Google Trends → Search",
"location_target": "<LOCATION_NAME>",
"extracted_at": "<ISO_TIMESTAMP>",
"method": "regex_text_parsing",
"screenshot": "<SCREENSHOT_FILENAME>.png"
},
"google_trends_data": {
"interest_over_time": {
"x_axis": ["<DATE_1>", "<DATE_2>", "<DATE_3>"],
"y_axis": ["<VALUE_1>", "<VALUE_2>", "<VALUE_3>"]
},
"interest_by_subregion": {
"<REGION_1>": "<SCORE_1>",
"<REGION_2>": "<SCORE_2>",
"<REGION_3>": "<SCORE_3>"
}
},
"summary": {
"subregions_count": "<INT>",
"timeline_points": "<INT>",
"regional_data": {
"<REGION_1>": "<SCORE_1>",
"<REGION_2>": "<SCORE_2>",
"<REGION_3>": "<SCORE_3>"
}
}
}
各字段含义如下:
- extraction_info
记录本次运行的一些元数据:所搜索的关键词、定位的目标位置、抓取发生的时间、所使用的方法,以及为调试或核验而保存的具体截图文件。
- google_trends_data
实际提取数据的存放位置:
◦ interest_over_time:两个一一对应的数组,x_axis保存时间轴标签,y_axis保存每个对应时间点的兴趣值。
◦ interest_by_subregion:地区与分数的映射关系,数据来自Google在页面上展示的内容。
- summary
一组可直接使用的快速验证数据:
◦ subregions_count:成功抓取到的地区数量。
◦ timeline_points:成功抓取到的时间点数量。
◦ regional_data:重复列出该数据,方便您快速获取,无需再四处查找。
点击此处获取完整代码。您现在已拥有一套功能完整的Google Trends爬虫配置,可适用于任意关键词或位置。
进阶数据抓取技巧
抓取相关查询数据
我们刚刚构建的这个规模不小的脚本,可以轻松获取基础关键词数据。不过您可能会注意到,它并不会抓取相关查询或相关主题,这是刻意为之,因为这些元素并不总会稳定出现在Trends结果中。
它们只有在满足以下几项特定条件时才可能出现:
- 搜索量需要足够大
- Google需要有足够的数据才能展示对比结果
- 您所选的位置必须能够访问该数据
- 所选时间范围需要足够宽,才能支持查看趋势
由于这些条件并非总能自然满足,实际做法是让爬虫具备足够的防御性:不要默认相关查询或主题一定存在,而是先添加检查确认它们确实存在,再尝试采集数据;如果存在就采集,如果不存在,就干净利落地跳过这部分流程。
由此,您需要开始关注日志记录。日志应能反映出某次运行中是否找到了相关查询或相关主题,而不只是记录脚本是否执行完毕。这种反馈正是了解 Google 在何时、在何种条件下才会真正暴露这类数据的关键。
使用 cron 任务或脚本实现自动化
假设您想让这个爬虫每天早上 6 点运行一次,获取 10 个关键词的数据,并切换不同地区,这正是 cron 任务能大大简化工作的地方:只需设置一次,它就会按计划自动运行,无需人工盯守。
下面从宏观角度说明具体做法:
- 步骤 1:为爬虫项目建立一个专用文件夹,将爬虫放入其中,再分别创建用于存放数据、日志和截图的子文件夹。
- 步骤 2:编写一个脚本,用于进入爬虫目录、激活 Python、运行爬虫,并将输出文件移动到对应文件夹,同时记录每一步操作的日志。
- 步骤 3:加入一些错误处理机制,包括超时、重试,以及在出现异常时发送通知。
- 步骤 4:设置日志记录,以便查看哪些步骤运行成功、哪些失败,以及输出结果存放在何处。
- 步骤 5:为 cron 任务设置具体运行时间,将任务添加到 cron 编辑器中,然后运行一次测试,确认脚本按预期运行。
- 步骤 6:将整套流程投入使用,密切关注最初几次运行情况,等真实日志开始积累后再进行调整。
这基本上就是打造一套能为业务持续提供数据的爬虫系统的方法——效果如同真人操作,而您除了设置和维护计划任务外,几乎无需再动手。
应对速率限制与 CAPTCHA
在处理速率限制方面,我们已经做得不错。不要指望这种情况短期内会有变化:除非使用官方 API,否则 Google 并不欢迎任何人用机器人爬取其数据。
问题在于,该 API 目前仍处于 Alpha 阶段,需要申请才能使用,而且目前仅面向研究人员开放,所以您暂时无法获取访问权限。
正因如此,我们特意将这个爬虫设计得对速率限制有较强的抵御能力,它几乎每一步都经过设计,以避免被 Google 封锁。首先,它不会像机器人那样机械地点击翻页,而是像真人一样以很慢的速度滚动页面、浏览内容,偶尔还会回头看一眼页面。
脚本还内置了一些轻度的交互行为,比如偶尔把鼠标悬停在链接上,让整个过程尽可能显得自然。您会发现几乎每一步的时间间隔都带有一定随机性,这并非偶然——如果试图加快节奏,很快就会被封锁。
由于没有任何系统能做到万无一失,我们也内置了一些检测机制,用来识别何时被封锁或出现 CAPTCHA。
这就带来了这种方法唯一的缺点:一旦出现 CAPTCHA,脚本会暂停并发出提示,交由真人介入处理;如果只是临时性封锁而非 CAPTCHA,脚本则会自动重试。
轮换代理
您可能已经注意到,我们是如何通过将伦敦代理与专属的伦敦身份池搭配、再进行轮换来充分利用它们的,这样做是正确的。
如果您今天需要从伦敦抓取数据,随后又要切换到美国或非洲,就需要为每个地区分别准备一套独立的代理列表,同时还需要与该 IP 地理位置相匹配的 User-Agent 和身份池。
拥有代理是一回事,干净利落地使用它又是另一回事。如果使用伦敦代理,却把浏览器语言设为法语、时区设为非洲,这种异常组合会非常显眼,容易触发软封锁,因为这类异常会被 Google 的反机器人机制判定为可疑会话。
代理的接入方式同样重要,以下是传统做法:
import requests
proxies = {'http': 'proxy_ip:port'}
response = requests.get(url, proxies=proxies)
这种方法足够简单,但也更容易被识别,因为基于 requests 的流量模式相当固定,而且无法呈现出浏览器应有的真实感,这正是我们采用不同做法的原因:
options.add_argument(f"--proxy-server=http://{host}:{port}")
driver = uc.Chrome(options=options)
这样做的话,Chrome 会原生运行该代理,您依然能获得完整的 JavaScript 执行能力,并且流量会经过真实的浏览器会话,而不是脆弱的 HTTP 客户端。
在 Chrome 中实现这一点最快捷的方式是使用 MarsProxies Chrome 插件——只需单击一下即可切换代理,无需改动系统设置。
处理请求头
如果您仍然遇到奇怪的速率限制问题,或偶尔出现 CAPTCHA,还有一个办法可以尝试:在浏览器会话启动时立即设置现代 Chrome 请求头,这样能让会话从第一个请求开始,在网络层面看起来更加真实可信。
在 launch_stealth_browser() 内部,紧跟在 driver = uc.Chrome(...) 之后加入以下内容:
# In launch_stealth_browser() function, after driver = uc.Chrome(...)
# ADD MODERN CHROME HEADERS
logging.info(" Setting modern Chrome security headers...")
try:
# Enable Network domain first
driver.execute_cdp_cmd("Network.enable", {})
# Set the exact headers modern Chrome sends
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {
"headers": {
# Language preferences
"Accept-Language": "en-GB,en;q=0.9",
# Content negotiation
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding": "gzip, deflate, br",
# Security upgrades
"Upgrade-Insecure-Requests": "1",
# ====== ANTI-BOT CRITICAL HEADERS ======
# Fetch Metadata API headers (Google checks these!)
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
# Client Hints (Modern Chrome)
"Sec-CH-UA": f'"Chromium";v="121", "Google Chrome";v="121", "Not-A.Brand";v="99"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": f'"{identity["platform"]}"',
# Do Not Track (common in UK browsers)
"DNT": "1",
# Cache control
"Cache-Control": "max-age=0",
}
})
logging.info(" Modern Chrome headers set successfully")
except Exception as e:
logging.warning(f" Could not set extra headers: {e}")
合乎道德的数据抓取实践指南
抓取公开可获取的数据并不必然违法,但这并不意味着可以为所欲为。如果想要以恰当的方式进行,需要牢记几个基本的道德准则:
- 注意数据类型
避免收集个人数据,如果您的使用场景不需要这些数据,就不要存储它们。
- 尽量减少足迹
采用较低的请求频率和保守的并发数,尤其是在那些并非为应对大量自动化流量而设计的网站上。
- 阅读 robots.txt 文件
它无法回答所有法律问题,但能告诉您该网站希望自动化访问者避免哪些行为。
Google Trends 抓取的替代方案
如果上述代码过于复杂,或运行和维护起来太耗时,以下是一些可以依赖的替代方案:
程序化获取 Trends 数据
访问权限需通过 Alpha 申请计划获取
您能获得 Alpha 访问资格,并希望使用官方途径
随时间变化的热度、按地区划分的热度、相关主题、相关查询
仍属于第三方数据提取,需依赖供应商的持续维护
您希望直接获得 Trends → JSON 数据,而无需自行运行爬虫
关键词热度随时间变化、特定地区的热度、相关主题/查询
需要按照其任务工作流程操作(提交任务、获取结果)
您正在搭建数据管道,需要吞吐量和可预测的计费方式
相关主题、相关查询、地区热度、随时间变化的热度、结构化 JSON
依赖供应商,如同所有第三方 API 一样
您希望快速集成,并获得一致的响应格式
实时热搜、随时间变化的热度、按地区划分的热度、相关查询与主题(因 actor 而异)
质量和稳定性因 actor 而异,定价可能为订阅制、按使用量计费或按结果计费,具体取决于所用 actor
您希望“运行即导出”,无需自行维护抓取基础设施
类似 Google Trends“探索”(Explore)功能的数据提取,支持多种搜索类型和筛选条件
文档中提示,抓取到的数据可能与浏览器实际显示的结果并不完全一致
您需要一种托管式方案,并能接受偶尔出现的数据差异
Google Trends 搜索数据(以 Trends 爬虫形式提供),外加更广泛的 SERP 工具
具体细节因端点而异,建议在文档中确认您所需的确切输出内容
您已经在使用某个 SERP 供应商,并希望将 Trends 数据纳入同一技术栈
因供应商而异——通常包括随时间变化的热度、按地区划分的热度、相关主题及查询
可靠性和数据结构因供应商而异,需要进行供应商筛选评估
您希望快速获得多种选项,并愿意测试多家供应商
2026年稳定抓取 Google Trends 数据,避开封锁
获取住宅代理结语
到这里,我们就讲完了。如果您一直读到这里,值得点赞。按照我们列出的步骤操作,您就能搭建一个行为表现接近真实用户的定制化 Google Trends 爬虫。在此基础上,您可以自行调整代码,获取任意关键词和地区的数据。如果想了解更多关于抓取 Google Trends 及其他网站的技巧,欢迎加入我们的 Discord。
Python 不太适合您的抓取技术栈?我们的Go 与 Python 对比一文详细分析了此类数据密集型项目的利弊权衡。
Google Trends 可以被抓取吗?
可以,它是可以抓取的。Google Trends 会把您看到的数据直接发送到浏览器,并在客户端完成渲染,但数据并不是以整齐、稳定的格式提供的。页面元素会随着搜索量、地区和时间范围的不同而不断出现和消失,这意味着要稳定地抓取这些数据,需要比单纯发送几个 HTTP 请求复杂得多的逻辑和浏览器自动化手段。
抓取 Google Trends 合法吗?
只要您只是访问公开可获取的信息,而没有试图绕过任何付费墙、身份验证或私有系统,抓取 Google Trends 本身是没问题的。真正可能带来麻烦的是您使用数据的方式——抓取的力度有多大,以及是否违反了 Google 的服务条款。
如何从 Google Trends 获取原始数据?
Google 并不会提供真正意义上的原始数据,也就是实际的搜索次数。您得到的是经过归一化处理、范围在 0 到 100 之间的数据。可以通过从界面中导出 CSV 文件、使用帮您完成这些工作的第三方 API,或者像我们这样自行搭建爬虫来获取这些数据。
适合抓取 Google Trends 的方法是什么?
适合的方法是使用浏览器自动化,并确保其具备可靠的反检测措施。Google Trends 高度依赖 JavaScript,数据以动态方式渲染,因此轻量级的请求方式往往容易失效或被封锁。我们的爬虫为抓取该网站提供了稳健的基础。
Google Trends 有 API 吗?
Google 确实有官方的 Trends API,但目前仍处于 Alpha 阶段,访问受限,主要面向研究人员开放。对大多数实际用途而言,人们通常会使用付费的第三方 API、托管式抓取服务,或者自行开发抓取方案。