2、爬虫数据解析

爬虫快速入门 / 2021-04-22

一、数据解析概述

数据解析即对通用爬虫抓取到的数据进行进一步的处理,从而获取其局部的指定数据信息。一般而言,需要解析的局部数据信息都在标签内部或者标签对应的属性值中存储。因此,我们需要对其进行标签定位,继而获取其内部信息或者标签对应的属性中指向的数据进行提取(解析)。

这些操作,我们可以称之为聚焦爬虫!有时候爬取的数据可能是图片、音视频等二进制数据,这时候需要用到requests模块里面的content方法来获取二进制数据。

数据解析的方式也有很多种,下面将通过一个个例子来学习如何对数据进行解析。

二、bs4

此方式流程是先实例化得到一个bs4对象,并将页面源码数据加载到该对象中,接着通过调用bs4对象中相关的属性或方法进行标签定位和数据提取。

学习BeautifulSoup如果有CSS基础,理解起来更容易!

2.1、环境安装

pip3 install bs4
pip3 install lxml

2.2、实例化产生对象

此处选择最快的lxml解析器,也可以看官方文档选择别的解析器:https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html#id8。

import requests
from bs4 import BeautifulSoup


url = 'https://mirrors.tuna.tsinghua.edu.cn/'
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}
response = requests.get(url=url, headers=headers).text
soup = BeautifulSoup(response, "lxml")

2.3、常用方法

2.3.1、标签查找
查找标签
print(soup.a)  # 等同于soup.find('a'),返回匹配的第一个标签

# 执行结果
<a href="/legacy_index">代用页面</a>
查找所有标签
print(soup.find_all('img'))

# 执行结果
[<img alt="" src="/static/img/logo-small.png" srcset="/static/img/logo-small.png 1x, 
/static/img/logo-small@2x.png 2x, /static/img/logo-small@3x.png 3x, /static/img/logo-small@4x.png 4x"/>, <img class="img-responsive thuhidden center-block" src="/static/img/logo-white.png" srcset="/static/img/logo-white.png 1x, /static/img/logo-white@2x.png 2x, /static/img/logo-white@3x.png 3x, /static/img/logo-white@4x.png 4x" style="margin-top:5%"/>, <img class="img-responsive thuhidden center-block" src="/static/img/logo-white.png" srcset="/static/img/logo-white.png 1x, /static/img/logo-white@2x.png 2x, /static/img/logo-white@3x.png 3x, /static/img/logo-white@4x.png 4x" style="margin-top:5%"/>]

2.3.2、标签属性定位

class属性定位必须跟下划线,返回匹配的第一个元素。

print(soup.find('button', class_='btn btn-info'))

# 执行结果
<button class="btn btn-info" data-target="#isoModal" data-toggle="modal" type="button">获取下载链接</button>
2.3.3、标签选择器

返回值是一个列表

选择所有

等同于find_all()

print(soup.select('img'))
层级选择

这里跟CSS语法选择器是一样的用法,只是这里空格表示多个层级,>表示一个层级。

print(soup.select('#download-link>h4>span'))

# 执行结果
[<span class="fa fa-file-archive-o"></span>]
2.3.4、获取标签文本

有三种方式:text、string、get_text()。

text、get_text()

可以获取标签中所有的文本信息,包括后代标签。

print(soup.div.get_text())
string

仅获取当前标签的文本信息。

print(soup.span.string)

# 执行结果
Toggle navigation
2.3.5、获取标签属性值
print(soup.button.get('class'))

# 执行结果
['navbar-toggle']

2.4、演示案例

根据上面所学的方法,我们来爬取三国演义作者罗贯中所写的三遂平妖传的所有章节标题及其对应的内容。

import requests
from bs4 import BeautifulSoup


url = 'https://ctext.org/wiki.pl?if=gb&res=119318'
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}
response = requests.get(url=url, headers=headers).text
soup = BeautifulSoup(response, "lxml")

