谷歌学术信息汇总爬虫:从搜索词到Excel的完整实现
简介这是一份面向高校计算机相关专业学生的课程实训资源聚焦谷歌学术搜索词汇的自动化信息提取与表格保存覆盖人工智能、通信工程、自动化、电子信息、物联网等方向可直接用于毕业设计、课程设计、大作业或初期项目演示。压缩包共190个文件体积约2.58MB核心代码为34个Python脚本用于爬虫调度与数据解析另有22个JavaScript、11个JSON、5个CSS、2个HTML等前端与配置文件以及1个xlsx结果表格构成从抓取、清洗到存储的完整链路同时包含C/C、C#等工程文件与详细设计文档便于理解整体架构与二次开发。资源已经严格测试功能完善稳定复现简单读者可在此基础上修改搜索词、调整输出格式或扩展为其他学术平台的数据采集工具。目前已有77人学习下载若运行配置遇到问题可获得远程指导与技术支持适合不同基础的学习者借鉴进阶。1. 课程实训包里最容易被低估的是把一个搜索词变成一张表的完整链路拿到“谷歌学术信息汇总爬虫”这类课程实训包解压后通常是一个爬虫脚本、一个依赖清单和一份文档。能跑通的人不少但真正能应对“换一个搜索词就报错、跑 50 个词被断掉、表格里字段对不上”的人不多。这个项目的核心并不只是爬虫本身而是把“搜索词汇列表”作为输入自动化提交到谷歌学术解析出标题、作者、年份、期刊、引用次数等结构化字段再保存成 Excel 或 CSV 的一整套流程。它同时涉及 requests 会话管理、HTML 解析、批量任务调度和表格写入是典型的 Python 爬虫入门到进阶的过渡项目。这篇文章会顺着这条链路把每一步的参数设置和常见坑位讲清楚适合正在做课设、以及想系统梳理爬虫工程化细节的开发者。2. 先拆解谷歌学术的信息结构搜索词汇自动化提取到底在提取什么2.1 谷歌学术结果页里可供提取的字段清单写爬虫之前得先看懂目标页面里有哪些值得落表的信息。谷歌学术的搜索结果页结构多年来相对稳定每个结果块通常包含以下几类内容标题与链接、作者列表与发表载体、发表年份、摘要片段、引用次数。其中“作者列表与发表载体”被放在同一行元信息里以逗号分隔这是解析时最容易出错的地方因为作者可能有多位期刊名里也可能包含逗号。字段名页面位置特征提取后用途title结果标题多为蓝色可点击链接论文题目去重、阅读判断link标题包裹的 href跳转原文、生成报告链接authors元信息行的第一个逗号片段合作者分析、检索人筛选venue / year元信息行的其余片段期刊来源与发表时间统计snippet摘要段落快速判断相关性cited_by“被引用次数”链接后的数字影响力排序、热词热度对比这个字段清单就是信息汇总的最小数据模型。爬虫写得再花哨最终表格里没有这些字段对使用者就没有价值。所以在写任何请求代码之前先把表头定下来search_word, title, authors, venue, year, snippet, cited_by, link, result_total, crawl_time。其中search_word和crawl_time是批量任务里额外加的两列用于区分“是哪次搜索产生的数据”以及“数据是什么时候抓的”。2.2 最小请求与 HTML 结构观察不要一上来就写完整爬虫。先发一个最简单请求把页面存成本地 HTML 文件用浏览器打开慢慢观察结构。这一步能帮你确认目标站点是否返回了预期内容也能让你避开“代码逻辑没错但页面结构已经不是预期”的尴尬。import requests url https://scholar.google.com/scholar?qknowledgegraphhlzh-CN resp requests.get(url, timeout15) print(resp.status_code, len(resp.text)) with open(scholar_preview.html, w, encodingutf-8) as f: f.write(resp.text)这段代码把请求结果写进本地文件。timeout15是必要的没有超时限制的请求会在网络异常时无限阻塞。hlzh-CN参数会让界面返回中文这会影响后续“被引用次数”这几个字的匹配方式需要注意。拿到 HTML 文件后重点搜索classgs_ri这是单个结果块的容器所有字段解析都围绕它展开。2.3 用 XPath 把结果块切成结构化记录谷歌学术没有官方公开的爬虫 API解析只能依赖页面结构。常见做法是先用lxml定位到每个.gs_ri结果块再在块内做二次提取。下面的parse_result函数接收一个结果块 Element返回一个字典这是后续所有工作的基础。from lxml import html import re def parse_result(item): title_el item.xpath(.//h3[classgs_rt]/a) if not title_el: return None title title_el[0].text_content().strip() link title_el[0].get(href, ) meta_el item.xpath(.//div[classgs_a]) authors venue_year if meta_el: parts [p.strip() for p in meta_el[0].text_content().split(,)] authors parts[0] if parts else venue_year ,.join(parts[1:]).strip() year_match re.search(r(\d{4}), venue_year) year year_match.group(1) if year_match else snippet_el item.xpath(.//div[classgs_rs]) snippet snippet_el[0].text_content().strip() if snippet_el else cited_el item.xpath(.//div[classgs_fl]//a[contains(text(), 被引用) or contains(text(), Cited)]) cited_by 0 if cited_el: cited_match re.search(r(\d), cited_el[0].text_content()) cited_by int(cited_match.group(1)) if cited_match else 0 return { title: title, authors: authors, venue_year: venue_year, year: year, snippet: snippet, cited_by: cited_by, link: link, }注意几个关键参数标题节点用h3[classgs_rt]/a定位但偶尔会有结果没有链接所以要做空值判断引用次数在.gs_fl容器中中文界面显示“被引用次数 12”英文界面显示“Cited by 12”正则用(\d)提取数字。这个函数只负责解析单条记录不关心请求怎么发隔离得越干净后续换页面结构时改动越小。3. 用 requests 跑通最小爬虫单个搜索词汇的自动化提取与表格落盘3.1 建立会话与搜索 URL 构造规则requests 的Session对象会保存 Cookie模拟连续浏览行为降低被断开的概率。搜索 URL 的构造其实就一个参数q。关键词中如果包含空格需要用urlencode处理或者干脆交给requests的params参数它会自动完成编码。import requests from urllib.parse import quote session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 }) def build_scholar_url(query, start0): base https://scholar.google.com/scholar return base f?q{quote(query)}hlzh-CNstart{start}start参数控制翻页每页 10 条结果第 2 页就是start10。quote(query)是 URL 编码避免中文关键词直接拼进链接导致乱码。User-Agent 用浏览器的不能让它看起来像脚本。3.2 单页抓取与多页遍历的完整闭环一个搜索词往往有多页结果单页抓取只是热身。下面的函数抓取某个查询词的前三页并把解析结果追加到列表里。抓取间隔设为随机 2 到 4 秒这是学术搜索场景下的惯例既不给服务器造成压力也不至于因为请求太密集被限制。import time import random from lxml import html def fetch_query(session, query, pages3): records [] for page in range(pages): url build_scholar_url(query, startpage * 10) try: resp session.get(url, timeout20) resp.raise_for_status() except requests.RequestException as e: print(f[{query}] page {page} failed: {e}) break tree html.fromstring(resp.text) items tree.xpath(//div[classgs_ri]) for item in items: parsed parse_result(item) if parsed: parsed[search_word] query records.append(parsed) time.sleep(random.uniform(2, 4)) return recordsresp.raise_for_status()会在响应码为 4xx 或 5xx 时直接抛异常避免把错误页面当成正常结果解析。random.uniform(2, 4)生成 2 到 4 秒之间的随机浮点数这个随机性比固定time.sleep(3)更接近人工操作也能避免多个任务同步踩点造成瞬时压力。3.3 表格保存CSV 先行Excel 收尾解析结果最终要落表。课程实训里最常见的保存需求是 Excel但从工程角度我建议先落 CSV确认数据没问题后再转 Excel。CSV 轻量、乱码可控、即使用记事本打开也能检查内容最适合做调试阶段的中间产物。import csv def save_to_csv(records, filenameresults.csv): if not records: print(no records, skip save) return fieldnames [search_word, title, authors, venue_year, year, cited_by, snippet, link] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(records)encodingutf-8-sig是关键参数它会在 CSV 文件头部写入 BOM这样 Excel 直接打开 CSV 时才不会出现中文乱码。newline是 csv 模块在 Windows 平台下的固定搭配避免每行之间多出空行。4. 多搜索词汇批量运行自动化提取的任务组织与断点续跑4.1 批量搜索词的三种组织方式课程实训场景下搜索词往往不是单个而是一组。常见的有三种组织方式写在 Python 列表里、放在 txt 文件每行一个、放在 CSV 的某一列。第三种最灵活因为可以附带说明、分组标签、预期结果数范围等元信息。with open(search_words.txt, r, encodingutf-8) as f: search_words [line.strip() for line in f if line.strip()]这段代码读取每行一个关键词的文本文件strip()去掉首尾空白和换行符if line.strip()过滤掉空行。用文件而不是硬编码列表最大的好处是换成下一个作业题目时不用动任何代码。4.2 断点续跑用已保存结果做增量抓取批量任务的真实痛点不是慢而是跑到一半中断。网络异常、机器重启、目标站行为变化都可能让前面几个小时的工作作废。解决办法是每完成一个搜索词就立即保存一个独立文件同时维护一个进度清单。import os from datetime import datetime def run_batch(search_words, out_diroutput): os.makedirs(out_dir, exist_okTrue) done_file os.path.join(out_dir, done_words.txt) done_words set() if os.path.exists(done_file): with open(done_file, r, encodingutf-8) as f: done_words set(line.strip() for line in f if line.strip()) for word in search_words: if word in done_words: print(fskip {word}, already done) continue records fetch_query(session, word, pages3) safe_name word.replace(/, _).replace(\\, _) save_to_csv(records, os.path.join(out_dir, f{safe_name}.csv)) with open(done_file, a, encodingutf-8) as f: f.write(word \n) print(f[{datetime.now()}] finished {word}, {len(records)} records)这里的关键机制是done_words。每次成功抓完一个词就把这个词追加写入done_words.txt。下次运行批量任务时先读这个文件跳过已经完成的词。这个机制不依赖数据库不依赖任务队列用最少的代码解决了“断点续跑”这个最实际的问题。文件名用safe_name清洗是因为 Windows 文件系统不允许文件名包含/和\\。4.3 失败重试指数退避而不是死磕单个请求失败时立即重试往往会再次失败。更好的策略是指数退避第一次等 3 秒第二次等 6 秒第三次等 12 秒超过最大次数就放弃这一页而不是放弃整个词。def get_with_retry(session, url, max_retries3, base_delay3): for attempt in range(max_retries): try: resp session.get(url, timeout20) if resp.status_code 200: return resp elif resp.status_code in (429, 403): wait base_delay * (2 ** attempt) random.uniform(0, 1) time.sleep(wait) except requests.RequestException: wait base_delay * (2 ** attempt) random.uniform(0, 1) time.sleep(wait) return Nonebase_delay * (2 ** attempt)是指数退避的核心算式第 0 次等 3 秒第 1 次等 6 秒第 2 次等 12 秒random.uniform(0, 1)加入抖动防止多个请求同时进入重试状态后继续同步踩踏。这里没有无限制重试max_retries3已经足够覆盖大部分瞬时异常。5. 爬虫并发设计到底哪个好线程池、异步与学术场景的特殊约束5.1 三种并发方案的适用边界“爬虫 并发设计 到底哪个好”是高频讨论但答案取决于目标站点。线程池ThreadPoolExecutor代码简单适合 IO 密集型任务是 requests 爬虫最常用的加速手段。异步asyncioaiohttp并发更高但要求把整个请求层改成非阻塞改动量大。多进程适合 CPU 密集场景对爬虫几乎没有意义因为你等的是网络响应不是计算。5.2 学术搜索场景的并发红线先控速再提速谷歌学术这类学术搜索引擎对单个来源的请求频率非常敏感。批量场景下我通常把总请求速率压在每秒 0.5 个以内也就是平均每 2 秒一个请求。用线程池加速的正确方式是限制同时运行的线程数而不是把所有词一次性丢进去。from concurrent.futures import ThreadPoolExecutor, as_completed def run_batch_concurrent(search_words, out_diroutput, max_workers3): os.makedirs(out_dir, exist_okTrue) with ThreadPoolExecutor(max_workersmax_workers) as executor: future_map { executor.submit(fetch_query, session, word, 3): word for word in search_words } for future in as_completed(future_map): word future_map[future] try: records future.result() safe_name word.replace(/, _).replace(\\, _) save_to_csv(records, os.path.join(out_dir, f{safe_name}.csv)) print(ffinished {word}, {len(records)} records) except Exception as e: print(ffailed {word}: {e})max_workers3是速度与安全之间的折中。3 个线程并行每个线程内部还有 2 到 4 秒的随机间隔实际请求频率大约是每 0.7 到 1.3 秒一个。如果还需要继续降速可以在每个fetch_query内部把random.uniform(2, 4)调大到random.uniform(5, 8)。成绩考核看的是结果质量和代码设计不是谁跑得快。5.3 千万别在进程级任务队列上加并发课程实训里最常见的过度设计是把 scrapy、celery 这类重型框架搬进来为 20 个搜索词维护一个分布式任务队列。这不是技术能力的问题是问题规模与工具不匹配。20 个词每词 3 页总共 60 个请求串行跑大约 3 分钟3 线程并发 1 分钟出头。这个量级下ThreadPoolExecutor是最合适的复杂度天花板。6. 表格汇总进阶多 Sheet 输出与数据质量校验批量跑完 20 个搜索词后会得到 20 个 CSV 文件。直接交给老师或同事 20 个文件不专业把它们合并成一个 Excel按搜索词分 Sheet再加一个汇总统计页才算真正完成了“信息汇总与表格保存”。import pandas as pd from pathlib import Path def merge_to_excel(csv_diroutput, excel_pathscholar_report.xlsx): csv_files list(Path(csv_dir).glob(*.csv)) with pd.ExcelWriter(excel_path, engineopenpyxl) as writer: all_data [] for csv_file in csv_files: df pd.read_csv(csv_file, dtype{year: str}) sheet_name csv_file.stem[:30] df.to_excel(writer, sheet_namesheet_name, indexFalse) all_data.append(df) if all_data: summary pd.concat(all_data, ignore_indexTrue) summary.to_excel(writer, sheet_name汇总, indexFalse) missing_cited summary[cited_by].isna().sum() missing_year summary[year].eq().sum() title_count summary[title].nunique() print(ftotal rows: {len(summary)}, unique titles: {title_count}) print(frows missing cited_by: {missing_cited}, missing year: {missing_year})dtype{year: str}用来防止年份被 pandas 读成浮点数2015 变成 2015.0。csv_file.stem[:30]截断 Sheet 名因为 Excel 的 Sheet 名最长 31 个字符。汇总 Sheet 还承担了数据质量校验的职责cited_by缺失数量如果超过总行数的 30%说明请求频率可能过高导致部分结果没有完整返回需要降速重跑。标题去重数量与总行数的偏差则可以侧面验证抓取结果是否覆盖了足够多的不同论文。这份多 Sheet Excel 表格就是整个爬虫链路最终交付物。搜索词列表改一改出口文件换一个名字整套流程就能复用到下一次课程实训或小型文献调研中这也正是把“信息汇总”四个字落到实处的意义所在。本文还有配套的精品资源点击获取