5.1 HTML 正文抽取
本小节讲解的是对HTML正文的抽取存储,主要是将HTML正文存储为两种格式:JSON和CSV。以一个盗墓笔记的小说阅读网(http://seputu.com/ )为例,抽取出盗墓笔记的标题、章节、章节名称和链接,如图5-1所示。
首先有一点需要说明,这是一个静态网站,标题、章节、章节名称都不是由JavaScript动态加载的,这是下面所进行的工作的前提。
这个例子使用第4章介绍的Beautiful Soup和lxml两种方式进行解析抽取,力求将之前的知识进行灵活运用。5.1.1小节,使用Beautiful Soup解析,5.1.2小节使用lxml方式解析。
5.1.1 存储为JSON
首先使用Requests访问http://seputu.com/ ,获取HTML文档内容,并打印文档内容。代码如下:
import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} r = requests.get('http://seputu.com/',headers=headers) print r.text
图5-1 盗墓笔记小说网
接着分析http://seputu.com/ 首页的HTML结构,确定要抽取标记的位置,分析如下:
标题和章节都被包含在<div class=“mulu”>标记下,标题位于其中的<div class=“mulu-title”>下的<h2>中,章节位于其中的<div class=“box”>下的<a>中,如图5-2所示。
图5-2 HTML结构分析
分析完成就可以进行编码了,代码如下:
soup = BeautifulSoup(r.text,'html.parser',from_encoding='utf-8')# html.parser for mulu in soup.find_all(class_="mulu"): h2 = mulu.find('h2') if h2!=None: h2_title = h2.string# 获取标题 for a in mulu.find(class_='box').find_all('a'):# 获取所有的a标记中url和章节内容 href = a.get('href') box_title = a.get('title') print href,box_title
这时已经成功获取标题、章节,接下来将数据存储为JSON。在第2章中,我们已经讲解了JSON文件的基本格式,下面讲解Python如何操作JSON文件。
Python对JSON文件的操作分为编码和解码,通过JSON模块来实现。编码过程是把Python对象转换成JSON对象的一个过程,常用的两个函数是dumps和dump函数。两个函数的唯一区别就是dump把Python对象转换成JSON对象,并将JSON对象通过fp文件流写入文件中,而dumps则是生成了一个字符串。下面看一下dumps和dump的函数原型:
dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, encoding='utf-8', default=None, sort_keys=False, **kw) dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, encoding='utf-8', default=None, sort_keys=False, **kw):
常用参数分析:
·Skipkeys:默认值是False。如果dict的keys内的数据不是python的基本类型(str、unicode、int、long、float、bool、None),设置为False时,就会报TypeError错误。此时设置成True,则会跳过这类key。
·ensure_ascii:默认值True。如果dict内含有非ASCII的字符,则会以类似“\uXXXX”的格式显示数据,设置成False后,就能正常显示。
·indent:应该是一个非负的整型,如果是0,或者为空,则一行显示数据,否则会换行且按照indent的数量显示前面的空白,将JSON内容进行格式化显示。
·separators:分隔符,实际上是(item_separator,dict_separator)的一个元组,默认的就是(’,‘,’:‘),这表示dictionary内keys之间用“,”隔开,而key和value之间用“:”隔开。
·encoding:默认是UTF-8。设置JSON数据的编码方式,在处理中文时一定要注意。
·sort_keys:将数据根据keys的值进行排序。
示例如下:
import json str =[{"username":"七夜","age":24},(2,3),1] json_str= json.dumps(str,ensure_ascii=False) print json_str with open('qiye.txt','w') as fp: json.dump(str,fp=fp,ensure_ascii=False)
输出结果:
[{"username": "七夜", "age": 24}, [2, 3], 1]
解码过程是把json对象转换成python对象的一个过程,常用的两个函数是load和loads函数,区别跟dump和dumps是一样的。函数原型如下:
loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw) load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)
常用参数分析:
·encoding:指定编码格式。
·parse_float:如果指定,将把每一个JSON字符串按照float解码调用。默认情况下,这相当于float(num_str)。
·parse_int:如果指定,将把每一个JSON字符串按照int解码调用。默认情况下,这相当于int(num_str)。
示例如下:
new_str=json.loads(json_str) print new_str with open('qiye.txt','r') as fp: print json.load(fp)
输出结果:
[{u'username': u'\u4e03\u591c', u'age': 24}, [2, 3], 1] [{u'username': u'\u4e03\u591c', u'age': 24}, [2, 3], 1]
通过上面的例子可以看到,Python的一些基本类型通过编码之后,tuple类型就转成了list类型了,再将其转回为python对象时,list类型也并没有转回成tuple类型,而且编码格式也发生了变化,变成了Unicode编码。具体转化时,类型变化规则如表5-1和表5-2所示。
表5-1 Python→JSON
表5-2 JSON→Python
以上就是Python操作JSON的全部内容,接下来将提取到的标题、章节和链接进行JSON存储。完整代码如下:
# coding:utf-8 import json from bs4 import BeautifulSoup import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} r = requests.get('http://seputu.com/',headers=headers) soup = BeautifulSoup(r.text,'html.parser',from_encoding='utf-8')# html.parser content=[] for mulu in soup.find_all(class_="mulu"): h2 = mulu.find('h2') if h2!=None: h2_title = h2.string# 获取标题 list=[] for a in mulu.find(class_='box').find_all('a'):# 获取所有的a标记中url和章节内容 href = a.get('href') box_title = a.get('title') list.append({'href':href,'box_title':box_title}) content.append({'title':h2_title,'content':list}) with open('qiye.json','wb') as fp: json.dump(content,fp=fp,indent=4)
打开qiye.json文件,效果如图5-3所示。
图5-3 qiye.json
5.1.2 存储为CSV
CSV(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。
CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其他字符或字符串,最常见的是逗号或制表符。通常,所有记录都有完全相同的字段序列。CSV文件示例如下:
ID,UserName,Password,age,country 1001,"qiye","qiye_pass",24,"China" 1002,"Mary","Mary_pass",20,"USA" 1003,"Jack","Jack_pass",20,"USA"
Python使用csv库来读写CSV文件。要将上面CSV文件的示例内容写成qiye.csv文件,需要用到Writer对象,代码如下:
import csv headers = ['ID','UserName','Password','Age','Country'] rows = [(1001,"qiye","qiye_pass",24,"China"), (1002,"Mary","Mary_pass",20,"USA"), (1003,"Jack","Jack_pass",20,"USA"), ] with open('qiye.csv','w') as f: f_csv = csv.writer(f) f_csv.writerow(headers) f_csv.writerows(rows)
里面的rows列表中的数据元组,也可以是字典数据。示例如下:
import csv headers = ['ID','UserName','Password','Age','Country'] rows = [{'ID':1001,'UserName':"qiye",'Password':"qiye_pass",'Age':24,'Country':" China"}, {'ID':1002,'UserName':"Mary",'Password':"Mary_pass",'Age':20,'Country':"USA"}, {'ID':1003,'UserName':"Jack",'Password':"Jack_pass",'Age':20,'Country':"USA"}, ] with open('qiye.csv','w') as f: f_csv = csv.DictWriter(f,headers) f_csv.writeheader() f_csv.writerows(rows)
接下来讲解CSV文件的读取。将之前写好的qiye.csv文件读取出来,需要创建reader对象,示例如下:
import csv with open('qiye.csv') as f: f_csv = csv.reader(f) headers = next(f_csv) print headers for row in f_csv: print row
运行结果:
['ID', 'UserName', 'Password', 'Age', 'Country'] ['1001', 'qiye', 'qiye_pass', '24', 'China'] ['1002', 'Mary', 'Mary_pass', '20', 'USA'] ['1003', 'Jack', 'Jack_pass', '20', 'USA']
在上面的代码中,row会是一个列表。因此,为了访问某个字段,你需要使用索引,如row[0]访问ID,row[3]访问Age。由于这种索引访问通常会引起混淆,因此可以考虑使用命名元组。示例如下:
from collections import namedtuple import csv with open('qiye.csv') as f: f_csv = csv.reader(f) headings = next(f_csv) Row = namedtuple('Row', headings) for r in f_csv: row = Row(*r) print row.UserName,row.Password print row
运行结果:
qiye qiye_pass Row(ID='1001', UserName='qiye', Password='qiye_pass', Age='24', Country='China') Mary Mary_pass Row(ID='1002', UserName='Mary', Password='Mary_pass', Age='20', Country='USA') Jack Jack_pass Row(ID='1003', UserName='Jack', Password='Jack_pass', Age='20', Country='USA')
它允许使用列名如row.UserName和row.Password代替下标访问。需要注意的是这个只有在列名是合法的Python标识符的时候才生效。
除了使用命名分组之外,另外一个解决办法就是读取到一个字典序列中,示例如下:
import csv with open('qiye.csv') as f: f_csv = csv.DictReader(f) for row in f_csv: print row.get('UserName'),row.get('Password')
运行结果:
qiye qiye_pass Mary Mary_pass Jack Jack_pass
这样就可以使用列名去访问每一行的数据了。比如,row[’UserName‘]或者row.get(’UserName‘)。
以上就是CSV文件读写的全部内容。接下来使用lxml解析http://seputu.com/ 首页的标题、章节和链接等数据,代码如下:
from lxml import etree import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} r = requests.get('http://seputu.com/',headers=headers) # 使用lxml解析网页 html = etree.HTML(r.text) div_mulus = html.xpath('.// *[@class="mulu"]')# 先找到所有的div class=mulu标记 for div_mulu in div_mulus: # 找到所有的div_h2标记 div_h2 = div_mulu.xpath('./div[@class="mulu-title"]/center/h2/text()') if len(div_h2)> 0: h2_title = div_h2[0] a_s = div_mulu.xpath('./div[@class="box"]/ul/li/a') for a in a_s: # 找到href属性 href=a.xpath('./@href')[0] # 找到title属性 box_title = a.xpath('./@title')[0].encode('utf-8')
将box_title数据抽取出来之后,里面的内容类似“[2014-10-2416:59:14]巫山妖棺第七十七章交心”这种形式。这里相较于5.1.1小节添加一步数据清洗,将内容里的时间和章节标题进行分离,这就要使用正则表达式,代码如下。
pattern = re.compile(r'\s*\[(.*)\]\s+(.*)') match = pattern.search(box_title) if match!=None: date =match.group(1) real_title= match.group(2)
最后将获取的数据按照title、real_title、href、date的格式写入到CSV文件中,解析存储的完整代码如下:
html = etree.HTML(r.text) div_mulus = html.xpath('.// *[@class="mulu"]')# 先找到所有的div class=mulu标记 pattern = re.compile(r'\s*\[(.*)\]\s+(.*)') rows=[] for div_mulu in div_mulus: # 找到所有的div_h2标记 div_h2 = div_mulu.xpath('./div[@class="mulu-title"]/center/h2/text()') if len(div_h2)> 0: h2_title = div_h2[0].encode('utf-8') a_s = div_mulu.xpath('./div[@class="box"]/ul/li/a') for a in a_s: # 找到href属性 href=a.xpath('./@href')[0].encode('utf-8') # 找到title属性 box_title = a.xpath('./@title')[0] pattern = re.compile(r'\s*\[(.*)\]\s+(.*)') match = pattern.search(box_title) if match!=None: date =match.group(1).encode('utf-8') real_title= match.group(2).encode('utf-8') # print real_title content=(h2_title,real_title,href,date) print content rows.append(content) headers = ['title','real_title','href','date'] with open('qiye.csv','w') as f: f_csv = csv.writer(f,) f_csv.writerow(headers) f_csv.writerows(rows)
运行效果如图5-4所示。
图5-4 qiye.csv
注意 1)在存储CSV文件时,需要统一存储数据的类型。代码中使用encode(’utf-8‘)作用就是将title、real_title、href、date变量类型统一为str。
2)5.1.1节BeautifulSoup如果使用lxml作为解析库,会发现解析出来的HTML内容缺失,这是由于BeautifulSoup为不同的解析器提供了相同的接口,但解析器本身是有区别的,同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档。因此如果遇到缺失的情况,BeautifulSoup可以使用html.parser作为解析器,或者单独使用lxml进行解析即可。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论