基于 Selenium 和 BeautifulSoup 的动态网页爬虫:一次对百度地图 POI 数据的深度模块化剖析
摘要: 本文深入探讨并实现了一个针对动态加载网页(以百度地图为例)的数据抓取解决方案。文章的核心亮点在于提出了一种“半自动”模式,即通过脚本启动一个带调试端口的独立 Chrome 浏览器,由用户手动完成复杂的登录操作,再由 Selenium 接管,从而巧妙地绕过了繁琐的模拟登录验证。本文将采用“深度模块化剖析”的方法,逐一解析代码的各个功能模块,详细阐述其设计理念、实现细节及在实际应用中遇到的挑战与解决方案,旨在为读者提供一个健壮、可扩展且易于理解的Web数据抓取框架。
关键词: Python爬虫;Selenium;BeautifulSoup;百度地图;动态网页;数据抓取;模块化设计
1. 背景与引言 (Introduction)
在数据驱动的时代,从网络上获取特定信息(即网络爬虫)已成为数据分析、市场研究和商业智能等领域不可或缺的一项技能。然而,随着前端技术的飞速发展,现代网站越来越多地采用 AJAX、JavaScript 动态加载等技术来提升用户体验。这给传统的、依赖静态 HTML 请求的爬虫技术(如 requests 库)带来了巨大挑战。网页的真实数据往往在初始 HTML 中并不存在,而是通过后续的异步请求动态渲染到页面上。
百度地图(map.baidu.com)是一个典型的例子。当用户搜索一个兴趣点(Point of Interest, POI)时,搜索结果列表并非一次性全部加载。它通过“加载更多”按钮和分页导航来动态呈现数据,这对自动化数据采集构成了主要障碍。
为了应对这些挑战,本文提出并实现了一个基于 Selenium 的自动化爬虫脚本。该脚本不仅能够模拟用户的真实浏览行为(如输入、点击),有效处理 JavaScript 渲染的动态内容,还创新性地引入了“用户协作”模式来解决登录验证难题,并通过模块化的代码设计,确保了项目的可读性、可维护性和可扩展性。
2. 需求分析
本项目的主要目标是:批量抓取百度地图上指定银行网点(或其他任何 POI)的详细信息,包括名称、地址和联系电话,并将结果保存为独立的 CSV 文件。
为实现这一目标,脚本必须满足以下具体需求:
批量处理能力: 能够接收一个包含多个搜索查询词的列表,并为每个查询词自动执行一次完整的抓取任务。
处理动态登录: 许多网站的查询功能需要用户登录后才能获得完整或准确的数据。脚本需要一种稳定、高效的方式来处理登录流程。
破解“加载更多”机制: 能够自动检测并点击搜索结果页下的“更多结果”按钮,直至所有数据被完全加载到当前页面。
智能分页导航: 在处理完当前页面的所有数据后,能够自动识别并点击下一页按钮,循环此过程直到最后一页。
健壮的数据解析: 无论页面结构如何,都能准确地从 HTML 中提取所需的目标字段。
结构化数据存储: 将抓取到的数据整理成规范的表格形式,并为每个搜索任务生成一个以查询词命名的 CSV 文件,便于后续分析。
良好的错误处理: 在遇到网络超时、元素未找到等异常情况时,脚本应能妥善处理,避免程序崩溃,并能继续执行后续任务。
3. 深度模块化剖析
我们将遵循“高内聚,低耦合”的设计原则,将整个爬虫脚本拆分为六个核心模块进行详细讲解。
3.1. 环境配置与浏览器自动启动模块
这是整个自动化流程的起点,也是本方案的创新之处。传统爬虫处理登录,要么是逆向分析加密的登录请求,过程复杂且易失效;要么是用 Selenium 模拟输入账号密码,但容易被网站的反爬虫机制(如滑块验证、短信验证)识破。
我们的策略是“人机协作”:让脚本启动一个“干净”的浏览器环境,用户在此环境中手动完成最复杂的登录操作,然后脚本再接管这个已经登录的浏览器会话。
# --- 1. Chrome 启动配置 ---
# 请根据您的实际安装路径修改
CHROME_EXE_PATH = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
# 用于自动化测试的独立Chrome用户数据文件夹路径
USER_DATA_DIR = r"C:\chrome_debug_profile"
DEBUGGING_PORT = 9222# --- 3. 构造并执行启动命令 ---
command = [CHROME_EXE_PATH,f"--remote-debugging-port={DEBUGGING_PORT}",f"--user-data-dir={USER_DATA_DIR}"
]
subprocess.Popen(command)# --- 4. 等待用户登录并确认 ---
while True:user_input = input("\n>>> 是否已完成登录并准备好开始执行自动化任务? (请输入 'W' 并按回车继续): ")if user_input.strip().upper() == 'W':break```
剖析:
CHROME_EXE_PATH
: 精确指定 Chrome 主程序的路径,确保脚本能找到并启动它。USER_DATA_DIR
: 这是一个关键参数。它让 Chrome 使用一个独立的用户配置文件夹。这样做的好处是:环境隔离:不会影响您日常使用的 Chrome 配置(书签、历史记录、插件等)。
状态保持:所有在这个窗口中产生的 Cookie 和登录状态都会被保存在这个文件夹里。下次用同样命令启动时,只要 Cookie 未过期,您可能仍然是登录状态。
--remote-debugging-port={DEBUGGING_PORT}
: 这是实现“接管”的核心。该命令会开启 Chrome 的远程调试协议(Chrome DevTools Protocol)服务,监听指定的端口(例如 9222)。这相当于给浏览器开了一个“后门”,允许其他程序(如我们的 Selenium 脚本)连接并控制它。subprocess.Popen(command)
: 使用 Python 的subprocess
模块,以非阻塞的方式在后台执行启动命令,打开新的 Chrome 窗口。while True
阻塞循环: 脚本在此处暂停,通过input()
函数等待用户的指令。这给予用户充足的时间在新打开的浏览器窗口中手动登录账号。当用户完成操作并输入 'W' 后,脚本才会继续执行,确保了后续操作是在一个已认证的会话中进行。
3.2. Selenium WebDriver 连接模块
当用户确认登录后,我们需要让 Selenium 连接到刚才由脚本启动的浏览器实例,而不是创建一个全新的、未登录的浏览器。
from selenium.webdriver.chrome.options import Options# ... 在主函数 main() 中 ...
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver = webdriver.Chrome(options=chrome_options)
print("成功连接到已打开的Chrome浏览器!")
剖析:
Options(): Selenium 的配置类,用于定制 WebDriver 的行为。
add_experimental_option("debuggerAddress", "127.0.0.1:9222"): 这是连接的魔法所在。我们告诉即将创建的 webdriver.Chrome 实例,不要去启动一个新浏览器,而是去连接已经存在于 127.0.0.1:9222 (即本机 9222 端口) 的 Chrome 调试服务。
driver = webdriver.Chrome(options=chrome_options): 此行代码执行后,driver 对象就获得了对用户已登录的那个 Chrome 窗口的完全控制权。后续所有 driver 的操作,都会实时反映在该窗口中。
3.3. 动态内容处理模块
这是应对 AJAX 动态加载的核心模块。百度地图的搜索结果列表在内容较多时,会显示一个“更多结果”的链接,点击后会通过异步请求加载更多数据项并插入到现有列表中。
def click_more_results_if_present(driver, wait):while True:try:initial_item_count = len(driver.find_elements(By.CSS_SELECTOR, "li.search-item"))more_results_link = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "li.more-result a")))driver.execute_script("arguments[0].click();", more_results_link)wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, "li.search-item")) > initial_item_count)time.sleep(1)except (NoSuchElementException, TimeoutException):print("当前页面没有 '更多结果' 按钮,内容已全部加载。")break
剖析:
while True 循环: 考虑到某些页面可能需要多次点击“加载更多”,使用无限循环是必要的。
WebDriverWait(driver, 3): 我们使用显式等待,但设置了一个较短的超时时间(3秒)。这意味着脚本会尝试寻找“更多结果”按钮,但如果3秒内找不到,就会抛出 TimeoutException,而不会长时间卡住。这是一种高效的“检查存在性”的方法。
initial_item_count: 在点击前,先记录下当前页面已有的结果数量。
driver.execute_script("arguments[0].click();", ...): 使用 JavaScript 点击,这种方式比 Selenium 的 .click() 方法在处理某些被遮挡或复杂的元素时更为稳定。
wait.until(lambda d: ...): 点击之后,最关键的一步是确认新数据已加载完成。我们再次使用显式等待,但等待的条件是一个 lambda 函数:len(d.find_elements(...)) > initial_item_count。这行代码的含义是:“持续检查,直到页面上的结果项总数大于我们点击前的数量”。这是一个非常可靠的判断数据加载完成的标志。
except (NoSuchElementException, TimeoutException): 当 WebDriverWait 在3秒内找不到按钮时,会触发这个异常。我们捕获它并认为这是“本页所有数据已加载完毕”的信号,然后通过 break 正常退出循环。
3.4. 数据解析模块
当一个页面的所有数据(包括通过点击“更多结果”加载的数据)都已呈现后,就轮到数据解析模块登场。此模块的职责是接收完整的页面 HTML 源码,并从中提取出我们需要的信息。
from bs4 import BeautifulSoupdef parse_page_data(page_source, query):soup = BeautifulSoup(page_source, 'lxml')bank_items = soup.find_all('li', class_='search-item')page_banks = []for item in bank_items:name_tag = item.select_one('a.n-blue')name = name_tag.text.strip() if name_tag else 'N/A'addr_tag = item.select_one('.addr span.n-grey')address = addr_tag.get('title', 'N/A').strip() if addr_tag else 'N/A'tel_tag = item.select_one('.tel')phone_raw = tel_tag.text.strip() if tel_tag else '电话: N/A'phone = phone_raw.replace('电话:', '').strip()bank_info = {'search_query': query, 'name': name, 'address': address, 'phone': phone}page_banks.append(bank_info)return page_banks
剖析:
BeautifulSoup(page_source, 'lxml'): 我们将 Selenium 的 driver.page_source (即当前浏览器渲染后的完整 HTML) 传给 BeautifulSoup,并使用高效的 lxml 解析器。
soup.find_all('li', class_='search-item'): 通过分析百度地图的页面结构,我们发现每个搜索结果都包含在一个
标签内,且该标签具有 class="search-item" 的属性。find_all 方法可以找到所有符合条件的结果项。
item.select_one(...): 对于每个结果项,我们使用 CSS选择器 (select_one) 来精确定位包含名称、地址和电话的子元素。CSS选择器语法灵活且强大,是解析 HTML 的首选工具。
健壮性处理: 代码中大量使用了 if name_tag else 'N/A' 这样的三元表达式。这是为了防止因某个结果项信息不全(例如没有电话)而导致 AttributeError (如对 None 对象调用 .text),从而保证了程序的稳定性。
数据结构化: 将提取出的信息存入一个字典 bank_info,并将搜索词 query 也一并存入,便于后续追溯数据来源。所有字典再添加到一个列表 page_banks 中,形成一个结构清晰的数据集合。
3.5. 主控逻辑与分页处理模块
这是整个脚本的“指挥中心”,负责调度其他模块,完成从搜索到数据抓取,再到翻页的完整流程。
def main():# ... 连接 WebDriver 的代码 ...wait = WebDriverWait(driver, 10)for query in bank_queries:# ... 导航、输入、点击搜索的代码 ...page_number = 1while True:print(f"--- 正在处理 '{query}' 的第 {page_number} 页 ---")# 优先处理“更多结果”click_more_results_if_present(driver, wait)# 解析完全加载的页面page_source = driver.page_sourcescraped_data = parse_page_data(page_source, query)current_bank_data.extend(scraped_data)# 尝试翻到下一页try:page_to_click = page_number + 1page_link_xpath = f"//div[@id='poi_page']//a[text()='{page_to_click}']"old_first_result = driver.find_element(By.CSS_SELECTOR, "ul.poilist li")next_page_link = wait.until(EC.element_to_be_clickable((By.XPATH, page_link_xpath)))driver.execute_script("arguments[0].click();", next_page_link)# 等待页面跳转完成wait.until(EC.staleness_of(old_first_result))page_number += 1time.sleep(3)except (NoSuchElementException, TimeoutException):print("未找到下一页链接,当前搜索任务完成。")break# ... 保存数据的代码 ...
剖析:
外层循环: for query in bank_queries: 遍历所有待搜索的关键词,确保任务的批量执行。
内层 while True 循环: 用于处理分页逻辑。
模块调用: 在循环内部,清晰地展示了模块间的调用顺序:先调用 click_more_results_if_present 确保当前页数据完整,再调用 parse_page_data 进行解析。
分页逻辑:
通过构造 XPath //div[@id='poi_page']//a[text()='{page_to_click}'] 来精确定位页码链接。例如,当 page_number 为 1 时,它会去寻找文本为 '2' 的分页链接。
old_first_result: 在点击下一页之前,获取当前页面列表的第一个元素。
wait.until(EC.staleness_of(old_first_result)): 这是判断翻页是否完成的绝佳方法。staleness_of 会等待,直到之前获取的那个元素在 DOM 中不再存在(或已过时)。这标志着页面已经刷新,加载了新的内容。这比简单的 time.sleep() 更可靠、更高效。
异常捕获: 当无法找到下一页的链接时(即到达最后一页),wait.until 会抛出 TimeoutException。我们捕获这个异常,并通过 break 优雅地结束当前搜索任务的循环。
3.6. 数据存储与辅助模块
此模块负责将辛勤抓取来的数据进行落地,并提供一些辅助功能。
import pandas as pd
import redef sanitize_filename(name):sanitized_name = re.sub(r'[\\/*?:"<>|]', "", name)return sanitized_name.replace(' ', '_')# ... 在 main() 函数的末尾 ...
if current_bank_data:df = pd.DataFrame(current_bank_data)safe_query_name = sanitize_filename(query)filename = f"{safe_query_name}_results.csv"df.to_csv(filename, index=False, encoding='utf-8-sig')print(f"数据已成功保存到 {filename} 文件中。")
剖析:
sanitize_filename(name): 一个非常实用的辅助函数。用户的搜索词可能包含 Windows 或其他操作系统不允许在文件名中使用的特殊字符(如 , /, * 等)。此函数使用正则表达式 re.sub 将这些非法字符替换为空,确保脚本在任何情况下都能生成合法的文件名。
pd.DataFrame(current_bank_data): 利用强大的 pandas 库,将存储字典的列表 current_bank_data 一键转换为结构化的数据框(DataFrame)。
df.to_csv(...): 将 DataFrame 导出为 CSV 文件。
index=False: 不将 DataFrame 的索引写入文件。
encoding='utf-8-sig': 这是一个非常重要的参数!使用 'utf-8-sig' 而不是普通的 'utf-8',会在文件开头写入一个 BOM (Byte Order Mark)。这能确保生成的 CSV 文件在用 Excel 打开时,能正确识别编码,不会出现中文乱码问题。
4. 难点与解决方案分析 (Challenges and Solutions)
难点 (Challenge) | 核心问题 | 解决方案 |
---|---|---|
复杂的登录验证 | 滑块、二维码、短信验证码等反爬机制让模拟登录变得异常困难且不稳定。 | “人机协作”模式 :通过远程调试端口,让脚本接管一个由用户手动登录的浏览器会话,完全绕过登录验证的技术难题。 |
AJAX 动态加载 | “更多结果”按钮点击后,数据通过异步请求加载,直接解析初始 HTML 无法获取全部数据。 | click_more_results_if_present 模块 :使用循环和智能等待机制,模拟用户点击行为,并通过比较点击前后的元素数量来确保新数据已完全加载。 |
分页导航 | 如何准确判断当前页已处理完毕,并成功跳转到下一页,以及如何识别最后一页。 | XPath 定位与 staleness_of 等待 :通过动态构造 XPath 定位页码链接。利用 EC.staleness_of 等待旧页面元素过时,作为页面成功刷新的可靠信号。通过捕获 TimeoutException 来判断已无下一页。 |
程序健壮性 | 网络波动、页面结构微调、数据项缺失等都可能导致程序意外中断。 | 全面的异常处理 :在关键操作(如元素查找、页面跳转)处使用 try...except 块。在数据解析时,对可能不存在的元素进行检查,提供默认值(如 'N/A')。 |
数据输出兼容性 | 直接保存的 CSV 文件用 Excel 打开时可能出现中文乱码。 | 指定 utf-8-sig 编码 :在调用 to_csv 方法时,明确设置 encoding='utf-8-sig',确保文件能被 Excel 正确识别和显示。 |
5. 总结与展望 (Conclusion)
本文详细设计并实现了一个功能强大且结构清晰的 Python 爬虫,成功解决了从百度地图这类高度动态化的网站上批量抓取 POI 数据的难题。通过采用“深度模块化剖析”的讲解方式,我们阐明了从浏览器启动、用户交互、动态内容处理、数据解析到最终存储的每一个环节的设计思想和技术细节。
该方案最大的亮点是“人机协作”的半自动化思想和对动态内容加载的精巧处理,这使得它在面对复杂登录和反爬机制时具有很高的实用性和稳定性。模块化的代码结构也使得该脚本易于理解、维护和扩展。例如,若要抓取不同网站或不同类型的数据,开发者只需修改 parse_page_data 模块的解析规则,以及 main 函数中的导航逻辑,而无需改动核心框架。
未来的工作可以在此基础上进行扩展,例如:
多线程/异步支持: 对于大量的搜索任务,可以引入 concurrent.futures 或 asyncio 来实现并行抓取,大幅提升效率。
代理IP池: 集成代理 IP 功能,以应对目标网站基于 IP 的访问频率限制。
图形用户界面(GUI): 使用 PyQt 或 Tkinter 为脚本封装一个简单的图形界面,让非技术人员也能方便地使用。
希望本文的剖析能为您在Web数据抓取的道路上提供有益的参考和启发。
6. 完整代码 (Full Code)
# ==============================================================================
# 模块:导入与全局配置
# ==============================================================================
import time
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException, TimeoutException
import subprocess
import os
import sys
import re# ✅ 添加这一行:定义 chromedriver 的路径(必须修改为你的实际路径!)
CHROMEDRIVER_PATH = r"D:\Program Files\chrome-win64\chromedriver.exe" # ← 修改为你的 chromedriver.exe 所在路径!
# ==============================================================================
# 模块:环境配置与浏览器自动启动
# 描述:该模块负责启动一个带远程调试端口的独立Chrome实例,并等待用户手动登录。
# ==============================================================================def launch_chrome_and_wait_for_user():"""启动 Chrome 浏览器并等待用户确认登录。"""# --- 1. Chrome 启动配置 ---# 请根据您的实际安装路径修改CHROME_EXE_PATH = r"D:\Program Files (x86)\chrome-win64\chrome.exe"# 用于自动化测试的独立Chrome用户数据文件夹路径USER_DATA_DIR = r"C:\chrome_debug_profile"DEBUGGING_PORT = 9222print("--- 自动化脚本启动 ---")# --- 2. 检查 Chrome.exe 是否存在 ---if not os.path.exists(CHROME_EXE_PATH):print(f"错误:未在指定路径找到 Chrome.exe: '{CHROME_EXE_PATH}'")print("请检查 CHROME_EXE_PATH 变量是否正确,或您的Chrome是否安装在默认位置。")sys.exit()# --- 3. 构造并执行启动命令 ---command = [CHROME_EXE_PATH,f"--remote-debugging-port={DEBUGGING_PORT}",f"--user-data-dir={USER_DATA_DIR}"]print(f"正在启动 Chrome,并开启调试端口 {DEBUGGING_PORT}...")try:subprocess.Popen(command)print("Chrome 已启动。")except Exception as e:print(f"启动 Chrome 失败: {e}")sys.exit()# --- 4. 等待用户登录并确认 ---print("\n" + "="*60)print("【请注意】一个新的 Chrome 窗口已经打开。")print("1. 请在该新窗口中,手动登录您需要操作的账号。")print("2. 登录完成后,请返回此控制台窗口。")print("="*60)while True:user_input = input("\n>>> 是否已完成登录并准备好开始执行自动化任务? (请输入 'W' 并按回车继续): ")if user_input.strip().upper() == 'W':print("用户确认完毕,脚本将继续执行...")breakelse:print("输入无效。请在登录后输入 'W' 继续。")# ==============================================================================
# 模块:数据解析 (`parse_page_data`)
# 描述:使用BeautifulSoup解析单个页面的HTML以提取银行数据。
# ==============================================================================
def parse_page_data(page_source, query):"""从页面源码中提取银行网点信息。:param page_source: 浏览器渲染后的HTML字符串。:param query: 当前的搜索关键词。:return: 一个包含多个字典的列表,每个字典代表一个银行网点信息。"""soup = BeautifulSoup(page_source, 'lxml')bank_items = soup.find_all('li', class_='search-item')page_banks = []if not bank_items:print("在当前页面上未找到银行条目。")return page_banksfor item in bank_items:name_tag = item.select_one('a.n-blue')name = name_tag.text.strip() if name_tag else 'N/A'addr_tag = item.select_one('.addr span.n-grey')address = addr_tag.get('title', 'N/A').strip() if addr_tag else 'N/A'tel_tag = item.select_one('.tel')phone_raw = tel_tag.text.strip() if tel_tag else '电话: N/A'phone = phone_raw.replace('电话:', '').strip()bank_info = {'search_query': query,'name': name,'address': address,'phone': phone}page_banks.append(bank_info)return page_banks# ==============================================================================
# 模块:数据存储与辅助 (`sanitize_filename`)
# 描述:提供辅助功能,如清理字符串以创建有效的文件名。
# ==============================================================================
def sanitize_filename(name):"""清理字符串,移除非法字符,使其成为有效的文件名。:param name: 原始字符串(通常是搜索词)。:return: 清理后的、可用作文件名的字符串。"""sanitized_name = re.sub(r'[\\/*?:"<>|]', "", name)sanitized_name = sanitized_name.replace(' ', '_')return sanitized_name# ==============================================================================
# 模块:动态内容处理 (`click_more_results_if_present`)
# 描述:检查并循环点击页面上的“更多结果”按钮,直到所有数据加载完毕。
# ==============================================================================
def click_more_results_if_present(driver, wait):"""自动检测并点击“更多结果”链接,直到该链接消失。:param driver: Selenium WebDriver实例。:param wait: WebDriverWait实例。"""while True:try:# 记录点击前列表中的项目数量initial_item_count = len(driver.find_elements(By.CSS_SELECTOR, "li.search-item"))# 等待“更多结果”链接最多3秒,如果不存在则会抛出异常more_results_link = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "li.more-result a")))print("发现 '更多结果' 按钮,正在点击...")# 使用JS点击,更稳定driver.execute_script("arguments[0].scrollIntoView(true);", more_results_link)driver.execute_script("arguments[0].click();", more_results_link)# 等待新内容加载完成的标志:列表项数量增加wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, "li.search-item")) > initial_item_count)print("'更多结果' 加载完成。")time.sleep(1) # 短暂等待,以防还有后续的JS渲染except (NoSuchElementException, TimeoutException):# 3秒内未找到按钮,说明内容已全部加载print("当前页面没有 '更多结果' 按钮,内容已全部加载。")break # 退出循环except Exception as e:print(f"点击 '更多结果' 时发生意外错误: {e}")break # 发生其他错误时也退出循环# ==============================================================================
# 模块:主控逻辑 (`main`)
# 描述:整个爬虫任务的调度中心,负责批量处理搜索、翻页和数据保存。
# ==============================================================================
def main():"""主函数,用于执行整个批量抓取任务。"""# --- 1. 配置连接已打开的 Chrome ---chrome_options = Options()chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")# --- 2. 使用 Service 指定 chromedriver 路径 ---from selenium.webdriver.chrome.service import Servicetry:service = Service(executable_path=CHROMEDRIVER_PATH)driver = webdriver.Chrome(service=service, options=chrome_options)print("✅ 成功连接到已打开的Chrome浏览器!")except Exception as e:print(f"❌ 连接到 Chrome 失败: {e}")print("可能原因:")print(" 1. chromedriver 路径不正确或文件不存在")print(" 2. chromedriver 版本与 Chrome 不匹配")print(" 3. 端口 9222 被占用或 Chrome 未正确启动调试")print(" 4. 杀毒软件/防火墙阻止了 chromedriver")sys.exit()wait = WebDriverWait(driver, 10)# --- 3. 遍历搜索任务列表 ---for query in bank_queries:print(f"\n{'=' * 50}")print(f"开始处理新的搜索任务: '{query}'")print(f"{'=' * 50}")current_bank_data = []# --- 4. 执行搜索 ---try:driver.get("https://map.baidu.com/")input_box = wait.until(EC.element_to_be_clickable((By.ID, "sole-input")))input_box.clear()input_box.send_keys(query)time.sleep(1)search_button = wait.until(EC.element_to_be_clickable((By.ID, "search-button")))search_button.click()print("已提交搜索,等待结果加载...")wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "ul.poilist li")))print("搜索结果已加载。")except TimeoutException:print(f"对于搜索词 '{query}',搜索超时或没有找到任何结果。继续下一个任务。")continueexcept Exception as e:print(f"搜索 '{query}' 时发生错误: {e}。继续下一个任务。")continue# --- 5. 处理分页与数据抓取 ---page_number = 1while True:print(f"--- 正在处理 '{query}' 的第 {page_number} 页 ---")click_more_results_if_present(driver, wait)page_source = driver.page_sourcescraped_data = parse_page_data(page_source, query)if not scraped_data and page_number == 1:print("第一页未抓取到数据,可能搜索无结果。")breakcurrent_bank_data.extend(scraped_data)# 翻页逻辑try:page_to_click = page_number + 1page_link_xpath = f"//div[@id='poi_page']//a[text()='{page_to_click}']"old_first_result = driver.find_element(By.CSS_SELECTOR, "ul.poilist li")next_page_link = wait.until(EC.element_to_be_clickable((By.XPATH, page_link_xpath)))driver.execute_script("arguments[0].click();", next_page_link)wait.until(EC.staleness_of(old_first_result))page_number += 1time.sleep(2)except (NoSuchElementException, TimeoutException):print("未找到下一页链接,当前搜索任务完成。")break# --- 6. 保存数据 ---if current_bank_data:print(f"\n--- 任务 '{query}' 完成,找到 {len(current_bank_data)} 条数据。---")df = pd.DataFrame(current_bank_data)try:safe_query_name = sanitize_filename(query)filename = f"{safe_query_name}_results.csv"df.to_csv(filename, index=False, encoding='utf-8-sig')print(f"数据已成功保存到 {filename} 文件中。")except Exception as e:print(f"为 '{query}' 保存到CSV文件时出错: {e}")else:print(f"\n--- 任务 '{query}' 完成,但未收集到任何数据 ---")print("\n--- 所有批处理任务均已完成! ---")driver.quit() # 注意:quit() 会关闭浏览器,如果不想关闭,用 close()# ==============================================================================
# 脚本入口
# ==============================================================================
if __name__ == "__main__":# 定义要批量搜索的银行名称列表bank_queries = ["工商银行",# 您可以在此添加更多搜索词]# 第一步:启动浏览器并等待用户登录launch_chrome_and_wait_for_user()# 第二步:执行主爬虫逻辑main()