Pythonic 解析程序
Python 爱好者热衷于赞扬我们这门语言的美好优点。大多数初学 Python 程序员在经典的“hello world”之后,都会被邀请从解释器中运行 import this
。运行该命令后,最受欢迎的俏皮话之一是
There should be one-- and preferably only one --obvious way to do it.
但是,Python 启蒙之路往往布满崎岖的地形,或隐藏在树叶下的荆棘。
一个困境说到这里,我最近不得不使用一些解析文件的代码。当 API 围绕着我想要解析的内容会在符合 POSIX 标准的系统的文件系统中找到的假设进行优化时,问题就出现了。该实现是一个类上的 staticmethod
,名为 from_filepath
。好吧,在 2013 年,我们倾向于忽略文件,并将 70 年代那些老旧的工具抛在脑后,转而支持闪亮的新型超级动力杰克锤,称为 NoSQL。
碰巧的是,我发现自己有一个字符串(从 NoSQL 数据库中提取),其中包含了我想要解析的文件内容。没有文件,没有文件名,只有数据。但是我的 API 仅支持通过文件名访问。
也许务实的解决方案是简单地将内容放入临时文件中,然后就完成了
import tempfile
data = get_string_data() # fancy call out to NoSQL
with tempfile.NamedTemporaryFile() as fp:
fp.write(data)
fp.seek(0)
obj = Foo.from_filepath(fp.name)
但我花了一些时间思考问题的根源,并想看看其他人是如何解决的。拥有一个仅支持解析字符串的解析接口可能是在另一个极端上的过早优化。
轻松阅读我的第一个想法是寻找真理的源泉——Python 标准库。它肯定会通过阐明“Python 之禅”的所有 19 条准则来启发我。我问自己,我使用标准库中的哪些模块进行解析,并列出了以下列表
json
pickle
xml.etree.ElementTree
xml.dom.minidom
ConfigParser
csv
(注意:以上所有都是模块名称。xml.*
的嵌套命名空间违反了 Zen 准则 #5 “扁平胜于嵌套”,而 ConfigParser
违反了 PEP 8 命名约定。对于资深的 Python 程序员来说,这已经不是什么新闻了,但对于新手来说,这是一剂现实。标准库并不完美,并且有其怪癖。即使在 Python 3 中也是如此。而这仅仅是冰山一角。)
我查阅了这些模块的文档和源代码,以确定解析的最佳、最 Pythonic、美观、显式、简单、可读、实用、非歧义且易于解释的解决方案。具体来说,我应该解析文件名、类文件对象还是字符串?这是我整理出的结果表格
模块 |
字符串数据 |
文件 |
文件名 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(注意:pyyaml
是一个第三方库,但最近围绕 load
的命名有很多争议,load
是不安全的(但可能是大多数人会使用的方法,除非他们真的仔细研读了文档),而 safe_load
是安全的(并隐藏在文档中)。)
这个表格的诀窍是旋转三圈,真正眯起眼睛,然后从文件列中选择一些东西。
全面考虑的答案最简单的解析解决方案是解析类文件对象。再次为新手们说明,我说类文件对象是因为 Python 具有鸭子类型。根据维基百科,鸭子类型这个术语源自一位老诗人詹姆斯·惠特科姆·莱利的名言
当我看到一只鸟,它走路像鸭子,游泳像鸭子,叫声像鸭子,我就称那只鸟为鸭子。
但我更喜欢《蒙提·派森与圣杯》中的场景,当聪明的贝德米尔运用他的逻辑能力来确定女巫是否由木头制成,因此易燃——适合被烧死
BEDEMIR: So, how do we tell whether she is made of wood?
VILLAGER #1: Build a bridge out of her.
...
BEDEMIR: Aah, but can you not also build bridges out of stone?
VILLAGER #2: Oh, yeah.
BEDEMIR: Does wood sink in water?
VILLAGER #1: No, no.
VILLAGER #2: It floats! It floats!
BEDEMIR: What also floats in water?
VILLAGER #1: Bread!
VILLAGER #2: Apples!
VILLAGER #3: Very small rocks!
VILLAGER #1: Cider!
VILLAGER #2: Great gravy!
VILLAGER #1: Cherries!
VILLAGER #2: Mud!
VILLAGER #3: Churches -- churches!
VILLAGER #2: Lead -- lead!
ARTHUR: A duck.
CROWD: Oooh.
BEDEMIR: Exactly! So, logically...,
VILLAGER #1: If... she.. weighs the same as a duck, she's made of wood.
BEDEMIR: And therefore--?
VILLAGER #1: A witch!
(感谢 sacred-texts.com/neu/mphg/mphg.htm 提供)
我们可以将相同的原则——鸭子类型——应用于文件对象。如果一个对象的行为像文件——具有 read
方法——那么它就是一个文件。通过拥有这个接口,我们可以轻松地处理类文件对象,以及字符串数据和文件名,正如将要说明的那样。
从上面的表格中另一个有趣的注释是,大多数解析机制都与类无关。大多数是创建对象、从文件中的数据填充对象并返回对象的函数(ConfigParser.readfp
是例外,因为它没有函数,尽管 ElementTree
模块既有 parse
函数,也有 ElementTree
类上的 parse
方法)。此外,将解析与数据隔离符合单一职责原则。
作者的观点是,重载函数或方法(例如 ElementTree.parse
,它接受文件和文件名,或者 pyyaml.load
,它接受字符串或文件)违反了“显式胜于隐式”和“当存在歧义时,拒绝猜测的诱惑”。这些方法不是在利用鸭子类型,而是魔法类型,超越了 Postel 法则。Python 语言严格区分了可迭代对象和迭代器之间的区别。文件名和文件对象之间也可以进行类似的比较。一个是鸭子,另一个是鸭子工厂。
下面的示例解析 Unix /etc/passwd
文件的内容,并将结果存储在 Passwd
对象内的列表中
from collections import namedtuple
User = namedtuple('User', 'name, pw, id, gid, groups, home, shell')
class Passwd(object):
def __init__(self):
self.users = []
def parse(fp):
pw = Passwd()
for line in fp:
data = line.strip().split(':')
pw.users.append(User(*data))
return pw
关于实现的几点说明
parse
函数填充Passwd
对象Passwd
对象有一个职责,即了解用户。它不负责解析
如果用户想从文件名——/etc/passwd
——创建一个 Passwd
对象,这很容易
# from filename
with open('/etc/passwd') as fin:
pw = parse(fin)
parse
函数和 Passwd
对象都不需要担心打开或关闭文件。
如果文件中的数据已在内存中,则利用内置模块 StringIO
可以从该端进行解析
# from string
from StringIO import StringIO as sio
fin = sio('''pulse:x:113:121:PulseAudio daemon,,,:/var/run/pulse:/bin/false\nsaned:x:114:123::/home/saned:/bin/false''')
pw = parse(fin)
文件接口的优势
希望这篇长篇大论的文章为您提供了一些关于 Python 中解析文件的见解。先例可以在标准库和第三方模块中找到。如果您想变得花哨且面面俱到,您可以这样做,但实际上您可能只是在增加不必要的复杂性。
回顾一个仅依赖于文件接口的解析方法是
- 可扩展 - 除了文件、套接字和
StringIO
之外,还接受可能非常长的生成器、迭代器 - 更易于管理 - 您不想从事处理和关闭用户文件的业务
- 适用于拥有文件名的用户
- 适用于拥有文件数据的用户
前进并解析吧!