Python爬虫实战:企业专利数据采集、清洗与可视化全流程

发布时间:2026/10/5 6:14:00
Python爬虫实战:企业专利数据采集、清洗与可视化全流程 做企业尽调、技术选型调研或者竞品分析的时候最折磨人的环节之一就是整理专利数据。以前我们团队查一家公司的专利基本靠人工在检索平台里一个关键词一个关键词地搜然后复制标题、申请号、状态再贴到Excel里面几页下来眼睛就花了遇到几十家目标企业整个人都是崩溃的。后来我干脆花了两个周末写了这套Python爬虫把企业专利信息自动抓回来清洗去重后存成结构化数据再顺手生成趋势图和词云。效果非常明显原来需要两天的工作量现在十分钟就能跑完。这篇就是我的完整落地记录包括技术选型、核心代码、常见坑位以及从数据到结论的整个链路希望对正准备做数据采集或者专利分析的朋友有点参考价值。1. 项目整体构思与技术选型1.1 先把数据源盘清楚专利信息藏在哪做爬虫之前我第一件事不是写代码而是研究数据源。公开的专利数据一般散落在两类平台一类是官方的专利检索系统数据权威、更新及时另一类是第三方专利数据服务商做了封装查询体验更好。这两类平台有个共同特点页面结构复杂、数据动态加载、请求频率一高就会被限制。所以我不建议一上来就盯着网页DOM硬解析。大部分检索平台的页面背后都有一个JSON接口页面上的表格只是把这些JSON数据渲染出来。直接抓接口拿到的数据是结构化的解析成本低一个量级。比如搜索一个企业名称返回的JSON里通常直接包含专利标题、申请号、公开号、申请人、发明人、申请日、公开日、法律状态、IPC分类号、摘要这些字段比从HTML里抠文本可靠得多。我在选数据源的时候还做了一件事看平台的robots.txt和用户协议优先选择允许公开访问的数据入口。做爬虫不是做攻击尊重数据源规则采集过程才能稳定持续。后面所有示例都以一个常见的专利检索接口结构为模板实际使用时替换成你所选平台的真实地址和参数就行。1.2 技术栈选型requests BeautifulSoup 还是 Scrapy很多新手一上来就问Scrapy怎么用其实技术选型要看采集规模。我把几个常用方案放在一起对比过思路大概是这样技术方案上手难度适合场景关键特征requests BeautifulSoup低采集量在千到万级、页面结构不复杂的项目灵活轻量断点调试方便Scrapy中大规模采集、需要定时调度和去重机制自带去重、管道、中间件扩展性强Selenium / Playwright高必须操作浏览器、完全绕不开动态渲染模拟真实浏览器稳定但慢、费资源我的选择是requests BeautifulSoup。原因很简单这个项目要采集的目标企业数量在几十到几百家每家专利数量几百条总量级在万级左右requests完全够用而且写起来直观遇到问题好排查。Scrapy虽然功能强大但框架引入的队列、管道、中间件概念对于这种中小型项目反而增加了心智负担。如果后期需要扩展到百万级数据我会把requests写好的解析函数迁移到Scrapy的Spider里逻辑不用重写只是换壳。先跑通再优化这是我做爬虫项目一贯的原则。1.3 从“查专利”到“看数据”的完整链路设计这个项目的核心不是把数据抓下来就完了而是要形成一条从查询到洞察的链路我给这个项目规划了六个阶段构造搜索条件、请求数据接口、解析专利字段、清洗去重、存储入库、统计可视化。构造搜索条件解决的是“我要查谁”的问题比如指定企业名称、申请年份范围、专利类型请求接口解决的是“数据从哪里来”用Session保持会话模拟浏览器的请求头解析字段是把半结构化的JSON转成表格化的记录清洗去重处理空值、格式不统一、重复数据存储入库让数据可以长期复用统计可视化是把数据变成图表支撑判断。后面所有章节都按这条链路展开。这样设计的好处是每一步都有明确的输入输出单独抽出哪一步都可以复用比如只保留前五步就是一个纯粹的数据采集工具只保留后两步就是一个轻量数据分析报表。2. 环境准备与请求基础2.1 Python环境与第三方库安装这一步比较基础但我还是踩过一次坑直接在全局环境里pip install装了一堆互不兼容的包后来项目多了才发现版本冲突。现在我都用虚拟环境隔离建议你也养成这个习惯。python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate激活虚拟环境后安装依赖库pip install requests beautifulsoup4 pandas matplotlib wordcloud openpyxlrequests发HTTP请求抓数据。beautifulsoup4解析HTML这里主要用于兜底场景比如接口挂了临时改解析页面。pandas做数据清洗和分析。matplotlib画趋势图和分布图。wordcloud生成关键词词云。openpyxlpandas写Excel的底层依赖不装的话导出Excel会报错。2.2 请求头伪装与Session保持专利检索平台基本都有基础的反爬策略其中最基础的就是校验请求头。如果一个请求没有User-Agent或者User-Agent看起来像Python的默认标识服务器大概率直接返回403。所以在代码里我会维护一个正常的请求头并用requests.Session()来保持会话。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://patent-search.example.com/, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, } session requests.Session() session.headers.update(HEADERS)这里的User-Agent建议用你自己浏览器里实际的值打开Chrome的开发者工具在Network面板随便点一个请求复制Request Headers里的User-Agent最靠谱。Referer也尽量补齐有些接口会校验来源页。Session的作用是自动管理Cookie相当于你访问一次搜索页后带着服务端发的小饼干去请求JSON接口不容易被识别成陌生访客。2.3 合规采集的底线这部分我必须多说两句。专利信息本身是公开数据采集用于学习研究、企业尽调、学术分析都没问题但有几条底线要守住控制请求频率不要高并发去打对方的服务器不采集不公开的数据不去尝试绕过登录权限数据使用范围保持在个人研究和合法业务内不贩卖原始数据同时尊重平台用户协议。我实际的做法是把单次请求间隔设置在1.5秒到3.5秒之间随机波动模拟人的操作节奏同时做好失败重试机制而不是靠加大并发去解决问题。这样做既保护目标网站的稳定也能让爬虫运行得更持久。技术能力是用来提效的不是用来破坏的这个尺度要把握好。3. 核心爬虫代码实现3.1 构造查询参数与分页逻辑专利检索接口通常支持关键词、申请人、日期范围等参数。这里以一个标准化接口为例构造参数的时候我建议把查询词、页码、每页条数都拆成独立变量方便后面批量调用。def build_params(company_name, page, page_size20): params { keyword: company_name, searchType: apply, # 按申请人字段搜索 page: page, pageSize: page_size, sort: applyDate desc # 按申请日倒序 } return params关键点在于searchType有些平台默认是全文检索传“apply”才是精确匹配申请人字段不然搜出来的专利可能是文中提到了公司名噪音会非常大。pageSize不建议设置太大很多接口有上限一次性拉50条以上容易被拒绝我用20条一页最稳妥。3.2 请求接口并解析JSON字段拿到参数后用session发起GET请求返回的通常是JSON格式。我先把核心请求函数封装起来并加上状态码异常处理。def fetch_patent_page(company_name, page): url https://patent-search.example.com/api/v1/patent/search params build_params(company_name, page) try: resp session.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() return data except requests.exceptions.RequestException as e: print(f[错误] 请求失败{company_name} 第{page}页 - {e}) return Nonedata的结构一般长这样{ code: 0, data: { total: 128, list: [ { patentId: CN202310123456.7, title: 一种基于深度学习的图像识别方法, applicant: 某某科技有限公司, inventor: 张三; 李四, applyDate: 2023-06-01, publicDate: 2023-10-15, ipc: G06F 17/30, legalStatus: 审中, summary: 本发明公开了一种... } ] } }解析函数负责把JSON里的list部分抽取成规范化字典并统一字段名。更重要的是我要把IPC分类号拆出小类比如G06F 17/30只取前4位“G06F”为后面的技术领域统计做准备。def parse_items(data): records [] items data.get(data, {}).get(list, []) if data else [] for item in items: ipc_full item.get(ipc, ) records.append({ patent_id: item.get(patentId, ), title: item.get(title, ).strip(), applicant: item.get(applicant, ).strip(), inventor: item.get(inventor, ), apply_date: item.get(applyDate, ), public_date: item.get(publicDate, ), ipc_main: ipc_full.split( )[0][:4] if ipc_full else , legal_status: item.get(legalStatus, ), summary: item.get(summary, ).strip() }) return records这种字段抽取逻辑可以应对大多数接口核心就是resilience字段缺失用空字符串兜底不因为一条脏数据让整个进程崩溃。3.3 多企业批量采集与日志记录单页能跑通后就要做批量采集。我写了一个crawl_company函数从一个企业名开始循环请求所有页直到返回列表为空或者达到最大页数。每次请求之间用random.uniform随机延时。import time import random import logging logging.basicConfig( filenamepatent_crawler.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) def crawl_company(company_name, max_pages50): all_records [] for page in range(1, max_pages 1): data fetch_patent_page(company_name, page) if not data: logging.warning(f{company_name} 第{page}页请求失败) break records parse_items(data) if not records: break all_records.extend(records) total data.get(data, {}).get(total, 0) logging.info(f{company_name} 第{page}页抓取{len(records)}条累计{len(all_records)}条总数{total}) time.sleep(random.uniform(1.5, 3.5)) return all_records日志有三个作用第一看爬取进度第二失败时知道卡在哪一页哪个企业第三采集完可以统计成功率和数据量。我发现很多新手不写日志导致程序跑崩之后只能从头再来这是非常低效的。3.4 断点续爬与失败重试机制批量采集几十上百家企业的时候中途难免遇到网络抖动或者被限流这时候全量重跑很浪费时间。我的做法是维护一个企业名单文件里面记录每个企业的采集状态采集成功的打上标记下次启动时自动跳过。def run_batch(company_list, state_filecrawl_state.txt): done set() if os.path.exists(state_file): with open(state_file, r, encodingutf-8) as f: done set(line.strip() for line in f) for company in company_list: if company in done: continue records crawl_company(company) if records: save_to_csv(records, patents_all.csv) with open(state_file, a, encodingutf-8) as f: f.write(company \n)这个断点续爬的思路价值很大尤其是采集总量几万条、耗时几小时的时候能容忍中途失败而是靠重复跑解决这是最稳的方式。另外我还会对同一页失败做三次重试后再放弃避免单次网络抖动导致数据缺失。def fetch_with_retry(company_name, page, retries3): for attempt in range(1, retries 1): data fetch_patent_page(company_name, page) if data: return data time.sleep(2 * attempt) return None4. 数据清洗、去重与存储4.1 字段清洗处理空值、日期和文本格式抓下来的原始数据通常比较脏不能直接拿来分析。我一般会做四步清洗去空、去重、格式化、截取。去空标题和专利号是核心字段为空就丢掉摘要为空但其他字段完整就保留。去重基于patent_id做去重同一件专利在不同接口里可能重复出现必须保证唯一。格式化把apply_date统一成YYYY-MM-DD格式方便后续按年、按月统计。截取IPC分类号只保留大类小类前缀发明人字段用分号拆分后转成列表。这一步用pandas操作效率很高import pandas as pd df pd.read_csv(patents_all.csv, dtypestr) df[patent_id] df[patent_id].str.strip() df df.drop_duplicates(subsetpatent_id, keepfirst) df[apply_date] pd.to_datetime(df[apply_date], errorscoerce) df df.dropna(subset[apply_date]) df[year] df[apply_date].dt.year df df.fillna()4.2 三种存储方案怎么选CSV、SQLite、MySQL存储方案我建议按使用场景来定不要一上来就上重型数据库。存储方案优点缺点适用场景CSV轻量、Excel直接打开、适合分享数据量大时读写慢、不支持并发万级以内、一次性分析SQLite单机免安装、支持SQL、查询方便并发写弱、不适合多端访问本地长期积累、个人分析MySQL支持并发、权限管理、适合团队需要安装维护、环境配置繁琐团队协作、构建分析平台国内知识产权分析工作流里CSV其实是使用率非常高的格式因为后续要导入Excel做报表或者用BI工具做展示CSV都是通用格式。我做这个项目时用CSV存原始数据用SQLite存清洗后的分析库两种配合使用既方便检查又方便查询。4.3 增量更新只抓新增数据不重复全量专利数据是会持续更新的比如一家公司每个月都有新申请公开。如果每次跑全量采集一是浪费时间二是给数据源增加负担。增量更新的核心是靠patent_id判重。def load_existing_ids(pathpatents_all.csv): try: df pd.read_csv(path, dtypestr) return set(df[patent_id].dropna()) except FileNotFoundError: return set() def filter_new_records(records, existing_ids): new_records [] for r in records: if r[patent_id] not in existing_ids: new_records.append(r) return new_records判断逻辑很简单采集前加载已有专利号集合新抓到的数据只保留不在集合里的记录。这个方案配合crawl_state.txt的断点续爬机制就能实现“每周定时跑一次增量”的效果。5. 数据可视化与商业洞察5.1 专利申请趋势图一眼看出技术发力节奏数据存好之后最直接的分析就是把专利按申请年份聚合看一家企业或者一个行业的专利数量时间分布。比如某公司从2018年开始专利数量明显增长说明那段时间在集中布局某条技术线如果某几年数量下滑可能意味着研发方向有调整。用matplotlib画折线图非常方便但有个坑必须提一下matplotlib默认字体不支持中文直接用plt.title(专利申请趋势)会显示成方块。解决办法是指定中文字体我用的是SimHei。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False trend df.groupby(year).size() fig, ax plt.subplots(figsize(10, 5)) ax.plot(trend.index, trend.values, markero, linewidth2) ax.set_title(专利年度申请趋势) ax.set_xlabel(申请年份) ax.set_ylabel(专利数量) ax.grid(alpha0.4) fig.tight_layout() fig.savefig(patent_trend.png, dpi150)图保存成PNG之后可以直接贴到调研报告或者PPT里。如果后面想做成网页看板可以把数据导出成JSON用ECharts在前端渲染效果更灵活。5.2 技术领域分布用IPC分类号统计分析企业专利的时候不能只看数量还要看技术构成。专利分类号IPC的前4位代表技术领域大类比如G06F代表电数字数据处理H04L代表数字信息传输。统计每个大类出现的频次就能画出这个企业在哪些技术方向上有积累。from collections import Counter ipc_counts Counter(df[ipc_main].dropna()) common ipc_counts.most_common(10) ipc_labels [x[0] for x in common] ipc_values [x[1] for x in common] fig, ax plt.subplots(figsize(8, 8)) ax.pie(ipc_values, labelsipc_labels, autopct%.1f%%, startangle90) ax.set_title(IPC技术领域分布 TOP10) fig.savefig(ipc_distribution.png, dpi150)第一次跑出这张图的时候我发现自己之前对某家企业的判断是错的原本以为它是做硬件设备的结果IPC分布显示它的专利集中在G06F和H04L其实是一家软件通信服务商。这就是数据分析带来的认知修正光看公司简介远远不够。5.3 关键词词云快速感知技术关键词词云虽然不是严谨的定量分析但非常适合快速感知一批专利的技术主题。把专利标题和摘要拼在一起用jieba分词提取关键词再去掉停用词就能生成词云图。import jieba from wordcloud import WordCloud text .join(df[title].dropna().tolist()) words .join(w for w in jieba.cut(text) if len(w) 1) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height400, background_colorwhite, max_words100 ).generate(words) wc.to_file(patent_wordcloud.png)注意WordCloud的font_path必须指定中文字体路径否则中文会变方框。Windows下一般用C:/Windows/Fonts/simhei.ttfLinux下可以用系统中的文泉驿或Noto CJK字体。我用词云的时候发现自动化、控制、系统、方法这些词出现频率极高说明这批专利以方法类和系统框架类为主不侧重具体硬件结构。5.4 数据洞察的应用场景扩展这套采集和可视化链路并不局限于单一场景。做企业并购尽调时可以批量分析标的公司的专利布局评估其技术壁垒做竞品监测时可以按季度增量采集对手新公开的专利识别其研发动向做行业趋势研究时可以抓取整个行业的专利数据找到技术演进方向和热门分支做人才评估时可以分析发明人字段找到某个细分领域的高产发明人。我实际做过的项目里这套方案在尽调报告里的价值甚至超过了传统财务指标分析因为专利数据能反映一家企业未来三到五年的技术储备。6. 常见问题与反爬实战笔记6.1 403、418状态码被识别成“非人类”怎么处理爬取专利平台时我最常遇到的是403和418。403一般是因为请求头信息不完整尤其是缺少User-Agent或Referer418则更具迷惑性表面上是“我是茶壶”实际上就是服务器识别出请求特征像爬虫直接拒绝服务。排查思路非常固定先看浏览器里实际请求长什么样把浏览器开发者工具里对应请求的所有Request Headers复制出来逐一补到代码请求头里。如果还不行就降低请求频率、加随机延时、去掉请求头里可能暴露Python特征的字段。状态码可能原因解决思路403缺少UA、Referer、Cookie补全请求头用Session保持会话418请求特征被识别为脚本降低频率增加随机延时429请求过于频繁暂停并指数退避错峰采集502/504目标服务器波动重试三次等待后继续6.2 动态加载的数据抓不到先找接口再考虑浏览器很多专利检索页是Vue或React写的数据都是异步加载直接requests抓到的HTML里根本没有表格数据。很多人第一反应是上Selenium模拟浏览器点击但这样效率低、内存占用高还不稳定。正确的做法是先打开浏览器的开发者工具切到Network面板勾选Fetch/XHR然后在页面里点一次搜索观察页面发了哪些异步请求返回JSON的那个请求就是目标接口。右键Copy as cURL用浏览器的fetch转curl工具或者Postman导入就能看到完整的请求头、查询参数和Cookie。照着这个构造requests请求是最省力的方式。6.3 中文乱码先检查响应编码再考虑文件写入方式另一个高频问题是中文乱码。很多接口返回的内容虽然看起来是JSON但因为HTTP响应头里没有显式声明charsetrequests默认猜测的编码可能是错误的。解决办法很直接拿到响应后立刻指定编码。resp session.get(url, paramsparams, timeout10) resp.encoding utf-8 data resp.json()另外写入CSV时如果中文在Excel里乱码这不是数据问题而是编码问题。写CSV时用utf-8-sig编码Excel打开就正常了df.to_csv(patents_all.csv, indexFalse, encodingutf-8-sig)我在第一次保存数据时用的utf-8Excel打开后中文全乱换成utf-8-sig之后立刻解决。这个细节很关键但很多教程不会提。6.4 IP被限制的应对思路慢才是最有效的策略爬虫跑得多了最难缠的是IP被限制表现是突然所有请求都返回429或者验证码。大多数情况下这不是数据源故意刁难而是短时间内请求量过大触发了风控。我的第一方案永远是降低采集速度把随机延时从1.5-3.5秒放大到3-6秒同时增加重试间隔。实际测试中1秒以内的并发在大型专利平台上基本坚持不了几十个请求就会触发限制而把节奏放到3秒以上一次跑几百页非常稳定。对于长时间批量采集我还会把任务拆成多个时间段执行比如每小时只跑一小批把总压力打散这样能显著降低被限流的概率。至于更高阶的分布式方案普通项目根本用不上先把速度和频率控制好才是性价比最高的稳定策略。7. 实战感悟与后续扩展方向这套爬虫项目跑通之后给我的最大感受是爬虫本身只是搬运工真正有价值的是搬下来的数据能不能变成判断依据。过去人工搜集专利信息要两天现在自动化采集加可视化不到十分钟省下来的时间可以用来做更深度的分析比如技术路线对比、发明人合作网络、专利引证关系这些才是真正能支撑决策的内容。后续如果要扩展我建议从两个方向入手。第一是模板化把企业名单放进Excel或数据库采集任务做成可配置的谁都能调用不用改代码第二是做定期调度用操作系统的定时任务每周自动跑一次增量采集把历史专利和新增专利分开管理维护一份长期的技术情报库。最后再分享一个小技巧采集大规模数据前先用一两家企业做小范围测试确认接口字段、频率限制、存储格式都没问题再放开到全量别一上来就梭哈。这个习惯帮我避开过很多坑希望对你也有用。