03聚焦式爬虫

AffettoIris 2023-3-5 2,573 3/5

聚焦爬虫是基于通用爬虫的,通用爬虫爬取整张网页后,经数据解析,就是聚焦爬虫。

数据解析分类:

  • 正则表达式

  • bs4

  • xpath(重点,最通用,xpath不止适用于python语言编写的爬虫)

数据解析原理概述

前端都知道,文字通常存在div、li等标签里,图片、视频等存于标签的src属性里,所以我们只需要定位到标签或src即可。

聚焦爬虫编码流程

  • 指定url

  • 发起请求

  • 获取响应数据

  • 数据解析

  • 持久化存储

基于正则表达式的数据解析

实战之保存图片

import re
import requests

if __name__ == '__main__':
    url = 'https://i0.hdslb.com/bfs/face/d7c4d7a191af8218450b2462657f5ffc15c05652.jpg@240w_240h_1c_1s.webp'
    # text返回字符串形式的响应数据,content返回二进制形式的响应数据,而图片正是二进制格式,json()则适用content-type: json
    # 试过了,用text,写入txt文件是一堆方框代表的乱码
    img = requests.get(url=url).content

    # wb:以二进制格式打开一个文件,只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
    with open('./qiutu.webp', 'wb') as file:  # 'wb'指write byte,即 写入二进制文件,写入时会覆盖文件,可用于下载和写入图片、视频、压缩包等二进制文件
        file.write(img)

如何查阅python官方文档

其实没必要,pycharm给你看的就是官方文档给你看的,并没有多详细

03聚焦式爬虫

03聚焦式爬虫

03聚焦式爬虫

03聚焦式爬虫

实战之基于通用式爬虫爬取整张网页,基于此用正则表达式数据分析出url,最后下载保存图片

import re
import requests
import os

if __name__ == '__main__':
    if not os.path.exists('./img'):
        os.mkdir('./img')
    url = 'https://anzhiy.cn/'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
    }
    text = requests.get(url=url, headers=headers).text

    pattern = r"""<div class="post_cover right">.*?<img.*?data-lazy-src="(.*?)!cover"""
    pic_list = re.findall(pattern, text, re.S)
    for i in range(len(pic_list)):
        img_name = pic_list[i].split('/')[-1]
        with open('./img/' + img_name, 'wb') as file:
            img = requests.get(url=pic_list[i], headers=headers).content
            file.write(img)
            file.close()
            print(img_name + "download successfully!")

    print("脚本over!")

基于Bs4的数据解析

前言:基于正则的数据解析,由于正则规则通用于各语言,所以它适用于其他语言的爬虫;而bs4是python独有的,所以基于bs4的数据解析只适用于python

bs4数据解析的原理

  1. 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中

  2. 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

  3. 初衷是:要是能把js的选择器语法搬到python中就好了

环境安装

  • pip install bs4

  • pip install lxml (一种数据解析库/器,辅助bs4和xpath)

