读写文本格式的数据

因为其简单的文件交互语法、直观的数据结构,以及诸如元组打包解包之类的便利功能,Python在文本和文件处理方面已经成为一门招人喜欢的语言。

pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。表6-1对它们进行了总结,其中read_csv和read_table可能会是你今后用得最多的。

读写文本格式的数据 - 图1

我将大致介绍一下这些函数在将文本数据转换为DataFrame时所用到的一些技术。这些函数的选项可以划分为以下几个大类:

·索引:将一个或多个列当做返回的DataFrame处理,以及是否从文件、用户获取列名。

·类型推断和数据转换:包括用户定义值的转换、缺失值标记列表等。

·日期解析:包括组合功能,比如将分散在多个列中的日期时间信息组合成结果中的单个列。

·迭代:支持对大文件进行逐块迭代。

·不规整数据问题:跳过一些行、页脚、注释或其他一些不重要的东西(比如由成千上万个逗号隔开的数值数据)。

类型推断(type inference)是这些函数中最重要的功能之一,也就是说,你不需要指定列的类型到底是数值、整数、布尔值,还是字符串。日期和其他自定义类型的处理需要多花点工夫才行。首先我们来看一个以逗号分隔的(CSV)文本文件:

  1. In [846]: !cat ch06/ex1.csv译注1
  2. a,b,c,d,message
  3. 1,2,3,4,hello
  4. 5,6,7,8,world
  5. 9,10,11,12,foo

由于该文件以逗号分隔,所以我们可以使用read_csv将其读入一个DataFrame:

  1. In [847]: df = pd.read_csv('ch06/ex1.csv')
  2.  
  3. In [848]: df
  4. Out[848]:
  5. a b c d message
  6. 0 1 2 3 4 hello
  7. 1 5 6 7 8 world
  8. 2 9 10 11 12 foo

我们也可以用read_table,只不过需要指定分隔符而已:

  1. In [849]: pd.read_table('ch06/ex1.csv', sep=',')
  2. Out[849]:
  3. a b c d message
  4. 0 1 2 3 4 hello
  5. 1 5 6 7 8 world
  6. 2 9 10 11 12 foo

注意:这里我用的是cat这个UNIX shell命令将文本的原始内容打印到屏幕上。如果你用的是Windows,则可以使用type来达到同样的目的。

并不是所有文件都有标题行。看看下面这个文件:

  1. In [850]: !cat ch06/ex2.csv
  2. 1,2,3,4,hello
  3. 5,6,7,8,world
  4. 9,10,11,12,foo

读入该文件的办法有两个。你可以让pandas为其分配默认的列名,也可以自己定义列名:

  1. In [851]: pd.read_csv('ch06/ex2.csv', header=None)
  2. Out[851]:
  3. X.1 X.2 X.3 X.4 X.5
  4. 0 1 2 3 4 hello
  5. 1 5 6 7 8 world
  6. 2 9 10 11 12 foo
  7.  
  8. In [852]: pd.read_csv('ch06/ex2.csv', names=['a', 'b', 'c', 'd', 'message'])
  9. Out[852]:
  10. a b c d message
  11. 0 1 2 3 4 hello
  12. 1 5 6 7 8 world
  13. 2 9 10 11 12 foo

假设你希望将message列做成DataFrame的索引。你可以明确表示要将该列放到索引4的位置上,也可以通过index_col参数指定"message":

  1. In [853]: names = ['a', 'b', 'c', 'd', 'message']
  2.  
  3. In [854]: pd.read_csv('ch06/ex2.csv', names=names, index_col='message')
  4. Out[854]:
  5. a b c d
  6. message
  7. hello 1 2 3 4
  8. world 5 6 7 8
  9. foo 9 10 11 12