title_list = soup.select('.ctext>span>a')  # 获取所有章节链接地址
with open('三遂平妖传.md', 'wt', encoding='utf-8') as file:
    for tag in title_list:
        title = tag.string  # 取得章节标题
        file.write('\n\n' + title)  # 写入标题

        article_detail_link = 'https://ctext.org/' + tag.get('href')  # 拼接章节详情链接
        detail = requests.get(url=article_detail_link, headers=headers).text  # 获取章节详情内容
        detail_soup = BeautifulSoup(detail, 'lxml')

        detail_list = detail_soup.select('.result')  # 取得详情内容中文本的标签
        for line in detail_list:
            file.write('\n' + line.td.next_sibling.get_text())  # 写入每一段落的文本
        print(title, '下载完毕')

三、xpath

最常用且便捷高效的解析方式,通用性也强!它是通过lxml中的etree类实例化得到一个etree对象,并将源码数据传入对象,再调用对象中的xpath方法结合xpath表达式实现标签定位和内容捕获。

3.1、环境安装

pip3 install lxml

3.2、实例化

分网络导入和本地文件导入两种

3.2.1、网络源码

这里为避免编码错误,requests我们选择用二进制格式content。

import requests
from lxml import etree


url = 'https://lxml.de/xpathxslt.html#xpath'
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}
response = requests.get(url=url, headers=headers).content
tree = etree.HTML(response)
print(tree)

# 执行结果
<Element html at 0x7f9ff3aeadc8>
3.2.2、本地文件
etree.parse(FILE_PATH)

3.3、xpath表达式

xpath表达式使用起来比bs4更为简洁

3.3.1、标签选择

/表示单个层级,//表示多级层级,相当于bs4中的空格,返回所有匹配到的结果并存放于列表中。

result = tree.xpath('/html/body/div')
print(result)

# 执行结果
[<Element div at 0x7f36480f7d88>, <Element div at 0x7f3648387308>]
3.3.2、属性定位

TAG[@attr="attr_name"]是固定语法,返回一个列表。

result = tree.xpath('//div[@class="menu"]')
print(result)

# 执行结果
[<Element div at 0x7fce51dcc288>]
3.3.3、索引定位

注意注意:xpath索引值是从1开始,而不是0。

result = tree.xpath('/html/body/div/div[2]')
print(result)

# 执行结果
[<Element div at 0x7fa1b2574d88>]
3.3.4、获取文本text

有获取当前标签文本和所有后代标签文本两种方式。

当前标签文本

仅获取当前标签所属文本信息

result = tree.xpath('/html/body//div[@id="namespaces-and-prefixes"]/h2/text()')
print(result)

# 执行结果
['Namespaces and prefixes']
所有子标签文本

获取当前标签及其后代标签所有文本信息,只需要把单斜杆改成双斜杆即可。

result = tree.xpath('/html/body//div[@id="namespaces-and-prefixes"]//text()')
print(result)