流程

  1. 实例化BeautifulSoup对象
    • from bs4 import BeautifulSoup

    • 将本地的html文档中的数据或网上爬到的页面源码加载到该对象中去

      file = open('./target.html', 'r', encoding='utf-8')
      soup = BeautifulSoup(file, 'lxml')  # 参数二是固定的,指定了BeautifulSoup使用lxml数据解析器解析参数一代表的文档 # 这步是借用了类的构造函数初始化实例
      
      text = response.text
      soup = BeautifulSoup(text, 'lxml')
  2. 调用BeautifulSoup api

    选中标签

    比较方便的是下面的api不止soup可用,api返回的结果亦可用,如soup.find(div).find('ul')

    • print(soup)是载入的整个html文档

    • soup.标签名如soup.a即可表示第一个a标签,含其子节点,就是js的document.getElementByTagName()

    • soup.find()

      • soup.find('标签名')如soup.find(div)等同于soup.div

      • soup.find(‘标签名’, class_='类名'),等同于document.getElementsByTagName('标签名')[0, 1, ..., n].getElementByClassName('类名'),之所以class_而非class是因为class是python关键字.。所得结果亦含子标签,只匹配第一个符合条件的标签
      • soup.find_all('标签名')返回符合要求的所有标签,结果以list形式返回

        soup.find_all(‘标签名’, class_='类名')
      • 事实上我们不仅可以用class筛选标签,看代码

        soup.find_all(‘a’, attrs={‘class’: ‘bets-name’})

        暂不细究。

    • soup.select(选择器如id、class、标签...选择器),结果以list形式返回,标签亦夹带子标签。如soup.select('div')、 soup.select('.className')、soup.select('#idName'),从他兄弟soup.select_one()可见前者相当于document.querySelectorAll(),后者相当于document.querySelector()。

      值得注意的是soup.select()还混用了css的选择器,如soup.select("#idName > ul > .className")或soup.select("#idName > ul .className"),即soup.select()可以使用层级选择器

    获取标签的文本数据或属性

    记obj = soup.select('div')[0]

    • 获取标签之间的文本数据

      obj.text或obj.string或obj.get_text(),只用记get_text()即可

      区别:text/get_ text():可以获取某一个标签中所有的文本内容,即使是其子标签的内容(非直系);string: 只可以获取该标签下面直系的文本内容

      例如对于<div>abc<a>123</a></div>来说,obj.text或obj.get_text()是abc123,对于obj.string理应是abc,其实输出None,BeautifulSoup的官方文档解释道:.string方法在tag包含多个子节点时,tag无法确定.string方法应该调用哪个子节点的内容,所以输出None。总结:弃用.string
    • 获取标签的属性值

      obj['href']、obj['src']

实战之批量爬取三国演义

import requests
import os
from bs4 import BeautifulSoup

if __name__ == '__main__':
    if not os.path.exists('./sanguo'):
        os.mkdir('./sanguo')
    url = 'https://www.shicimingju.com/book/sanguoyanyi.html'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
    }
    response = requests.get(url=url, headers=headers)
    # 可以F12检查<meta charset='字符集是啥'>,然后灵活变换下行的值
    response.encoding = 'utf-8'  # 如果不加上,下面打印出来的是第ä¸å·宴æ¡å豪æ°ä¸ç»ä¹  æ©é»å·¾è±éé¦ç«å
    sanguoyanyi_text = response.text
    soup = BeautifulSoup(sanguoyanyi_text, 'lxml')
    li_list = soup.select('.book-mulu > ul > li')
    href_list = []
    title_list = []
    for li in li_list:
        href_list.append('https://www.shicimingju.com' + li.a['href'])
        title_list.append(li.find('a').text)

    print(title_list[0])  # 打印:第一回·宴桃园豪杰三结义  斩黄巾英雄首立功

    for i in range(len(href_list)):
        with open('./sanguo/' + title_list[i] + '.txt', 'w', encoding='utf-8') as file:
            sub_response = requests.get(url=href_list[i], headers=headers)
            sub_response.encoding = 'utf-8'
            sub_text = sub_response.text
            sub_soup = BeautifulSoup(sub_text, 'lxml')
            file.write(sub_soup.select('.bookmark-list .chapter_content')[0].text)
            file.close()
            print("download第" + str(i) + "个 successfully!")

基于Xpath的聚焦爬虫

xpath解析是三种方案里最方便、最通用的方案,即亦可以应用到其他语言中。

xpath解析原理

  • 实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中。

  • 调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕获。

环境的安装

pip install lxml

如何实例化一个etree对象

from lxml import etree

  • 将本地的html文档中的源码数据加载到etree对象(寓意element tree,文档树)中:tree= etree. parse(filePath)

    其实文件输入流即tree = etree.parse(open('bili.html', 'r+', encoding='utf-8'))也是OK的,记得选用可读且不清空文本的模式。

  • 可以将从互联网上获取的源码数据加载到该对象中tree= etree.HTML(page_text)

    注意,我遇到一个没指定编码格式导致etree看到的是乱码带来报错说是非法的标签名:lxml.etree.XMLSyntaxError: StartTag: invalid element name, line 1, column 2,解决:

    parser = etree.HTMLParser(encoding="utf-8")
    tree = etree.parse('./temp/二手房.html', parser=parser)
  • xpath只需要掌握这一个函数,重点在xpath表达式,tree.xpath(‘xpath表达式’)

