ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python爬虫实现学术机构研究方向自动采集与趋势分析

Python爬虫实现学术机构研究方向自动采集与趋势分析 每年到做选题、报课题、写综述的时候总有一件事让我头疼到不行想快速知道某个学院或者研究所最近都在研究什么前沿方向在哪但又不想靠肉眼一页页翻官网。手动翻几十页新闻公告、课题公示、论文列表不仅慢还容易漏。后来我干脆用 Python 写了个爬虫把学术机构公开的研究主题自动化采集下来再做关键词统计和趋势分析整个过程从“翻一下午网页”压缩到了“喝杯咖啡的时间”。这篇文章就是我从零做完这件事的完整记录适合有 Python 基础、想上手爬虫的开发者也适合科研助理、课题组负责人这类经常需要“看方向”的人参考。1. 思路先行学术主题采集爬虫的整体方案怎么定1.1 先确认目标数据长什么样动手写代码之前我做的第一件事不是装库而是去把目标网站反复看了几遍。很多新手上来就写 requests.get结果连自己要爬哪个字段都没想清楚爬到一半才返工特别耽误时间。学术机构的“研究主题”一般藏在这几个地方官网“科研动态”或“学术新闻”栏目里面有近期研究进展、获奖消息、成果公告“科研项目”或“立项公示”页面会列出课题名称、负责人、经费等信息“论文发表”列表通常有论文标题、作者、期刊、年份“研究团队”介绍页团队描述里往往包含方向关键词。我最终选择的是“科研动态 立项公示”这两个栏目因为它们的标题本身信息密度很高比如“面向xxx的xxx方法研究”这句话里的关键词基本就能代表研究方向。采集字段也锁定四个标题、发布时间、链接、摘要。摘要不是必须的但有了它做关键词统计时语料会更充足。这里有个经验不要一上来就追求“全站爬取”。先圈定一两个高频更新、结构清晰的栏目跑通流程后面再扩展其他页面效率高得多。1.2 技术选型requests BeautifulSoup 够用吗经常有人问我为什么不用 Scrapy不用 Playwright甚至不用现在很火的 AI 辅助爬虫。我的回答是看场景。这个任务的特点是目标页面是静态 HTML数据存在网页源码里不需要模拟点击、不需要登录、不依赖 JavaScript 动态渲染。学术机构官网普遍还停留在传统后端渲染模式这正好是 requests 和 BeautifulSoup 的舒适区。方案优点缺点适合场景requests BeautifulSoup轻量、上手快、完全可控无法处理复杂动态页面静态页面、只需要部分字段Scrapy并发能力强、中间件丰富学习曲线陡、工程化重大规模采集、多级页面Playwright / Selenium能跑 JS、模拟真实操作资源占用高、速度慢强交互、动态渲染页面所以如果你想在半天内搞定一个学术界信息采集脚本requests BeautifulSoup 是性价比最高的组合。等以后数据量到了每天几万条、需要分布式调度的时候再迁移到 Scrapy 也不迟。说实话很多学术采集场景单线程加一个进程池就完全够用根本不需要复杂架构。1.3 合规边界公开数据也不是想怎么爬就怎么爬说到爬虫合规永远是绕不开的话题。我的原则很简单只采集公开信息不碰需登录的个人数据不突破技术限制不把对方网站爬挂。具体到学术机构场景需要注意这几点检查目标网站的 robots.txt确认哪些目录允许爬取控制请求频率单线程加随机延时通常每秒 1-3 个请求就足够了不采集用户个人信息、内部系统数据数据仅用于学术调研、趋势分析并在结果中标注出处。我做这个爬虫的核心目的是辅助判断研究方向而不是对某个机构做恶意采集或商业用途。这个边界守住后面写代码心里才有底。2. 上手前的准备环境、请求头与页面定位一个都不能少2.1 环境安装其实只需要一行命令建议使用 Python 3.8 以上版本我自己用的是 3.10。安装依赖这一步很简单pip install requests beautifulsoup4 lxml如果后面要做关键词统计和可视化可以再装pip install jieba pandas matplotlib这里说个坑BeautifulSoup 的解析器最好指定 lxml。默认的 html.parser 在遇到一些不规范 HTML 时解析结果会有偏差而 lxml 容错性强速度也快很多。我在代码里统一用BeautifulSoup(html, lxml)这种写法。2.2 请求头与 Session别让服务器一眼识破浏览器访问网页时会带上 User-Agent、Accept、Referer 等一堆请求头。而 requests 默认的 User-Agent 是一长串python-requests/x.x.x很多网站一看就知道是脚本直接给你 403。我的做法是准备一个小型 User-Agent 池每次请求随机取一个import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/121.0, ] def get_headers(): return { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.5,en;q0.3, Connection: keep-alive, }除此之外用requests.Session()创建一个会话对象可以在多次请求之间复用 TCP 连接速度更快也更接近浏览器的访问习惯。import requests session requests.Session() session.headers.update(get_headers()) resp session.get(url, timeout10)注意这里的timeout一定要写不然遇到无响应页面程序可能卡住很久。2.3 页面结构定位用开发者工具把元素“揪”出来写解析代码之前我习惯先用浏览器打开目标页面按 F12 进入开发者工具点击左上角的“选取元素”按钮再悬停到标题列表上。这样能快速确认标题在 HTML 里长什么样。学术机构的列表页结构通常非常规律常见有两种模式第一种所有标题都在ul或div下面每一条是一个li标题文字在a标签里。比如ul classnews-list li a href/news/2024/0512.html基于深度学习的城市交通流预测方法研究/a span2024-05-12/span /li li a href/news/2024/0510.html面向医疗影像的弱监督分割算法探讨/a span2024-05-10/span /li /ul第二种数据不是直接渲染在 HTML 里而是内嵌在script标签的 JSON 字符串中页面通过 JS 再渲染出来。这种情况 BeautifulSoup 也能处理先取出 script 内容再用json.loads解析。我后面在问题排查部分会专门讲。针对第一种解析代码可以这样写from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) items soup.select(ul.news-list li) for item in items: a_tag item.find(a) if not a_tag: continue title a_tag.get_text(stripTrue) link a_tag.get(href) date_tag item.find(span) date date_tag.get_text(stripTrue) if date_tag else print(title, link, date)select支持 CSS 选择器比手写find_all直观很多建议熟练掌握。像ul.news-list li这种写法一眼就能看出来是“class 为 news-list 的 ul 下所有 li”。3. 从零开始写代码单页采集、分页并发与数据落库3.1 单页面采集先把一个页面跑通很多人一上来就写多线程结果单页逻辑都没调通报错了都不知道去哪里看。我自己的流程是先写一个函数处理单页面跑通了再考虑并发和数据存储。核心函数就两个一个是抓取页面一个是解析页面。import time import random import requests from bs4 import BeautifulSoup def fetch_page(url, session, retries3): for attempt in range(retries): try: resp session.get(url, timeout10) if resp.status_code 200: return resp.text else: print(fHTTP {resp.status_code}: {url}) except requests.RequestException as e: print(f第 {attempt1} 次请求失败: {e}) time.sleep(random.uniform(1, 2)) return None def parse_page(html): soup BeautifulSoup(html, lxml) data [] items soup.select(ul.news-list li) for item in items: a_tag item.find(a) if not a_tag: continue title a_tag.get_text(stripTrue) link a_tag.get(href) if link and not link.startswith(http): link https://www.example.edu.cn link date_tag item.find(span) date date_tag.get_text(stripTrue) if date_tag else data.append({title: title, link: link, date: date}) return data这里有两个细节值得注意第一链接补全。页面里的href很可能是相对路径比如/news/2024/0512.html如果不拼上域名后面跳转或做详情页采集时会一脸懵。第二重试机制。网络请求有时候会突然抽风加个 for 循环重试 3 次每次失败后停 1-2 秒能显著提高成功率。这个习惯是从采集稳定性角度养成的因为学术网站偶尔会响应慢重试比报错退出好太多。3.2 分页采集与多线程提速讲究的是“稳中求快”单个页面通常只有十几条数据远远不够做趋势分析。所以必须处理分页。大部分列表页的 URL 有规律比如https://www.example.edu.cn/research/news?page1 https://www.example.edu.cn/research/news?page2直接用一个循环拼接页码就行。但如果每页访问间隔 2 秒50 页就要 100 秒有点慢。这时候可以用ThreadPoolExecutor做并发但要控制并发数我一般设 3-5 个线程配合随机延时速度和礼貌兼得。from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_many_pages(base_url, total_pages, session): all_data [] urls [f{base_url}?page{i} for i in range(1, total_pages 1)] with ThreadPoolExecutor(max_workers4) as executor: future_map {executor.submit(fetch_page, url, session): url for url in urls} for future in as_completed(future_map): html future.result() if html: page_data parse_page(html) all_data.extend(page_data) print(f完成: {future_map[future]}获取 {len(page_data)} 条) time.sleep(random.uniform(0.5, 1.5)) return all_data有朋友问并发和分布式到底哪个好我的看法很直接这个规模用不到分布式。并发解决的是“单机多任务并行”分布式解决的是“机器集群调度”那是另一个量级的问题。学术机构页面通常也就几百个列表页4 个线程加随机延时已经绰绰有余堆太多线程反而容易被网站限流得不偿失。3.3 数据清洗与落库抓下来只是开始抓下来的数据不是拿来就能用文本里有大量空格、换行、异常字符标题里可能混入“!--”这种注释残留。我习惯用一个小函数做清洗import re def clean_text(text): if not text: return text re.sub(r\s, , text) # 多个空白压缩成一个 text text.replace(\u3000, ) # 去掉全角空格 return text.strip()清洗完后把数据存成 CSV 或者 SQLite。CSV 适合给人看、给 Excel 打开SQLite 适合后续做查询和统计。我建议两个都写反正代码也不复杂import csv import sqlite3 def save_to_csv(data, filenameresearch_data.csv): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, link, date]) writer.writeheader() writer.writerows(data) def save_to_sqlite(data, db_pathresearch.db): conn sqlite3.connect(db_path) conn.execute(CREATE TABLE IF NOT EXISTS papers (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, link TEXT, date TEXT)) seen set() for item in data: if item[title] in seen: continue seen.add(item[title]) conn.execute( INSERT INTO papers (title, link, date) VALUES (?, ?, ?), (item[title], item[link], item[date]), ) conn.commit() conn.close()编码这里有个常见坑如果直接存 CSV 用utf-8用 Excel 打开会出现中文乱码。需要改成utf-8-sigExcel 才能正确识别。这个坑我踩过不止一次写出来省得大家再踩。3.4 从标题到“前沿趋势”关键词统计才是这个项目的灵魂采集只是第一步真正让这个项目有价值的是后面的分析。我做趋势分析核心就三步分词、过滤、统计。中文分词用的是 jieba它是目前最方便的中文分词库。研究标题里有很多专业术语比如“深度学习”“图像分割”它们不是一个字一个字拆开的而是一个完整词汇jieba 能根据词典把它们切开。import jieba from collections import Counter STOPWORDS set([ 一种, 一个, 基于, 及其, 方法, 研究, 分析, 应用, 中, 的, 与, 和, 面向, 提出, ]) def extract_keywords(text): words jieba.lcut(text) return [w for w in words if len(w.strip()) 1 and w.strip() not in STOPWORDS and not w.isdigit()] def keyword_trend(data): all_words [] for item in data: title item.get(title, ) year item.get(year, 未知) words extract_keywords(title) all_words.extend(words) counter Counter(all_words) return counter.most_common(20)如果还想看时间趋势可以按年份分组分别统计关键词频次比如 2022 年出现最多的 10 个词2023 年的是什么2024 年又变成什么。这样就能直观看出“某某技术”是否在从热门走向衰退或者“某个新词”是不是最近一年突然火了。再进一步可以用 matplotlib 画个柱状图或词云。数据量不大画图完全没压力。我个人觉得柱状图比词云更适合趋势判断因为它能精确看到频次差异词云只能看个大概热度。4. 扒一扒高频踩坑点请求失败、解析为空与反爬应对4.1 请求异常状态码、超时和重试这个项目最常遇到的是网络请求问题。status code 直接决定程序走向我把常见情况整理成了一张表现象可能原因处理方式403 Forbidden请求头不规范被服务器拒绝换 User-Agent或换访问频率404 Not Found页面 URL 写错或栏目结构变了检查列表页链接和分页规律503 Service Unavailable服务器过载或临时限制停止请求等待几分钟再试超时 timeout网络波动或页面响应慢增加 timeout加重试机制我封装fetch_page时已经把重试机制写进去了核心参数是 retries3。如果你发现某个站点特别慢可以把 timeout 从 10 改成 20重试次数改成 5但不要无限重试否则问题页面会拖垮整体效率。4.2 解析结果为空选择器、动态渲染和 JSON 内嵌解析结果是空的大部分原因有三个。第一个是选择器写错了。网站改版后 class 名称变了或者列表结构调整了。解决方法是回到浏览器开发者工具里重新复制选择器。我自己会先在 Python 交互式环境里单独输出soup.title和len(soup.select(...))确认选择器命中几个元素排查效率很高。第二个是数据是 JS 动态加载的。请求返回的 HTML 里根本没有标题列表文字是浏览器执行 JS 后才渲染出来的。对于学术机构网站这种情况不算少见尤其是一些用 Vue、React 做的后台展示站。这时候有两个选择换 Playwright 渲染页面或者去源码里找 JSON 数据。我个人更倾向后者因为不引入重型浏览器框架也能解决问题。第三个是数据内嵌在 script 标签里的 JSON 中。你可以用正则或 BeautifulSoup 把 script 内容抠出来再 json.loads 解析。例如import json import re script soup.find(script, textre.compile(rwindow\.data\s*)) if script: raw script.string match re.search(rwindow\.data\s*\s*(\[.*?\]);, raw, re.S) if match: data json.loads(match.group(1))这种处理方式能绕开 JS 渲染拿到干净的数据结构非常推荐掌握。4.3 反爬与访问频率怎么做到“不被封”和“不添乱”学术机构网站的反爬通常没有电商网站那么严格但也不代表可以无限制访问。我遇到过几次 403 和临时封 IP都是因为自己在调试时频繁刷新页面。后来我总结了几条接地气的经验单线程或低并发优先4 线程以内基本够用每两次请求之间加随机延时设置 0.5-2 秒随机值让访问节奏更自然如果发现页面开始返回 403 或者验证码立刻停止程序隔几分钟再继续尽量避开对方网站的访问高峰比如工作日的白天反而是他们服务器最忙的时候晚上跑会顺畅很多。这里顺便说一句网上经常有人讨论 IP 代理池怎么搭我的态度是对学术公开数据采集来说根本用不到那一套。设置合理频率、做好重试比堆 IP 有用得多也更省心。不要一上来就考虑“高并发 代理池”那是大数据采集场景才需要考虑的复杂度。4.4 中文乱码与编码别忽略响应头的 charset中文乱码是这个项目里特别容易遇到又特别容易被忽略的问题。requests 在解析响应内容时会根据 HTTP 头里的 charset 判断编码方式。但有些网站响应头没写清楚 charsetrequests 就会假设为 ISO-8859-1导致中文变成乱码。解决办法非常直接拿到响应后手动设置编码resp.encoding resp.apparent_encodingapparent_encoding是从内容本身推断出来的编码通常比响应的默认编码更可靠。如果这个办法还是乱码可以先根据页面源码的meta charset...判断再手动指定为utf-8或gbk。5. 我踩过之后才总结出来的几个细节多写几个功能不难但把爬虫写“稳”才是关键。整个过程我踩了一堆坑这几个细节如果提前知道能省不少时间。第一个细节先小批量测试再全量跑。我第一次直接跑了 200 页结果到了第 80 页 URL 规律变了爬到一半全是重复数据。正确做法是先爬 2 页打印结果看看确认无误后再放开全量。第二个细节保存数据时一定要带上时间戳和来源域名。采集时间、来源站点、原始链接都值得留下来。数据是动态变化的隔一段时间再爬一次对比两个版本才能知道哪些研究方向在升温、哪些在消退。没有时间戳的数据没法做时间趋势分析。第三个细节分词词典要动态补充。jieba 默认词典对学术术语的支持没有想象中友好比如“图神经网络”这种长词可能被切开。可以在代码里手动加入用户词典一行代码搞定jieba.add_word(图神经网络) jieba.add_word(大语言模型)第四个细节把请求模块和分析模块分开。不要把所有逻辑堆在一个文件里。我自己习惯分成crawler.py、parser.py、analyzer.py三个文件这样采集和统计解耦调试起来轻松很多。这个项目做完之后我把同样的套路套在了其他几个科研机构的页面上无非是把选择器和分页规则换一换整体框架完全复用。如果你只在某个站点上做一次性采集没必要追求复杂架构但如果想持续跟踪学术趋势建议把采集脚本做成可以定时跑的模块配合 pandas 做周期性对比这个价值会比单纯爬数据大得多。
返回列表