如果希望将多个列做成一个层次化索引,只需传入由列编号或列名组成的列表即可:

  1. In [855]: !cat ch06/csv_mindex.csv
  2. key1,key2,value1,value2
  3. one,a,1,2
  4. one,b,3,4
  5. one,c,5,6
  6. one,d,7,8
  7. two,a,9,10
  8. two,b,11,12
  9. two,c,13,14
  10. two,d,15,16
  11.  
  12. In [856]: parsed = pd.read_csv('ch06/csv_mindex.csv', index_col=['key1', 'key2'])
  13. In [857]: parsed
  14. Out[857]:
  15. value1 value2
  16. key1 key2
  17. one a 1 2
  18. b 3 4
  19. c 5 6
  20. d 7 8
  21. two a 9 10
  22. b 11 12
  23. c 13 14
  24. d 15 16

有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其他模式译注2)。对于这种情况,可以编写一个正则表达式来作为read_table的分隔符。看看下面这个文本文件:

  1. In [858]: list(open('ch06/ex3.txt'))
  2. Out[858]:
  3. [' A B C\n',
  4. 'aaa -0.264438 -1.026059 -0.619500\n',
  5. 'bbb 0.927272 0.302904 -0.032399\n',
  6. 'ccc -0.264273 -0.386314 -0.217601\n',
  7. 'ddd -0.871858 -0.348382 1.100491\n']

该文件各个字段由数量不定的空白符分隔,虽然你可以对其做一些手工调整,但这个情况还是处理比较好。本例的这个情况可以用正则表达式\s+表示,于是我们就有了:

  1. In [859]: result = pd.read_table('ch06/ex3.txt', sep='\s+')
  2.  
  3. In [860]: result
  4. Out[860]:
  5. A B C
  6. aaa -0.264438 -1.026059 -0.619500
  7. bbb 0.927272 0.302904 -0.032399
  8. ccc -0.264273 -0.386314 -0.217601
  9. ddd -0.871858 -0.348382 1.100491

这里,由于列名比数据行的数量少译注3,所以read_table推断第一列应该是DataFrame的索引。

这些解析器函数还有许多参数可以帮助你处理各种各样的异形文件格式(参见表6-2)。比如说,你可以用skiprows跳过文件的第一行、第三行和第四行:

  1. In [861]: !cat ch06/ex4.csv
  2. # hey!
  3. a,b,c,d,message
  4. # just wanted to make things more difficult for you
  5. # who reads CSV files with computers, anyway?
  6. 1,2,3,4,hello
  7. 5,6,7,8,world
  8. 9,10,11,12,foo
  9. In [862]: pd.read_csv('ch06/ex4.csv', skiprows=[0, 2, 3])
  10. Out[862]:
  11. a b c d message
  12. 0 1 2 3 4 hello
  13. 1 5 6 7 8 world
  14. 2 9 10 11 12 foo

缺失值处理是文件解析任务中的一个重要组成部分。缺失数据经常是要么没有(空字符串),要么用某个标记值表示。默认情况下,pandas会用一组经常出现的标记值进行识别,如NA、-1.#IND以及NULL等:

  1. In [863]: !cat ch06/ex5.csv
  2. something,a,b,c,d,message
  3. one,1,2,3,4,NA
  4. two,5,6,,8,world
  5. three,9,10,11,12,foo
  6. In [864]: result = pd.read_csv('ch06/ex5.csv')
  7.  
  8. In [865]: result
  9. Out[865]:
  10. something a b c d message
  11. 0 one 1 2 3 4 NaN
  12. 1 two 5 6 NaN 8 world
  13. 2 three 9 10 11 12 foo
  14. In [866]: pd.isnull(result)
  15. Out[866]:
  16. something a b c d message
  17. 0 False False False False False True
  18. 1 False False False True False False
  19. 2 False False False False False False

na_values可以接受一组用于表示缺失值的字符串:

  1. In [867]: result = pd.read_csv('ch06/ex5.csv', na_values=['NULL'])
  2.  
  3. In [868]: result
  4. Out[868]:
  5. something a b c d message
  6. 0 one 1 2 3 4 NaN
  7. 1 two 5 6 NaN 8 world
  8. 2 three 9 10 11 12 foo

