1. 项目概述:为什么选择Selenium来抓取政府网站?
最近在做一个政策分析的项目,需要收集大量地方政府发布的公开政策文件。一开始我也尝试过用requests+BeautifulSoup这种经典组合,但很快就碰壁了。很多政府网站的前端交互比较复杂,政策列表页是动态加载的,翻页按钮点了没反应,或者搜索框输入后页面内容是通过JavaScript异步渲染的。用传统的静态解析方法,你拿到的HTML源码里根本就没有你想要的数据,全是空的<div>标签。这时候,就该Selenium登场了。
简单来说,Selenium是一个浏览器自动化工具。它不像requests那样只是简单地请求一个网页地址然后获取返回的文本,而是能启动一个真实的、完整的浏览器(比如Chrome),像真人一样去点击、输入、滚动页面。浏览器里JavaScript执行后生成的最终内容,Selenium都能拿到。这对于处理那些重度依赖前端框架(如Vue.js, React)或反爬机制(如动态令牌、复杂验证)的网站来说,几乎是唯一的选择。政府网站虽然数据公开,但技术栈可能比较老旧或独特,动态加载情况很常见,用Selenium能省去大量逆向分析接口的麻烦。
这个项目,我将带你从零开始,搭建一个基于Python、Selenium和ChromeDriver的爬虫,目标是批量、自动化地抓取指定政府网站上的政策文本。我会附上完整的、可运行的代码,并重点讲解其中每一步的“为什么”——为什么这么配置?为什么这样处理异常?为什么用这个选择器?这些都是我踩过坑后总结的经验。无论你是刚入门爬虫的新手,还是想了解Selenium在复杂场景下应用的老手,这篇内容都能给你直接的参考。
注意:爬虫行为必须合法合规。务必遵守目标网站的
robots.txt协议,尊重版权,严格控制访问频率,避免对目标服务器造成压力。我们的目的是技术学习与合规的数据收集。
2. 环境搭建与核心工具选型解析
工欲善其事,必先利其器。在开始写代码之前,我们需要把环境配置妥当。这里的选择背后都有其考量。
2.1 Python与Selenium库安装
首先确保你安装了Python(建议3.7及以上版本)。安装Selenium库非常简单,通过pip一行命令即可:
pip install selenium我选择Selenium而不是Puppeteer(Node.js)或Playwright,主要基于生态和上手难度。Selenium支持多种语言(Python, Java, C#等),社区庞大,遇到任何问题几乎都能找到解决方案。对于Python数据分析生态来说,Selenium获取的数据可以无缝对接Pandas,NumPy等库进行处理,流程更顺畅。
2.2 浏览器与WebDriver的选择:为什么是ChromeDriver?
Selenium需要驱动一个真实的浏览器,而ChromeDriver就是Selenium控制Chrome浏览器的“桥梁”或“遥控器”。这里有几个关键点:
- 浏览器选择:我选择Chrome,是因为它的市场占有率最高,开发者工具强大,且
ChromeDriver的更新和维护非常活跃。Firefox的GeckoDriver也是不错的选择,但考虑到一些网站对Chrome的兼容性更好,首选Chrome。 - 版本匹配:这是新手最大的坑!你必须确保你安装的
ChromeDriver版本与你电脑上已安装的Chrome浏览器主版本号完全一致。比如你的Chrome是120.0.6099.130,那么你需要下载主版本为120的ChromeDriver。 - 下载与放置:
- 官方下载:访问 ChromeDriver官网 或国内的镜像站(如淘宝NPM镜像),找到对应版本下载。
- 放置路径:有两种常用方法:
- 方法一(推荐):将下载的
chromedriver(Windows下为chromedriver.exe)文件放在一个你记得住的目录,比如D:\webdriver\。然后在代码中指定这个完整路径。 - 方法二:将其放在
Python安装目录下的Scripts文件夹(Windows)或/usr/local/bin(Mac/Linux)中,这样系统环境变量PATH会自动找到它,代码中无需指定路径。
- 方法一(推荐):将下载的
实操心得:我强烈推荐方法一。因为当你电脑上的Chrome自动更新后,浏览器版本变了,你需要重新下载匹配的
ChromeDriver。如果放在固定目录,你只需要替换该目录下的文件即可,管理起来更清晰,也避免影响其他可能依赖全局ChromeDriver的工具。
2.3 IDE的选择:VSCode配置
写Python代码,一个好用的编辑器能事半功倍。VSCode轻量、免费、插件生态丰富,是我的首选。你需要安装Python扩展(由Microsoft发布),它提供了代码高亮、智能提示、调试、 linting等功能。配置好Python解释器路径后,你就可以愉快地编码了。相比一些重型IDE,VSCode对爬虫这种脚本类项目的快速编写和调试支持得更好。
3. 爬虫核心思路与架构设计
在动手写代码前,我们先在脑子里把整个爬虫的流程过一遍。一个健壮的、用于生产环境的爬虫,不应该只是一个简单的脚本,而应该是一个有清晰结构和异常处理能力的程序。
3.1 目标分析:政府网站常见结构
政府政策发布页面通常有比较规整的结构,这是我们爬虫能够成功的前提。一般流程是:
- 门户/首页:找到“政务公开”、“政策文件”、“通知公告”等栏目入口。
- 列表页:栏目点进去后,是一个分页的政策列表。每一条记录包含政策标题、发布日期、文号等,以及最重要的——详情页链接。
- 详情页:点击列表项进入,这里包含了政策的完整正文文本、发文机关、附件等核心信息。
我们的爬虫任务就是:模拟人工操作,从列表页开始,遍历所有分页,提取每一个详情页的链接,然后逐个访问详情页,抓取并保存所需的文本信息。
3.2 技术难点与应对策略
- 动态加载:列表页的“下一页”按钮可能触发AJAX请求,直接点击无效。策略:使用
Selenium的点击操作,等待页面内容更新。 - 反爬机制:一些网站可能有简单的反爬,如检测
WebDriver特征、要求登录等。策略:为ChromeDriver添加options参数来隐藏自动化特征;对于需要登录的,先用Selenium模拟登录并管理cookies。 - 网络不稳定与元素定位失败:这是爬虫运行时最常见的问题。策略:加入显式等待(
WebDriverWait),而不是死板的sleep;使用更健壮的元素选择方式(如CSS Selector,XPath);完善的try-except异常处理。 - 数据存储:抓取的数据需要结构化保存。策略:根据数据量选择,小项目可以用
CSV或JSON,大项目建议用数据库(如SQLite,MySQL)。
3.3 代码架构设计
我们将代码模块化,使其更易读、易维护:
main.py:主程序入口,控制整个流程。config.py:配置文件,存放URL、等待时间、保存路径等常量。browser.py:封装浏览器启动、关闭和基本操作函数。crawler.py:核心爬取逻辑,包含列表页遍历和详情页解析。utils.py:工具函数,如保存数据、清理文本、日志记录。
下面,我们就进入最核心的实操环节。
4. 实战代码拆解:从启动浏览器到数据落地
让我们一段代码一段代码地看,理解每一行背后的意图。
4.1 初始化浏览器驱动与基础配置
首先,我们在browser.py中创建一个函数来启动并配置浏览器。
# browser.py from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options import time def init_browser(driver_path): """ 初始化并返回一个配置好的Chrome浏览器驱动实例 :param driver_path: ChromeDriver可执行文件的完整路径 :return: WebDriver 实例 """ chrome_options = Options() # 1. 添加常用选项 # 避免浏览器窗口弹出,在后台运行(调试时可注释掉) # chrome_options.add_argument('--headless') # 禁用GPU加速,避免一些潜在问题 chrome_options.add_argument('--disable-gpu') # 禁用浏览器正在被自动化程序控制的提示栏 chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) # 2. 隐藏WebDriver特征(关键反反爬措施) # 通过CDP(Chrome DevTools Protocol)执行脚本,覆盖navigator.webdriver属性 chrome_options.add_argument("--disable-blink-features=AutomationControlled") # 3. 创建Service对象并启动浏览器 service = Service(executable_path=driver_path) driver = webdriver.Chrome(service=service, options=chrome_options) # 4. 进一步执行CDP命令,确保webdriver属性被覆盖 driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' }) # 设置隐式等待(全局等待元素出现的超时时间) driver.implicitly_wait(10) # 单位:秒 return driver代码解读与注意事项:
--headless:无头模式,不显示浏览器GUI,节省资源。但在开发调试阶段,务必注释掉这行,以便观察浏览器实际操作情况,排查问题。excludeSwitches和useAutomationExtension:这两个实验性选项是去除Chrome顶部“正受到自动测试软件控制”提示的标准做法。- 隐藏WebDriver特征:这是避免被简单反爬识别为机器人的关键。我们通过
add_argument和execute_cdp_cmd两种方式双管齐下,修改浏览器环境变量,让网站检测不到Selenium的痕迹。 Service对象:这是新版本Selenium(>=4.6.0)推荐的方式,比旧版直接指定executable_path更规范。implicitly_wait:隐式等待。它告诉driver,在查找任何一个元素时,如果立即没找到,就等待一段时间(这里10秒),期间会持续尝试查找。这比固定的time.sleep()更智能。
4.2 列表页遍历:如何智能翻页与链接提取
假设我们要爬取的政府政策列表页URL是:http://www.example.gov.cn/zwgk/zcfg/index.html。列表页的翻页按钮可能是“下一页”,也可能是数字页码。
# crawler.py from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from browser import init_browser import time def crawl_list_page(base_url, driver, max_pages=10): """ 爬取政策列表页,收集所有详情页的链接 :param base_url: 列表页首页URL :param driver: 浏览器驱动实例 :param max_pages: 最大爬取页数,防止意外无限循环 :return: 详情页链接列表 """ detail_links = [] current_page = 1 driver.get(base_url) while current_page <= max_pages: print(f"正在爬取第 {current_page} 页列表...") # --- 核心:等待列表内容加载完成 --- # 这里需要根据目标网站的实际HTML结构来修改选择器 # 假设列表项的容器是 <ul class='policy-list'>,每一项是<li> try: list_container = WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, "ul.policy-list")) ) except Exception as e: print(f"第{current_page}页列表容器加载失败: {e}") break # 或进行其他错误处理 # --- 提取当前页所有详情链接 --- # 假设每个列表项里,详情页链接在 <a class='detail-link'> 标签的href属性里 link_elements = list_container.find_elements(By.CSS_SELECTOR, "li a.detail-link") for link_element in link_elements: href = link_element.get_attribute('href') if href and href not in detail_links: # 去重 detail_links.append(href) print(f" 找到链接: {href}") # --- 尝试翻到下一页 --- current_page += 1 if current_page > max_pages: break # 查找“下一页”按钮。这里需要根据网站实际情况调整。 # 情况1:有明确的“下一页”按钮 next_button = driver.find_elements(By.CSS_SELECTOR, "a.next-page") # 情况2:是数字页码,需要计算或点击特定页码 # next_button = driver.find_elements(By.XPATH, f"//a[text()='{current_page}']") if next_button: try: # 滚动到元素位置,确保其可点击 driver.execute_script("arguments[0].scrollIntoView(true);", next_button[0]) time.sleep(0.5) # 短暂停顿,让滚动生效 # 使用JavaScript点击,有时比click()更稳定 driver.execute_script("arguments[0].click();", next_button[0]) # 等待下一页列表加载(通过判断旧元素是否失效或新元素出现) WebDriverWait(driver, 15).until( EC.staleness_of(list_container) # 等待旧的列表容器失效 ) print("成功翻页。") time.sleep(2) # 翻页后额外等待,确保页面稳定 except Exception as e: print(f"翻页到第{current_page}页时失败: {e}") break # 翻页失败,退出循环 else: print("未找到下一页按钮,可能已到最后一页。") break print(f"列表页爬取结束,共收集到 {len(detail_links)} 个详情页链接。") return detail_links关键技巧解析:
- 显式等待(WebDriverWait):这是
Selenium编程的黄金法则。presence_of_element_located等待元素出现在DOM树中,staleness_of等待某个元素从DOM中移除(常用于判断页面已跳转)。这比隐式等待更精确,能极大提高爬虫的稳定性和速度。 - 元素选择器(CSS Selector / XPath):如何找到页面上的元素?打开目标网站,按F12进入开发者工具,使用“检查”功能点选元素,在
Elements面板中右键该元素,选择Copy -> Copy selector或Copy -> Copy XPath。CSS Selector通常更简洁高效,优先使用;XPath功能更强大,但表达式可能更复杂。 - JavaScript点击:有些网站对
Selenium的.click()方法有防范,或者元素被遮挡。这时用driver.execute_script(“arguments[0].click();”, element)直接执行JavaScript点击事件,成功率更高。 - 翻页策略:代码中展示了两种常见翻页方式的处理。关键在于翻页后要有一个明确的等待条件,确保新页面的内容已经加载完成,否则后续操作会定位到旧页面的元素导致失败。
EC.staleness_of(list_container)是一个巧妙的判断。
4.3 详情页解析:精准提取政策正文与元数据
拿到详情页链接后,我们需要进入每个页面,提取标题、发布日期、正文等。
# crawler.py def parse_detail_page(url, driver): """ 解析单个政策详情页,提取所需信息 :param url: 详情页地址 :param driver: 浏览器驱动实例 :return: 包含政策信息的字典,解析失败返回None """ print(f"正在解析: {url}") try: driver.get(url) # 等待详情页核心内容区域加载 # 假设标题在<h1 class='policy-title'>里 title_element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "h1.policy-title")) ) title = title_element.text.strip() # 假设发布日期在 <span class='publish-date'> 里 date_element = driver.find_element(By.CSS_SELECTOR, "span.publish-date") publish_date = date_element.text.strip() if date_element else "未知日期" # 假设正文在 <div class='policy-content'> 里 # 注意:有些正文可能包含在多个子标签或iframe中,需要具体情况具体分析 content_element = driver.find_element(By.CSS_SELECTOR, "div.policy-content") # 获取元素内的所有文本,包括子标签的文本 full_text = content_element.text.strip() # 或者获取原始HTML,但需要进一步清理 # full_html = content_element.get_attribute('outerHTML') # 清理文本:去除多余的空行、空格 cleaned_text = '\n'.join([line.strip() for line in full_text.split('\n') if line.strip()]) policy_info = { 'url': url, 'title': title, 'publish_date': publish_date, 'content': cleaned_text, 'crawl_time': time.strftime('%Y-%m-%d %H:%M:%S') } return policy_info except Exception as e: print(f"解析详情页 {url} 时发生错误: {e}") # 可以在这里截图,方便后期排查 # driver.save_screenshot(f'error_screenshot_{int(time.time())}.png') return None数据提取的深度技巧:
.textvs.get_attribute(‘innerHTML/outerHTML’):.text获取的是渲染后用户可见的纯文本,通常就是我们需要的。.get_attribute(‘innerHTML’)获取的是元素内部的HTML源码,如果正文里包含复杂的标签结构(如表格、图片描述),用.text可能会丢失格式或顺序,这时可能需要解析HTML。- 处理富文本:如果正文里包含图片、表格、附件链接,你可能需要额外处理。例如,提取所有
<img>的src属性下载图片,或者解析<table>标签来结构化表格数据。 - 文本清洗:从网页抓取的文本常常包含大量换行、空格、不可见字符。简单的清洗如代码所示,复杂的可能需要用到正则表达式(
re库)。
4.4 数据存储与主流程调度
最后,我们将所有功能串联起来,并保存数据。
# main.py import json from browser import init_browser from crawler import crawl_list_page, parse_detail_page from config import DRIVER_PATH, LIST_URL, MAX_PAGES, OUTPUT_FILE def main(): # 1. 初始化浏览器 print("正在启动浏览器...") driver = init_browser(DRIVER_PATH) all_policies = [] try: # 2. 爬取列表页,获取所有详情链接 print("开始爬取政策列表...") detail_urls = crawl_list_page(LIST_URL, driver, max_pages=MAX_PAGES) if not detail_urls: print("未获取到任何详情页链接,程序退出。") return # 3. 遍历详情页,解析并保存数据 print(f"开始解析 {len(detail_urls)} 个详情页...") for i, url in enumerate(detail_urls, 1): print(f"[{i}/{len(detail_urls)}]") policy_data = parse_detail_page(url, driver) if policy_data: all_policies.append(policy_data) # 礼貌性延迟,减轻服务器压力 time.sleep(1) # 4. 保存数据到JSON文件 print("爬取完成,正在保存数据...") with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: # ensure_ascii=False 确保中文正常显示,indent=2 美化格式 json.dump(all_policies, f, ensure_ascii=False, indent=2) print(f"数据已成功保存至 {OUTPUT_FILE},共 {len(all_policies)} 条政策。") # 也可以选择保存为CSV,方便用Excel打开 # import pandas as pd # df = pd.DataFrame(all_policies) # df.to_csv(OUTPUT_CSV_FILE, index=False, encoding='utf-8-sig') except KeyboardInterrupt: print("\n用户中断程序。") except Exception as e: print(f"主程序运行出错: {e}") finally: # 5. 无论如何,最后都要关闭浏览器 print("正在关闭浏览器...") driver.quit() print("程序结束。") if __name__ == "__main__": main()配置文件示例 (config.py):
# config.py # ChromeDriver路径(请根据你的实际路径修改) DRIVER_PATH = r"D:\webdriver\chromedriver.exe" # 政策列表页起始URL LIST_URL = "http://www.example.gov.cn/zwgk/zcfg/index.html" # 最大爬取页数 MAX_PAGES = 5 # 输出文件 OUTPUT_FILE = "policies.json" # 请求间隔时间(秒),用于控制访问频率 REQUEST_INTERVAL = 25. 进阶优化与反爬对抗策略
基础的爬虫写好了,但在实际运行中,你可能会遇到各种问题。下面分享一些进阶的优化和应对策略。
5.1 应对检测与反爬
- User-Agent轮换:虽然
Selenium启动的浏览器有默认UA,但固定UA容易被识别。可以定期更换。from fake_useragent import UserAgent ua = UserAgent() chrome_options.add_argument(f'user-agent={ua.random}') - IP代理池:如果网站有IP访问频率限制,你需要使用代理IP。
Selenium配置代理:
注意:免费代理大多不稳定,商业项目建议使用付费代理服务,并实现IP自动切换和失效检测的逻辑。chrome_options.add_argument('--proxy-server=http://your-proxy-ip:port') - 行为模拟:完全模拟人的操作节奏,包括随机延迟、鼠标移动轨迹模拟等。可以使用
ActionChains来模拟更复杂的鼠标操作。from selenium.webdriver.common.action_chains import ActionChains element = driver.find_element(...) actions = ActionChains(driver) actions.move_to_element(element).pause(1).click().perform() - 验证码处理:遇到验证码是最头疼的。简单图形验证码可以尝试用OCR库(如
ddddocr,pytesseract)识别,但成功率有限。复杂验证码(如点选、滑动)通常需要接入打码平台(人工或AI识别),或者考虑是否触及了网站的数据获取边界,应停止爬取。
5.2 提升爬虫的健壮性
- 完善的日志系统:不要只用
print。使用logging模块记录不同级别(INFO, WARNING, ERROR)的日志,方便事后排查问题。 - 断点续爬:爬取大量数据时,程序可能因网络、异常而中断。可以将已成功爬取的URL列表保存下来(如存到文件或数据库),下次启动时先加载这个列表,跳过已爬取的URL。
- 异步与并发:
Selenium本身是同步的,每个浏览器实例资源消耗大。对于大量详情页的抓取,可以考虑使用threading或multiprocessing启动多个浏览器实例并行工作,但务必严格控制并发数,避免被封IP。更高级的方案是使用Selenium Grid进行分布式爬取。
5.3 资源管理与效率优化
- 浏览器资源回收:确保在
finally块或异常处理中调用driver.quit(),彻底关闭浏览器进程,防止内存泄漏。 - 复用浏览器会话:如果网站需要登录,登录成功后可以将
cookies保存下来,下次启动时加载cookies,避免重复登录。 - 禁用不必要的加载:可以配置
ChromeOptions来禁止图片、CSS、字体等资源的加载,大幅提升页面加载速度,因为我们只关心文本。prefs = {"profile.managed_default_content_settings.images": 2} chrome_options.add_experimental_option("prefs", prefs)
6. 常见问题排查与实战心得
这里记录了我爬取各类网站,特别是政府网站时,遇到的一些典型问题和解决方法。
6.1 元素定位失败(NoSuchElementException)
这是最常遇到的错误。
- 可能原因1:页面未加载完。解决方案:在操作元素前,增加显式等待(
WebDriverWait)。 - 可能原因2:元素在iframe或shadow DOM内。解决方案:需要先切换到对应的
iframe(driver.switch_to.frame(frame_element)),或使用JavaScript穿透shadow root。 - 可能原因3:选择器写错了或页面结构变了。解决方案:重新用开发者工具检查元素,更新选择器。尽量使用相对稳定、有语义化的
class或id,避免使用绝对XPath(如/html/body/div[3]/div[2]/...)。 - 可能原因4:元素被遮挡或不可见。解决方案:尝试先滚动到元素位置 (
scrollIntoView),或使用JavaScript点击。
6.2 点击/交互无效
点击了按钮但没反应。
- 可能原因1:点错了元素。有些按钮可能由多个重叠的
<div>组成,你点在了没有绑定事件的父层上。解决方案:在开发者工具的Elements面板仔细检查,找到真正绑定onclick事件的元素。 - 可能原因2:页面有遮罩层(如弹窗、加载动画)。解决方案:等待遮罩层消失后再操作,或者直接执行JavaScript点击底层元素。
- 可能原因3:网站拦截了
Selenium的click()事件。解决方案:优先使用driver.execute_script(“arguments[0].click();”, element)。
6.3 爬取速度慢
- 优化等待策略:减少固定的
time.sleep,多用智能的显式等待。将全局的implicitly_wait时间设短一点(如5秒),在关键操作处使用显式等待。 - 启用无头模式:生产环境运行时,加上
--headless参数。 - 禁用媒体加载:如上文所述,通过
prefs设置禁止加载图片、视频等。 - 评估并发可行性:在遵守
robots.txt和不对服务器造成压力的前提下,可谨慎使用多线程。
6.4 被网站屏蔽或跳转到验证页
- 检查
ChromeDriver特征隐藏是否生效:访问https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html等测试页面,查看WebDriver属性是否被成功覆盖。 - 降低访问频率:在请求间增加更长的、随机的延迟(如
time.sleep(random.uniform(2, 5)))。 - 检查请求头:确保
User-Agent是正常的浏览器UA。 - 终极方案:如果对方网站反爬极其严格,可能需要考虑使用更底层的请求库(如
requests)配合逆向工程分析其数据接口(AJAX请求),这比模拟浏览器更高效且隐蔽,但技术难度也更高。
最后,也是最重要的心得:爬虫的本质是模拟合法用户的访问。保持礼貌(低频率、非高峰时段)、遵守规则(robots.txt)、只获取公开数据,是项目能长期稳定运行的基础。在开始大规模爬取前,最好先手动浏览一下目标网站,了解其结构和规律,并用小规模测试验证你的爬虫脚本是否工作正常。希望这份详细的指南和代码能帮助你顺利完成政府政策数据的抓取任务。