场景
- 在开发生成
docx
文档时,也需要生成内部的word/document.xml
文档, 而生成xml
避免不了需要校验xml
的元素标签完整性,即开始和结束标签匹配。如果每次生成docx
文档还需要解压获取打开document.xml
文件来手动判断有效性效率就太低了。前面讲过可以通过python
来解压zip(docx)
格式, 当然也可以通过python
来校验生成的docx
文档是否有损坏。
说明
-
docx
实际上就是zip
格式, 我们可以把后缀名改为zip
,之后就能解压出来一些文件,里面的文字部分就是存储在word/document.xml
里的. -
在没有
python
之前,可以通过微软的XML Notepad免费软件打开xml
文档,根据弹出的错误提示窗口来判断XML
文档哪里有问题。我这里说的XML
文档都是上10M
的,用普通的文档编辑器打开是看不出问题的。 -
python
可以使用xml.sax
标准库模块来进行校验,速度快,内存使用少,无需生成庞大的DOM
对象。 -
xml.sax
的错误提示最重要的是指示某行:某列
出现什么错误: 比如
error.xml:5:6: mismatched tag
。但是这个错误提示并不全,最好能给出错误位置的前后100
个字符,这样能方便推理出什么原因造成这个XML
解析错误的. 我们可以通过错误提示的某行某列,某m行某n列即第m行,第n个字符,注意,并不是第n个字节,这对于中文多字节字符定位更加准确。 -
我这里写了一个方法
charSeek
来读取字符串到某行某列,并输出它的前后x个字符作为输出参考。注意,我们读取文本文件,内部是使用的TextIOBase,它不能使用文件对象的seek
来跳转,因为它是跳转字节的;但是可以使用readxx(size)
方法,它是读入指定个字符,通过readxx
方法,我们可以实现定位到第m行第n列的字符。
例子
XmlValidator.py
from io import SEEK_CUR, SEEK_SET
from xml.sax.handler import ContentHandler
from xml.sax import SAXParseException, make_parser
import click
class MyContentHandler(ContentHandler):
def startElement(self, name, attrs):
self.elementName = name
self.elementAttrs = attrs
self.isElementStart = True
self.elementConent = ""
def endElement(self, name):
self.elementName = name
self.isElementStart = False
def characters(self, content):
if content != None:
self.elementConent = content
def printElement(elementName,isStartElement,elementAttris,elementContent):
print("[<",end="")
if not isStartElement:
print("/",end="")
print(elementName,end="")
if(elementAttris != None):
for key,value in elementAttris.items():
print(" "+key+"="+value,end=" ")
print(">",end="")
if(elementContent != None):
print(elementContent,end="")
print("]")
def charSeek(file,offset,maxOffsetLength):
maxBuf = 1024
if maxBuf > offset:
maxBuf = offset
pos = 0
line = ""
while pos < offset:
if (pos + maxBuf) > offset:
maxBuf = offset - pos
line = file.readline(maxBuf)
pos = pos + maxBuf
return line[-maxOffsetLength:]
def readFileLineColumnContext(fileName,lineNumber,columnNumber,maxOffsetLength):
# print("[File:%s]-[Line:%d]-[Column:%d]-[MaxOffsetLength:%d]"
# % (fileName,lineNumber,columnNumber,maxOffsetLength))
with open(fileName,encoding="utf-8") as f:
n = 1
maxBuf = 1024
line = ""
while n < lineNumber:
n = n +1
lineEnd = False
while not lineEnd:
line = f.readline(maxBuf)
# print(line)
a = line[-1:]
if a == "\n":
lineEnd = True
break
# f.seek是针对字节的,不要使用,不然如果位置在一个多字节字符的中间,read解码utf-8字符会报错.
preLine = charSeek(f,columnNumber,maxOffsetLength)
b1 = f.read(maxOffsetLength)
print("-->前一行:"+line[-maxOffsetLength:],end="")
print("-->当前行:" +preLine+b1)
pass
@click.command(help="""校验XML格式完整性,请使用--help查看帮助\n
XmlValidator --path XML路径""")
@click.option('--path',default="",help="XML文件路径")
@click.pass_context
def parseFile(ctx,path):
if(len(path) == 0):
print("请输入有效XML路径!")
return
parser = make_parser()
handler1 = MyContentHandler()
parser.setContentHandler(handler1)
try:
parser.parse(path)
except SAXParseException as e:
print("-->解析错误: ["+str(e)+"]")
print("-->解析起始元素上下文-1: ",end="")
printElement(handler1.elementName,handler1.isElementStart,
handler1.elementAttrs,handler1.elementConent)
readFileLineColumnContext(path,e.getLineNumber(),e.getColumnNumber(),20)
def test():
return
if __name__ == "__main__":
print("Begin Parse!")
parseFile(obj={})
print("End Parse!")
pass
error.xml
<html>国
<div>中国的xxxxx</div>
<div>x国内国内国内国内国内x国内国内xx</div>
<p>asdfasdfa国内的.sdfasfa
</html>
使用和输出
python XmlValidator.py --path error.xml
-->解析错误: [..\tests\resources\error.xml:5:6: mismatched tag]
-->解析起始元素上下文-1: [<p> ]
-->前一行:sdfasdfa国内的.sdfasfa
-->当前行: </html>
下载
- 使用
pyinstaller
编译出的独立的EXE
程序。AutomaticPython