第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