文本获取:Web爬虫基础、学术API使用

文本从哪来?这是动手做知识图谱之前第一个要解决的问题。

很多人一上来就扎进爬虫,到处找代理、调浏览器模拟、绕反爬。嗯,我不反对这种方式,但说实话,在学术知识图谱这个领域,**80%的高质量数据都可以通过学术API拿到**。直接爬网页往往性价比不高——你花了大量时间处理页面结构,最终拿到的还是非结构化文本,还得自己拆。

这一章我会带你摸清两条路:什么时候用爬虫,什么时候调API。这两条路不冲突,关键看场景。

文献文本获取核心路径 获取源 Web爬虫(网页获取) 请求:Requests / urllib 解析:BeautifulSoup / lxml 提取非结构化文本 学术API(结构化获取) Semantic Scholar API 响应JSON解析 提取结构化元数据 数据清洗 → 入库

Web爬虫:什么时候用?怎么用?

学术API覆盖不了所有场景。比如你想爬某个学院的全部教师主页,或者某些会议网站的PDF附件链接,这些数据API一般不提供。这时候就只能上爬虫了。

我第一次写爬虫踩过一个坑——直接用浏览器开发者工具拷贝XPath,跑起来全挂。为什么?因为页面是动态拼接的,拷贝的XPath压根匹配不上真实DOM。后来我养成了一个习惯:先用 requests 拿到页面原始HTML,再用 lxml 的 etree.HTMLParser 手动构造解析树,一层一层试。笨是笨了点,但稳。

HTTP请求:基础中的基础

Python里做请求,你选哪个?我建议新人直接从 requests 库入手。API设计友好,出错信息也比较清晰。

import requests

url = "https://example.edu/projects"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
response = requests.get(url, headers=headers, timeout=10)

if response.status_code == 200:
    html_content = response.text
    print(f"页面长度: {len(html_content)} 字符")
else:
    print(f"请求失败: {response.status_code}")
小习惯:每次请求都带上合理的User-Agent。我见过有人空着UA直接请求学术网站,被403拦了一整个下午。

解析:从HTML到结构化文本

拿到HTML之后,怎么抽取出你要的论文标题、摘要、作者列表?

我用得最多的组合是 lxml.html + CSS选择器。相比XPath,CSS选择器更直观,尤其对于前端开发者熟悉的class和id选择方式。

from lxml import html

tree = html.fromstring(html_content)

# 提取所有论文标题(假设页面中标题在 .paper-title 的h3标签内)
titles = tree.cssselect(".paper-title h3")
for t in titles:
    print(t.text_content().strip())

# 提取摘要(.abstract 下的p标签)
abstracts = tree.cssselect(".abstract p")
for a in abstracts:
    print(a.text_content().strip()[:200])

这里有个细节我要特意说一下:text_content() 拿到的是所有子文本的拼接,包括子标签里的内容。如果你只想取当前标签的直接文本,用 text 属性。

注意:很多学术网站会动态加载数据。requests拿到的可能只是一个空白骨架。碰到这种情况,你得检查页面源代码(Ctrl+U)确认目标内容是否真的在HTML里。如果不在,要么用Selenium模拟浏览器,要么放弃爬虫改用API。

爬虫的“度”:别把别人服务器爬崩了

我见过有人写了一个for循环,不设延迟,直接对同一台服务器发起每秒几十次请求。结果IP被永久封禁。倒不是对方小气,是你的爬虫行为太像攻击了。

我自己通常会加三样东西:

  • 请求间隔:至少1秒,最好2-3秒
  • 重试机制:502/503时等几秒再试一次
  • robots.txt检查:别碰被禁止的路径
import time
import random

def crawl_with_gap(url, min_delay=2):
    time.sleep(min_delay + random.uniform(0, 1))
    return requests.get(url, headers=headers)

# 使用示例
for page_id in range(1, 11):
    resp = crawl_with_gap(f"https://example.edu/papers?page={page_id}")
    # 处理resp...

学术API:高效获取结构化数据

做知识图谱我最喜欢用API。为什么?因为它直接返回JSON,字段清晰,不用你从HTML里费劲地抽。 Semantic Scholar API、OpenAlex、CrossRef,这些都是业内常用的。

先看Semantic Scholar API。这个API对学术文本抽取来说特别好用——它不只能返回论文基本信息,还能返回引用关系、影响力数据、甚至嵌入向量。

基本调用

import requests

base_url = "https://api.semanticscholar.org/graph/v1/paper/search"
params = {
    "query": "knowledge graph construction",
    "limit": 10,
    "fields": "title,abstract,authors,year,externalIds"
}
response = requests.get(base_url, params=params, timeout=10)

if response.status_code == 200:
    data = response.json()
    for paper in data["data"]:
        print(f"标题: {paper['title']}")
        print(f"年份: {paper['year']}")
        print(f"摘要: {paper['abstract'][:150] if paper['abstract'] else '无'}")
        print("---")
else:
    print(f"API错误: {response.status_code}")

核心技巧:用 fields 参数控制返回字段,减少不必要的数据传输。学术API通常有速率限制,Semantic Scholar API的免费额度是100次/分钟,对大多数项目都够用。

批量获取:用论文ID查询

有时候你已经有论文列表(比如从某个会议网站爬到的DOI或ArXiv ID),想批量拉元数据。这时候用批量查询接口最合适。

papers_ids = [
    "CorpusId:215416146",
    "ArXiv:1806.01261",
    "DOI:10.1145/3340531.3412037"
]

bulk_url = "https://api.semanticscholar.org/graph/v1/paper/batch"
payload = {"ids": papers_ids}
resp = requests.post(bulk_url, json=payload, timeout=15)

if resp.status_code == 200:
    results = resp.json()
    for paper in results:
        if paper:
            print(paper.get("title", "Unknown"))
else:
    print(f"批量查询失败: {resp.status_code}")
我踩过的一个坑:批量接口POST请求,参数得放JSON body里,不能拼在URL上。我第一次用的时候把ids直接放到URL里,结果返回400——接口抱怨请求太大,因为URL长度超限了。

引用关系抽取:知识图谱的核心

做知识图谱离不开引用关系。Semantic Scholar API提供了专门引用图接口,可以拿到论文的引用和被引用列表。

paper_id = "CorpusId:215416146"
citations_url = f"https://api.semanticscholar.org/graph/v1/paper/{paper_id}/citations"
params = {
    "fields": "title,authors,year",
    "limit": 20
}
resp = requests.get(citations_url, params=params, timeout=10)

if resp.status_code == 200:
    citation_data = resp.json()
    for item in citation_data["data"]:
        citing_paper = item["citingPaper"]
        print(f"引用来源: {citing_paper['title']} ({citing_paper['year']})")

爬虫 vs API:怎么选?

我把两者的适用场景整理成了一个表,你选方案前可以参考:

维度 Web爬虫 学术API
数据类型 非结构化HTML / PDF文本 结构化JSON / XML元数据
获取成本 需要解析,开发量大 直接解析JSON,开发量小
速率限制 取决于网站策略,容易被封 有明确API限流文档
覆盖范围 几乎所有公开网页 仅限于该平台收录的文献
典型场景 教授主页、PDF附件、不在API内的内容 论文元数据、引用网络、影响力指标

从我的经验来看,混合方案往往最实用。先用API拿到元数据和引用关系,再用爬虫补充那些API覆盖不到的文本内容。两条腿走路,比单靠哪一条都快。