爬虫 | 童年回忆宝可梦数据抓取

发布时间 2023-07-22 12:43:13作者: 张Zong在修行

本文将带你认识一个爬取重点解析库 lxml ,该库属于爬虫“必考”知识点之一,介绍 lxml 的同时会给你介绍两种解析语法,一种深度结合前端知识进行操作,一种语法简洁,处理速度快。以上两部分内容分别为 cssselectXPath

知识点

  • lxml 库与 cssselect 库
  • XPath 语法初识

lxml 库与 cssselect 库

lxml 是 Python 中一个常见的解析库,支持 HTML 与 XML 的解析,支持 XPath 解析方式,lxml 库是通过 C 语言实现的,解析速度非常快,在爬虫知识体系中属于“必考”项。如果你英语还不错,部分知识可以从 官网 获取。

本实验需要用到的 lxml 库与 cssselect 库,本地进行试验的同学提前安装即可。

lxml 解析之后的 Element 对象

通过 lxml 解析之后的内容为一种特定类型的对象,一般称作 Element 对象,下文所有内容都是围绕该对象的属性与方法展开。

首先说明一下本实验的目标网站为:宝可梦数据 ,需要抓取宝可梦 pidname,即编号与名称。

为方便爬取,该页面只保留了 HTML 文档结构,网页如下图所示。

lxml 解析库需要配合 requests 库使用,先通过 requests 库获取一下网页源码:

import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}

def get_pokemons():
    res = requests.get(
        "https://labfile.oss.aliyuncs.com/courses/3086/target.html", headers=headers)
    res.encoding = 'utf-8'
    data = res.text
    print(data)

if __name__ == "__main__":
    get_pokemons()

该代码运行之后就可以得到目标网站所有的源码,后续所有解析工作都是基于该源码进行,为了测试方便和避免给对方服务器造成压力,先将网页源码存储在本地,后续直接打开本地存储的网页源码进行相关知识学习

将如下代码写入 /home/project/demo.py 文件:

import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}

def get_pokemons():
    res = requests.get(
        "https://labfile.oss.aliyuncs.com/courses/3086/target.html", headers=headers)
    res.encoding = 'utf-8'
    data = res.text
    # 存储网页源码到本地,注意用该办法学习有时存在本地源码和浏览器源码不一致问题,需要特别注意。
    with open("./target.html", "w", encoding="utf-8") as f:
        f.write(data)

if __name__ == "__main__":
    get_pokemons()

代码运行完毕,保存了名为target.html的文件:

接下来就可以导入 lxml 库进行解析了,该内容需要配合 cssselect 库实现,编码过程中需要一些 CSS 选择器相关知识,不熟悉的同学可以自行补充该部分内容。

如果环境中没有安装 cssselect,要先安装才可以使用。

pip3 install cssselect

通过 lxml 获取网页标题,测试一下基本使用是否存在问题。将如下代码覆盖写入 /home/project/demo.py 文件:

import requests
from lxml import html

# 从网络获取的图片,本实验中表示从本地存储的 HTML 读取的数据
def get_pokemons_fromfile():
    try:
        with open("./target.html", "r", encoding="utf-8") as f:
            data = f.read()
            return data
    except Exception as e:
        return None

def analysis(data):
    # 将网页源码格式化成 element 对象
    page_tree = html.fromstring(data)
    print(page_tree)
    print(type(page_tree))
    # 通过 CSS 选择器获取 所有 title 标签
    eles = page_tree.cssselect('title')
    print(eles)
    print(type(eles))
    # 获取 title 标签内文本
    print(eles[0].text)

if __name__ == "__main__":
    data = get_pokemons_fromfile()
    analysis(data)

# 运行结果
<Element html at 0x26d1a38af90>
<class 'lxml.html.HtmlElement'>
[<Element title at 0x26d1a3905e0>]
<class 'list'>
主题:宝可梦 - PMGBA口袋妖怪专题百科

