第2章:文献数据源类型——PDF、HTML、XML、数据库

做文献抽取,第一关就是跟数据源打交道。

你拿到的材料是PDF还是HTML?是XML还是直接从数据库导出的?这直接决定了你的解析策略。说实话,我在这个坑里摔过好几次,今天跟你聊聊我的经验。

2.1 四大数据源概览

先看一张总览图,把四种数据源的逻辑关系搞明白。

文献数据源类型及其抽取策略 文献数据源 PDF HTML XML 数据库 特征: 排版固定 文本/图像混合 OCR需求 特征: 结构扁平 标签嵌套 CSS干扰 特征: Schema约束 结构化 XPath抽取 特征: 字段类型 SQL查询 关联表 抽取难度:PDF ≈ HTML > XML > 数据库 说明:深色节点为数据源类型,黄色框为特征说明,底层为难度排序。

四种数据源,各有各的脾气。我个人习惯先把它们分成两大类:结构化和非结构化。

数据源 结构化程度 主战场 核心工具
PDF 低(视觉排版) 学术论文、报告、扫描件 PyMuPDF、Poppler、OCR库
HTML 中(半结构化) 网页文章、期刊网站 BeautifulSoup、LXML
XML 高(Schema约束) JATS、PubMed、TEI XPath、XSLT
数据库 最高(关系模型) 文献管理系统、元数据仓库 SQL、ORM

2.2 PDF:最难缠的对手

PDF几乎是每个做文献抽取的人都会遇到的第一堵墙。为什么?因为PDF本质上是打印描述语言,不是文档描述语言。它只告诉渲染器"在坐标(100,200)处放字母'A'",至于这个'A'是标题还是正文,它不管。

我在项目里遇到过一本医学期刊的PDF,文字被分割得七零八落。一段正常的摘要,读出来是一串乱码。原因很简单——PDF生成工具把每个字符当独立对象处理了。

2.2.1 常见PDF类型

  • 文本型PDF:文字可选中、可拷贝,直接提取即可
  • 扫描型PDF:其实是图像,需要OCR技术
  • 混合型PDF:部分文字可选,部分为图像(如公式、图表)
  • 表单型PDF:有填充字段,少见但携带结构化数据

避坑指南:我曾经以为所有PDF都能用同一个库搞定。结果在一次批量抽取时,PyMuPDF读出来全是空白,换pdfplumber也不行。最后发现文件是扫描版,得先跑OCR。所以……拿到PDF先看类型,别盲目上工具。

2.2.2 抽取策略

文本型PDF,用PyMuPDF(fitz)或者pdfplumber。扫描型,得上Tesseract OCR或者云服务。我的经验是:能用文本提取就别OCR,OCR会引入拼写错误和版面混乱。

import fitz  # PyMuPDF

doc = fitz.open("paper.pdf")
for page_num, page in enumerate(doc):
    text = page.get_text()
    print(f"--- Page {page_num+1} ---")
    print(text)

小技巧:如果你发现提取的文本顺序混乱,试试设置sort=True参数。PyMuPDF默认按物理坐标排序,有时候能救你一命。

2.3 HTML:半结构化的双刃剑

HTML比PDF友好得多——至少它有标签。但别高兴太早。实际网页的HTML通常夹杂大量广告、导航栏、侧边栏、推荐链接。你的目标文本可能藏在第5层嵌套的<div>里。

说白了,HTML抽取的核心不是提取,是去除噪声。

2.3.1 工具选择

  • BeautifulSoup:上手快,适合中小规模
  • LXML:速度快,适合大规模批处理
  • Readability算法:自动提取正文(Mozilla出品,适合新闻类页面)
from bs4 import BeautifulSoup

html = requests.get("https://example.com/article").text
soup = BeautifulSoup(html, "html.parser")

# 定位特定div,比如class="article-body"
content = soup.find("div", class_="article-body")
if content:
    text = content.get_text(separator="\n", strip=True)
    print(text)

避坑指南:不要直接用soup.get_text()提取全文。我踩过一次,抓下来500KB文本,实际有效内容只有20KB,其余全是页脚、版权声明和广告。先用选择器缩小范围。

2.3.2 动态HTML问题

现在很多期刊网站用JavaScript渲染内容,直接请求HTML拿不到数据。这需要Selenium或者Playwright来模拟浏览器。我不建议一上来就搞动态抓取——能静态搞定就别上浏览器,慢得太多了。

2.4 XML:结构化爱好者的天堂

如果你遇到XML,恭喜你,这是最规范的数据源。像JATS(Journal Article Tag Suite)这种标准格式,每个元素都有明确的语义标记。

比如JATS中的典型片段:

<article>
  <front>
    <article-meta>
      <title-group>
        <article-title>Deep Learning Applications in Medical Imaging</article-title>
      </title-group>
    </article-meta>
  </front>
  <body>
    <sec>
      <title>Introduction</title>
      <p>Recent advances in deep learning ...</p>
    </sec>
  </body>
</article>

XML的抽取规则很简单——用XPath或XSLT。我建议优先用XPath,因为它直接、可读。

from lxml import etree

tree = etree.parse("article.xml")
titles = tree.xpath("//article-title/text()")
abstracts = tree.xpath("//abstract/p/text()")

for title, abstract in zip(titles, abstracts):
    print(f"标题: {title}")
    print(f"摘要: {abstract}")
    print("---")

小技巧:如果XML有命名空间(namespace),XPath写法会变复杂。用xpath(namespaces)参数指定前缀映射,或者直接用local-name()绕过命名空间。

2.5 数据库:最省心的选择

数据库里的数据已经是结构化字段了。你要做的不是"抽取",而是"查询"。常见的场景包括:从文献管理系统的MySQL/PostgreSQL里导出元数据,或者从MongoDB里读取非标文档。

对于结构化数据库,核心问题反而不是抽取技术,而是数据质量和字段理解。比如同一个期刊名称在库里可能有三种写法:"Nature"、"Nature Journal"、"Nat."。

import sqlite3

conn = sqlite3.connect("papers.db")
cursor = conn.cursor()
cursor.execute("""
    SELECT title, authors, year, doi
    FROM papers
    WHERE year >= 2019
    ORDER BY year DESC
""")
for row in cursor.fetchall():
    print(f"[{row[2]}] {row[0]} — {row[1]}")

避坑指南:我碰到过一个数据库,同一个字段里既有作者全名,又混了机构名和邮箱,用逗号分隔。这种"伪结构化"数据比纯文本还坑,抽出后一定要做清洗和校验。

2.6 数据源选择优先级

如果让你重新选数据源,我的建议优先级是这样的:

  1. 数据库 > XML > HTML > PDF
  2. 为什么?结构化程度越高,抽取的准确率越高,质控成本越低。
  3. 现实情况是:你手里往往只有PDF或HTML,那就只能硬着头皮上。