Skip to main content

Pywhio

官方 @ PywhiO. Python大数据, AI技术, 微服务, DRF+REACT前后端分离实践.

4 min read · April 6th 2020 (originally published at pywhio)

基于ordered dict 的XML 应用编程方案

阅读:-


1 基于ordered dict 的XML 应用编程方案

1.1 基础语法:

1.1.1 os.path.basename(path)

返回path最后的文件名。如果path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素。

>>> import os

>>> path = '/Users/beazley/Data/data.csv'

>>> # Get the last component of the path

>>> os.path.basename(path)

'data.csv'

1.1.2 Csv组件写入

lines = [range(3) for i in range(5)] #[[0,1,2],[0,1,2],[0,1,2],[0,1,2],[0,1,2]]
writer.writerows(lines) #同时写入多行信息

或:

for line in lines:
    writer.writerow(line) #一次写入一行信息

1.1.3 Set()

集合(set)是一个无序的不重复元素序列。

可以使用大括号 { } 或者 set() 函数创建集合,注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。

虽然大家目前还未发现set这种结构的优势,如果你需要处理大规模的数据的时候,set就可以实现非常高效的数据检索处理。

从某种意义上说,大家可以吧set理解为是一种 hash化的dict{};

1.1.4 sys.path.append

当我们导入一个模块时:import xxx,默认情况下python解析器会搜索当前目录、已安装的内置模块和第三方模块

当运行脚本文件和导入模块不再同一目录下

import sys
sys.path.append(r‘/home/***/work/)

就能继续import进去该模块了

永久添加路径到sys.path中,方式有三,如下: 1)将写好的py文件放到 已经添加到系统环境变量的 目录下 ; 2) 在 /usr/lib/python2.6/site-packages 下面新建一个.pth 文件(以pth作为后缀名)

将模块的路径写进去,一行一个路径,如: vim pythonmodule.pth /home/liu/shell/config /home/liu/shell/base 3) 使用PYTHONPATH环境变量 export PYTHONPATH=$PYTHONPATH:/home/liu/shell/config

1.2 设计方案概述

由于xml文件解析是一个非常通用的组件,存在很大的复用价值,所以我们考虑采用对象的方式来封装这个功能,虽然从面向对象理论的角度,要定义面向对象非常的负责,而从应用的对象的角度,我们主要理解如何观察一个对象就ok了。 观察的视角见下图: 1586134679576

图 7-1 如何观察一个对象

1.2.1 Class Ticket(object)

 在这里,我们考虑封装了一个Class  Ticket(object) 类,其中包含了用于进行xml解析的主要的方法。相关的类图说明如下:

1586134663625

       图 7-1Ticket(object)的信息结构和operation

从上图可见,因为本案例中是要将xml结构映射为CSV结构,所以我们需要一种信息结构(容器)来承载相关的信息结构。下面我们看看相关类Ticket(object)

    表 7-1 用于实现xml2csv的数据对象
序号名称类型功能
1filename字符串待解析文件名
2header_deflist[]期待输出CSV表头
3headerOrderedDict()保存csv的表头信息
4recordOrderedDict()保存cSV的行数据
5recordslist[]保存多行数据
6parsed布尔类型记录文件是否解析

1.2.1.1 Header+records

在实现一个xml文件转换为csv文件的时候,核心是要能够表达相关的信息结构,就是表头和数据。

相关的声明信息结构的代码是:

       header = OrderedDict() # OrderdDict是顺序存储字典,Dict能够有效映射半结构数据,但Python的默认Dict是随机存储。#
       records = []
       record = OrderedDict()

借助python强大的内生结构,我们可以很方便的表达CSV数据结构。这个结构简要说明如下:

1,records 是一个顺序存储字典,用于记录一个CSV的表头信息结构。

2,records 是一个列表,用于记录CSV文件的多个行数据。一个records是由多个record组成的。

3,record是一个顺序存储字典,用于记录一行数据。

下面是一条记录,供智慧的你来理解这个数据结构。

1586134860425

图 7-2 用于管理CSV的数据结构

1.2.2 parse_xml(self)

 这是此处最重要的一个构造函数,实现对xml文件的解析处理, 相关的解析思路,在4.2.4 XML解析的后续遍历法已经载明。

此处使用了ETree 的核心语句是:

for event, elem in ET.iterparse(self.filename)

iterparse 默认使用end event, 即碰到尾标签后生成对应的elem。因此,解析逻辑可以从相关叶子结点写,想当于树遍历算法的【后序遍历】(Postorder Traversal)