上述代码请注意先导入 lxml 模块下面的 html 对象,然后通过 html 对象的 fromstring 方法将网页源码实例化为 element 对象,在对实例化的 element 对象调用 cssselect 方法,该方法的参数为 CSS 选择器。(此处需要对 CSS 选择器基础知识熟悉,可以在 W3School 学习),还要注意 page_tree.cssselect('title') 表示选择网页源码中的所有 title 标签,获取到的是一个 list 对象,所以在获取标签内部文本时需要通过 eles[0].text 获取。

通过 cssselect 库解析出所有宝可梦

当获取到网页标题之后,稍微进行扩展就可以得到网页中所有的宝可梦名称,先对网页目标数据进行分析寻求解决方案。

image-20230722101358031

框选区域即为目标数据。

打开开发者工具,定位到 妙蛙种子,截图如下:

接下来会用到一些前端知识,如果对前端语言不熟悉,可以简单跟随步骤进行尝试或者直接略过本部分,进入 XPath 部分学习。

注意从上述截图最顶部 div 开始,找到该 div 标签的 id 为 mw-content-text 之后,查阅该标签的后代标签,发现 table 标签部分是目标数据,在 table 标签下直接定位到 li 标签即可找到数据所在区域,关键元素如下图所示。

箭头所指为数据所在的父级 div,子标签由 table,h2,h3 等内容构成,其中 table 为重要标签,而上文已经说到,红色的 table 是最终的目标数据所在标签,所以一会编写 CSS 选择器的时候,需要定位该部分内容。

基于此编写了一个通用的 CSS 选择器用于 cssselect 中,选择器为:

div#mw-content-text table[class^=colortable] li

该部分与前端知识比较紧密,如果理解有难度跳过该部分即可。

  • div#mw-content-text 表示选择 id=mw-content-textdiv 标签;
  • table[class^=colortable] 表示选择 table 标签中 class 属性以 colortable 开始的标签;
  • 上述选择器中的空格依次表示选择 div 的后代标签 table,选择 table 的后代标签 li

修改上文中的 analysis 函数:

def analysis(data):
    # 将网页源码格式化成 Element 对象
    page_tree = html.fromstring(data)
    eles = page_tree.cssselect(
        'div#mw-content-text table[class^=colortable] li')
    print(eles)

运行之后发现捕获到一个列表,列表中每一项都是一个 Element 对象:

[<Element li at 0x2072bb15f90>, <Element li at 0x2072ca2a040>, <Element li at 0x2072d25b810>,...,<Element li at 0x2072d27a310>, <Element li at 0x2072d27a360>]

eles 列表中的第一项内容输出成 HTML 格式,查看是否匹配成功,具体代码如下:

def analysis(data):
    # 将网页源码格式化成 Element 对象
    page_tree = html.fromstring(data)
    eles = page_tree.cssselect(
        'div#mw-content-text table[class^=colortable] li')
    print(html.tostring(eles[0]))

该部分输出的内容是下述代码部分,确定是目标数据的第 1 项,其中部分数据被编码(暂不处理):

b'<li> <span class="js-sprite" data-ver="pi" data-pid="001.00"></span> 001. <a href="/wiki/%E5%A6%99%E8%9B%99%E7%A7%8D%E5%AD%90" title="&#22937;&#34521;&#31181;&#23376;">&#22937;&#34521;&#31181;&#23376;</a></li>\n'

该部分内容与网页源码对比:

所有数据确认无问题,接下来需要再提取两点数据:一个为序号 pid,另一个为宝可梦的名字 name。修改代码如下:

def analysis(data):
    # 将网页源码格式化成 Element 对象
    page_tree = html.fromstring(data)
    eles = page_tree.cssselect(
        'div#mw-content-text table[class^=colortable] li')
    # print(html.tostring(eles[0]))
    # 测试,获取第一项中的 span 标签的 data-pid 属性
    print(eles[0].cssselect('span')[0].get("data-pid")) # 001.00

上述代码有一个新的知识点,Element 对象调用 get 方法之后,get 中可以获取 HTML 标签的任意属性,只需要写入属性名即可。

