简介:这是一份面向高校计算机相关专业学生的课程实训资源,聚焦谷歌学术搜索词汇的自动化信息提取与表格保存,覆盖人工智能、通信工程、自动化、电子信息、物联网等方向,可直接用于毕业设计、课程设计、大作业或初期项目演示。压缩包共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?q=knowledge+graph&hl=zh-CN" resp = requests.get(url, timeout=15) print(resp.status_code, len(resp.text)) with open("scholar_preview.html", "w", encoding="utf-8") as f: f.write(resp.text)这段代码把请求结果写进本地文件。timeout=15是必要的,没有超时限制的请求会在网络异常时无限阻塞。hl=zh-CN参数会让界面返回中文,这会影响后续“被引用次数”这几个字的匹配方式,需要注意。拿到 HTML 文件后,重点搜索class="gs_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[@class="gs_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[@class="gs_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[@class="gs_rs"]') snippet = snippet_el[0].text_content().strip() if snippet_el else "" cited_el = item.xpath('.//div[@class="gs_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[@class="gs_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, start=0): base = "https://scholar.google.com/scholar" return base + f"?q={quote(query)}&hl=zh-CN&start={start}"start参数控制翻页,每页 10 条结果,第 2 页就是start=10。quote(query)是 URL 编码,避免中文关键词直接拼进链接导致乱码。User-Agent 用浏览器的,不能让它看起来像脚本。
3.2 单页抓取与多页遍历的完整闭环
一个搜索词往往有多页结果,单页抓取只是热身。下面的函数抓取某个查询词的前三页,并把解析结果追加到列表里。抓取间隔设为随机 2 到 4 秒,这是学术搜索场景下的惯例,既不给服务器造成压力,也不至于因为请求太密集被限制。
import time import random from lxml import html def fetch_query(session, query, pages=3): records = [] for page in range(pages): url = build_scholar_url(query, start=page * 10) try: resp = session.get(url, timeout=20) 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[@class="gs_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, filename="results.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="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(records)encoding="utf-8-sig"是关键参数,它会在 CSV 文件头部写入 BOM,这样 Excel 直接打开 CSV 时才不会出现中文乱码。newline=""是 csv 模块在 Windows 平台下的固定搭配,避免每行之间多出空行。
4. 多搜索词汇批量运行:自动化提取的任务组织与断点续跑
4.1 批量搜索词的三种组织方式
课程实训场景下,搜索词往往不是单个,而是一组。常见的有三种组织方式:写在 Python 列表里、放在 txt 文件每行一个、放在 CSV 的某一列。第三种最灵活,因为可以附带说明、分组标签、预期结果数范围等元信息。
with open("search_words.txt", "r", encoding="utf-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_dir="output"): os.makedirs(out_dir, exist_ok=True) done_file = os.path.join(out_dir, "done_words.txt") done_words = set() if os.path.exists(done_file): with open(done_file, "r", encoding="utf-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(f"skip {word}, already done") continue records = fetch_query(session, word, pages=3) safe_name = word.replace("/", "_").replace("\\", "_") save_to_csv(records, os.path.join(out_dir, f"{safe_name}.csv")) with open(done_file, "a", encoding="utf-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_retries=3, base_delay=3): for attempt in range(max_retries): try: resp = session.get(url, timeout=20) 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_retries=3已经足够覆盖大部分瞬时异常。
5. 爬虫并发设计到底哪个好:线程池、异步与学术场景的特殊约束
5.1 三种并发方案的适用边界
“爬虫 并发设计 到底哪个好”是高频讨论,但答案取决于目标站点。线程池(ThreadPoolExecutor)代码简单,适合 IO 密集型任务,是 requests 爬虫最常用的加速手段。异步(asyncio+aiohttp)并发更高,但要求把整个请求层改成非阻塞,改动量大。多进程适合 CPU 密集场景,对爬虫几乎没有意义,因为你等的是网络响应,不是计算。
5.2 学术搜索场景的并发红线:先控速,再提速
谷歌学术这类学术搜索引擎对单个来源的请求频率非常敏感。批量场景下,我通常把总请求速率压在每秒 0.5 个以内,也就是平均每 2 秒一个请求。用线程池加速的正确方式,是限制同时运行的线程数,而不是把所有词一次性丢进去。
from concurrent.futures import ThreadPoolExecutor, as_completed def run_batch_concurrent(search_words, out_dir="output", max_workers=3): os.makedirs(out_dir, exist_ok=True) with ThreadPoolExecutor(max_workers=max_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(f"finished {word}, {len(records)} records") except Exception as e: print(f"failed {word}: {e}")max_workers=3是速度与安全之间的折中。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_dir="output", excel_path="scholar_report.xlsx"): csv_files = list(Path(csv_dir).glob("*.csv")) with pd.ExcelWriter(excel_path, engine="openpyxl") 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_name=sheet_name, index=False) all_data.append(df) if all_data: summary = pd.concat(all_data, ignore_index=True) summary.to_excel(writer, sheet_name="汇总", index=False) missing_cited = summary["cited_by"].isna().sum() missing_year = summary["year"].eq("").sum() title_count = summary["title"].nunique() print(f"total rows: {len(summary)}, unique titles: {title_count}") print(f"rows 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 表格就是整个爬虫链路最终交付物。搜索词列表改一改,出口文件换一个名字,整套流程就能复用到下一次课程实训或小型文献调研中,这也正是把“信息汇总”四个字落到实处的意义所在。
本文还有配套的精品资源,点击获取