第4章:文本数据解析——PDF、Word、HTML、Markdown、JSON与CSV

做数据工程这几年,我处理过最多的就是文本数据。说实话,文本解析这件事,看着简单,坑却不少。你想想看,光是PDF这一种格式,就有扫描件、文字版、表格混排、带水印的……每种情况处理方式都不一样。

这一章,我带你逐个击破。咱们从最头疼的PDF开始。

4.1 PDF解析:PyMuPDF实战

PDF解析,我个人最常用的是PyMuPDF(也就是fitz库)。为什么选它?一个字:快。而且它对中文支持很好,我在项目中遇到过不少PDF,用其他库解析出来全是乱码,换PyMuPDF就搞定了。

先看基础用法:

import fitz  # PyMuPDF

# 打开PDF
doc = fitz.open("example.pdf")

# 遍历每一页
for page_num in range(len(doc)):
    page = doc[page_num]
    # 提取纯文本
    text = page.get_text()
    print(f"第{page_num+1}页内容:\n{text}")

doc.close()

嗯,这里要注意:get_text()默认提取的是纯文本,会丢失排版信息。如果你需要保留段落结构,可以用get_text("blocks")

我的小技巧: 遇到表格型PDF,先用page.get_text("dict")获取每个文本块的位置信息,再根据坐标重建表格。我在做财报数据提取时就是这么干的,准确率能到95%以上。

再来看一个更完整的例子,处理多页PDF并保存为结构化数据:

import fitz
import json

def parse_pdf_to_json(pdf_path):
    doc = fitz.open(pdf_path)
    result = []
    
    for page_num in range(len(doc)):
        page = doc[page_num]
        blocks = page.get_text("blocks")
        
        page_data = {
            "page_number": page_num + 1,
            "content": []
        }
        
        for block in blocks:
            # block结构: (x0, y0, x1, y1, text, block_no, block_type)
            page_data["content"].append({
                "text": block[4],
                "position": {"x": block[0], "y": block[1]},
                "type": "text" if block[6] == 0 else "image"
            })
        
        result.append(page_data)
    
    doc.close()
    return result

# 使用示例
data = parse_pdf_to_json("report.pdf")
with open("report.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
避坑指南: 我曾经遇到过一个PDF,用PyMuPDF解析出来全是空白。折腾了半天才发现,那是个扫描件,文字是图片形式。这种情况得先用OCR,后面我会专门讲。

4.2 Word文档解析

Word文档分两种:老版的.doc和新版的.docx。我建议你优先处理.docx,因为它是基于XML的,解析起来干净利落。

Python里处理.docx,我用python-docx这个库。它不仅能提取文字,还能保留样式信息。

from docx import Document

doc = Document("example.docx")

# 提取所有段落
for para in doc.paragraphs:
    print(f"样式:{para.style.name},内容:{para.text}")

# 提取表格
for table in doc.tables:
    for row in table.rows:
        for cell in row.cells:
            print(cell.text)

说实话,Word文档最麻烦的是表格。有些表格嵌套了好几层,提取时容易乱。我的做法是:先判断表格结构是否规整,不规整的就按纯文本处理。

核心要点: 处理Word文档时,别忘了检查doc.sections。页眉页脚里的信息有时候比正文还重要,比如文档版本号、日期等。

4.3 HTML与Markdown解析

HTML解析,我首推BeautifulSoup4。它容错性好,就算HTML写得乱七八糟也能解析。Markdown解析则用markdown库,或者直接转成HTML再处理。

先看HTML解析:

from bs4 import BeautifulSoup
import requests

# 从网页抓取
response = requests.get("https://example.com")
soup = BeautifulSoup(response.text, "html.parser")

# 提取所有段落
for p in soup.find_all("p"):
    print(p.get_text(strip=True))

# 提取表格数据
for table in soup.find_all("table"):
    for row in table.find_all("tr"):
        cells = row.find_all(["td", "th"])
        print([cell.get_text(strip=True) for cell in cells])

Markdown解析就简单多了:

import markdown

md_text = """
# 标题
这是**加粗**文字,这是*斜体*文字。
- 列表项1
- 列表项2
"""

# 转成HTML
html = markdown.markdown(md_text, extensions=['extra'])
print(html)

# 或者用mistune库,速度更快
import mistune
markdown_parser = mistune.create_markdown()
html_fast = markdown_parser(md_text)
我的经验: 处理HTML时,记得用soup.get_text(separator='\n', strip=True)来提取纯文本。这样能保留段落结构,不会所有文字挤在一起。

4.4 JSON与CSV处理

JSON和CSV是结构化数据里最常见的两种格式。JSON灵活,CSV简洁。处理它们,Python标准库就够了。

先看JSON:

import json

# 读取JSON文件
with open("data.json", "r", encoding="utf-8") as f:
    data = json.load(f)

# 处理嵌套结构
def flatten_json(data, parent_key='', sep='_'):
    items = []
    for k, v in data.items():
        new_key = f"{parent_key}{sep}{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(flatten_json(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

# 展平后转成表格
flat_data = flatten_json(data)
print(flat_data)

再看CSV:

import csv

# 读取CSV
with open("data.csv", "r", encoding="utf-8-sig") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row)  # 每行是一个字典