通过上述内容,成功获取到 妙蛙种子data-pid001.00,该值可特别记录一下,如果爬取宝可梦的头像图片会用到。

获取全部数据并输出

将上述代码中测试的部分编写完整即可获取所有宝可梦数据,核心修改在 analysis 函数部分:

def analysis(data):
    # 将网页源码格式化成 Element 对象
    page_tree = html.fromstring(data)
    eles = page_tree.cssselect(
        'div#mw-content-text table[class^=colortable] li')
    # 循环获取全部数据
    for ele in eles:
        pid, name = ele.cssselect('span')[0].get(
            "data-pid"), ele.cssselect('a')[0].get("title")
        print(pid, name)

上述代码在运行时发现,运行到第 808 只宝可梦出现错误,错误提示为:

001.00 妙蛙种子
002.00 妙蛙草
003.00 妙蛙花
...
IndexError: list index out of range

该异常出现表示未匹配到 span 元素,解决办法也是查阅 HTML 标签,核对之后发现,在第 808 宝可梦标签处,不存在 span 标签,而是由 img 标签替换了,故需要解决该异常,修改代码如下:

def analysis(data):
    # 将网页源码格式化成 Element 对象
    page_tree = html.fromstring(data)
    eles = page_tree.cssselect(
        'div#mw-content-text table[class^=colortable] li')
    # 循环获取全部数据
    for ele in eles:
        # 通过 text_content 获取标签内所有文本内容,需要移除左右空格部分
        content = ele.text_content().strip()
        # 拆分字符串
        pid, name = content.split(' ')
        print(pid,name)

以上代码直接用了一个讨巧的方法 text_content 获取了 li 标签内所有的文本内容,也就是下图红框部分的内容,注意移除数字前面的空格。

代码运行之后获取到了所有的宝可梦以及编号,目前累计 893 只宝可梦,与官网描述一致。

XPath 语法初识

本实验截止到这里已经可以算是完成了目标,但上述代码有一个非常不友好的地方,就是你除了 Python 基础需要掌握以外,还需要有一些 HTML+CSS 的知识,也可以理解成前端基础。因为学习爬虫再去学习一下前端知识,时间稍显不足,并且这不是学习 lxml 的核心目的,所以再给大家介绍一款新的解析方式 XPath

XPath,全称 XML Path Language,即 XML 路径语言,XPath 可以基于 XML 的树状结构,提供在数据结构树中找寻节点的能力。

最早的时候 XPath 的设计的初衷是将其作为一个通用的、介于 XPointer(XML 指针语言)与 XSL(扩展样式表语言)间的语法模型。但是 XPath 很快的被开发者采用来当作小型查询语言,配合 lxml 库使用能极大的提高选取网页元素的速度。

XPath 快速上手

对于一个新的语法内容,如果直接翻阅手册,很容易陷入细节当中,建议直接先使用起来,在对技术细节进行补充,那么先将 XPathlxml 结合运行起来,在针对疑问进行学习。

特别注意,在 XML 中表示一个元素一般称作节点,在下文因为主要匹配的是 HTML,故后文统一称作标签

import requests
from lxml import etree

def get_pokemons_fromfile():
    try:
        with open("./target.html", "r", encoding="utf-8") as f:
            data = f.read()
            return data
    except Exception as e:
        return None

def analysis_byxpath(data):
    html = etree.HTML(data)
    print(html)
    # 选取页面中 title 标签
    title = html.xpath("//title")
    print(title)
    # 选取页面中所有的 a 标签
    all_a = html.xpath("//a")
    # 输出所有 a 标签的文本
    all_a_text = html.xpath("//a/text()")
    print(all_a_text)
    # 输出 id="firstHeading" 的 h1 标签的文本内容
    h1_text = html.xpath("//h1[@id='firstHeading']/text()")
    print(h1_text)

if __name__ == "__main__":
    data = get_pokemons_fromfile()
    analysis_byxpath(data)

运行结果:

<Element html at 0x24511191740>
[<Element title at 0x24511287ac0>]
['属性', '互相克制', '能力', '基础点数', '关都图鉴', '城都图鉴', '丰缘图鉴', '神奥图鉴', '合众图鉴', '卡洛斯图鉴', '阿罗拉图鉴', '妙蛙种子', , ...,'http://www.pokemon.name/w/index.php?title=主题:宝可梦&oldid=269499', '\n\t\t\t\t最后编辑于2020年10月9日 (星期五) 12:05\n\t\t\t', '知识共享署名-非商业性使用-相同方式共享', '隐私', '桌面版']
[]

首先导入 lxml 库中的 etree 对象,在 analysis_byxpath 函数中,通过 etree.HTML 方法将网页源码实例化成 Element 对象,之后就可以通过 XPath 执行选择操作了。

第一段代码,选取页面中的所有 title 标签,当然本实验涉及的网页只有一个 title 标签:

# 选取页面中 title 标签
title = html.xpath("//title")
print(title)

特别注意 xpath 方法中的参数 //title 表示从根目录匹配所有的 title 标签。其中的 // 的含义需要说明一下,在很多教材中该内容说的都有点绕,你可以将其理解成在当前网页文档中进行全局检索,只要找到名字叫做 title 的标签就能匹配到(本例子用到的是 title,你修改成 a,就是在网页文档中全局检索 a 标签)。

// 对应的是 / 该符号表示从根结点开始查找,相同的写法例如在下述 HTML 代码中寻找 title,可以看一下两个写法的区别,先提供一段待解析 HTML 代码:

<html>
  <head>
    <title>这是橡皮擦的测试网页</title>
  </head>
  <body>
    <h1>标题内容</h1>
  </body>
</html>

匹配 title// 的写法是 //title ,而 / 的写法是从根结点 html 开始查找,故写法为 /html/head/title,甚至你可以写成 /html//title,它的含义是现在根结点开始找到 html 标签,再在 html 标签中全局检索 title(有前端基础的同学可以配合后代标签学习)。以上内容展开讲解为绝对路径与相对路径问题,本系列实验以应用为主,不在细挖差异化内容,有兴趣的同学在掌握基本用法之后,可以通过搜索引擎补充相关知识。

继续查看上述 Python 代码,在获取 a 标签所有文字的代码段中,通过 //a/text() 可以获取 a 标签中的文本,核心代码为 text(),获取双标签中的文本都采用该方式即可,例如获取网页标题,可以通过 //title/text() 获取。

接下来要说明的是 @,在上述代码中应用的部分为 //h1[@id='firstHeading']/text(),该内容表示选择 h1 标签,但前提是该 h1 标签的 id 属性要等于 firstHeading,这里就涉及了 @ 的用法,该符号表示选取属性,即 HTML 标签属性。选择 class 属性可以用 @class,选择 href 属性用 @href。如果选择特定携带某些属性的标签,使用 标签名[@属性名=属性值] 即可。

学习到这里,对于 XPath 的基础语法,你应该有了初步认知,下面进行一下总结。

XPath 语法初步总结

应用 lxml 的 XPath 语法需要首先导入对象,之后调用对象的 HTML 方法,实例化为 Element 对象。

from lxml import etree
html = etree.HTML(data)

1. 获取指定标签:

# 指定标签获取:
html.xpath('//标签名称')
html.xpath('/标签名称/标签名称')

2. 获取标签文本

# 标签文本获取:
html.xpath('//标签名称/text()')
html.xpath('/标签名称/标签名称/text()')

3. 匹配携带属性的标签

# 只要携带某属性就匹配
html.xpath('//标签名称[@某属性]')
# 携带某属性并且该属性等于某个值
html.xpath('//标签名称[@某属性=某个值]')

对于 XPath,本实验只揭开了冰山一角,如果等不及可以提前打开 菜鸟教程 进行预习,对后续实验学习有非常大的帮助。

XPath 提取数据

下面就通过已经学习到的 XPath 选择语法进行数据的匹配与提取,过程中如果用到其它 XPath 知识,将进行补充说明。