可以用一个字典为各列指定不同的NA标记值:

  1. In [869]: sentinels = {'message': ['foo', 'NA'], 'something': ['two']}
  2.  
  3. In [870]: pd.read_csv('ch06/ex5.csv', na_values=sentinels)
  4. Out[870]:
  5. something a b c d message
  6. 0 one 1 2 3 4 NaN
  7. 1 NaN 5 6 NaN 8 world
  8. 2 three 9 10 11 12 NaN

读写文本格式的数据 - 图2

读写文本格式的数据 - 图3

读写文本格式的数据 - 图4

逐块读取文本文件

在处理很大的文件时,或找出大文件中的参数集以便于后续处理时,你可能只想读取文件的一小部分或逐块对文件进行迭代。

  1. In [871]: result = pd.read_csv('ch06/ex6.csv')
  2.  
  3. In [872]: result
  4. Out[872]:
  5. <class 'pandas.core.frame.DataFrame'>
  6. Int64Index: 10000 entries, 0 to 9999
  7. Data columns:
  8. one 10000 non-null values
  9. two 10000 non-null values
  10. three 10000 non-null values
  11. four 10000 non-null values
  12. key 10000 non-null values
  13. dtypes: float64(4), object(1)

如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:

  1. In [873]: pd.read_csv('ch06/ex6.csv', nrows=5)
  2. Out[873]:
  3. one two three four key
  4. 0 0.467976 -0.038649 -0.295344 -1.824726 L
  5. 1 -0.358893 1.404453 0.704965 -0.200638 B
  6. 2 -0.501840 0.659254 -0.421691 -0.057688 G
  7. 3 0.204886 1.074134 1.388361 -0.982404 R
  8. 4 0.354628 -0.133116 0.283763 -0.837063 Q

要逐块读取文件,需要设置chunksize(行数):

  1. In [874]: chunker = pd.read_csv('ch06/ex6.csv', chunksize=1000)
  2.  
  3. In [875]: chunker
  4. Out[875]: <pandas.io.parsers.TextParser at 0x8398150>

read_csv所返回的这个TextParser对象使你可以根据chunksize对文件进行逐块迭代。比如说,我们可以迭代处理ex6.csv,将值计数聚合到"key"列中,如下所示:

  1. chunker = pd.read_csv('ch06/ex6.csv', chunksize=1000)
  2.  
  3. tot = Series([])
  4. for piece in chunker:
  5. tot = tot.add(piece['key'].value_counts(), fill_value=0)
  6.  
  7. tot = tot.order(ascending=False)

于是我们就有了:

  1. In [877]: tot[:10]
  2. Out[877]:
  3. E 368
  4. X 364
  5. L 346
  6. O 343
  7. Q 340
  8. M 338
  9. J 337
  10. F 335
  11. K 334
  12. H 330

TextParser还有一个get_chunk方法,它使你可以读取任意大小的块。

将数据写出到文本格式

数据也可以被输出为分隔符格式的文本。我们再来看看之前读过的一个CSV文件:

  1. In [878]: data = pd.read_csv('ch06/ex5.csv')
  2.  
  3. In [879]: data
  4. Out[879]:
  5. something a b c d message
  6. 0 one 1 2 3 4 NaN
  7. 1 two 5 6 NaN 8 world
  8. 2 three 9 10 11 12 foo

利用DataFrame的to_csv方法,我们可以将数据写到一个以逗号分隔的文件中:

  1. In [880]: data.to_csv('ch06/out.csv')
  2.  
  3. In [881]: !cat ch06/out.csv
  4. ,something,a,b,c,d,message
  5. 0,one,1,2,3.0,4,
  6. 1,two,5,6,,8,world
  7. 2,three,9,10,11.0,12,foo