xpath表达式

xpath通过层级关系构建表达式并且只能通过层级关系构建表达式。例如获取<title></title>节点,tree.xpath('html/head/title'),但是html来自根节点,要前缀/,即tree.xpath('/html/head/title')

实战发现tree = etree.pase(filePath)中filePath的文件的html标签要严丝合缝地闭合:

反例:
<img src="">
<meta charset="UTF-8">
link rel="shortcut icon" href="logo.ico">
正例:
<img src="" />
<meta charset="UTF-8" />
link rel="shortcut icon" href="logo.ico" />

不然会报错:lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: meta line 6 and head, line 8, column 8,好消息是报错会告诉你在第几line没闭合。

实现定位的xpath表达式语法

from lxml import etree

if __name__ == '__main__':
    tree = etree.parse('bili.html')
    r = tree.xpath('/html/head/title')
    print(r)  # [<Element title at 0x18632903880>]
    # 分析:列表,返回了标题对象地地址组成的list

亲子选择器/和后代选择器//

对于<body>
    <div>1<div>4</div></div>
    <div>2</div>
    <div>3</div>
</body>来说print(tree.xpath('/html/body/div'))  # [<Element div at 0x26518df3640>, <Element div at 0x26518df3880>, <Element div at 0x26518df3980>]
print(tree.xpath('/html/div')) # []
可见这种方式有层层剥皮的韵味,外皮没剥就不能剥内皮,得从根皮开始剥。换个前端人易理解的话,这里的 父标签/子标签 相当于BeautfulSoup.select()>或者说css选择器的亲子选择器>;当/出现在xpath表达式的最左侧表示文档根标签如tree.xpath('/html/head');当然也就有 祖先标签//后代标签 等价于css的后代选择器 祖先标签空格后代标签:
print(tree.xpath('/html//div')) # [<Element div at 0x1fc8e8135c0>, <Element div at 0x1fc8e813500>, <Element div at 0x1fc8e813800>, <Element div at 0x1fc8e813900>]
如果//作用于xpath表达式的最左侧,如obj.xpath('//div')则表示可以从tree的任意位置去寻找div标签,请注意,我说明的很准确,即使是甲div含着乙ul,乙含着丙li,然后丙li.xpath('//div')都能选中甲,正确的做法是丙li.xpath('.//div'),这样只会在丙里查找div
print(tree.xpath('///div')) # [<Element div at 0x1fc8e8135c0>, <Element div at 0x1fc8e813500>, <Element div at 0x1fc8e813800>, <Element div at 0x1fc8e813900>]

通过属性值定位元素

对于<body>
    <div class="demo">1<div>4</div></div>
    <div data-index="0">2</div>
    <div>3</div>
</body>来说也可以通过属性键值对选中标签,但目前这是唯一一处不用.#的语法:
tree,xpath('//标签名[@属性名="属性值"]')  # 标签除了div等,还可以是通配符*,代表任意标签
print(tree.xpath('//div[@data-index="0"]'))  # [<Element div at 0x15aa5be3740>]
print(tree.xpath('//div[@class="demo"]'))  # [<Element div at 0x15aa5be3780>]

索引定位

对于<body>
    <div><li>1</li>
        <li>2</li>
        <li>3</li></div>
</body>来说print(tree.xpath('//div/li[3]'))  # [<Element li at 0x28957816340>]
很无语索引是从第1个开始计数的

obj.xpath()的obj可以是整个网页文档树tree,也可以是某个标签

li = etree.parse('1.html').xpath('/html/body/ul/li')[0]
li.xpath('./div')  # 此时可以.开头,不可以/开头,.代表当前节点li,就像上行最左的/代表根节点'1.html'
li.xpath('.//div') 
而li.xpath('//div') 依旧是从html根开始匹配

定位后如何取文本

