返回介绍

5.1 HTML 正文抽取

发布于 2024-01-26 22:39:51 字数 11429 浏览 0 评论 0 收藏 0

本小节讲解的是对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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文