1.2.2.1 待解析的xml 结构

借助上面的树形结构的概念,我们来看看银行的ticket交易清单的XML文件的树形结构。

1586134892481

图 7-3 银行 ticket 的XML 树形结构

从上图可见, 我们将一个具体的交易清单记录封装在子节点measurement下。 在这个节点下,一共有三个字节点:

子节点1: objecttype 用于描述这个数据结构在现实世界的对象名称。

子节点2:ticket name 用于描述银行交易清单的表头结构(column field) 。

子节点3: ticket data 用于记录多条具体的交易记录的数据。子节点三下面还有多个子节点, 子节点的数量和ticket data中包含的记录数相当。

从上面的描述中,聪明的你是否感觉到很奇怪,为什么要把ticket name,和ticket data 分离在不同的子节点中。 因为从现实的事件来看,ticket name 代表的是key, 而ticket data 代表的是 value。 大家直观的想法,是参考fileheader 这个节点的表达方式,将key ,和value 放置在同一个节点中进行表达?

这是因为,在现实的情况中,可能会存在着很多条交易记录, 假如将(key,value )的结构在同一个节点中表达,那么会耗费很多的空间来保存key的信息,而这些信息是完全一致的,存在大量的冗余信息。

所以在这里,采用的是将ticket name(key), ticket data(value) 进行分离保存的方案。 在这个方案中,压缩了key的保存空间,所以比较优势一些。当然,这种处理方案也存在一个缺点,就是在解码的时候,会增加一些障碍。而这正是python的优势,可以使用一些善巧的结构,例如dict 和set来简洁的处理这种情况。

1.2.2.2 解析方案讨论

![1586135081073](./1586135081073.png)

 图 7-4  常见的XML解析方案

Python 提供三种场景的XML方案, 其中ElememtTree 的方案 可以提供一种即易于理解,又高效利用内存的解决方案。所以对于立志攻克python大数据的你来说,选择ElememtTree是最具有生产力的一种方案。

在使用Element tree 的时候,常常要考虑使用一种“非阻塞的增量解析法”,相关的文档描述如下:

Most parsing functions provided by this module require the whole document to be read at once before returning any result. It is possible to use an XMLParser and feed data into it incrementally, but it is a push API that calls methods on a callback target, which is too low-level and inconvenient for most needs. Sometimes what the user really wants is to be able to parse XML incrementally, without blocking operations, while enjoying the convenience of fully constructed Element objects.

翻译:Element tree module提供的绝大部分parsing 方法需要首先将整个xml文档加载到内存中去,再返回解析响应结果。而在内存受限的情况下,我们有可能更希望使用一个XMLParser ,采用增量加载的方式向解析器传递数据,但是这是一种基于callback() 编程的一种push API的应用模式,而回调编程是一种非常底层的函数调用编程技术,往往很难理解。有时,应用编程人员希望使用一种可以增量解析XML的接口,是非阻塞操作的模式,同时可以基于一种结构化的 Element 对象来设计。

从上面的文字我们可以发现, 假如我们把整个XML文件加载到内存中,会导致巨大的内存开销; 而假如我们使用一种push API的模式来进行增量解析,需要使用回调的编程模型,这个对非计算机专业的你来说,可能很难理解。所以作为一个大数据的新人,我们最好是选择一种折衷的方案,即能够通过XML文件部分加载的方式进行文件处理,又可以使用一种完全结构化的方案来进行处理。这样兼顾了效率和编程的简洁性。这就是所有的pull API的模式。

具体的参数见下面的文字 说明:

Parses an XML section into an element tree incrementally, and reports what’s going on to the user. source is a filename or file object containing XML data. events is a sequence of events to report back. The supported events are the strings “start”, “end”, “start-ns” and “end-ns” (the “ns” events are used to get detailed namespace information). If events is omitted, only “end” events are reported. parser is an optional parser instance. If not given, the standard XMLParser parser is used. parser must be a subclass of XMLParser and can only use the default TreeBuilder as a target. Returns an iterator providing (event, elem) pairs.

翻译:解析器会将一个xml的局部内容增量的解析成为一个完整的节点树,同时将解析过程中识别的事件情况报告给应用程序。 其中输入数据是一个文件名,或者是一个包含XML data的文件对象。而events 是用于向应用程序(解析器的调用者)反馈的事件序列。解析器支持的事件包括下列字符串: “start”, “end”, “start-ns” and “end-ns”(“ns” events是用于获取详细的 namespace 的信息结构)。If events is omitted, only “end” events are reported. 在缺省的情况下,只有”end” 事件会回传给应用程序。解析器会反馈一个迭代器,用于提供(event, elem) 信息结构.