/text() 取标签中直系的文本内容,返回list
//text() 取标签中直系和非直系的文本内容(所有的文本内容),返回list
对于<ul><li>1</li></ul>来说
print(tree.xpath('//ul/text()'))  # []
print(tree.xpath('//ul//text()'))  # ['1']
print(tree.xpath('//ul/li/text()'))  # ['1']
对于
<ul> <!-- 记为A -->
    <li>1<!-- 记为B --></li><!-- 记为C -->
    <li>2<!-- 记为D --></li><!-- 记为E -->
</ul>来说
print(tree.xpath('//ul/text()'))  # ['\n        ', '\n        ', '\n    '] 即[A, B, C]
print(tree.xpath('//ul//text()'))  # ['\n        ', '1', '\n        ', '2', '\n    ']即[A、B、C、D、E]
print(tree.xpath('//ul/li/text()'))  # ['1', '2']即[B、D]

请注意,/text()和//text()返回list,这是为什么呢?产生多个结果有两种因:

  • 像上例的第一个print(),就有三个值,三个值源于值在heml中被其他子标签分割

  • tree.xpath('/html/body/ul/li')的选出的li结果可能是多个,例如三个ul,每个ul有2个li,那么就选除了6个li,所以tree.xpath('/html/body/ul/li/text()')的list至少有6项。

定位后如何取属性

选中元素后接/@属性名即可,例如tree.xpath('//img/@src')

xpath表达式之 A表达式 | B表达式

对于<ul><li>1</li></ul>
    <ol><li>2</li></ol>来说与其
print(tree.xpath(r'//ul/li/text()'))  # ['1']
print(tree.xpath(r'//ol/li/text()'))  # ['2'] 不如
print(tree.xpath(r'//ul/li/text() | //ol/li/text()'))  # ['1', '2']

哦,对了,浏览器选中元素,右键,不止能复制节点,还能复制节点的xpath。

不是很推荐,这是浏览器给的xpath: '/html/body/div[6]/div[2]/div[2]/div[1]/div[7]/div[2]/ul/li[1]/a'。看得出来它是严格的索引定位,当有的网页段落多,有的网页段落少,本网页的索引序号可能不适用另一个网页了。

指定编码方案总结

  • response = requests.get(url=url, headers=headers)
    response.encoding = 'utf-8  # 这样会把抓到的整个文档都指定编码,如果你想保留原样,只针对某一块内容指定编码,请看方案二。这是从被解析的文本的角度设置编码方案
    但是这样子还是需要爬虫工程师手动去看网页的<meta charset='啥'>,不如
    response.encoding = response.apparent_encoding
    因为print(response.apparent_encoding)会得到原网页的原始编码方案如GB2312
  • page_text= = requests.get(url=url, headers=headers).text
    img_name = page_text.xpath('//img/@title')[0]
    img_name.encode('iso-8859-1').decode('gbk')  # 只截取了图片名字做编码处理,iso-8859-1和gbk适用于中文被乱码了
  • page_text= = requests.get(url=url, headers=headers).text
    file = open('1.txt', 'w', encoding='utf-8')
    file.write(page_text)
  • page_text= = requests.get(url=url, headers=headers).text
    parser = etree.HTMLParser(encoding='utf-8')
    tree = etree.HTML(page_text, parser=parser)  # 有时page_text是乱码,导致etree认不出方框乱码是不是标签,可以从解析器的角度设置编码方案

bs4好还是xpath好?

xpath好。首先xpath不仅适用python语言,也适用于其他语言编写的爬虫;其次xpath只需要学会一个函数,bs4有四五个;最后,xpath自由度更高,我曾经做过一个案例,xpath比bs4少写个for in 循环。

实战 - 城市名称爬取

import requests
from lxml import etree

if __name__ == '__main__':
    url = 'http://www.aqistudy.cn/historydata/'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
    }
    page_text = requests.get(url=url, headers=headers).text
    parser = etree.HTMLParser(encoding='utf-8')
    tree = etree.HTML(page_text, parser=parser)
    a_list = tree.xpath(r'//div[@class="bottom"]/ul[@class="unstyled"]/li/a/text()')
    print(a_list)
- THE END -

AffettoIris

10月16日16:06

最后修改:2023年10月16日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论