由于爬取的网页已经存储在本地,会与浏览器渲染出的网页结构有所差异,所以在这里先将本地网页在浏览器打开,之后的网页结构以本地为准。

打开刚才爬取到本地的文件 target.html,在 妙蛙种子 超链接上点击鼠标右键,选择检查之后出现开发者工具,观察之后注意到该内容被包含在一个 class 属性等于 mf-section-1div 标签中,代码通过 XPath 获取该标签即可。

XPath 部分用中文可描述为下述内容:

在整个文档中去检索 class 属性为 mf-section-1 的 div,之后在这个 div 中检索 li 标签,并获取 li 标签中的文本内容

中文描述已经整理完毕,只需要进行一下简单翻译即可:

//div[@class="mf-section-1"]//li/text()

接下来大胆的测试一下这段 XPath 是否正确,如果错误在进行反复修改。

# 其余代码在上文中已经存在,本部分只展示差异部分
def analysis_byxpath(data):
    html = etree.HTML(data)
    all_li = html.xpath("//div[@class='mf-section-1']//li/text()")
    print(all_li)

运行结果发现并未达到预期效果,匹配到了一堆空格和数字但并不是最终目标,继续修改 XPath 部分,修改为分步获取。

只匹配 li 标签,核对数量是否正确,测试代码如下:

def analysis_byxpath(data):
    html = etree.HTML(data)

    all_li = html.xpath("//div[@class='mf-section-1']//li")
    # 核对数量
    print(len(all_li))

结果输出为 893 只宝可梦,证明数据获取无问题,继续修改代码如下:

def analysis_byxpath(data):
    html = etree.HTML(data)

    all_li = html.xpath("//div[@class='mf-section-1']//li")
    for li in all_li:
        # 注意 上述 all_li 是一个由 Element 对象组成的列表,循环遍历时所有的 li 都是 Element 对象
        # 故 li 也可以调用 xpath 方法
        # ./ 中 . 也是 XPath 中语法内容,表示当前标签,即 li 标签,./text()获取 li 标签下的文本
        pid = li.xpath("./text()")
        print(pid)

相关说明已经写在注释中,重点关注 ./,你可按照硬盘上文件路径相关知识进行理解,运行之后获取到的结果如下图所示。

拿到上图数据,只需要最后再调整一步即可,注意下面代码修改部分。

def analysis_byxpath(data):
    html = etree.HTML(data)
    all_li = html.xpath("//div[@class='mf-section-1']//li")
    for li in all_li:
        pid = li.xpath("./text()")[1]
        print(pid)

pid已经获取到,继续获取宝可梦名称name

def analysis_byxpath(data):
    html = etree.HTML(data)
    all_li = html.xpath("//div[@class='mf-section-1']//li")
    for li in all_li:
        pid = li.xpath("./text()")[1]
        name = li.xpath("./a/text()")[0]
        print(pid,name)

运行代码,最终结果已经获取完毕,返回数据格式为:

001.  妙蛙种子
002.  妙蛙草
003.  妙蛙花
004.  小火龙
005.  火恐龙
006.  喷火龙
007.  杰尼龟
008.  卡咪龟
009.  水箭龟
010.  绿毛虫
011.  铁甲蛹

数据获取完毕,需要将其存放到本地文件中,本次实验将数据存储在 CSV 文件中,后续可用于分析与二次爬取(基于该数据进行其他内容的爬取)。

CSV 文件存储属于 Python 基础支持内容,不再进行过多说明。

实验总结

本实验重点需要理解与掌握 lxml 库与 XPath 解析语法的初步结合应用,学习完本实验之后,再进行爬虫代码的编写,你将从正则表达式的繁琐里面彻底解放出来,极大地提高解析数据的速度。本实验学习完毕希望你对 XPath 有初步认知,如果你想提前预习一部分 XPath 相关知识,可参照文章中提及的学习地址或自行通过搜索引擎学习。

以上内容都完成之后,如果还有富余时间,建议大家可以学习一些前端知识,毕竟对于一个爬虫编写者来说,目标数据的载体很多时候都是网站程序。