# 执行结果
['\n', 'Namespaces and prefixes', '\n', 'If your XPath expression uses namespace prefixes, you must define them\nin a prefix mapping.  To this end, pass a dictionary to the\n', 'namespaces', ' keyword argument that maps the namespace prefixes used\nin the XPath expression to namespace URIs:', '\n', '>>> ', 'f', ' ', '=', ' ', 'StringIO', '(', "'''", '\\', '\n', '... ', '<a:foo xmlns:a="http://codespeak.net/ns/test1"', '\n', '... ', '       xmlns:b="http://codespeak.net/ns/test2">', '\n', '... ', '   <b:bar>Text</b:bar>', '\n', '... ', '</a:foo>', '\n', '... ', "'''", ')', '\n', '>>> ', 'doc', ' ', '=', ' ', 'etree', '.', 'parse', '(', 'f', ')', '\n\n', '>>> ', 'r', ' ', '=', ' ', 'doc', '.', 'xpath', '(', "'/x:foo/b:bar'", ',', '\n', '... ', '              ', 'namespaces', '=', '{', "'x'", ':', ' ', "'http://codespeak.net/ns/test1'", ',', '\n', '... ', '                          ', "'b'", ':', ' ', "'http://codespeak.net/ns/test2'", '})', '\n', '>>> ', 'len', '(', 'r', ')', '\n', '1', '\n', '>>> ', 'r', '[', '0', ']', '.', 'tag', '\n', "'{http://codespeak.net/ns/test2}bar'", '\n', '>>> ', 'r', '[', '0', ']', '.', 'text', '\n', "'Text'", '\n', '\n', 'The prefixes you choose here are not linked to the prefixes used\ninside the XML document.  The document may define whatever prefixes it\nlikes, including the empty prefix, without breaking the above code.', '\n', 'Note that XPath does not have a notion of a default namespace.  The\nempty prefix is therefore undefined for XPath and cannot be used in\nnamespace prefix mappings.', '\n', 'There is also an optional ', 'extensions', ' argument which is used to\ndefine ', 'custom extension functions', ' in Python that are local to this\nevaluation.  The namespace prefixes that they use in the XPath\nexpression must also be defined in the namespace prefix mapping.', '\n']
3.3.5、获取属性值

固定语法:在标签后跟上:@属性名

result = tree.xpath('//a[@id="id1"]/@href')
print(result)

# 执行结果
['#xpath']
3.3.6、与或运算

xpath支持与或运算 |

3.4、实例演示

通过几个例子来进一步巩固xpath的用法。

3.4.1、中文乱码解决
requests对象解码
response = requests.get(url=url, headers=headers).content.decode('utf-8')
通用方法
encode('iso-8859-1').decode('utf-8')
3.4.2、58二手房源信息

以58二手房的广州为例,.为当前标签,..为父标签,encode('iso-8859-1').decode('utf-8')为通用的解决中文乱码的方法。

import requests
from lxml import etree


url = 'https://gz.58.com/ershoufang/?PGTID=0d200001-0000-3511-b1e9-5dc919c202be&ClickID=1'
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 '
                  'Safari/537.36',
}
response = requests.get(url=url, headers=headers).content.decode('utf-8')
tree = etree.HTML(response)
result = tree.xpath('//div[@class="property"]')
for title in result:
    print(title.xpath('./a//h3/text()')[0])
    
# 执行结果太多,我就放一点出来,这房价让人窒息。
君怡大厦满五  住房,真盘实价990万方正采光好
融创广府壹号 9折优惠 一步天河全龄教育 地铁6号线黄陂地铁
荔城中心地铁口 合汇中央广场 大三房 中楼层 视野开阔
(  工程款单位)富力新城 广外附属学校 只有5!
(近地,铁 精装两房送装修家电) 实用两房有钥匙方便看房
内部特价单位 找我享9折钜惠 名校学府 8号线地铁江府站
龙光天瀛 市桥商圈 番禺城芯 大夫山旁 南向三房
3.4.3、爬取4K图片

其实这里爬取的是缩略图,因为完整图片是要登录才能看得到的,哈哈,下章节就学习。

import requests
from lxml import etree
import os


if not os.path.exists('4k_img'):
    os.mkdir('4k_img')

url = 'https://pic.netbian.com/4kfengjing/'
headers = {
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
}

response = requests.get(url=url, headers=headers).content
tree = etree.HTML(response)
img_list = tree.xpath('//div[@class="slist"]/ul/li')

for img in img_list:
    img_title = img.xpath('./a/b/text()')[0]  # 图片标题
    img_path = '4k_img/' + img_title + '.png'  # 图片存放路径
    img_link = 'https://pic.netbian.com' + img.xpath('./a/img/@src')[0]  # 图片链接
    img_data = requests.get(url=img_link, headers=headers).content  # 下载图片
    with open(img_path, 'wb') as file:  # 写入本地文件
        file.write(img_data)
    print(img_title, '下载完毕')
世间微尘里 独爱茶酒中