# 2009-2022专利数据一键归档:Python自动化爬取+Excel整理全流程
最近在帮一个做市场研究的朋友处理一份长达十几年的专利统计分析报告,他需要整理2009年到2022年每年的专利申请、授权、有效和行政执法数据。手动一年一年点开网页,再一张一张表格下载,光是想想就让人头皮发麻。这种重复性高、规律性强的数据收集工作,正是自动化脚本大显身手的绝佳场景。
我花了点时间,用Python搭建了一套混合技术栈的自动化方案,核心思路是:先用轻量级的Requests库配合lxml解析器,快速遍历和定位所有目标数据页面的链接;然后启动Selenium控制的浏览器,模拟真实用户点击“下载Excel”按钮的动作,将文件自动保存到本地预先规划好的文件夹结构中。整个过程从网页遍历、链接生成、自动下载到本地文件分类归档,形成了一条完整的流水线。对于从事市场分析、行业研究、政策评估的朋友来说,这套方法能直接将数天甚至数周的手工劳动压缩到几十分钟内完成,解放出来的时间可以更专注于数据本身的分析与洞察。
下面,我就把这套方案的详细设计思路、关键代码实现、以及我在实践中踩过的坑和优化技巧,毫无保留地分享出来。无论你是刚接触Python爬虫的新手,还是希望优化现有数据收集流程的老手,相信都能从中获得启发。
## 1. 项目规划与环境搭建
在动手写代码之前,清晰的规划能避免后期大量返工。我们的核心目标是:自动获取2009年至2022年,每年四个类别(专利申请受理、授权、有效、行政执法)下的所有统计表格,并以Excel格式下载,按“年份/类别”的目录结构本地存储。
### 1.1 技术栈选择与核心思路
面对一个结构规整的政府统计数据网站,我们有两种主流的技术路径:
* **路径A:直接解析HTML表格并写入Excel。** 即用Requests获取页面HTML源码,用XPath或BeautifulSoup解析出`<table>`标签内的所有数据,再使用`pandas`或`openpyxl`库重新组装并写入Excel文件。这种方法看似直接,但实际挑战巨大:表格结构可能每年微调,合并单元格处理繁琐,数据清洗工作量惊人,代码健壮性差。
* **路径B:模拟点击“下载Excel”按钮。** 我观察到目标网站每个数据表格页面底部,通常都提供了一个“下载”或“导出为Excel”的官方按钮。这个按钮背后链接着一个已经生成好的、格式规范的Excel文件。我们的任务就简化为:找到所有包含这个按钮的页面,然后让程序去“点击”它。
**毫无疑问,路径B是更优解。** 它直接获取官方打包好的数据文件,格式统一,无需二次清洗,成功率极高。这引出了我们的混合技术栈:
1. **Requests + lxml:** 负责高效的“侦察兵”工作。快速发起HTTP请求,获取目录页面的HTML,并用lxml的XPath解析出所有目标子页面的URL列表。这一步不涉及浏览器渲染,速度极快。
2. **Selenium:** 负责复杂的“交互兵”工作。启动一个可控的浏览器(如Chrome),加载每一个目标子页面,定位到“下载Excel”按钮元素,并执行点击操作。它能完美处理需要JavaScript渲染或模拟用户交互的场景。
### 1.2 环境准备与依赖安装
首先,确保你的电脑上安装了Python(建议3.7及以上版本)。然后,我们通过pip安装所需的库。
打开你的终端或命令提示符,逐行执行以下命令:
```bash
# 安装HTTP请求库和解析库
pip install requests lxml
# 安装浏览器自动化工具
pip install selenium
# 可选但强烈推荐:用于更优雅地处理数据(虽然本项目不直接用于解析,但对后续分析有益)
pip install pandas openpyxl
```
安装Selenium后,你还需要下载对应的浏览器驱动。以最常用的Chrome浏览器为例:
1. 查看你电脑上Chrome浏览器的版本(在浏览器地址栏输入 `chrome://settings/help`)。
2. 访问 [ChromeDriver官网](https://chromedriver.chromium.org/) 或国内镜像站,下载与你的Chrome版本号匹配的`chromedriver`。
3. 将下载的`chromedriver.exe`文件放在一个你记得住的路径下,例如 `C:\WebDriver\bin\`。**记住这个路径,后续代码中需要指定。**
> **注意:** 驱动版本必须与浏览器版本匹配,否则Selenium无法启动浏览器。这是一个常见的报错点。
## 2. 核心逻辑拆解:链接发现与规律总结
任何自动化爬取的第一步,都是人工分析目标网站的结构和URL规律。我们假设目标数据网站的基址结构是固定的。
### 2.1 分析URL构成规律
通过手动浏览2009、2010等不同年份的页面,我们很可能发现URL模式如下:
* **年度目录页:** `https://example.com/tjxx/jianbao/year2018/a.html`
* 其中 `year2018` 代表年份,`a.html` 代表类别(a=专利申请,b=授权,c=有效,h=行政执法)。
* **具体数据表格页:** `https://example.com/tjxx/jianbao/year2018/a/a1.html`
* 在类别目录下,有 `a1.html`, `a2.html` ... 等多个具体表格页面。
基于这个观察,我们可以用程序批量生成所有需要访问的页面URL,而无需先去抓取一个索引页。这大大简化了逻辑。
```python
base_url_template = "https://example.com/tjxx/jianbao/year{year}/{category}.html"
detail_url_template = "https://example.com/tjxx/jianbao/year{year}/{category}/{category}{table_index}.html"
# 示例:生成2018年专利申请(a)的第一个表格链接
year = 2018
category = 'a'
table_index = 1
detail_url = detail_url_template.format(year=year, category=category, table_index=table_index)
print(detail_url) # 输出: https://example.com/tjxx/jianbao/year2018/a/a1.html
```
### 2.2 使用Requests+lxml侦察页面结构
虽然我们可以按规律生成链接,但更稳妥的做法是,先访问年度目录页,确认该年份下某个类别到底有多少个表格页面。这可以防止因网站结构调整(某年缺少某个表格)而导致程序生成无效链接报错。
我们写一个函数来完成这件事:
```python
import requests
from lxml import etree
def get_table_count(year, category):
"""
获取指定年份和类别下的表格数量
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
category_url = base_url_template.format(year=year, category=category)
try:
resp = requests.get(category_url, headers=headers, timeout=10)
resp.raise_for_status() # 检查请求是否成功
resp.encoding = resp.apparent_encoding # 自动识别编码
html = etree.HTML(resp.text)
# 假设表格链接都在一个class为'table-list'的ul的li标签内
# 这是一个示例XPath,需要根据实际网站结构调整
table_links = html.xpath('//ul[@class="table-list"]/li/a/@href')
# 或者,如果链接文本有规律,可以直接计数
# 这里假设我们通过其他方式知道了数量,例如页面上的统计信息
# 实际项目中,这里需要你根据真实网页结构编写准确的XPath
# 为了示例,我们假设每个类别固定有15个表格
# 真实情况请替换为你的解析逻辑
estimated_count = 15
print(f"年份{year}, 类别{category},探测到约{estimated_count}个表格。")
return estimated_count
except requests.RequestException as e:
print(f"请求{category_url}失败: {e}")
return 0
```
这个函数体现了**防御性编程**的思想:添加异常处理、检查HTTP状态码、设置超时。在实际项目中,你需要使用浏览器的开发者工具(F12)仔细查看目录页的HTML结构,并编写出能精准定位表格链接数量的XPath表达式。
## 3. Selenium自动化下载与文件管理
获取到所有目标页面的URL列表后,就进入了下载环节。这里我们使用Selenium来模拟点击。
### 3.1 配置Selenium浏览器选项
为了让下载过程更高效、更符合我们的归档需求,需要对Selenium启动的Chrome浏览器进行一系列配置。
```python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import os
def setup_chrome_driver(download_dir):
"""
配置并返回一个设置好下载选项的Chrome WebDriver实例
"""
chrome_options = Options()
# 1. 设置默认下载目录(最关键的一步)
prefs = {
"download.default_directory": download_dir, # 设置默认下载路径
"download.prompt_for_download": False, # 下载时不弹出提示框
"download.directory_upgrade": True, # 启用目录升级
"safebrowsing.enabled": True # 安全浏览,可选
}
chrome_options.add_experimental_option("prefs", prefs)
# 2. 启用无头模式(不显示浏览器界面,节省资源)
chrome_options.add_argument('--headless=new') # 新版本Chrome推荐写法
# 3. 其他常用优化参数
chrome_options.add_argument('--disable-gpu') # 禁用GPU加速,在某些环境下更稳定
chrome_options.add_argument('--no-sandbox') # 在Linux服务器或某些Docker环境下可能需要
chrome_options.add_argument('--disable-dev-shm-usage') # 解决共享内存问题
chrome_options.add_argument('--window-size=1920,1080') # 设置浏览器窗口大小
# 指定chromedriver路径
driver_path = r'C:\WebDriver\bin\chromedriver.exe' # 请修改为你的实际路径
# 对于Mac/Linux,可能是 /usr/local/bin/chromedriver
driver = webdriver.Chrome(executable_path=driver_path, options=chrome_options)
return driver
```
**关键点解析:**
* `download.default_directory`: 确保文件直接下载到我们指定的文件夹,而不是系统默认的“下载”目录。
* `--headless`: 无头模式让程序在后台运行,不需要图形界面,非常适合服务器或专注执行任务的环境。
* **路径问题:** `driver_path` 必须指向你下载的`chromedriver`可执行文件。也可以将其所在目录添加到系统PATH环境变量中,这样只需 `webdriver.Chrome()` 即可。
### 3.2 定位下载按钮并触发点击
每个数据表格页面的结构可能不同,但下载按钮通常有规律可循。同样,使用开发者工具查看按钮的HTML属性。
```python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
def download_single_table(driver, table_url, max_wait=30):
"""
访问单个表格页面并触发下载
"""
driver.get(table_url)
# 等待页面加载完成,特别是等待下载按钮出现
try:
# 示例:假设下载按钮的ID是'exportExcelBtn'
# 使用显式等待,更智能
download_button = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "exportExcelBtn"))
)
# 如果按钮是链接,也可能是By.LINK_TEXT或By.PARTIAL_LINK_TEXT
# 例如: driver.find_element(By.LINK_TEXT, "下载Excel")
# 滚动到按钮位置,确保其可见(某些页面可能需要)
driver.execute_script("arguments[0].scrollIntoView();", download_button)
time.sleep(1) # 短暂停顿
# 点击按钮
download_button.click()
print(f"已触发下载: {table_url}")
# 等待一段时间,确保下载对话框处理完毕(如果无头模式设置正确,应自动开始下载)
time.sleep(3)
except Exception as e:
print(f"在页面 {table_url} 上定位或点击下载按钮时出错: {e}")
# 可以在这里截图,便于调试
# driver.save_screenshot('error_screenshot.png')
```
**为什么用 `WebDriverWait`?**
网络有延迟,页面元素加载需要时间。`WebDriverWait` 会周期性地检查条件(如元素是否存在),直到它满足或超时。这比简单的 `time.sleep(固定秒数)` 更高效、更可靠。
### 3.3 构建自动化归档主流程
现在,我们将所有模块组合起来,形成完整的主程序逻辑。
```python
import os
def main():
base_dir = "./专利统计数据_2009_2022" # 总根目录
os.makedirs(base_dir, exist_ok=True) # 创建根目录,exist_ok=True表示如果已存在则不报错
categories = {'a': '专利申请受理状况', 'b': '专利申请授权情况', 'c': '专利有效状况', 'h': '专利行政执法状况'}
for year in range(2009, 2023): # range(2009, 2023) 包含2009,不包含2023
year_dir = os.path.join(base_dir, str(year))
os.makedirs(year_dir, exist_ok=True)
for cat_code, cat_name in categories.items():
# 为每个类别创建子目录
category_dir = os.path.join(year_dir, cat_name)
os.makedirs(category_dir, exist_ok=True)
print(f"\n开始处理 {year}年 - {cat_name}...")
# 步骤1:获取该类别下的表格数量(使用之前定义的函数)
table_count = get_table_count(year, cat_code)
if table_count == 0:
print(f" 跳过,未找到表格。")
continue
# 步骤2:为该类别初始化一个浏览器实例,并设置下载目录
driver = setup_chrome_driver(category_dir)
# 步骤3:遍历每个表格页面进行下载
for idx in range(1, table_count + 1):
table_url = detail_url_template.format(year=year, category=cat_code, table_index=idx)
print(f" 正在处理表格 {idx}/{table_count}: {table_url}")
download_single_table(driver, table_url)
# 短暂间隔,避免请求过于频繁
time.sleep(1)
# 步骤4:处理完一个类别后,关闭浏览器释放资源
driver.quit()
print(f" 完成 {year}年 - {cat_name} 的下载。")
print("\n全部任务完成!数据已按年份和类别归档至:", os.path.abspath(base_dir))
if __name__ == '__main__':
main()
```
这个主流程清晰地体现了“年份 -> 类别 -> 单个表格”的三层循环结构,并在内存和资源管理上做了优化:**为每个类别单独启动和关闭一个浏览器实例**。这样做虽然稍微增加了一点开销,但避免了长时间运行单个浏览器实例可能遇到的内存泄漏或页面状态累积问题,使程序更稳定。
## 4. 高级技巧与实战问题排查
在实际运行中,你几乎一定会遇到各种预料之外的情况。下面分享几个我踩过坑后总结的进阶技巧。
### 4.1 处理动态加载与反爬策略
现代网站越来越多地使用JavaScript动态加载内容。如果`Requests`获取的HTML里找不到表格链接,但浏览器里能看到,说明数据是动态加载的。
**解决方案:**
1. **继续使用Selenium:** 对于这类页面,直接从目录页开始就用Selenium加载,然后用Selenium的`find_element`方法来获取链接。这会慢一些,但更可靠。
2. **分析网络请求(推荐):** 打开浏览器的开发者工具,切换到“Network”(网络)选项卡,刷新页面,观察有哪些XHR/Fetch请求。很可能表格列表是通过一个API接口(返回JSON数据)获取的。直接模拟请求这个API接口,效率远高于使用Selenium渲染整个页面。
```python
# 假设发现获取列表的API接口
import json
api_url = "https://example.com/api/getTableList?year=2018&type=a"
headers = {'X-Requested-With': 'XMLHttpRequest'} # 有时需要这个头
resp = requests.get(api_url, headers=headers)
table_list = json.loads(resp.text) # 假设返回JSON
# 从table_list中解析出真正的表格页面路径
```
### 4.2 文件下载确认与重命名
Selenium点击下载按钮后,如何知道文件是否真的下载完成了?特别是无头模式下。
**策略:**
1. **监听下载目录:** 在点击下载后,程序可以轮询指定的下载目录,检查是否有新的`.xlsx`或`.xls`文件出现,并检查其文件大小是否在短时间内不再变化(表示下载完成)。
2. **自定义文件名:** 网站提供的Excel文件名可能是无意义的(如`download.xlsx`)。我们可以在下载前,通过解析页面标题或表格标题,为其生成一个更有意义的文件名。但这通常需要更复杂的交互,例如在点击前先获取标题文本。
```python
import os
import time
def wait_for_download_complete(download_dir, expected_filename=None, timeout=60):
"""
等待下载目录中出现新文件并稳定下来
"""
if expected_filename:
target_file = os.path.join(download_dir, expected_filename)
else:
# 如果不知道具体文件名,就找最新的文件
initial_files = set(os.listdir(download_dir))
start_time = time.time()
while time.time() - start_time < timeout:
time.sleep(2)
if expected_filename:
if os.path.exists(target_file):
# 检查文件大小是否稳定
size1 = os.path.getsize(target_file)
time.sleep(1)
size2 = os.path.getsize(target_file)
if size1 == size2 and size1 > 0:
return target_file
else:
current_files = set(os.listdir(download_dir))
new_files = current_files - initial_files
xlsx_files = [f for f in new_files if f.endswith(('.xlsx', '.xls', '.csv'))]
if xlsx_files:
# 假设只有一个新文件
latest_file = os.path.join(download_dir, xlsx_files[0])
# 同样检查文件大小稳定性
size1 = os.path.getsize(latest_file)
time.sleep(1)
size2 = os.path.getsize(latest_file)
if size1 == size2 and size1 > 0:
return latest_file
print("文件下载等待超时。")
return None
```
### 4.3 错误处理与日志记录
一个健壮的自动化脚本必须能处理网络波动、页面结构微调、元素缺失等异常,并记录下发生了什么。
```python
import logging
from datetime import datetime
# 配置日志
log_filename = f"patent_data_crawler_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_filename, encoding='utf-8'),
logging.StreamHandler() # 同时输出到控制台
]
)
def robust_download(driver, table_url, retries=3):
"""
带有重试机制的下载函数
"""
for attempt in range(retries):
try:
download_single_table(driver, table_url)
# 调用上面提到的 wait_for_download_complete 确认下载成功
if wait_for_download_complete(driver.download_dir):
logging.info(f"成功下载: {table_url}")
return True
else:
logging.warning(f"第{attempt+1}次尝试,下载未确认: {table_url}")
except Exception as e:
logging.error(f"第{attempt+1}次尝试失败,错误: {e} - URL: {table_url}")
time.sleep(5 * (attempt + 1)) # 重试间隔逐渐变长
logging.error(f"下载失败,已重试{retries}次: {table_url}")
return False
```
在主循环中,使用`robust_download`替代简单的`download_single_table`,并记录关键步骤和错误。这样,即使程序运行中断,你也可以通过日志文件精准定位到问题年份和类别,手动补爬,而无需从头开始。
### 4.4 性能优化与速率控制
大规模爬取时,需要尊重目标网站,避免对其服务器造成压力。
* **添加延迟:** 在请求之间使用 `time.sleep(random.uniform(1, 3))` 添加随机间隔,模拟人类操作。
* **使用会话(Session):** Requests库的`Session`对象可以复用TCP连接,提高效率,并保持cookies。
* **分布式与断点续传:** 对于超大规模任务,可以考虑将任务列表(年份-类别)保存到文件或队列中,使用多线程/多进程(需谨慎处理资源竞争),或者将任务分片在不同的机器上执行。同时,记录已成功完成的任务,下次运行时跳过它们,实现断点续传。
最后,这套自动化归档方案的价值,不仅在于一次性完成了历史数据的收集,更在于其可复用性。当2023年、2024年的数据发布后,你只需要微调年份范围,或者稍作检查网站结构是否变化,就可以再次运行脚本,轻松地将最新数据纳入你的分析体系,真正实现了一劳永逸的数据管道搭建。