Note that while iterparse() builds the tree incrementally, it issues blocking reads on source (or the file it names). As such, it’s unsuitable for applications where blocking reads can’t be made. For fully non-blocking parsing, see XMLPullParser.

請注意,儘管iterparse()逐步構建樹,但它會在源(或其命名的文件)上發出阻塞讀取。 因此,它不適用於無法進行阻止讀取的應用程序。 有關完全無阻塞的分析,請參見XMLPullParser。

使用的函数方式是:

xml.etree.ElementTree.``**iterparse**(source, events=None, parser=None)

1.2.2.3 XML解析的后序遍历法

  要想做好XML的文件解析,有一个要义是“从后向前,从内到外”。

简要解释如下:

1,从后向前: 在识别XML标签的时候,需要定位一个标签,识别出一个标签,会促发一个event,并转入后续的处理,比较好的方式是以定位结尾标签,作为触发事件。具体参数选择是”end”。

2,从内到外: 在提取数据的时候,先提取最内层(最底层)叶子节点的信息结构,在提取高一层的叶子阶段,采用逐层向上的提取方式,最后提取根节点的信息结构。

1586135131363

  图 4-5 增量的提取方案

从上图可见,整个的处理过程中,是以识别“尾标签”为触发的识别事件。 所以在识别的过程中

1,首先识别出ticket name 中包含的column field的尾部标签(N标签) ,总共有8个 取值。也就是8个叶子节点的数据全部识别出来。

2,接下来识别到父亲节点的尾部标签,这就提取了ticket name这个信息结构。这样就完成了针对表头数据部分的识别。

3,接下来识别到第一个ticket data 的 information field 的尾部标签, 提取其中的取值。在这个层级,总共有8个叶子标签。(V标签)

4,接下来识别对于的父亲节点的尾部标签,也就是ticket的尾部标签,这标志着第一个行数据的结束。

随着从上往下的文件数据装载, 循环执行第3步,第4步,直到把所有的行数据都识别成功。

5,最后识别到ticket data 的尾部标签,完成对数据部分的识别。

讨论1: 大家看看,这种从后往前提取的优势。

1,首先采用从尾部标签定位的方式,对信息结构的定界比较简单。可以显式的知道一个信息结构的定界结尾。

2,第二利用从尾部标签定位的方式,可以先定位儿子标签,再定位父亲标签,这样对构造父亲标签的结构比较简单,消除构建数据结构的不确定性。(因为完成父亲标签构造的时候,已经搞清楚了儿子标签的状况)

3, 先识别的底层标签,识别完毕以后,就可以释放叶子节点的数据,这样需要缓存的数据比较少,内存占用比较经济。

采用这种方案,可以非常优雅的处理XML 文件,而且处理的方式非常的易于理解。

1586135145369

  图 7-6 基于代码的解读

1.2.3 header_check(self)

在xml解析的时候,往往不需要提取全部的字段,往往是按需提取字段,所以我们可以确定待解析的XML文件中,是否包含了期待的数据结构。所以我们就设计了一个函数header_check(self),用于判断待解析的文件,是否可以正常进行解析。

   header_def = set(self.header_def) #将Dict转换为Set,便于进行比较操作。

    header = set(self.header.values()) #Dict的keys()和values()返回iter key or value

    return header <= header_def #Python Set 可以使用 <= 表达 issubset操作。

1.2.4 对象实例化

经过类的封装以后,后续就可以多次使用相关的实例对象,相关的代码见下图:

1586135184313

当我们有多个xml文件需要解析的时候,就可以方便进行处理。

1.2.5 讨论

1586135202146

  图1: XML 解析过程中的数据模型

从上图可见,由于xml解析过程中,利用ET的后向增量解析法, 是一种动态的解析方法,所以需要一种结构来记录相关的elem对象中包含的数据结构。此时 ordered dict {} 就是非常适合的一种结构。

header = OrderedDict() # OrderdDict是顺序存储字典,Dict能够有效映射半结构数据,但Python的默认Dict是随机存储。由于dict的高效的内存特性,我们可以非常方便的管理大规模的数据映射。所以也非常适合,动态的数据映射处理过程。

