维基百科某网页表格的csv保存的分析
1、在阅读《Python网络数据采集》第五章的时候看到的案例,记录细节分析。
目标网址:
http://en.wikipedia.org/wiki/Comparison_of_text_editors
中的一个表格,
这是结果图:
2、代码:
import csv
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://en.wikipedia.org/wiki/Comparison_of_text_editors")
bsObj = BeautifulSoup(html, "html.parser")
#The main comparison table is currently the first table on the page
table = bsObj.findAll("table",{"class":"wikitable"})[0]
# print(table)
rows = table.findAll("tr")
# print(rows)
csvFile = open("c:\\van\\editors.csv", 'wt', newline='', encoding='utf-8')
writer = csv.writer(csvFile)
try:
for row in rows:
csvRow = []
for cell in row.findAll(['td', 'th']):
csvRow.append(cell.get_text())
writer.writerow(csvRow)
finally:
csvFile.close()
3、上述代码的主要目的,是把对应url的表格的文字信息,提取出来,保存到csv。那么他是怎么一步步做到的?
首先,我们来打开url看下网站的目标内容,如下图是一个色彩鲜艳的表格:
其次,我们查看下网页源代码,格式清晰的很,并且代码很长一共7910行的html,可这只是维基百科的一页而已!并且从这么多的html代码中快速的提取出我们需要的数据信息,应该怎么做呢?
一般来说,提取数据有2个比较通用的方法,
第一、无视他的源代码,我就查看目标内容的路径,可通过浏览器自带的copy xpath配合lxml提取,或者如果你习惯bs4的话,用类似方法。
第二,根据F12找到目标区域,比如一个表格的所在大的路径,然后由大往小的逐步提取。显然本文使用的是这个方法。当鼠标移动到
<table class="wikitable sortable jquery-tablesorter" style="text-align: center; font-size: 85%; width: auto; table-layout: fixed;">
这一行代码的时候,整个目标表格的颜色就变了。如下图,
4、现在重点分析下这行代码:
table = bsObj.findAll(“table”,{“class”:”wikitable”})[0]
根据第三条中的分析,已经知道把对应表格中所有含有wikitable的class找出来,那么为什么要这么写?
此时,对照下本例python代码中,按照class搜索{“class”:”wikitable”},实际上得到的是搜索class=”wikitable sortable”,如下的html代码(其中一条):
<table class="wikitable sortable" style="text-align: center; font-size: 85%; width: auto; table-layout: fixed;">
也就是说,bs4的findall是找到了类名带有”wikitable”,就自动把”wikitable sortable”也找出来,但对照lxml的xpath来说,如果class=”wikitable”,则搜索结果为空,要写完整的class=“wikitable sortable“,另外要注意这里有一个大坑,因为F12下的class是”wikitable sortable jquery-tablesorter”,和源代码是不对应的,这会导致python里用xpath找不到内容!
那么为何要在代码的后面加上[0] ? 如果只用下面的代码:
bsObj.findAll("table",{"class":"wikitable"})
就本例使用bs4分析来说, 其实得到的是bs4.element.ResultSet ,从字面翻译,可理解成bs4的结果集,不过应该是一个列表, 而要提取里面的内容,就加上[0],此时从bs4的角度来说,得到了一个bs4.element.Tag , 从列表的内容提取来说,得到了列表里第一个元素的内容。
5、接下来分析这一句:
rows = table.findAll("tr")
这一行是得到表格中所有按行的内容,这包含了表格头的黑色字体。如下图: (顺便请翻到此贴底部的参考资料,学习下tr,th,td的区别。
6、接下来,当开始写入csv的时候,是按行写的,包含了表格头的内容,看下代码:
try:
for row in rows:
csvRow = []
for cell in row.findAll(['td', 'th']):
csvRow.append(cell.get_text())
writer.writerow(csvRow)
其中 ‘th’ 是表格头,‘td’是表格内容。
7、相对与上述第二种提取方法,这里详细说下第一种提取方法:
检测表格中黑体的表格头(以name为例)和表格内容(以’acme’为例):
//*[@id=”mw-content-text”]/table[2]/thead/tr/th[1]
//*[@id=”mw-content-text”]/table[2]/tbody/tr[1]/th/a
分别得到他们的xpath地址,但格式并不统一,出现了thead和tbody。
不过好在表格内容的xpath都是有规律的,
千万要注意的是:用xpath提取表格的内容要千万小心,这是因为按照上述路径,测试结果,得到的text()返回值为空,
所以要修正下xpath路径,lxml的解析和网页源代码是有出入的,尤其遇到tboday和thead的时候,经过测试,在很多时候,python要把/thead和/tbody才能显示出内容,但这不是绝对的,因为我也遇到保留tbody才能提取成功的案例。
但还有额外的问题,因为这个表格同时有表头和表内容,而这个案例需要同时提取。而表格还有一个captain = “List of text editors”(表格的标题),也就是说,如果我们要通过直接全部提取整个表格的内容,会多出来captain的内容.
而如果把表头和表内容分开提取的话,他们的xpath在去掉/thead和/tbody之后的形式是这样的: 这里由于情况复杂,我先分析表头部分:
A)表头:
string(//*[@id="mw-content-text"]/table[2]/tr)
为何要这么写,而不用直接的text()模式呢?我们来看下
//*[@id="mw-content-text"]/table[2]/tr//text()
如果这么写,得到的结果是带有空格的,如图:
可以发现,不仅多出了空格,在Cost(US$)的栏目,还分了3行,而我们需要连续的,
综上,我们只有通过string功能来实现把空格去掉,同时把Cost(US$)合并在一起,得到的结果将是这样的:
Name
Creator
First public release
Latest stable version
Programming language
Cost (US$)
Software license
Open source
貌似thead部分的提取还比较顺利,可接下来tbody部分呢?
B)表内容:
我们先看开头的2行对应的xapth地址:
# tbody
# Acme xpath: //*[@id="mw-content-text"]/table[2]/tbody/tr[1]/th/a
# AkelPad xpath: //*[@id="mw-content-text"]/table[2]/tbody/tr[2]/th/a
如果去掉/tbody后, 又要把表格内容全部提取,又要去掉tbody,发现只能这么写:
string(//*[@id="mw-content-text"]/table[2])
可这样是不行的,因为他不仅把表头的内容也算进去了,还把标题captain的内容也一起搞进去了。
此时,又根据表内容的序号格式,尝试这么写:
string(//*[@id="mw-content-text"]/table[2]//th/a)
期望的是,得到统一的表内容,可实际返回的却是:
Programming language
这又是什么鬼呢?
原来,代码识别的是满足上述格式条件的第一个/a 路径下的文字,而在表头里,从Programming language开始,他有a属性。
此时,又发现,既然string返回的是第一个满足条件的,那么刚修正过的表头的string表达式,其实也适合表内容的表达式啊。看来,我们只好先再提高一个层级,用:
string(//*[@id="mw-content-text"]/table[2])
貌似进入了xpath分析的死循环,做下数据清理应该也是一个路子。
8、上面分析了一大堆,结果不满意,那么lxml的有没有类似bs4的findall功能呢?
∙ iterfind() iterates over all Elements that match the path expression
∙ findall() returns a list of matching Elements
∙ find() efficiently returns only the first match
∙ findtext() returns the .text content of the first match
不过当用:
table = selector.findall('.//*[@id="mw-content-text"]/table[2]/tr')
得到的结果显示的都是element的list,不显示里面的文字,提取的命令貌似在官网上也没找到,并且尝试:print(each.text) 完全空结果,这就有点坑了。
9、从第8点的分析来看,使用bs4在提取表格的时候,优势还是较大的,因为其返回的是列表形式的html内容,使得具体的提取方便,而lxml的返回的是一个看不到内容的
import requests
from lxml import etree
html = urlopen("http://en.wikipedia.org/wiki/Comparison_of_text_editors")Comparison_of_text_editors")
selector = etree.HTML(html)
#The main comparison table is currently the first table on the page
table = selector.findall('.//*[@id="mw-content-text"]/table[2]/tr')
content = []
for i in range(len(table)):
text = selector.xpath('string(//*[@id="mw-content-text"]/table[2]/tr[%d + 1])'%i)
print(text)
print(len(text))
content.append(text)
print(content)
虽然通过lxml.etree 的findall 以及 xpath配合得到了表格里的需要的文字,但是,这样的格式,要再插入csv却非常的麻烦,这是因为我们要按行插入,而返回的列表如下面的格式(下面显示的是列表第一项):
['\nName\nCreator\nFirst public release\nLatest stable version\nProgramming language\nCost (US$)\nSoftware license\nOpen source\n',
如果打印出来则是一行行的文字形式,则形如:
Name
Creator
First public release
Latest stable version
Programming language
Cost (US$)
Software license
Open source
Acme
Rob Pike
1993
Plan 9 and Inferno
C
Free
LPL (OSI approved)
Yes
要想得到bs4的那种csv的表格形式 ,还需要额外增加很多的数据清洗代码,看上去简单,实际很复杂,比如当你想通过len()的时候计算第一项有几个单词,按我们需求是从Name到Open source 的8个单词对应csv的8个列,但python是按照字符串去统计的,返回的每个列表元素的长度肯定不同的。而如果想把\n都替换掉,则前后所需单词后紧挨在一起了。总之最后反而得不偿失,尤其是如果希望使用wt的csv写入格式的话。希望以后能找到更好的办法来解决lxml这样的表格提取问题。
In [4]: a
Out[4]: '\nName\nCreator\nFirst public release\nLatest stable version\nProgramming language\nCost (US$)\nSoftware license\nOpen source\n'
In [5]: a.replace('\n', '')
Out[5]: 'NameCreatorFirst public releaseLatest stable versionProgramming languageCost (US$)Software licenseOpen source'
In [6]: length = len(a)
In [7]: length
Out[7]: 118
10、参考:
http://jingyan.baidu.com/article/636f38bb1eb1aad6b8461088.html