如何在Python中读取WPF扩展?

1 投票
1 回答
517 浏览
提问于 2025-05-18 21:18

我正在尝试用Python读取一个旧的文本文件。

这个文件是1995年写的,扩展名是“.WPF”。

我试过了:

f = open('/Users/zachary/Downloads/2R.WPF', mode = 'r')  
print(f.read())

如果我通过LibreOffice打开它,内容是可以正常显示的。

有没有什么提示可以告诉我如何用Python处理.WPF格式的文本?

链接地址:WTO争端解决DS2小组报告

有人把这个问题标记为重复,认为这个文件只是名字起错了,但看起来它并不是.doc文件,因为使用textract.process时返回了“这不是.doc”的错误。

相关问题:

  • 暂无相关问题
暂无标签

1 个回答

3

从文件的前几个字节就可以判断,这个文件是一个WordPerfect 5.x格式的文件(其中的x可以是012),这种文件格式大约可以追溯到1989年。

根据它的描述,Tika的Python接口应该能够帮你转换这个文件,不过对于文字处理软件的格式来说,这些较老的WordPerfect文件其实很容易解码,只需要普通的Python安装就可以了。

这个格式包含一个大的头部(其中包含了很多信息,比如文档是为哪个打印机格式化的、使用的字体列表,以及一些基本的“样式”信息——我在下面的程序中选择完全跳过这些),然后是一些普通文本,文本中夹杂着控制格式的二进制代码。

这些二进制代码有三种不同的形式:

  1. 单字节:0x0A表示回车,0xA9表示断行连字符,0xAA表示在该位置断行时的连字符,等等。
  2. 固定长度的多字节:这个字节后面跟着一个或多个说明。例如,0xC0是一个“特殊字符代码”。它后面跟着字符集的索引和该字符集内实际字符的索引。固定长度代码的最后一个字节总是再次是起始字节。
  3. 可变长度的多字节:这个代码确定了一个主要的格式类别,后面跟着一个次类别的指示;之后,两个字节(小端序)表示后续数据的长度(不包括前面的4个字节)。这个代码总是以相同的项目反向结束:两个字节(小端序)表示长度、次类别,然后是主要类别。

代码在0x00..0x1F0x7F..0xBF之间是单字节控制代码(并不是所有的都被使用)。0xC0..0xCF之间的代码是固定长度控制代码,具有各种预定义的长度。从0xD0开始的代码总是可变长度的。

仅凭这些信息,就可以提取出这个文档的纯文本,而跳过所有可能的格式。将输出代码与同一网站的PDF进行比较,可以揭示一些代码的含义,比如各种类型的回车、制表符,以及普通文本格式,比如粗体和斜体。

此外,脚注是以可变长度代码的形式存储在文本中的,因此需要某种形式的可重入解析器。

下面的Python 3程序是一个基本框架,你可以直接使用(它提取文本,并给出脚注的提示),或者可以启用底部注释掉的行,获取更多关于解析更多格式代码的信息。

# -*- coding: utf-8 -*-
import sys

WPType_None = 0
WPType_Text = 1
WPType_Byte = 2
WPType_Fixed = 3
WPType_Variable = 4

plain_remap = {10:'\n', 11:' ', 13:' ', 160:' ', 169:'-', 170:'-'}

WpCharacterSet = { 0x0121:'à', 0x0406:'§', 0x041c:u'’', 0x041d:u'‘', 0x041f:'”', 0x0420:'“', 0x0422:'—' }

textAttributes = [
    "Extra Large",
    "Very Large",
    "Large",
    "Small",
    "Fine",
    "Superscript",
    "Subscript",
    "Outline",
    "Italic",
    "Shadow",
    "Redline",
    "Double Underline",
    "Bold",
    "Strikeout",
    "Underline",
    "SmallCaps" ]

class WPElem:
    def __init__(self, type=WPType_None, data = [], code=None):
        self.type = type
        self.code = code
        if type == WPType_Text:
            self.data = data
        else:
            self.data = data