1.3 流穿越

1.3.1 xml_parser

用例代码如下:

1586135267503

1.3.1.1 filelist

1586135294633

1.3.1.2 for fn in filelist :

fn数据结构如下:

1586135312750

1.3.1.3 fn_csv = dist_dir + os.path.basename(fn) if dist_dir else fn

fn_csv

1586135350715

fn_csv = os.path.splitext(fn_csv)[0]+‘.csv’

fn_csv

1586135363467

1.3.2 类 Ticket(object)的初始化方法

用例代码如下:

1586135408446

1.3.2.1 ext= os.path.splitext(self.filename)[1].lower()

ext:

1586135478741

用于识别输入文件对象的后缀

1.3.3 parse_xml

用例代码如下:

1586135513130

1.3.3.1 header[elem.attrib[‘i’]] = elem.text

1586135527297

图 7-7 表头数据

处理的代码为:

if elem.tag == ‘N’ :

header[elem.attrib[‘i’]] = elem.text

# iterparse 默认使用end event, 即碰到尾标签后生成对应的elem。因此,解析逻辑可以从相关叶子结点写,想当于树遍历算法的【后序遍历】(Postorder Traversal)

elem.attrib[‘i’]

1586135545407

elem.text

1586135574063

header 利用ordered Dict() 结构保存的csv文件的头部信息

1586135742279

Colname=header[elem.attrib[‘i’]]

1586135775468

1.3.3.2 record[colname] = elem.text.strip()

1586135847211

图7-8 V标签的信息结构

record = OrderedDict()

if elem.tag == 'V' :

   colname = header[elem.attrib['i']]

   record[colname] = elem.text.strip()
利用ordered Dict() 结构保存的csv文件的数据信息 

1586136054717

1.3.3.3 records.append(record)

这个结构就完美的实现了对CSV的多行数据的存储 1586135202146

records

1586136078318

1.3.3.4 self.records = records

由于python的class的高度封装特性,我们可以一个list作为类的一个属性。 1586134663625

1586136171240

1.3.3.5 elem内存回收机制

elem.clear() #在处理完当前标签数据后,立即清理内存,可显著节省内存开销。

目前的模式中,是每次处理完了1条elem就回收1次,这样就避免了内存的过度占用。

前面有讨论过:

有时,应用编程人员希望使用一种可以增量解析XML的接口,是非阻塞操作的模式,同时可以基于一种结构化的 Element 对象来设计。

所以,这里及时的进行清空操作,是非常重要的。

1.3.3.6 ‘V’标签覆盖机制

 这里的record 作为一种有序的dict{}, 在遇到一组新的V标签的时候,是一种覆盖模式呀,所以不需要显式的来清空 record 这个字典。

代码说明如下:

1586136301621

1.3.1 get_FileSize文件大小测量板块

使用组件: OS

语法os.path.getsize(filename)测量文件大小

round(fsize,2)保留两位小数

流穿越:

1586136319654

代码:

1586136329445

1.3.2 header_check

用例代码如下:

1586136345539

1.3.2.1 self.header_def

1586136372080

1.3.2.2 header_def = set(self.header_def)

1586136388004

   header_def = set(self.header_def) #将Dict转换为Set,便于进行比较操作。#
   header = set(self.header.values()) #Dict的keys()和values()返回iter key or value#
   return header <= header_def #Python Set 可以使用 <= 表达 issubset操作。#

这里请大家注意,这是set的一种高级用法

1.3.2.3 self.header.values()

1586136591597

1.3.2.4 header = set(self.header.values())

header

1586136616934

1.3.2.5 header <= header_def

1586136625982

1.3.3 to_csv

用例代码如下:

1586136655406

1.3.3.1 filename:

1586136668341

1.3.3.2 self.filename

1586136677185

1.3.3.3 with open(filename, ‘w’, newline=”, encoding=“gbk”) as csvfile:

csvfile

1586136690612

1.3.3.4 csvwriter = csv.writer(csvfile)

csvwriter:

1586136701393

1.3.3.5 csvwriter.writerow(self.header_def)

self.header_def

1586136711963

1.3.3.6 for record in self.records:

self.records:

1586136722494

record

1.3.3.7 row = [ record[colname] for colname in self.header_def]

row:

1586136734292

1.3.3.8 csvwriter.writerow(row)

1586136782695

分享到微博

Previous

Python-Dict应用编程指南-下篇
Starter
MicroServ
Tutorials
Report
Blog