提取rtf文件中的高亮文本
1. 问题描述
我最近需要处理一个 rtf 格式的命名实体识别的数据集,其中包含了一些高亮文本,不同的颜色代表不同的实体类型。我需要提取这些高亮文本,以便后续处理。
rtf 文件中的内容如图所示:
而 rtf 的格式比较复杂,下面是使用文本编辑器打开的 rtf 文件的内容:
{\rtf1\ansi\ansicpg936\deff0\deflang1033\deflangfe2052{\fonttbl{\f0\fnil\fprq2\fcharset134 \'cb\'ce\'cc\'e5;}{\f1\fswiss\fcharset238{\*\fname Arial;}Arial CE;}}
{\colortbl ;\red0\green0\blue0;\red255\green255\blue255;\red34\green177\blue76;\red255\green242\blue0;\red237\green28\blue36;\red214\green214\blue214;\red185\green122\blue87;\red63\green72\blue204;\red255\green174\blue201;\red163\green73\blue164;}
\viewkind4\uc1\pard\lang2052\f0\fs30\par
\'d7\'d3\'d3\'ce\'a3\'ba\cf5\highlight6\'bf\'d7\'d7\'d3\cf0\highlight0\'b5\'c4\'d6\'f8\'c3\'fb\'b5\'dc\'d7\'d3\'a3\'ac\'d0\'d5\'d1\'d4\'a3\'ac\'c3\'fb\'d9\'c8\'a3\'ac\'d7\'d6\'d7\'d3\'d3\'ce\'a3\'ac\'d2\'e0\'b3\'c6\ldblquote\'d1\'d4\'d3\'ce\rdblquote\ldblquote\'ca\'e5\'ca\'cf\rdblquote\'a3\'ac\cf4\highlight7\'b4\'ba\'c7\'ef\cf0\highlight0\'c4\'a9\cf8\highlight9\b\'ce\'e2\'b9\'fa\cf0\highlight0\b0\'c8\'cb\'a3\'ac\'d3\'eb\cf5\highlight6\'d7\'d3\'cf\'c4\cf0\highlight0\'a1\'a2\cf5\highlight6\b\'d7\'d3\'d5\'c5\cf0\highlight0\b0\'c6\'eb\'c3\'fb\'a3\'ac\ldblquote\cf3\highlight4\ul\b\'bf\'d7\'c3\'c5\'ca\'ae\'d5\'dc\cf0\highlight0\ulnone\b0\rdblquote\'d6\'ae\'d2\'bb\'a3\'ac\'d4\'f8\'ce\'aa\cf8\highlight9\b\'ce\'e4\'b3\'c7\cf2\highlight10\'d4\'d7\cf0\highlight0\b0\'a1\'a3\'d7\'c8\'ba\'f1\'a3\'ba\'d7\'cc\'c8\'f3\'d4\'f3\'b1\'bb\'a1\'a3\'d7\'c8\'a3\'ac\'cd\'a8\ldblquote\'d7\'cc\rdblquote\'a1\'a3\fs18\par
}
可以看到如果直接读取 rtf 文件的内容,工作量会比较大。所以需要借助一些工具来处理 rtf 文件。
2. 方法探索
一开始根据 chatgpt 的建议使用了 Python 的 Rtf15Reader 或者 PyRTF 来处理 rtf 文件,其中使用了 Rtf15Reader 的部分代码如下:
from pyth.plugins.rtf15.reader import Rtf15Reader
from pyth.plugins.plaintext.writer import PlaintextWriter
doc = Rtf15Reader.read(rtf_content)
text = PlaintextWriter.write(doc).getvalue()
但是这两个库都无法正确从我目前使用的 rtf 文件中提取高亮文本,由于我对 rtf 文件的格式也不是很了解,所以我也不知道这两个库无法正确处理的原因。
3. 解决方案
由于直接处理不好处理,所以我打算把 rtf 文件转换成 html 文件,然后再从 html 文件中提取高亮文本,毕竟自己对 html 文件比较了解,更容易提取出其中的高亮文本。
3.1. rtf 转 html
转换方式是使用 Python 的 win32 模块来调用 Word 来打开 rtf 文件并保存为 html 文件。
import os
import win32com.client as win32
import codecs
import shutil
# 创建一个Word应用对象
word = win32.gencache.EnsureDispatch('Word.Application')
# 获取文件夹中的所有文件
rtf_folder = 'rtf'
html_folder = 'html'
if not os.path.exists(html_folder):
os.makedirs(html_folder)
current_folder = os.path.dirname(os.path.abspath(__file__))
# 先获取一下rtf文件夹的所有文件,防止word打开时出现文件夹中有临时文件的情况
# 不然有可能会报错: pywintypes.com_error: (-2147352567, '发生意外。', (0, 'Microsoft Word', '很抱歉,找不到您的文件。该项目是否已移动、重命名或删除?\r (D:\\projects\\python\\...\\rtf\\~$015.rtf)', 'wdmain11.chm', 24654, -2146823114), None)
rtf_files = os.listdir(rtf_folder)
for filename in rtf_files:
# 检查文件是否是RTF文件
if filename.endswith('.rtf'):
# 获取文件的完整路径
rtf_path = os.path.join(current_folder, rtf_folder, filename)
# 打开RTF文件
doc = word.Documents.Open(rtf_path)
# 另存为HTML
wdFormatHTML = 8 # Word的HTML格式常数
html_path = os.path.join(current_folder, html_folder, filename.replace('.rtf', '.html'))
try:
doc.SaveAs(html_path, FileFormat=wdFormatHTML)
# 关闭文档
doc.Close()
# 以UTF-8编码保存HTML文件
with codecs.open(html_path, 'r', 'gbk') as file:
content = file.read()
with codecs.open(html_path, 'w', 'utf-8') as file:
file.write(content)
except Exception as e:
print('Error File:', filename)
print(e)
# 移除错误的html文件, 包括xxx.html和xxx.files目录
os.remove(html_path)
shutil.rmtree(html_path.replace('.html', '.files'), ignore_errors=True)
continue
# 关闭Word应用
word.Quit()
上述代码中有两个需要注意的地方:
- 由于 Word 打开 rtf 文件时会生成一些临时文件,如果直接使用
os.listdir
获取文件列表,可能会读取到这些临时文件从而导致报错,所以需要先获取文件列表,然后再处理文件。 - 由于 Word 保存 html 文件时默认使用 gbk 编码,不方便后续处理,所以需要读取后再以 utf-8 编码保存。此时如果使用浏览器打开 html 文件,可能会出现乱码,但是不影响后续处理。
3.2. 提取高亮文本
读取上一步生成的 html 文件,解析带有background
属性的span
标签,提取其中的文本。html 的格式大概如下:
<div class="WordSection1">
<p class="MsoNormal">
<span>other text</span>
<!-- 高亮文本在b标签内 -->
<b>
<span style="..."></span>
</b>
</p>
</div>
在读取 html 方面,js 具有一定优势,所以采用 nodejs 来处理 html 文件。cheerio 是一个类似 jQuery 的库,可以方便地解析 html 文件。
const fs = require("fs");
const path = require("path");
const cheerio = require("cheerio");
function parseHtmlFile(filePath) {
const content = fs.readFileSync(filePath, "utf-8");
const $ = cheerio.load(content);
let wordIndex = 0;
let allText = "";
// 高亮颜色与类型的对应关系
const colorMap = {
silver: "人物",
yellow: "知识点",
aqua: "朝代",
red: "文献",
lime: "事件",
gray: "官职",
fuchsia: "地名",
};
const resultMap = {};
$(".MsoNormal")
.children()
.each((i, elem) => {
// 依次遍历子元素
const text = $(elem).text();
allText += text;
if (elem.name === "b") {
// 找到高亮文本
const span = $(elem).find("span");
const style = span.attr("style");
const match = style.match(/background:(.*?)(;|$)/); // (;|$) 匹配分号或者字符串结尾
if (match) {
let color = match[1];
color = colorMap[color] || color;
const spanText = span.text();
if (!resultMap[color]) {
resultMap[color] = [];
}
resultMap[color].push({
text: spanText,
start: wordIndex,
end: wordIndex + spanText.length,
});
}
}
wordIndex += text.length;
});
return {
content: allText,
resultMap: resultMap,
};
}
function parseHtmlFilesInDirectory(dirPath) {
const files = fs.readdirSync(dirPath);
const results = [];
for (const file of files) {
if (path.extname(file) === ".html") {
const filePath = path.join(dirPath, file);
const result = parseHtmlFile(filePath);
results.push({ file, result });
}
}
return results;
}
const dirPath = "html";
const results = parseHtmlFilesInDirectory(dirPath);
/**
* 输出示例结果
* @param {*} results
*/
function getExampleOutput(results) {
const outputDir = "html-output";
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
// 写入到文件, utf-8 编码格式
// fs.writeFileSync('result.json', JSON.stringify(results, null, 2), 'utf-8');
for (const { file, result } of results) {
fs.writeFileSync(
path.join(outputDir, file.replace(".html", ".json")),
JSON.stringify(result, null, 2),
"utf-8"
);
}
}
/**
* 输出doccano格式结果, jsonl格式
* {"id": 1, "text": "text", "labels": [[0, 4, "label"]]}
* @param {*} results
*/
function getDoccanoOutput(results) {
const doccanoOutputDir = "doccano-output";
if (!fs.existsSync(doccanoOutputDir)) {
fs.mkdirSync(doccanoOutputDir);
}
const jsonlData = [];
for (const { file, result } of results) {
const id = file.replace(".html", "");
const { content, resultMap } = result;
const labels = [];
for (const key in resultMap) {
const items = resultMap[key];
for (const item of items) {
labels.push([item.start, item.end, key]);
}
}
// 按照起始位置排序
labels.sort((a, b) => a[0] - b[0]);
jsonlData.push({
id,
text: content,
label: labels,
Comments: [],
});
}
fs.writeFileSync(
path.join(doccanoOutputDir, "data.jsonl"),
jsonlData.map((item) => JSON.stringify(item)).join("\n"),
"utf-8"
);
}
// getExampleOutput(results);
getDoccanoOutput(results);
检测背景色采用了正则表达式,匹配background
属性的值,然后根据颜色值来判断高亮文本的类型。上面的colorMap
是颜色值与类型的对应关系,是通过事先输出所有的颜色和对应的文本,从而分析出来的。