当然,还可以使用其他分隔符(由于这里直接写出到sys.stdout,所以仅仅是打印出文本结果而已):

  1. In [882]: data.to_csv(sys.stdout, sep='|')
  2. |something|a|b|c|d|message
  3. 0|one|1|2|3.0|4|
  4. 1|two|5|6||8|world
  5. 2|three|9|10|11.0|12|foo

缺失值在输出结果中会被表示为空字符串。你可能希望将其表示为别的标记值:

  1. In [883]: data.to_csv(sys.stdout, na_rep='NULL')
  2. ,something,a,b,c,d,message
  3. 0,one,1,2,3.0,4,NULL
  4. 1,two,5,6,NULL,8,world
  5. 2,three,9,10,11.0,12,foo

如果没有设置其他选项,则会写出行和列的标签。当然,它们也都可以被禁用:

  1. In [884]: data.to_csv(sys.stdout, index=False, header=False)
  2. one,1,2,3.0,4,
  3. two,5,6,,8,world
  4. three,9,10,11.0,12,foo

此外,你还可以只写出一部分的列,并以你指定的顺序排列:

  1. In [885]: data.to_csv(sys.stdout, index=False, cols=['a', 'b', 'c'])
  2. a,b,c
  3. 1,2,3.0
  4. 5,6,
  5. 9,10,11.0

Series也有一个to_csv方法:

  1. In [886]: dates = pd.date_range('1/1/2000', periods=7)
  2.  
  3. In [887]: ts = Series(np.arange(7), index=dates)
  4.  
  5. In [888]: ts.to_csv('ch06/tseries.csv')
  6.  
  7. In [889]: !cat ch06/tseries.csv
  8. 2000-01-01 00:00:00,0
  9. 2000-01-02 00:00:00,1
  10. 2000-01-03 00:00:00,2
  11. 2000-01-04 00:00:00,3
  12. 2000-01-05 00:00:00,4
  13. 2000-01-06 00:00:00,5
  14. 2000-01-07 00:00:00,6

虽然只需一点整理工作(无header行,第一列作索引)就能用read_csv将CSV文件读取为Series,但还有一个更为方便的from_csv方法:

  1. In [890]: Series.from_csv('ch06/tseries.csv', parse_dates=True)
  2. Out[890]:
  3. 2000-01-01 0
  4. 2000-01-02 1
  5. 2000-01-03 2
  6. 2000-01-04 3
  7. 2000-01-05 4
  8. 2000-01-06 5
  9. 2000-01-07 6

更多信息请在IPython中查看to_csv和from_csv的文档。

手工处理分隔符格式

大部分存储在磁盘上的表格型数据都能用pandas.read_table进行加载。然而,有时还是需要做一些手工处理。由于接收到含有畸形行的文件而使read_table出毛病的情况并不少见。为了说明这些基本工具,看看下面这个简单的CSV文件:

  1. In [891]: !cat ch06/ex7.csv
  2. "a","b","c"
  3. "1","2","3"
  4. "1","2","3","4"

对于任何单字符分隔符文件,可以直接使用Python内置的csv模块。将任意已打开的文件或文件型的对象传给csv.reader:

  1. import csv
  2. f = open('ch06/ex7.csv')
  3.  
  4. reader = csv.reader(f)

对这个reader进行迭代将会为每行产生一个元组译注4(并移除了所有的引号):

  1. In [893]: for line in reader:
  2. ....: print line
  3. ['a', 'b', 'c']
  4. ['1', '2', '3']
  5. ['1', '2', '3', '4']