# 写入CSV
with open("output.csv", "w", newline="", encoding="utf-8-sig") as f:
    fieldnames = ["name", "age", "city"]
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerow({"name": "张三", "age": 28, "city": "北京"})
注意: CSV文件编码是个大坑。我遇到过好多次,用Excel打开乱码。解决办法是:写入时用utf-8-sig编码,这样Excel能正确识别。读取时也建议用utf-8-sig,兼容性最好。

4.5 实战:统一解析接口

在实际项目中,你不可能每次手动判断文件格式。我习惯写一个统一的解析器,自动识别并处理:

import os
import json
import csv
from pathlib import Path

class DocumentParser:
    def __init__(self, file_path):
        self.file_path = Path(file_path)
        self.extension = self.file_path.suffix.lower()
    
    def parse(self):
        """根据文件扩展名自动选择解析方法"""
        parsers = {
            '.pdf': self._parse_pdf,
            '.docx': self._parse_docx,
            '.html': self._parse_html,
            '.md': self._parse_markdown,
            '.json': self._parse_json,
            '.csv': self._parse_csv
        }
        
        parser = parsers.get(self.extension)
        if not parser:
            raise ValueError(f"不支持的文件格式: {self.extension}")
        
        return parser()
    
    def _parse_pdf(self):
        import fitz
        doc = fitz.open(self.file_path)
        text = ""
        for page in doc:
            text += page.get_text()
        doc.close()
        return text
    
    def _parse_docx(self):
        from docx import Document
        doc = Document(self.file_path)
        return "\n".join([p.text for p in doc.paragraphs])
    
    def _parse_html(self):
        from bs4 import BeautifulSoup
        with open(self.file_path, "r", encoding="utf-8") as f:
            soup = BeautifulSoup(f.read(), "html.parser")
        return soup.get_text(separator="\n", strip=True)
    
    def _parse_markdown(self):
        import mistune
        with open(self.file_path, "r", encoding="utf-8") as f:
            md_text = f.read()
        html = mistune.create_markdown()(md_text)
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        return soup.get_text(separator="\n", strip=True)
    
    def _parse_json(self):
        with open(self.file_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        return json.dumps(data, ensure_ascii=False, indent=2)
    
    def _parse_csv(self):
        with open(self.file_path, "r", encoding="utf-8-sig") as f:
            reader = csv.DictReader(f)
            return "\n".join([str(row) for row in reader])

# 使用示例
parser = DocumentParser("report.pdf")
content = parser.parse()
print(content[:500])  # 打印前500个字符
总结一下: 文本解析的核心就三点——选对工具、处理编码、统一接口。你把这三点吃透了,不管遇到什么格式都能从容应对。下一章,咱们聊聊更复杂的场景:扫描件和图片中的文字提取。

公众号:蓝海资料掘金营,微信deep3321