3个坑搞定中国专利检索网爬虫新手避坑指南
刚学完 Python 语法,是不是觉得代码写得飞起,但一到实际项目就懵圈?看着满屏的 import 和 def,却不知道数据从哪来、逻辑怎么串、异常怎么处理。很多新手在掘金技术社区发帖求助,问得最多的就是:“语法都懂了,为什么连个简单的数据抓取脚本都跑不通?”
这就是典型的“知道怎么做”和“知道怎么落地”之间的鸿沟。今天我们就以中国专利检索网的数据采集为案例,从零搭建一个实战项目。这不是为了搞破坏,而是为了让你彻底搞懂:一个完整的数据处理流程长什么样。通过这个项目,你能看清网络请求、数据解析、异常处理、数据存储这四大核心环节是如何咬合在一起的。
新手避坑的关键,不在于你记住了多少库,而在于你是否理解数据流动的每一个节点。下面我们就拆开揉碎,一步步来。
项目目标与环境准备
我们要做什么?从公开渠道获取专利的基础信息,包括专利号、申请日、发明人、摘要等。这些数据结构化程度高,非常适合用来练习。
为什么选这个场景?
- 数据结构清晰:专利列表页通常是表格或列表形式,DOM 结构相对规律。
- 数据量适中:不需要处理千万级并发,单机即可运行,适合本地调试。
- 合规性考量:我们仅获取公开元数据,控制请求频率,遵守 robots.txt 精神,不触碰隐私数据。
环境准备清单:
- Python 3.9+
requests:用于发送 HTTP 请求。lxml或BeautifulSoup4:用于解析 HTML。pandas:用于数据存储和简单分析。fake-useragent:用于生成随机 User-Agent,模拟真实浏览器。
打开终端,创建虚拟环境并安装依赖:
# 创建虚拟环境
python -m venv patent_env# 激活环境 (Windows)
patent_env\Scripts\activate# 激活环境 (Mac/Linux)
source patent_env/bin/activate# 安装依赖
pip install requests lxml pandas fake-useragent
很多新手在这里卡住,明明 pip install 成功了,但 import 报错。90% 的情况是虚拟环境没激活,或者装到了系统 Python 里。养成用虚拟环境隔离项目的习惯,这是工程化的第一步。
目录结构与模块划分
别把所有代码都塞在 main.py 里。一个可维护的项目,结构必须清晰。我们采用模块化的方式,将不同职责的代码分离。
patent_crawler/
├── config.py # 配置信息:URL、请求头、重试次数
├── crawler.py # 核心爬虫逻辑:请求、解析、提取
├── storage.py # 数据存储:CSV/Excel 写入
├── utils.py # 工具函数:日志、随机延迟、UA 生成
├── main.py # 入口文件:控制流程
└── data/ # 存储数据的文件夹└── patents.csv
config.py 示例:
import osBASE_URL = "https://www.cnipa.gov.cn/..." # 替换为实际公开查询接口或页面
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
# 请求间隔,单位秒,模拟人工浏览,避免被识别为恶意攻击
REQUEST_DELAY = 2
# 重试次数
MAX_RETRIES = 3
# 输出文件路径
OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "data", "patents.csv")
为什么要把配置单独拿出来?
因为 URL 可能会变,请求头需要根据目标站点调整,重试策略需要根据网络状况优化。当这些参数集中在一个文件里时,你修改起来极其方便,不用去翻几百行代码找哪里写了 "3" 秒延迟。
核心代码实现与逐行讲解
这是项目的核心。我们将分为三部分:请求获取、数据解析、异常处理。
1. 工具函数:伪装与日志
在 utils.py 中,我们需要两个核心工具:生成随机 UA 和记录日志。
import logging
import time
from fake_useragent import UserAgent# 配置日志,打印到控制台,格式清晰
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s'
)ua = UserAgent()def get_random_ua():"""获取随机 User-Agent"""return ua.randomdef safe_sleep(seconds):"""安全休眠,加入随机抖动,避免固定频率被检测"""import randomactual_delay = seconds + random.uniform(0, 1)time.sleep(actual_delay)logging.info(f"休眠 {actual_delay:.2f} 秒...")
新手避坑点:不要写 time.sleep(2) 这种固定值。服务器端的风控系统很容易通过请求间隔的方差来判断你是否是机器人。加入 random.uniform 产生的抖动,让请求间隔在 2.0-3.0 秒之间随机浮动,更像真人。
2. 数据解析:从 HTML 到结构化数据
假设我们获取到了一个包含专利列表的 HTML 片段。我们需要从中提取关键字段。这里以 lxml 为例,性能优于 BeautifulSoup。
在 crawler.py 中:
import requests
from lxml import html
from config import HEADERS, REQUEST_DELAY, MAX_RETRIES
from utils import get_random_ua, safe_sleep, loggingdef fetch_html(url):"""获取网页 HTML 内容,包含重试机制"""headers = HEADERS.copy()headers["User-Agent"] = get_random_ua()for i in range(MAX_RETRIES):try:logging.info(f"第 {i+1} 次请求: {url}")response = requests.get(url, headers=headers, timeout=10)# 检查状态码if response.status_code == 200:return response.textelif response.status_code == 429:# 429 Too Many Requests,说明触发了限流,需增加延迟logging.warning("触发限流,增加休眠时间")safe_sleep(REQUEST_DELAY * 2)else:logging.error(f"HTTP 错误: {response.status_code}")except requests.exceptions.RequestException as e:logging.error(f"请求异常: {e}")# 如果还没成功,休眠后重试safe_sleep(REQUEST_DELAY)logging.error("达到最大重试次数,放弃请求")return Nonedef parse_patent_list(html_content):"""解析 HTML,提取专利列表数据注意:以下选择器需根据实际页面结构调整"""tree = html.fromstring(html_content)patents = []# 假设专利列表在 class="patent-list" 的 div 中# 每个专利项在 class="patent-item" 的 li 中items = tree.xpath('//div[@class="patent-list"]/ul/li[@class="patent-item"]')for item in items:# 提取专利名称name_nodes = item.xpath('.//a[@class="patent-name"]/text()')name = name_nodes[0].strip() if name_nodes else "N/A"# 提取专利号num_nodes = item.xpath('.//span[@class="patent-number"]/text()')patent_num = num_nodes[0].strip() if num_nodes else "N/A"# 提取申请日期date_nodes = item.xpath('.//span[@class="filing-date"]/text()')filing_date = date_nodes[0].strip() if date_nodes else "N/A"# 提取发明人inventor_nodes = item.xpath('.//span[@class="inventor"]/text()')inventor = inventor_nodes[0].strip() if inventor_nodes else "N/A"patents.append({"patent_name": name,"patent_number": patent_num,"filing_date": filing_date,"inventor": inventor})return patents
逐行讲解关键点:
HEADERS.copy():每次请求前复制 headers,并更新 UA。如果直接修改原字典,所有请求的 UA 都会变成最后一次设置的值,导致指纹单一,容易被封。timeout=10:必须设置超时。如果服务器无响应,默认情况下requests会无限等待,导致脚本卡死。xpath相对路径:注意./开头。//是全文档搜索,.//是当前节点下的搜索。在循环中,必须用.//,否则每次都会从根节点找,效率极低且容易出错。- 空值处理:
if name_nodes else "N/A"。网页数据不总是完美的,字段可能缺失。如果不做防御性编程,.strip()会抛出IndexError,导致整个程序崩溃。
3. 数据存储:追加模式与去重
在 storage.py 中,我们使用 pandas 来保存数据。
import pandas as pd
import os
from config import OUTPUT_FILEdef save_to_csv(new_data, file_path=OUTPUT_FILE):"""将新数据追加到 CSV 文件"""if not new_data:return# 如果文件不存在,创建 DataFrame 并写入if not os.path.exists(file_path):df = pd.DataFrame(new_data)df.to_csv(file_path, index=False, encoding='utf-8-sig')else:# 如果文件存在,读取旧数据,合并后去重,再写入try:old_df = pd.read_csv(file_path, encoding='utf-8-sig')new_df = pd.DataFrame(new_data)# 以专利号为唯一键进行去重combined_df = pd.concat([old_df, new_df], ignore_index=True)combined_df.drop_duplicates(subset=['patent_number'], keep='first', inplace=True)combined_df.to_csv(file_path, index=False, encoding='utf-8-sig')logging.info(f"数据已保存,当前总记录数: {len(combined_df)}")except Exception as e:logging.error(f"保存数据出错: {e}")
为什么用 utf-8-sig?
在 Windows 环境下,Excel 打开 UTF-8 编码的 CSV 文件时,中文通常会乱码。utf-8-sig 会在文件头加入 BOM 标记,Excel 能正确识别为 UTF-8,从而正常显示中文。这是一个极小的细节,却决定了数据的可用性。
运行与测试:主流程控制
在 main.py 中,我们将所有模块串联起来。
import logging
from crawler import fetch_html, parse_patent_list
from storage import save_to_csv
from config import BASE_URL, REQUEST_DELAYdef main():logging.info("开始采集专利数据...")# 1. 获取第一页 HTMLhtml_content = fetch_html(BASE_URL)if not html_content:logging.error("无法获取初始页面,程序退出")return# 2. 解析数据patents = parse_patent_list(html_content)logging.info(f"解析到 {len(patents)} 条数据")if patents:# 3. 保存数据save_to_csv(patents)# 4. 模拟翻页逻辑 (此处简化,实际需分析下一页 URL 规律)# next_url = get_next_page_url(html_content)# if next_url:# # 递归或循环调用 fetch_html(next_url)# passelse:logging.warning("未解析到任何数据,请检查选择器")if __name__ == "__main__":main()
如何测试?
- 断点调试:在 IDE 中打断点,观察
response.text是否包含预期的 HTML 标签。 - 日志监控:关注控制台输出。如果看到“触发限流”,说明你的
REQUEST_DELAY太短,或者 UA 太单一。 - 数据校验:打开生成的
patents.csv,随机抽取几条,去官网人工核对字段是否准确。
新手避坑点:不要只看程序跑完了就完事。数据准确性比运行速度更重要。如果解析错了,存一万条垃圾数据没有任何价值。
优化扩展与进阶技巧
基础功能跑通后,我们可以做哪些优化?
并发请求: 如果数据量巨大,单线程太慢。可以使用
concurrent.futures.ThreadPoolExecutor进行多线程并发。from concurrent.futures import ThreadPoolExecutor, as_completeddef worker(urls):for url in urls:html = fetch_html(url)if html:data = parse_patent_list(html)save_to_csv(data) # 注意:多线程写文件需加锁或改用队列注意:多线程下直接写 CSV 会导致文件损坏。生产环境中,建议将数据放入
queue.Queue,由单独的线程负责写入文件。代理 IP 池: 如果频繁被封,需要引入代理 IP。在
config.py中维护一个代理列表,每次请求随机选择一个。PROXIES_LIST = ["http://user:pass@ip1:port","http://user:pass@ip2:port" ]在
requests.get中传入proxies={"http": proxy, "https": proxy}。动态渲染处理: 如果目标网站数据是通过 JavaScript 动态加载的,
requests拿不到数据。此时需要引入Selenium或Playwright。from selenium import webdriver driver = webdriver.Chrome() driver.get(url) html_content = driver.page_source性能权衡:Selenium 比 requests 慢 10-50 倍。仅在必要时使用。
数据清洗: 原始数据中可能包含多余空格、换行符、HTML 标签残留。在存入数据库前,增加清洗步骤:
def clean_text(text):if not text:return ""import re# 去除 HTML 标签text = re.sub(r'<[^>]+>', '', text)# 去除多余空白text = re.sub(r'\s+', ' ', text).strip()return text
小结
这个项目虽然小,但涵盖了网络爬虫的核心要素:配置管理、请求伪装、健壮解析、安全存储、异常处理。
很多新手觉得爬虫难,其实是难在“细节”。UA 怎么变?超时怎么设?数据缺失怎么办?文件编码怎么防乱码?这些看似琐碎的问题,拼凑起来就是一个能稳定运行的工程。
新手避坑的最后一招:写文档。在每个函数上方加上 docstring,说明输入输出、参数含义、可能抛出的异常。半年后你自己再看代码,或者交给同事接手,他们会感谢现在的你。
编程不是背语法,而是解决具体问题。当你能够独立搭建这样一个小工具,并理解其背后的每一个决策时,你就跨过了“只会写代码”到“能交付项目”的门槛。
这个知识点你面试被问过吗?比如“如何处理反爬机制”或者“多线程爬取时的数据一致性”,留言说说你的答案,我们一起聊聊。