class WordPerfect:
    def __init__(self, filename):
        with open(filename, "rb") as file:
            self.data = bytearray(file.read())
        sig = ''.join(chr(x) for x in self.data[1:4])
        if self.data[0] != 255 or sig != 'WPC':
            raise TypeError('Invalid file type')
        self.data_start = self.data[4]+256*(self.data[5]+256*(self.data[6]+256*self.data[7]))
        self.length = len(self.data)
        self.elements = []
        self.parse (self.data_start, self.length)

    def parse (self, start,maxlength):
        pos = start
        while pos < maxlength:
            byte = self.data[pos]
            if byte in plain_remap:
                byte = ord(plain_remap[byte])
            if byte == 10 or byte >= 32 and byte <= 126:
                if len(self.elements) == 0 or self.elements[-1].type != WPType_Text:
                    self.elements.append(WPElem(WPType_Text, ''))
                self.elements[-1].data += chr(byte)
                pos += 1
            elif byte == 12:
                self.elements.append(WPElem(WPType_Text, '\n\n'))
                pos += 1
            elif byte == 0x8c:  # [HRt/Pg Break]
                self.elements.append(WPElem(WPType_Text, '\n'))
                pos += 1
            elif byte == 0x8d:  # [Ftn Num]
                self.elements.append(WPElem(WPType_Text, '[Ftn Num]'))
                pos += 1
            elif byte == 0x99:  # [HRt/Top of Pg]
                self.elements.append(WPElem(WPType_Text, '\n'))
                pos += 1
            elif byte == 0xc0 and pos+3 < maxlength and self.data[pos+3] == 0xc0:
                wpchar = self.data[pos+1]+256*self.data[pos+2]
                if wpchar in WpCharacterSet:
                    self.elements.append(WPElem(WPType_Text, WpCharacterSet[wpchar]))
                else:
                    self.elements.append(WPElem(WPType_Text, '{CHAR:%04X}' % wpchar))
                pos += 4
            elif byte == 0xc1 and self.data[pos+8] == 0xc1:
                # self.elements.append(WPElem(WPType_Fixed, self.data[pos:pos+7]))
                self.elements.append(WPElem(WPType_Text, '\t'))
                pos += 9
            elif byte == 0xc2 and self.data[pos+10] == 0xc2:
                # self.elements.append(WPElem(WPType_Fixed, self.data[pos:pos+9]))
                self.elements.append(WPElem(WPType_Text, '\t'))
                pos += 11
            elif byte == 0xc3:
                self.elements.append(WPElem(WPType_Fixed, self.data[pos:pos+1], '%s On' % textAttributes[self.data[pos+1]]))
                pos += 3
            elif byte == 0xc4:
                self.elements.append(WPElem(WPType_Fixed, self.data[pos:pos+1], '%s Off' % textAttributes[self.data[pos+1]]))
                pos += 3
            elif byte == 0xc6:
                self.elements.append(WPElem(WPType_Fixed, self.data[pos:pos+5]))
                pos += 6
            elif byte == 0xd6 and self.data[pos+1] == 0:    # Footnote
                self.elements.append(WPElem(WPType_Text, '[Footnote:'))
                length = self.data[pos+2]+256*self.data[pos+3]
                self.parse (pos+0x13, pos+length)
                pos += 4+length
                self.elements.append(WPElem(WPType_Text, ']'))

            else:
                self.elements.append(WPElem(WPType_Byte, [byte]))
                if byte >= 0xd0 and pos+4 <= maxlength:
                    length = self.data[pos+2]+256*self.data[pos+3]
                    if pos+4+length <= self.length:
                        if pos+4+length <= self.length and self.data[pos+4+length-1] == byte:
                            self.elements[-1].type = WPType_Variable
                            self.elements[-1].data += [x for x in self.data[pos+1:pos+length]]
                            pos += 4+length
                        else:
                            pos += 1
                    else:
                        pos += 1
                else:
                    pos += 1


if len(sys.argv) != 2:
    print("usage: read_wpf.py [suitably ancient WordPerfect file]")
    sys.exit(1)

wpdata = WordPerfect (sys.argv[1])

for i in wpdata.elements:
    if i.type == WPType_Text:
        print (i.data, end='')
'''
    elif i.code:
        print ('[%s]' % i.code, end='')
    elif i.type == WPType_Variable:
        print ('[%02X:%d]' % (i.data[0],i.data[1]), end='')
    else:
        print ('[%02X]' % i.data[0], end='')
'''

运行它会将文本打印到控制台:

$ python3 read_wpf.py 2R.WPF
        RESTRICTED
World Trade WT/DS2/R
    29 January 1996
Organization    
    (96-0326)
(.. several thousands of lines omitted for brevity ..)
8.2 The Panel recommends that the Dispute Settlement Body request the
United States to bring this part of the Gasoline Rule into conformity
with its obligations under the General Agreement.

你可以选择重写程序,将其存储到一个纯文本文件中,或者通过控制台重定向到一个文件。

我只为样本文件中出现的一小部分特殊字符添加了翻译。如果你想要一个功能齐全的版本,你需要查找90年代的资料,并为成千上万的字符提供Unicode翻译。

同样,我只“解析”了一些特殊格式代码,而且范围非常有限。如果你需要提取特定的格式——比如制表设置、边距、字体大小等等——你必须找到文件格式的完整规范,并为这些功能添加特定的解析代码。

撰写回答