现在,为了使数据格式合乎要求,你需要对其做一些整理工作:

  1. In [894]: lines = list(csv.reader(open('ch06/ex7.csv')))
  2.  
  3. In [895]: header, values = lines[0], lines[1:]
  4.  
  5. In [896]: data_dict = {h: v for h, v in zip(header, zip(*values))}
  6.  
  7. In [897]: data_dict
  8. Out[897]: {'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}

CSV文件的形式有很多。只需定义csv.Dialect的一个子类即可定义出新格式(如专门的分隔符、字符串引用约定、行结束符等):

  1. class my_dialect(csv.Dialect):
  2. lineterminator = '\n'
  3. delimiter = ';'
  4. quotechar = '"'
  5.  
  6. reader = csv.reader(f, diaect=my_dialect)

各个CSV语支的参数也可以关键字的形式提供给csv.reader,而无需定义子类:

  1. reader = csv.reader(f, delimiter='|')

可用的选项(csv.Dialect的属性)及其功能如表6-3所示。

读写文本格式的数据 - 图5

注意:对于那些使用复杂分隔符或多字符分隔符的文件,csv模块就无能为力了。这种情况下,你就只能使用字符串的split方法或正则表达式方法re.split进行行拆分和其他整理工作了。

要手工输出分隔符文件,你可以使用csv.writer。它接受一个已打开且可写的文件对象以及跟csv.reader相同的那些语支和格式化选项:

  1. with open('mydata.csv', 'w') as f:
  2. writer = csv.writer(f, dialect=my_dialect)
  3. writer.writerow(('one', 'two', 'three'))
  4. writer.writerow(('1', '2', '3'))
  5. writer.writerow(('4', '5', '6'))
  6. writer.writerow(('7', '8', '9'))

JSON数据

JSON(JavaScript Object Notation的简称)已经成为通过HTTP请求在Web浏览器和其他应用程序之间发送数据的标准格式之一。它是一种比表格型文本格式(如CSV)灵活得多的数据格式。下面是一个例子:

  1. obj = """
  2. {"name": "Wes",
  3. "places_lived": ["United States", "Spain", "Germany"],
  4. "pet": null,
  5. "siblings": [{"name": "Scott", "age": 25, "pet": "Zuko"},
  6. {"name": "Katie", "age": 33, "pet": "Cisco"}]
  7. }
  8. """

除其空值null和一些其他的细微差别(如列表末尾不允许存在多余的逗号)之外,JSON非常接近于有效的Python代码。基本类型有对象(字典)、数组(列表)、字符串、数值、布尔值以及null。对象中所有的键都必须是字符串。许多Python库都可以读写JSON数据。我将使用json,因为它是构建于Python标准库中的。通过json.loads即可将JSON字符串转换成Python形式:

  1. In [899]: import json
  2.  
  3. In [900]: result = json.loads(obj)
  4.  
  5. In [901]: result
  6. Out[901]:
  7. {u'name': u'Wes',
  8. u'pet': None,
  9. u'places_lived': [u'United States', u'Spain', u'Germany'],
  10. u'siblings': [{u'age': 25, u'name': u'Scott', u'pet': u'Zuko'},
  11. {u'age': 33, u'name': u'Katie', u'pet': u'Cisco'}]}

相反,json.dumps则将Python对象转换成JSON格式:

  1. In [902]: asjson = json.dumps(result)

如何将(一个或一组)JSON对象转换为DataFrame或其他便于分析的数据结构就由你决定了。最简单方便的方式是:向DataFrame构造器传入一组JSON对象,并选取数据字段的子集译注5

  1. In [903]: siblings = DataFrame(result['siblings'], columns=['name', 'age'])
  2.  
  3. In [904]: siblings
  4. Out[904]:
  5. name age
  6. 0 Scott 25
  7. 1 Katie 33

第7章中关于USDA Food Database的那个例子进一步讲解了JSON数据的读取和处理(包括嵌套记录)。

注意:pandas团队正致力于为pandas添加原生的高效JSON导出(to_json)和解码(from_json)功能。不过目前还没开发完成。

XML和HTML:Web信息收集

Python有许多可以读写HTML和XML格式数据的库。lxml(http://lxml.de)就是其中之一,它能够高效且可靠地解析大文件。lxml有多个编程接口。首先我要用lxml.html处理HTML,然后再用lxml.objectify做一些XML处理。

许多网站都将数据放到HTML表格中以便在浏览器中查看,但不能以一种更易于机器阅读的格式(如JSON、HTML或XML)进行下载。我发现Yahoo!Finance的股票期权数据就是这样。可能你对这种数据不熟悉:期权是指使你有权从现在开始到未来某个时间(到期日)内以某个特定价格(执行价)买进(看涨期权)或卖出(看跌期权)某公司股票的衍生合约。人们的看涨和看跌期权交易有多种执行价和到期日,这些数据都可以在Yahoo!Finance的各种表格中找到。

首先,找到你希望获取数据的URL,利用urllib2将其打开,然后用lxml解析得到的数据流,如下所示:

  1. from lxml.html import parse
  2. from urllib2 import urlopen
  3.  
  4. parsed = parse(urlopen('http://finance.yahoo.com/q/op?s=AAPL+Options'))
  5.  
  6. doc = parsed.getroot()

通过这个对象,你可以获取特定类型的所有HTML标签(tag),比如含有所需数据的table标签。给这个简单的例子加点启发性,假设你想得到该文档中所有的URL链接。HTML中的链接是a标签。使用文档根节点的findall方法以及一个XPath(对文档的“查询”的一种表示手段):

  1. In [906]: links = doc.findall('.//a')
  2.  
  3. In [907]: links[15:20]
  4. Out[907]:
  5. [<Element a at 0x6c488f0>,
  6. <Element a at 0x6c48950>,
  7. <Element a at 0x6c489b0>,
  8. <Element a at 0x6c48a10>,
  9. <Element a at 0x6c48a70>]

但这些是表示HTML元素的对象。要得到URL和链接文本,你必须使用各对象的get方法(针对URL)和text_content方法(针对显示文本):

  1. In [908]: lnk = links[28]
  2.  
  3. In [909]: lnk
  4. Out[909]: <Element a at 0x6c48dd0>
  5.  
  6. In [910]: lnk.get('href')
  7. Out[910]: 'http://biz.yahoo.com/special.html'
  8.  
  9. In [911]: lnk.text_content()
  10. Out[911]: 'Special Editions'

因此,编写下面这条列表推导式(list comprehension)即可获取文档中的全部URL:

  1. In [912]: urls = [lnk.get('href') for lnk in doc.findall('.//a')]
  2.  
  3. In [913]: urls[-10:]
  4. Out[913]:
  5. ['http://info.yahoo.com/privacy/us/yahoo/finance/details.html',
  6. 'http://info.yahoo.com/relevantads/',
  7. 'http://docs.yahoo.com/info/terms/',
  8. 'http://docs.yahoo.com/info/copyright/copyright.html',
  9. 'http://help.yahoo.com/l/us/yahoo/finance/forms_index.html',
  10. 'http://help.yahoo.com/l/us/yahoo/finance/quotes/fitadelay.html',
  11. 'http://help.yahoo.com/l/us/yahoo/finance/quotes/fitadelay.html',
  12. 'http://www.capitaliq.com',
  13. 'http://www.csidata.com',
  14. 'http://www.morningstar.com/']

现在,从文档中找出正确表格的办法就是反复试验了。有些网站会给目标表格加上一个id属性。我确定有两个分别放置看涨数据和看跌数据的表格:

  1. tables = doc.findall('.//table')
  2. calls = tables[9]
  3. puts = tables[13]

每个表格都有一个标题行,然后才是数据行:

  1. In [915]: rows = calls.findall('.//tr')

对于标题行和数据行,我们希望获取每个单元格内的文本。对于标题行,就是th单元格,而对于数据行,则是td单元格:

  1. def _unpack(row, kind='td'):
  2. elts = row.findall('.//%s' % kind)
  3. return [val.text_content() for val in elts]

这样,我们就得到了:

  1. In [917]: _unpack(rows[0], kind='th')
  2. Out[917]: ['Strike', 'Symbol', 'Last', 'Chg', 'Bid', 'Ask', 'Vol', 'Open Int']
  3.  
  4. In [918]: _unpack(rows[1], kind='td')
  5. Out[918]:
  6. ['295.00',
  7. 'AAPL120818C00295000',
  8. '310.40',
  9. ' 0.00',
  10. '289.80',
  11. '290.80',
  12. '1',
  13. '169']

现在,把所有步骤结合起来,将数据转换为一个DataFrame。由于数值型数据仍然是字符串格式,所以我们希望将部分列(可能不是全部)转换为浮点数格式。虽然你可以手工实现该功能,但是pandas恰好就有一个TextParser类可用于自动类型转换(read_csv和其他解析函数其实在内部都用到了它):

  1. from pandas.io.parsers import TextParser
  2.  
  3. def parse_options_data(table):
  4. rows = table.findall('.//tr')
  5. header = _unpack(rows[0], kind='th')
  6. data = [_unpack(r) for r in rows[1:]]
  7. return TextParser(data, names=header).get_chunk()

最后,我对那两个lxml表格对象调用该解析函数并得到最终的DataFrame:

  1. In [920]: call_data = parse_options_data(calls)
  2.  
  3. In [921]: put_data = parse_options_data(puts)
  4.  
  5. In [922]: call_data[:10]
  6.  
  7. Out[922]:
  8. Strike Symbol Last Chg Bid Ask Vol Open Int
  9. 0 295 AAPL120818C00295000 310.40 0.0 289.80 290.80 1 169
  10. 1 300 AAPL120818C00300000 277.10 1.7 284.80 285.60 2 478
  11. 2 305 AAPL120818C00305000 300.97 0.0 279.80 280.80 10 316
  12. 3 310 AAPL120818C00310000 267.05 0.0 274.80 275.65 6 239
  13. 4 315 AAPL120818C00315000 296.54 0.0 269.80 270.80 22 88
  14. 5 320 AAPL120818C00320000 291.63 0.0 264.80 265.80 96 173
  15. 6 325 AAPL120818C00325000 261.34 0.0 259.80 260.80 N/A 108
  16. 7 330 AAPL120818C00330000 230.25 0.0 254.80 255.80 N/A 21
  17. 8 335 AAPL120818C00335000 266.03 0.0 249.80 250.65 4 46
  18. 9 340 AAPL120818C00340000 272.58 0.0 244.80 245.80 4 30

利用lxml.objectify解析XML

XML(Extensible Markup Language)是另一种常见的支持分层、嵌套数据以及元数据的结构化数据格式。本书所使用的这些文件实际上来自于一个很大的XML文档。

之前,我介绍了lxml库及其lxml.html接口。这里我将介绍另一个用于操作XML数据的接口,即lxml.objectify。

纽约大都会运输署(Metropolitan Transportation Authority,MTA)发布了一些有关其公交和列车服务的数据资料(http://www.mta.info/developers/download.html)。这里,我们将看看包含在一组XML文件中的运行情况数据。每项列车或公交服务都有各自的文件(如Metro-North Railroad的文件是Performance_MNR.xml译注6),其中每条XML记录就是一条月度数据,如下所示:

  1. <INDICATOR>
  2. <INDICATOR_SEQ>373889</INDICATOR_SEQ>
  3. <PARENT_SEQ></PARENT_SEQ>
  4. <AGENCY_NAME>Metro-North Railroad</AGENCY_NAME>
  5. <INDICATOR_NAME>Escalator Availability</INDICATOR_NAME>
  6. <DESCRIPTION>Percent of the time that escalators are operational
  7. systemwide. The availability rate is based on physical observations performed the morning of regular business days only. This is a new indicator the agency began reporting in 2009.</DESCRIPTION>
  8. <PERIOD_YEAR>2011</PERIOD_YEAR>
  9. <PERIOD_MONTH>12</PERIOD_MONTH>
  10. <CATEGORY>Service Indicators</CATEGORY>
  11. <FREQUENCY>M</FREQUENCY>
  12. <DESIRED_CHANGE>U</DESIRED_CHANGE>
  13. <INDICATOR_UNIT>%</INDICATOR_UNIT>
  14. <DECIMAL_PLACES>1</DECIMAL_PLACES>
  15. <YTD_TARGET>97.00</YTD_TARGET>
  16. <YTD_ACTUAL></YTD_ACTUAL>
  17. <MONTHLY_TARGET>97.00</MONTHLY_TARGET>
  18. <MONTHLY_ACTUAL></MONTHLY_ACTUAL>
  19. </INDICATOR>

我们先用lxml.objectify解析该文件,然后通过getroot得到该XML文件的根节点的引用:

  1. from lxml import objectify
  2.  
  3. path = 'Performance_MNR.xml'
  4. parsed = objectify.parse(open(path))
  5. root = parsed.getroot()

root.INDICATOR返回一个用于产生各个<INDICATOR>XML元素的生成器。对于每条记录,我们可以用标记名(如YTD_ACTUAL)和数据值填充一个字典(排除几个标记)译注7

  1. data = []
  2.  
  3. skip_fields = ['PARENT_SEQ', 'INDICATOR_SEQ', 'DESIRED_CHANGE', 'DECIMAL_PLACES']
  4.  
  5. for elt in root.INDICATOR:
  6. el_data = {}
  7. for child in elt.getchildren():
  8. if child.tag in skip_fields:
  9. continue
  10. el_data[child.tag] = child.pyval
  11. data.append(el_data)

最后,将这组字典转换为一个DataFrame:

  1. In [927]: perf = DataFrame(data)
  2.  
  3. In [928]: perf
  4. Out[928]:
  5. <class 'pandas.core.frame.DataFrame'>
  6. Int64Index: 648 entries, 0 to 647
  7. Data columns:
  8. AGENCY_NAME 648 non-null values
  9. CATEGORY 648 non-null values
  10. DESCRIPTION 648 non-null values
  11. FREQUENCY 648 non-null values
  12. INDICATOR_NAME 648 non-null values
  13. INDICATOR_UNIT 648 non-null values
  14. MONTHLY_ACTUAL 648 non-null values
  15. MONTHLY_TARGET 648 non-null values
  16. PERIOD_MONTH 648 non-null values
  17. PERIOD_YEAR 648 non-null values
  18. YTD_ACTUAL 648 non-null values
  19. YTD_TARGET 648 non-null values
  20. dtypes: int64(2), object(10)
  21.  
  22. Empty DataFrame
  23. Columns: array([], dtype=int64)
  24. Index: array([], dtype=int64)

XML数据可以比本例复杂得多。每个标记都可以有元数据。看看下面这个HTML的链接标记(它也算是一段有效的XML):

  1. from StringIO import StringIO
  2. tag = '<a href="http://www.google.com">Google</a>'
  3.  
  4. root = objectify.parse(StringIO(tag)).getroot()

现在就可以访问链接文本或标记中的任何字段了(如href):

  1. In [930]: root
  2. Out[930]: <Element a at 0x88bd4b0>
  3.  
  4. In [931]: root.get('href')
  5. Out[931]: 'http://www.google.com'
  6.  
  7. In [932]: root.text
  8. Out[932]: 'Google'

译注1:还是那句话,作者用的是UNIX,Windows下得用type。

译注2:这里的“模式”一词表示的是“字符串”。如果对此概念较模糊,建议阅读《数据结构》。

译注3:准确的说法应该是:列名的数量比列的数量少1。完整的说法应该是:列名“行”中“有内容的”字段数量比其他数据“行”中“有内容的”字段数量少1。

译注4:很明显,这里得到的结果不是元组而是列表。

译注5:意思是说可以选一部分字段。当然也可以全部选完。

译注6:该文件已经更名,但还是可以下载到相关的文件。

译注7:由于数据文件格式已经改变,所以这段代码不能直接执行了,需要按照新的数据格式稍微调整一下,不过也不麻烦,留给读者当做练习吧。