【Python爬虫】Python爬虫三大基础模块(urllib & BS4 & Selenium)

【Python爬虫】Python爬虫三大基础模块(urllib & BS4 & Selenium)

文章目录

  • 【Python爬虫】Python爬虫三大基础模块(urllib & BS4 & Selenium)
    • 一、Python爬虫的基本知识
      • 1、网络爬虫是什么?
        • 1)爬虫分类
        • 2)爬虫应用
      • 2、Python编写爬虫的流程
        • 1)编写爬虫的基础流程
        • 2)第一个Python爬虫程序
          • a)获取响应对象
          • b) 输出HTML信息
    • 二、Python爬虫常用库
      • 1、urllib基础介绍
        • 1)urllib.request 模拟浏览器发起请求
          • a)urlopen发起URL请求
          • b)request.Request模拟浏览器携带header信息
          • c)解决urllib无法获取动态网页的问题(使用selenium)
        • 2)urllib.error 异常请求处理
        • 3)urllib.parse 解析URL
          • a)urlparse 解析URL属性
          • b)urlencode 对传参的请求编码 & unquote对请求解码
        • 4)urllib.robotparser 解析`robots.txt`文件规则
          • a)实例1:解析baidu的`robots`协议文件
          • b)实例2:解析bilibili的`robots`协议文件
          • c)实例3:`urllib.can_fetch()` 作为条件判断
        • 5)IP代理避免反爬虫
      • 2、Bs4基础介绍
        • 1)soup文档对象 及 对象类型
          • a)初始化`soup`文档对象
          • b)`Tag`标签对象类型
          • c)`NavigableString`字符串对象类型
          • d)`Comment`注释类型(`prettify()`对标签内容进行美化)
        • 2)遍历
          • a)子节点:`contents`,`children`和`descendants`
          • b)父节点:`parent`和`parents`
          • c)兄弟节点:`next_sibling(s) & previous_sibling(s)`
          • d)上/下一个`HTML`解析对象:`next_element(s) & previous_element(s)`
        • 3)文档搜索
          • a)`find_all()`搜索子孙节点(`find`等价于`find_all(limit=1)`)
          • b)其他`find_xxx()`方法
          • c)css选择器
        • 4)文档修改
          • a)直接赋值修改
          • b)append追加内容(标签/文本)
          • c)insert指定位置插入(标签/文本)
          • d)clear & extract & decompose:移除内容/移除标签
          • e)wrap & unwrap:包装和解包
      • 3、Selenium基础介绍
        • 1)安装浏览器驱动
        • 2)第一个Selenium脚本
        • 3)常用方法
          • a)`find_element()`
          • b)`find_elements()`
          • c)获取/设置 cookies 或 token
          • d)下拉滚动条
        • 4)模拟登录淘宝
          • a)selenium自动开启新的浏览器(登录失败)
          • b)selenium绑定用户开启的浏览器(登录成功)
        • 5)其他问题

参考资料

  • Python爬虫教程(从入门到精通)

  • Python urllib | 菜鸟教程

  • Beautiful Soup 4 入门手册_w3cschool

  • Selenium入门指南

  • Selenium教程

  • 什么是 Scrapy|极客教程

  • Scrapy入门教程

一、Python爬虫的基本知识

1、网络爬虫是什么?

我们所熟悉的一系列搜索引擎都是大型的网络爬虫,比如百度、搜狗、360浏览器、谷歌搜索等等。每个搜索引擎都拥有自己的爬虫程序,比如 360 浏览器的爬虫称作 360Spider,搜狗的爬虫叫做 Sogouspider。

1)爬虫分类

爬虫可分为三大类:通用网络爬虫、聚焦网络爬虫、增量式网络爬虫。

  • 通用网络爬虫

    是搜索引擎的重要组成部分,上面已经进行了介绍,这里就不再赘述。通用网络爬虫()需要遵守 robots 协议,网站通过此协议告诉搜索引擎哪些页面可以抓取,哪些页面不允许抓取。

    robots 协议:是一种“约定俗称”的协议,并不具备法律效力,它体现了互联网人的“契约精神”。行业从业者会自觉遵守该协议,因此它又被称为“君子协议”。

    为了限制爬虫带来的危险,大多数网站都有良好的反爬措施,并通过 robots.txt 协议做了进一步说明,下面是淘宝网 robots.txt 的内容:

    User-agent: Baiduspider 
    Disallow: /baidu Disallow: /s? 
    Disallow: /ulink? 
    Disallow: /link? 
    Disallow: /home/news/data/ 
    Disallow: /bh
    .....
    User-agent: * 
    Disallow: /
    

    从协议内容可以看出,淘宝网对不能被抓取的页面做了规定。因此大家在使用爬虫的时候,要自觉遵守 robots 协议

  • 聚焦网络爬虫

    是面向特定需求的一种网络爬虫程序。它与通用爬虫的区别在于,聚焦爬虫在实施网页抓取的时候会对网页内容进行筛选和处理,尽量保证只抓取与需求相关的网页信息。聚焦网络爬虫极大地节省了硬件和网络资源,由于保存的页面数量少所以更新速度很快,这也很好地满足一些特定人群对特定领域信息的需求。

  • 增量式网络爬虫

    是指对已下载网页采取增量式更新,它是一种只爬取新产生的或者已经发生变化网页的爬虫程序,能够在一定程度上保证所爬取的页面是最新的页面

2)爬虫应用

随着网络的迅速发展,万维网成为大量信息的载体,如何有效地提取并利用这些信息成为一个巨大的挑战,因此爬虫应运而生,它不仅能够被使用在搜索引擎领域,而且在大数据分析,以及商业领域都得到了大规模的应用。

  • 数据分析:在数据分析领域,网络爬虫通常是搜集海量数据的必备工具。对于数据分析师而言,要进行数据分析,首先要有数据源,而学习爬虫,就可以获取更多的数据源。在采集过程中,数据分析师可以按照自己目的去采集更有价值的数据,而过滤掉那些无效的数据。

  • 商业领域:对于企业而言,及时地获取市场动态、产品信息至关重要。企业可以通过第三方平台购买数据,比如贵阳大数据交易所、数据堂等,当然如果贵公司有一个爬虫工程师的话,就可通过爬虫的方式取得想要的信息。

2、Python编写爬虫的流程

Python 语言支持多个爬虫模块,比如 urllibrequestsBs4 等。Python 的请求模块和解析模块丰富成熟,并且还提供了强大的 Scrapy 框架,让编写爬虫程序变得更为简单。

1)编写爬虫的基础流程

爬虫程序与其他程序不同,它的的思维逻辑一般都是相似的, 所以无需我们在逻辑方面花费大量的时间。下面对 Python 编写爬虫程序的流程做简单地说明:

  • 先由 urllib 模块的 request 方法打开 URL 得到网页 HTML 对象。
  • 使用浏览器打开网页源代码分析网页结构以及元素节点
  • 通过 Beautiful Soup 或者正则表达式提取数据。
  • 存储数据到本地磁盘或数据库。

当然也不局限于上述一种流程。编写爬虫程序,需要您具备较好的 Python 编程功底,这样在编写的过程中您才会得心应手。爬虫程序需要尽量伪装成人访问网站的样子,而非机器访问,否则就会被网站的反爬策略限制,甚至直接封杀 IP,相关知识会在后续内容介绍。

2)第一个Python爬虫程序

a)获取响应对象

向百度(http://www.baidu.com/)发起请求,获取百度首页的 HTML 信息,代码如下:

#导包,发起请求使用urllib库的request请求模块
import urllib.request
# urlopen() 向URL发请求,返回响应对象,注意url必须完整
response = urllib.request.urlopen('http://www.baidu.com/')
print(response)

上述代码会返回百度首页的响应对象, 其中 urlopen()表示打开一个网页地址。注意:请求的url必须带有http或者https传输协议。

输出结果,如下所示:

<http.client.HTTPResponse object at 0x032F0F90>
b) 输出HTML信息

在上述代码的基础上继续编写如下代码:

#提取响应内容
html = response.read().decode('utf-8')
#打印响应内容
print(html)

输出结果如下,由于篇幅过长,此处只做了简单显示,可以看到解码格式为utf-8charset=utf-8"

<!DOCTYPE html><!--STATUS OK--> <html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta content="always" name="referrer"><meta name="theme-color" content="#2932e1"><meta name="description" content="全球最大的中文搜索引擎、致力于让网民更便捷地获取信息,找到...">...</html>

通过调用response响应对象的 read()方法提取 HTML 信息,该方法返回的结果是字节串类型(bytes),因此需要使用decode()转换为字符串。程序完整的代码程序如下:

from urllib import request

#访问完整的url链接,返回响应对象
response = request.urlopen("http://www.baidu.com")
#通过响应对象获取html内容
html = response.read().decode("utf-8")
print(html)

通过上述代码获取了百度首页的 html信息,这是最简单、最初级的爬虫程序。后续我们还学习如何分析网页结构、解析网页数据,以及存储数据等。

二、Python爬虫常用库

1、urllib基础介绍

参考

  • Python urllib | 菜鸟教程

  • 网络爬虫urllib库常用函数解析

Python urllib库是Python内置库,用于获取网页HTML信息, 并对网页的内容进行抓取处理。

urllib库包含以下几个模块:

  • urllib.request:打开和读取 URL。
  • urllib.error :包含 urllib.request 抛出的异常。
  • urllib.parse :解析 URL。
  • urllib.robotparser :解析 robots.txt 文件。

1)urllib.request 模拟浏览器发起请求

urllib.request定义了一些打开 URL 的函数和类,包含授权验证、重定向、浏览器 cookies等。urllib.request可以模拟浏览器的一个请求发起过程

a)urlopen发起URL请求

我们可以使用urllib.requesturlopen方法来打开一个URL,语法格式如下:

urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)

  • url:url 地址。
  • data:发送到服务器的其他数据对象,默认为 None。
  • timeout:设置访问超时时间。
  • cafile 和 capath:cafile 为 CA 证书, capath 为 CA 证书的路径,使用 HTTPS 需要用到。
  • cadefault:已经被弃用。
  • context:ssl.SSLContext类型,用来指定 SSL 设置。

实例如下:

from urllib.request import urlopen

myURL = urlopen("https://www.runoob.com/")
print(myURL.read())   #二进制的html内容
---
b'<!Doctype html>\n<html>\n<head>\n\t<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n\t<meta name="viewport" content="width=device-width, initial-scale=1.0" />\n\t<title>\xe8\x8f\x9c\xe9\xb8\x9f\xe6\x95\x99\xe7\xa8\x8b - \xe5\xad\xa6\xe7\x9a\x84\xe4\xb8\x8d\xe4\xbb\x85\xe6\x98\xaf\xe6\x8a\x80\xe6\x9c\xaf\xef\xbc\x8c\xe6\x9b\xb4\xe6\x98\xaf\xe6\xa2\xa6\xe6\x83\xb3\xef\xbc\x81</title>\n\n  <meta name=\'robots\' content=\'max-image-preview:large\' />\n<link rel=\'stylesheet\' 
...

print(myURL.read().decode("utf-8"))  #utf-8格式的html内容
---
<!Doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>菜鸟教程 - 学的不仅是技术,更是梦想!</title>
...

以上代码使用 urlopen 打开一个 URL,然后使用read()函数获取网页的 HTML 实体代码。read()是读取整个网页内容,也可以指定读取的长度:

from urllib.request import urlopen

myURL = urlopen("https://www.runoob.com/")
print(myURL.read(50).decode("utf-8"))   #utf-8格式的html内容
---
<!Doctype html>
<html>
<head>
    <meta http-equiv="C
b)request.Request模拟浏览器携带header信息

Http请求的header中可以携带请求端能够处理的压缩编码类型、浏览器和服务器端的连接类型(keepAlive表示传输HTTP数据建立的TCP连接/close表示TCP连接关闭)、操作系统和浏览器名称及版本等,参考浅谈Http请求中header的作用 - 简书

我们抓取网页一般需要对 headers(网页头信息)进行模拟,这时候需要使用到 urllib.request.Request类:

urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
  • url:url 地址。
  • data:发送到服务器的其他数据对象,默认为 None。
  • headers:HTTP 请求的头部信息,字典格式。
  • origin_req_host:请求的主机地址,IP 或域名。
  • unverifiable:很少用整个参数,用于设置网页是否需要验证,默认是False。。
  • method:请求方法, 如 GET、POST、DELETE、PUT等。

有些网站在处理http请求时,如果发现未携带header信息会请求失败:

import urllib.request

url = "https://movie.douban.com/"

resp = urllib.request.urlopen(url)
# 从响应结果中读取数据
html = resp.read().decode("utf-8")
print(html)
---
...
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 418: 

主要原因是豆瓣服务器做了反爬虫措施,拒绝非浏览器的访问,检测到爬虫就返回一个 418 响应。参考爬取豆瓣电影top250 - 状态码 418 的解决方案 - 简书

解决方法如下实例

import urllib.request

url = "https://movie.douban.com/"

headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36'}
req = Request(url,headers=headers)
resp = urllib.request.urlopen(req)

# 从响应结果中读取数据
html = resp.read().decode("utf-8")
print(html)
c)解决urllib无法获取动态网页的问题(使用selenium)

参考

  • Python爬虫:为什么你爬取不到网页数据

  • Python爬虫:爬取动态网页数据“你”需要知道的事

  • selenium和bs4的联合使用

先看看下面这段代码的执行结果:

from urllib import request
from urllib.request import Request

up_mainPage_url = 'https://space.bilibili.com/1641484091/?spm_id_from=333.999.0.0'
# 添加请求头
headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3877.400 QQBrowser/10.8.4507.400'
}
req = Request(up_mainPage_url, headers=headers)  # 如果不添加headers无法正常访问b站完整的html内容
response = request.urlopen(req)
html_byte = response.read()  # 通过响应对象获取html内容
out_file = open('temp001.html','wb+')
out_file.write(html_byte)
out_file.close()

可以看到urllib只能抓取静态网页的内容,对于ajax异步渲染的完整html页面并不能获取得到,这里可以通过selenium来实现(selenium的使用教程参考第3小节)

from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

up_mainPage_url = 'https://space.bilibili.com/1641484091/?spm_id_from=333.999.0.0'# 加载动态网页
option = ChromeOptions()
# 设置浏览器为无头模式,使用过程中不会弹出浏览器页面
option.headless = True
driver = Chrome(service=ChromeService(ChromeDriverManager().install()), options=option)  # 自动更新chrome驱动
driver.get(up_mainPage_url)
driver.implicitly_wait(10)
html_byte = driver.page_source.encode(encoding='utf-8')
out_file = open('temp002.html', 'wb+')
out_file.write(html_byte)
out_file.close()

可以看到这两个文件大小相差很大,后者获取的是完整的html:

在这里插入图片描述

2)urllib.error 异常请求处理

urllib.error 模块为 urllib.request 所引发的异常定义了异常类,基础异常类是 URLError

urllib.error包含了两个方法,URLErrorHTTPError

  • URLErrorOSError的一个子类,用于处理程序在遇到问题时会引发此异常(或其派生的异常),包含的属性reason为引发异常的原因

  • HTTPErrorURLError的一个子类,用于处理特殊HTTP错误例如作为认证请求的时候,包含的属性 code 为 HTTP 的状态码, reason为引发异常的原因,headers 为导致 HTTPError 的特定 HTTP 请求的 HTTP 响应头

对不存在的网页抓取并处理异常,实例如下:

from urllib.request import urlopen
from urllib import error

myURL = urlopen("https://www.runoob.com/")
print(f"myURL status: {myURL.getcode()}")  # 获取HTTP状态码

#请求异常捕获
try:
   noURL = urlopen("https://www.runoob.com/no.html")
except error.HTTPError as e :
   if e.code == 404:
      print("noURL status: 404")
---
myURL status: 200
noURL status: 404

3)urllib.parse 解析URL

a)urlparse 解析URL属性

urllib.parse.urlparse用于解析URL,格式如下:

urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)
  • urlstring为字符串的 url 地址,scheme为协议类型,

  • allow_fragments参数为false,则无法识别片段标识符。相反,它们被解析为路径,参数或查询组件的一部分,并 fragment 在返回值中设置为空字符串。实例如下:

from urllib.parse import urlparse  

 # 获取url的解析结果对象,包含10个属性:(scheme,netloc,pth,params,query,fragment,username,password,hostname,port) 
parse_result = urlparse("https://www.bilibili.com/video/BV1XY41117Zq/?spm_id_from=333.1007.tianma.4-2-12.click&vd_source=c15f1a3e96ac60bc285f138ab7aeaa1c")
print(parse_result)  
---
ParseResult(scheme='https', netloc='www.bilibili.com', path='/video/BV1XY41117Zq/', params='', query='spm_id_from=333.1007.tianma.4-2-12.click&vd_source=c15f1a3e96ac60bc285f138ab7aeaa1c', fragment='')

从结果可以看出,内容是一个元组,包含 6 个字符串:协议,位置,路径,参数,查询,判断。我们可以直接读取协议内容,实例如下:

from urllib.parse import urlparse  

parse_result = urlparse("https://www.bilibili.com/video/BV1XY41117Zq/?spm_id_from=333.1007.tianma.4-2-12.click&vd_source=c15f1a3e96ac60bc285f138ab7aeaa1c")

print(f"scheme: {parse_result.scheme}, netloc: {parse_result.netloc}, "
      f"pth: {parse_result.path}, params: {parse_result.params}, "
      f"query: {parse_result.query}, fragment: {parse_result.fragment}, "
      f"username: {parse_result.username}, password: {parse_result.password}, "
      f"hostname: {parse_result.hostname}, port: {parse_result.port}")
---
scheme: https, netloc: www.bilibili.com, pth: /video/BV1XY41117Zq/, params: , query: spm_id_from=333.1007.tianma.4-2-12.click&vd_source=c15f1a3e96ac60bc285f138ab7aeaa1c, fragment: , username: None, password: None, hostname: www.bilibili.com, port: None

完整内容如下:

属性 索引 值(如果不存在)
scheme 0 URL协议 scheme参数
netloc 1 网络位置部分 空字符串
path 2 分层路径 空字符串
params 3 最后路径元素的参数 空字符串
query 4 查询组件 空字符串
fragment 5 片段识别 空字符串
username 用户名 None
password 密码 None
hostname 主机名(小写) None
port 端口号为整数(如果存在) None
b)urlencode 对传参的请求编码 & unquote对请求解码
from urllib.request import Request
from urllib.parse import urlencode
from urllib.parse import unquote

url = 'https://search.bilibili.com/all?'  # bilibili搜索页面
dic = {'keyword': '氰化物欢乐秀合集 p1'}
full_url = url + urlencode(dic)  # 对请求进行编码
print(full_url)  
print(unquote(full_url))  # 对请求进行解码

header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36'
}  # 头部信息
request = Request(full_url, headers=header)
reponse = urlopen(request).read()
print(reponse)

fh = open("./urllib_test_bilibili_search.html", "wb")  # 以二进制的形式将文件写入到当前目录中
fh.write(reponse)
fh.close()

---
https://search.bilibili.com/all?keyword=%E6%B0%B0%E5%8C%96%E7%89%A9%E6%AC%A2%E4%B9%90%E7%A7%80%E5%90%88%E9%9B%86+p1
https://search.bilibili.com/all?keyword=氰化物欢乐秀合集+p1
b'<!DOCTYPE html>\n<html lang="zh-CN">\n  <head>\n    <meta charset="UTF-8" />\n    \n      <title>\xe6\xb0\xb0\xe5\x8c\x96\xe7\x89\xa9\xe6\xac\xa2\xe4\xb9\x90\xe7\xa7\x80\xe5\x90\x88\xe9\x9b\x86 p1-\xe5\x93\x94\xe5\x93\xa9\xe5\x93\x94\xe5\x93\xa9_Bilibili</title>\n      <meta name="description" content="bilibili\xe6\x98\xaf\xe5\x9b\xbd\xe5\x86\x85\xe7\x9f\xa5\xe5\x90\x8d\xe7\x9a\x84\xe8\xa7\x86\xe9\xa2\x91\xe5\xbc\xb9\xe5\xb9\x95\xe7\xbd\x91
...

4)urllib.robotparser 解析robots.txt文件规则

参考【爬虫篇】根据网站的robots.txt文件判断一个爬虫是否有权限爬取这个网页

urllib.robotparser 用于解析robots.txt文件。

robots.txt(统一小写)是一种存放于网站根目录下的 robots 协议,它通常用于告诉搜索引擎对网站的抓取规则。urllib.robotparser提供了 RobotFileParser 类,语法如下:

urllib.robotparser.RobotFileParser(url='')

这个类提供了一些可以读取、解析robots.txt文件的方法:

  • set_url(url) : 设置robots.txt文件的 URL。

  • read() : 读取robots.txt URL 并将其输入解析器。

  • parse(lines) : 解析行参数。

  • can_fetch(useragent, url) : 如果允许 useragent 按照被解析 robots.txt 文件中的规则来获取 url 则返回 True

  • mtime() : 返回最近一次获取 robots.txt文件的时间。 这适用于需要定期检查 robots.txt文件更新情况的长时间运行的网页爬虫。

  • modified() : 将最近一次获取 robots.txt文件的时间设置为当前时间。

  • crawl_delay(useragent): 为指定的 useragent 从 robots.txt 返回 Crawl-delay 形参。 如果此形参不存在或不适用于指定的 useragent 或者此形参的 robots.txt 条目存在语法错误,则返回 None。

  • request_rate(useragent): 以 named tuple RequestRate(requests, seconds) 的形式从robots.txt返回 Request-rate 形参的内容。 如果此形参不存在或不适用于指定的 useragent 或者此形参的 robots.txt 条目存在语法错误,则返回 None。

  • site_maps(): 以 list()的形式从 robots.txt 返回 Sitemap 形参的内容。 如果此形参不存在或者此形参的 robots.txt条目存在语法错误,则返回 None。

https://www.baidu.com/robots.txt 的内容如下:

User-agent: Baiduspider Disallow: /baidu Disallow: /s? Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh
User-agent: Googlebot Disallow: /baidu Disallow: /s? Disallow: /shifen/ Disallow: /homepage/ Disallow: /cpro Disallow: /ulink? Disallow: /link? Disallow: /home/news/data/ Disallow: /bh
...

https://www.bilibili.com/robots.txt 的内容如下:

User-agent: Yisouspider Allow: /
User-agent: Applebot Allow: /
User-agent: bingbot Allow: /
User-agent: Sogou inst spider Allow: /
User-agent: Sogou web spider Allow: /
User-agent: 360Spider Allow: /
User-agent: Googlebot Allow: /
User-agent: Baiduspider Allow: /
User-agent: Bytespider Allow: /
User-agent: PetalBot Allow: /
User-agent: * Disallow: /
a)实例1:解析baidu的robots协议文件
from urllib import robotparser

# 参考 https://blog.csdn.net/weixin_40458518/article/details/122785687
#获取robotParser解析对象
robotp = robotparser.RobotFileParser("https://www.baidu.com/robots.txt")
#用来解析robots.txt文件并进行分析:该语句不能缺失,否则can_fetch方法中全都判断为False
robotp.read()
# 允许百度爬虫访问 域名的根目录
print(robotp.can_fetch('Baiduspider', 'https://www.baidu.com'))  # True
# 允许百度爬虫访问 目录/homepage/
print(robotp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))  # True
# 禁止谷歌爬虫访问 目录/homepage/
print(robotp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))  # False
print(robotp.can_fetch('Googlebot', 'https://www.baidu.com'))  # True
# 爬虫名称为空或在robots协议中找不到,就匹配"User-agent: *",禁止所有爬虫访问所有目录
print(robotp.can_fetch('', 'https://www.baidu.com'))  # False
b)实例2:解析bilibili的robots协议文件
from urllib import robotparser

# 参考 https://blog.csdn.net/weixin_40458518/article/details/122785687
#获取robotParser解析对象
robotp = robotparser.RobotFileParser("https://www.bilibili.com/robots.txt")
#用来解析robots.txt文件并进行分析:该语句不能缺失,否则can_fetch方法中全都判断为False
robotp.read()
# 允许百度爬虫访问 域名的根目录
print(robotp.can_fetch('Baiduspider', 'https://www.bilibili.com'))  # True
# 允许百度爬虫访问 目录/homepage/
print(robotp.can_fetch('Baiduspider', 'https://www.bilibili.com/'))  # True
# 禁止谷歌爬虫访问 目录/homepage/
print(robotp.can_fetch('Googlebot', 'https://www.bilibili.com/'))  # True
# 爬虫名称为空或在robots协议中找不到,就匹配"User-agent: *",禁止所有爬虫访问所有目录
print(robotp.can_fetch('', 'https://www.bilibili.com/'))  # False
c)实例3:urllib.can_fetch() 作为条件判断
from urllib import robotparser
from urllib.request import Request
from urllib.parse import urlencode
from urllib.parse import unquote

rp = robotparser.RobotFileParser("https://www.bilibili.com/robots.txt")
        rp.read()

dic = {'keyword': '间谍过家家'}
url = 'https://search.bilibili.com/bangumi?' + urlencode(dic)  #番剧
url2 = 'https://search.bilibili.com/article?' + urlencode(dic) #专栏

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                          'Chrome/98.0.4758.80 Safari/537.36'
}


if rp.can_fetch('Baiduspider', url):
    print(f'正在爬取网站:{unquote(url)}')  #解码后的url
    res = Request(url=url, headers=headers)
    print(res)
if rp.can_fetch('Baiduspider', url2):
    print(f'正在爬取网站:{unquote(url2)}')  #解码后的url
    res = Request(url=url2, headers=headers)
    print(res)
else:
    print('网站不允许任何爬虫,爬取网站的内容。')

---
正在爬取网站:https://search.bilibili.com/bangumi?keyword=间谍过家家
<urllib.request.Request object at 0x0000020DC0D2CC40>
正在爬取网站:https://search.bilibili.com/article?keyword=间谍过家家
<urllib.request.Request object at 0x0000020DC0D2CC70>

5)IP代理避免反爬虫

参考Python3网络爬虫(四):使用User Agent和代理IP隐藏身份

2、Bs4基础介绍

参考Beautiful Soup 4 入门手册_w3cschool

1)soup文档对象 及 对象类型

a)初始化soup文档对象
 from bs4 import BeautifulSoup

 soup = BeautifulSoup(open("urllib_test_bilibili_search.html",encoding="utf-8"))
 print(soup)
 print("*********************************")
 soup = BeautifulSoup("<html>data</html>")
 print(soup)  # <html><body><p>data</p></body></html>

 输出结果:
 ---
 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<title>氰化物欢乐秀合集 p1-哔哩哔哩_Bilibili</title>
<meta content="bilibili是国内知名的视频弹幕网站,这里有及时的动漫新番,活跃的ACG氛围,有创意的Up主。大家可以在这里找到许多欢乐。" name="description"/>
<meta content="B站,弹幕,字幕,AMV,MAD,MTV,ANIME,动漫,动漫音乐,游戏,游戏解说,ACG,galgame,动画,番组,新番,初音,洛天依,vocaloid" name="keywords"/>
...
 *********************************
 <html><body><p>data</p></body></html>
b)Tag标签对象类型

参考Beautiful Soup 4 对象类型_w3cschool

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构, 每个节点都是Python对象,所有对象可以归纳为4种:Tag, NavigableString, BeautifulSoup, Comment

  • 获取标签内容即类型:

    '''Tag类型: tag能够包含字符串或是其它tag'''
    soup = BeautifulSoup('<body>'
                         '<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                         '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                         '<div class="medium strikeout" style="color:blue">medium</div> '
                         '</body>')
    tag = soup.b  #获取文档对象中的b标签
    print(f"tag标签内容:{tag},b标签的类型={type(tag)}")
    
    输出结果
    ---
    tag标签内容:<b class="boldest strikeout" style="color:red">Extremely bold</b>,b标签的类型=<class 'bs4.element.Tag'>
    
  • 获取标签下的其他对象类型:

    '''Tag类型: tag能够包含字符串或是其它tag'''
    soup = BeautifulSoup('<body>'
                         '<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                         '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                         '<div class="medium strikeout" style="color:blue">medium</div> '
                         '</body>')
    
    '''获取Tag标签下的其他tag'''
    print(f"<body>标签下的其他tag:{soup.body.contents}")
    print(f"<body>标签下的其他tag:{soup.body.tag}",end="\n\n")
    
    输出结果
    ---
    
    <body>标签下的其他tag:[<b class="boldest strikeout" style="color:red">Extremely bold</b>, ' ', <b class="lightest strikeout" style="color:black">Extremely light</b>, ' ', <div class="medium strikeout" style="color:blue">medium</div>, ' ']
    <body>标签下的其他tag:None
    
  • 获取标签的name属性:

    '''Tag类型: tag能够包含字符串或是其它tag'''
    soup = BeautifulSoup('<body>'
                         '<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                         '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                         '<div class="medium strikeout" style="color:blue">medium</div> '
                         '</body>')
    tag = soup.b  #获取文档对象中的b标签
    
    '''Tag的name属性: 获取Tag标签名称'''
    #1. 通过Tag.name获取标签的名字
    print(f"origin tag.name: {tag.name},tag标签内容:{tag}")
    #2. 通过对Tag.name赋值修改标签名字
    tag.name = "helloWorld"
    print(f"update tag.name: {tag.name},tag标签内容:{tag}",end="\n\n")
    
    输出结果
    ---
    
      origin tag.name: b,tag标签内容:<b class="boldest strikeout" style="color:red">Extremely bold</b>
      update tag.name: helloWorld,tag标签内容:<helloWorld class="boldest strikeout" style="color:red">Extremely bold</helloWorld>
    
  • 获取标签的属性(单个属性/多个属性):

    '''Tag类型: tag能够包含字符串或是其它tag'''
    soup = BeautifulSoup('<body>'
                        '<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                        '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                        '<div class="medium strikeout" style="color:blue">medium</div> '
                        '</body>')
    tag = soup.b  #获取文档对象中的b标签
    
    '''Tag的attrs:获取Tag标签的属性'''
    #1. 获取Tag标签的所有属性
    print(f"tag.attrs:{tag.attrs}")  #只获取第一次出现该标签的所有属性
    #2. 获取Tag标签的单个属性
    print(f"tag['class']:{tag['class']}")  #只获取第一次出现该标签的class属性
    #3. 修改标签的class属性
    tag["class"] = ["helloWorld","HeySiri"]
    print(f"tag['class']:{tag['class']}",end="\n\n")
    
    输出结果
    ---
    tag.attrs:{'class': ['boldest', 'strikeout'], 'style': 'color:red'}
    tag['class']:['boldest', 'strikeout']
    tag['class']:['helloWorld', 'HeySiri']
    
  • 修改标签属性内容、 删除标签属性:

    '''Tag类型: tag能够包含字符串或是其它tag'''
    soup = BeautifulSoup('<body>'
                         '<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                         '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                         '<div class="medium strikeout" style="color:blue">medium</div> '
                         '</body>')
    tag = soup.b  #获取文档对象中的b标签
    
    #4. 删除标签的class属性
    del tag["class"]
    print(f"tag内容:{tag}",end="\n\n")
    # 添加并修改标签的class属性:会报错`return self.attrs[key] KeyError`: 'class',因为之前删除了该标签的class属性
    # tag["class"] = "helloWorld"
    # print(f"tag['class']:{tag['class']}")
    
    输出结果
    ---
    tag内容:<helloWorld style="color:red">Extremely bold</helloWorld>
    
c)NavigableString字符串对象类型

字符串常被包含在tag内。Beautiful Soup用 ​NavigableString​ 类来包装tag中的字符串(参考Beautiful Soup 4 可遍历的字符串_w3cschool):

  • 获取NavigableString内容即类型:

    soup = BeautifulSoup('<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                                '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                                '<div class="medium strikeout" style="color:blue">medium</div>')
    tag = soup.b  # 获取文档对象中的b标签
    unicode_string = tag.string  #通过unicode()方法可以直接将 NavigableString 对象转换成Unicode字符串
    print(f"tag标签内容:{tag},b标签的类型={type(tag)}")
    print(f"unicode_string内容:{unicode_string},unicode_string的类型={type(unicode_string)}",end="\n\n")
    
    输出结果
    ---
    tag标签内容:<b class="boldest strikeout" style="color:red">Extremely bold</b>,b标签的类型=<class 'bs4.element.Tag'>
    unicode_string内容:Extremely bold,unicode_string的类型=<class 'bs4.element.NavigableString'>
    
  • 修改标签的string文本属性(两种修改方式):

    soup = BeautifulSoup('<b class="boldest strikeout" style="color:red">Extremely bold</b> '
                                 '<b class="lightest strikeout" style="color:black">Extremely light</b> '
                                 '<div class="medium strikeout" style="color:blue">medium</div>')
    tag = soup.b  # 获取文档对象中的b标签
    tag.string = "Helloworld"
    print(f"tag标签内容:{tag},unicode_string内容:{tag.string}")
    tag.string.replace_with("fuckWorld")
    print(f"tag标签内容:{tag},unicode_string内容:{tag.string}")
    
    输出结果
    ---
    tag标签内容:<b class="boldest strikeout" style="color:red">Helloworld</b>,      unicode_string内容:Helloworld
    tag标签内容:<b class="boldest strikeout" style="color:red">fuckWorld</b>,unicode_string内容:fuckWorld
    
d)Comment注释类型(prettify()对标签内容进行美化)

参考Beautiful Soup 4 注释怎么写_w3cschool

  • 获取注释内容即类型:Comment其实是NavigableString的子类

    markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
    soup = BeautifulSoup(markup)
    comment = soup.b.string
    print(f"b标签的注释内容:{comment},注释类型={type(comment)}")  #bs4.element.Comment其实是NavigableString的子类
    
    输出结果
    ---
    b标签的注释内容:Hey, buddy. Want to buy a used parser?,注释类型=<class 'bs4.element.Comment'>
    
  • 注释美化和注释修改:Beautiful Soup中定义的其它类型都可能会出现在XML的文档中: CData , ProcessingInstruction , Declaration, Doctype .与 Comment对象类似,这些类都是NavigableString的子类

    markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
    soup = BeautifulSoup(markup)
    comment = soup.b.string
    
    '''注释美化'''
    print(f"注释美化: {soup.b.prettify()}")
    
    '''注释修改'''
    # Beautiful Soup中定义的其它类型都可能会出现在XML的文档中: CData , ProcessingInstruction , Declaration, Doctype .与 Comment对象类似,这些类都是NavigableString的子类,
    # 只是添加了一些额外的方法的字符串独享.下面是用CDATA来替代注释的例子:
    cdata = CData("A CDATA block")
    comment.replace_with(cdata)
    print(f"b标签的注释内容:{comment}")
    
    输出结果
    ---
    注释美化: <b>
     <!--Hey, buddy. Want to buy a used parser?-->
    </b>
    b标签的注释内容:Hey, buddy. Want to buy a used parser?
    

2)遍历

参考Beautiful Soup 4 遍历_w3cschool

a)子节点:contentschildrendescendants
  • 获取直接子节点(.contents.children):

    html_doc = """
              <html>
                  <head><title>The Dormouse's story</title></head>
                  <body>
                      <p class="title"><b>The Dormouse's story</b></p>
                      <p class="story">Once upon a time there were three little sisters; and their names were
                          <a href="http://example.com/elsie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link1">Elsie</a>,
                          <a href="http://example.com/lacie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link2">Lacie</a> and
                          <a href="http://example.com/tillie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link3">Tillie</a>;
                          and they lived at the bottom of a well.
                      </p>
                      <p class="story">...</p>
                  </body>
              </html>
              """
            soup = BeautifulSoup(html_doc,"html.parser")
    
            '''tag的 .contents 属性可以将tag的子节点以列表的方式输出:'''
            head = soup.head
            print(f"输出head标签的子节点:{head.contents}, type(head.contents):{type(head.contents)}")
            title_tag = head.contents[0]
            print(f"title_tag : {title_tag}, type(title_tag) : {type(title_tag)}", end="\n\n")
    
            '''字符串没有.contents 属性, 因为字符串没有子节点'''
            title_str = title_tag.contents[0]
            print(f"title_str : {title_str}, type(title_str) : {type(title_str)}",end="\n\n")
            # print(f"title_str.contents : {title_str.contents}")  #AttributeError: 'NavigableString' object has no attribute 'contents'
    
            '''通过tag的.children生成器,可以对tag的第一级子节点进行循环: 输出仅包含tag的直接子节点'''
            for index,child in enumerate(soup.children):
                print(f"{index} : {child}")
            print("**************************")
            for index,child in enumerate(soup.head.children):
                print(f"{index} : {child}")
            print("**************************")
            for index,child in enumerate(soup.body.children):
                print(f"{index} : {child}")
    
    输出结果
    ---
    
     输出head标签的子节点:[<title>The Dormouse's story</title>], type(head.contents):<class 'list'>
    
      title_tag : <title>The Dormouse's story</title>, type(title_tag) : <class 'bs4.element.Tag'>
    
      title_str : The Dormouse's story, type(title_str) : <class 'bs4.element.NavigableString'>
    
        0 : 
    
      1 : <html>
      <head><title>The Dormouse's story</title></head>
      <body>
      <p class="title"><b>The Dormouse's story</b></p>
      <p class="story">Once upon a time there were three little sisters; and their names were
                                  <a class="sister" href="http://example.com/elsie" id="link1" rel="external nofollow" target="_blank">Elsie</a>,
                                  <a class="sister" href="http://example.com/lacie" id="link2" rel="external nofollow" target="_blank">Lacie</a> and
                                  <a class="sister" href="http://example.com/tillie" id="link3" rel="external nofollow" target="_blank">Tillie</a>;
                                  and they lived at the bottom of a well.
                              </p>
      <p class="story">...</p>
      </body>
      </html>
      2 : 
    
      **************************
    
      0 : <title>The Dormouse's story</title>
    
      **************************
    
      0 : 
    
      1 : <p class="title"><b>The Dormouse's story</b></p>
      2 : 
    
      3 : <p class="story">Once upon a time there were three little sisters; and their names were
                                  <a class="sister" href="http://example.com/elsie" id="link1" rel="external nofollow" target="_blank">Elsie</a>,
                                  <a class="sister" href="http://example.com/lacie" id="link2" rel="external nofollow" target="_blank">Lacie</a> and
                                  <a class="sister" href="http://example.com/tillie" id="link3" rel="external nofollow" target="_blank">Tillie</a>;
                                  and they lived at the bottom of a well.
                              </p>
      4 : 
    
      5 : <p class="story">...</p>
      6 : 
    
  • 递归获取子孙节点(.descendants):

    '''
    .contents 和 .children 属性仅包含tag的直接子节点.例如,<head>标签只有一个直接子节点<title>
    但是<title>标签也包含一个子节点:字符串 “The Dormouse’s story”,
    这种情况下字符串 “The Dormouse’s story”也属于<head>标签的子孙节点。.descendants 属性可以对所有tag的子孙节点进行递归循环
    '''
    html_doc = """
              <html>
                  <head><title>The Dormouse's story</title></head>
                  <body>
                      <p class="title"><b>The Dormouse's story</b></p>
                      <p class="story">Once upon a time there were three little sisters; and their names were
                          <a href="http://example.com/elsie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link1">Elsie</a>,
                          <a href="http://example.com/lacie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link2">Lacie</a> and
                          <a href="http://example.com/tillie" rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  class="sister" id="link3">Tillie</a>;
                          and they lived at the bottom of a well.
                      </p>
                      <p class="story">...</p>
                  </body>
              </html>
              """
    soup = BeautifulSoup(html_doc, "html.parser")
    
    for index,child in enumerate(soup.descendants):
        print(f"{index} : {child}")
    
    print(f"len(soup.children):{len(list(soup.children))}, len(soup.descendants):{len(list(soup.descendants))}")
    
    输出结果
    ---
    ... #递归输出所有节点
    len(soup.children):3, len(soup.descendants):30
    
  • 获取子节点文本内容:

    • 如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点;如果tag包含了多个子节点,tag就无法确定,.string 的输出结果是None

    • 如果tag中包含多个字符串,可以使用.strings 来循环获取;为了去除掉多余的空格内容,可以使用stripped_strings

    html_doc = """
              <html>
                  <head><title>The Dormouse's story</title></head>
                  <body>
                      <p class="title"><b>The Dormouse's story</b></p>
                      <p class="story"> story </p>
                  </body>
              </html>
              """
    soup = BeautifulSoup(html_doc, "html.parser")
    #如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点:
    print(f"soup.html.head.title.string: {soup.html.head.title.string}")
    #如果tag包含了多个子节点,tag就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None
    print(f"soup.html.string: {soup.html.string}",end="\n\n")
    
    # 如果tag中包含多个字符串,可以使用 .strings 来循环获取,
    # 其中输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容:
    # for index,string in enumerate(soup.strings):
    for index,string in enumerate(soup.stripped_strings):
      print(f"{index} : {string}")
    
    输出结果:
    ---
    soup.html.head.title.string: The Dormouse's story
    soup.html.string: None
    
    0 : The Dormouse's story
    1 : The Dormouse's story
    2 : story
    
b)父节点:parentparents
  • 文档树中每个tag或字符串都有父节点:.parent获取到的父节点可以是Tag类型、BeautifulSoup对象或者None

    '''文档树中每个tag或字符串都有父节点:被包含在某个tag中'''
    html_doc = """
                <html>
                    <head><title>The Dormouse's story</title></head>
                    <body>
                        <p class="title"><b>The Dormouse's story</b></p>
                        <p class="story"> story </p>
                    </body>
                </html>
                """
    soup = BeautifulSoup(html_doc, "html.parser")
    title_tag = soup.html.head.title
    
    # 获取title标签的父标签
    print(f"title_tag: {title_tag}, title_tag.parent: {title_tag.parent}")
    #文档title的字符串 NavigableString没有子节点,但有父节点:<title>标签
    title_str = title_tag.string
    print(f"title_str: {title_str}, title_str.parent: {title_str.parent}",end="\n\n")
    
    #文档的顶层节点比如<html>的父节点是 BeautifulSoup 对象
    html_tag = soup.html
    print(f"html_tag.parent: {html_tag.parent}, type(html_tag.parent): {type(html_tag.parent)}")
    # BeautifulSoup 对象的 .parent 是None:
    print(f"soup.parent: {soup.parent}")
    
    输出结果
    ---
    title_tag: <title>The Dormouse's story</title>, title_tag.parent: <head><title>The Dormouse's story</title></head>
    title_str: The Dormouse's story, title_str.parent: <title>The Dormouse's story</title>
    
    html_tag.parent: 
    <html>
    <head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>
    <p class="story"> story </p>
    </body>
    </html>
    , type(html_tag.parent): <class 'bs4.BeautifulSoup'>
    soup.parent: None
    
  • 通过.parents递归获取父节点:

    html_doc = """
                <html>
                    <head><title>The Dormouse's story</title></head>
                    <body>
                        <p class="title"><b>The Dormouse's story</b></p>
                        <p> 
                            <a class="sister" href="http://example.com/elsie" 
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  
                                rel="external nofollow" target="_blank"  id="link1">
                            Elsie
                            </a>
                        </p>
                        <p class="story"> story </p>
                    </body>
                </html>
                """
    
    soup = BeautifulSoup(html_doc, "html.parser")
    a_tag = soup.a
    #通过 .parents生成器,遍历<a>标签到根节点的所有节点.
    for index,parent in enumerate(a_tag.parents):
        if(parent == None): #到达根节点
            print(f"{index},{parent}")
        else:
            print(f"{index},{parent.name}")
    
    输出结果
    ---
    0,p
    1,body
    2,html
    3,[document]
    
c)兄弟节点:next_sibling(s) & previous_sibling(s)
  • 获取未美化html代码的兄弟节点

    sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c><d>text3</d></a>")
    print(f'标签内容美化:{sibling_soup.prettify()}')  #其中<b>标签和<c>标签是同一层,及两者为兄弟节点
    
    #在文档树中, 使用 .next_sibling 和 .previous_sibling 属性来查询兄弟节点
    b_tag = sibling_soup.b
    c_tag = sibling_soup.c
    print(f"b_tag.next_sibling : {b_tag.next_sibling}, c_tag.next_sibling : {c_tag.next_sibling}")
    print(f"b_tag.previous_sibling : {b_tag.previous_sibling}, c_tag.previous_sibling : {c_tag.previous_sibling}")
    
    输出结果
    ---
    标签内容美化:<html>
    <body>
      <a>
      <b>
        text1
      </b>
      <c>
        text2
      </c>
      <d>
        text3
      </d>
      </a>
    </body>
    </html>
    b_tag.next_sibling : <c>text2</c>, c_tag.next_sibling : <d>text3</d>
    b_tag.previous_sibling : None, c_tag.previous_sibling : <b>text1</b>
    
  • 获取美化html代码后的兄弟节点:实际文档中的tag的 .next_sibling.previous_sibling 属性通常是字符串或空白, .next_sibling.next_sibling才能获取到相应的兄弟节点。

    sibling_soup = BeautifulSoup('<a><b>text1</b><c>text2</c><d id="link3">text3</d></a>')
    prettify_soup = BeautifulSoup(sibling_soup.prettify())
    
    # 实际文档中的tag的 .next_sibling 和 .previous_sibling 属性通常是字符串或空白, .next_sibling.next_sibling才能获取到相应的兄弟节点
    b_tag = prettify_soup.b
    c_tag = prettify_soup.c
    print(f"b_tag.next_sibling : {b_tag.next_sibling}, c_tag.next_sibling : {c_tag.next_sibling}")
    print(f"b_tag.next_sibling.next_sibling : {b_tag.next_sibling.next_sibling}, c_tag.next_sibling.next_sibling : {c_tag.next_sibling.next_sibling}")
    
    输出结果
    ---
    b_tag.next_sibling : 
    , c_tag.next_sibling : 
    
    b_tag.next_sibling.next_sibling : <c>
        text2
      </c>, c_tag.next_sibling.next_sibling : <d id="link3">
        text3
      </d>
    
  • 递归获取所有的兄弟节点(通过soup.find(attrs={})来获得指定属性内容的标签对象)

    sibling_soup = BeautifulSoup('<a><b>text1</b><c>text2</c><d id="link3" class="link3">text3</d></a>')
    prettify_soup = BeautifulSoup(sibling_soup.prettify())
    
    # 通过 .next_siblings生成器 属性可以对当前节点的兄弟节点迭代输出:
    for index,next_sibling in enumerate(prettify_soup.next_siblings):
        print(f"{index},{next_sibling}")
    
    #通过 .previous_siblings生成器 属性可以对当前节点的兄弟节点迭代输出:
    # tag = prettify_soup.find(id="link3")  #按id来查询标签
    tag = prettify_soup.find(attrs={"class" : "link3"})  #按class来查询标签, 参考https://blog.csdn.net/weixin_30629653/article/details/94979800/
    for index,previous_sibling in enumerate(tag.previous_siblings):
        print(f"{index},{previous_sibling}")
    
    输出结果
    ---
    tag : <d class="link3" id="link3">
        text3
      </d>, previous_tags: 
    0,
    
    1,<c>
        text2
      </c>
    2,
    
    3,<b>
        text1
      </b>
    4,
    
d)上/下一个HTML解析对象:next_element(s) & previous_element(s)

HTML解析器把这段字符串转换成一连串的事件:

  • “打开<html>标签”, ”打开一个<head>标签”, ”打开一个<title>标签”, ”添加一段字符串”, ”关闭标签”, ”打开<code><p></code>标签”,等等。<code>Beautiful Soup</code>提供了<strong>重现解析器初始化过程</strong>的方法。
  • 获取下一个/上一个解析对象

    html_doc = '''<a><b>text1</b><c>text2</c><d id="link3">text3</d><e>text4</e></a>'''
    html_soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
    
    #.next_element 属性指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的
    d_tag = html_soup.find(attrs={"id":"link3"})
    print(f"d_tag.next_sibling:{d_tag.next_sibling}, d_tag.next_element:{d_tag.next_element}")
    print(f"d_tag.previous_sibling:{d_tag.previous_sibling}, d_tag.next_element:{d_tag.previous_element}")
    
    输出结果       
    ---
    d_tag.next_sibling:
    , d_tag.next_element:
        text3
    
    d_tag.previous_sibling:
    , d_tag.next_element:
    
  • 递归获取下一个/上一个解析对象

    html_doc = '''<a><b>text1</b><c>text2</c><d id="link3">text3</d><e>text4</e></a>'''
    html_soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
    
    d_tag = html_soup.find(attrs={"id": "link3"})
    print(f"d_tag : {d_tag}, previous_elements: ")
    for index, previous_element in enumerate(d_tag.previous_elements):
        print(f"{index},{previous_element}")
    
    输出结果       
    ---
    d_tag : <d id="link3">
        text3
      </d>, previous_elements: 
    0,
    
    1,
        text2
    
    2,<c>
        text2
      </c>
    3,
    
    4,
        text1
    
    5,<b>
        text1
      </b>
    6,
    
    7,<a>
    <b>
        text1
      </b>
    <c>
        text2
      </c>
    <d id="link3">
        text3
      </d>
    <e>
        text4
      </e>
    </a>
    8,
    
    9,<body>
    <a>
    <b>
        text1
      </b>
    <c>
        text2
      </c>
    <d id="link3">
        text3
      </d>
    <e>
        text4
      </e>
    </a>
    </body>
    10,
    
    11,<html>
    <body>
    <a>
    <b>
        text1
      </b>
    <c>
        text2
      </c>
    <d id="link3">
        text3
      </d>
    <e>
        text4
      </e>
    </a>
    </body>
    </html>
    

3)文档搜索

BeautifulSoup 搜索文档主要有两个方法:find & find_all,参考Beautiful Soup 4 搜索文档_w3cschool

a)find_all()搜索子孙节点(find等价于find_all(limit=1)

find_all参数细节:find_all(name , attrs , recursive , string , **kwargs )

  • name参数:搜索 name 参数的值可以使任一类型的 过滤器,字符串,正则表达式,列表,方法或是 True

    '''
    find_all方法
    '''
    html_doc = """<a><b>text1</b><c class="hello">text2</c><d class="world" id="link3">text3</d><e>text4</e></a>"""
    soup = BeautifulSoup(html_doc)
    
    '''Step1: 初步使用find_all'''
    # 1.参数为字符串
    print(f'soup.find_all("b") : {soup.find_all("b")}',end="\n\n")
    
    # 2.参数为正则表达式
    import re
    for index,tag in enumerate(soup.find_all(re.compile("^b"))):  #获取以b开头的所有标签
        print(f"以b开头的标签{index}{tag.name}")
    print(end="\n\n")
    
    # 3.参数为列表
    print(f'find_all(["a", "b"]) : {soup.find_all(["a", "b"])}',end="\n\n")
    
    # 4.参数为布尔值(输出所有标签)
    for index,tag in enumerate(soup.find_all(True)):
        print(f"{index},{tag.name}")
    print(end="\n\n")
    
    # 5.参数为方法,自定义过滤器
    '''
    如果没有合适过滤器,那么还可以定义一个方法:
        方法只接受一个元素Tag参数,如果这个方法返回 True 表示当前元素Tag匹配并且被找到,如果不是则返回False
    '''
    def has_class_but_no_id(tag):
        # 方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True
        return tag.has_attr('class') and not tag.has_attr('id')
    print(f"使用has_class_but_no_id作为find_all参数: {soup.find_all(has_class_but_no_id)}",end="\n\n")
    
    def surrounded_by_strings(tag):
        #过滤出在解析过程中前后都有文字的标签
        return (isinstance(tag.next_element, NavigableString)
                and isinstance(tag.previous_element, NavigableString))
    print(f"使用surrounded_by_strings作为find_all参数: {soup.find_all(surrounded_by_strings)}", end="\n\n")
    
    输出结果
    ---
    soup.find_all("b") : [<b>text1</b>]
    
    以b开头的标签0:body
    以b开头的标签1:b
    
    find_all(["a", "b"]) : [<a><b>text1</b><c class="hello">text2</c><d class="world" id="link3">text3</d><e>text4</e></a>, <b>text1</b>]
    
    0,html
    1,body
    2,a
    3,b
    4,c
    5,d
    6,e
    
    使用has_class_but_no_id作为find_all参数: [<c class="hello">text2</c>]
    
    使用surrounded_by_strings作为find_all参数: [<c class="hello">text2</c>, <d class="world" id="link3">text3</d>, <e>text4</e>]
    
  • keyword参数:使用多个指定名字的参数可以同时过滤tag的多个属性, 通过 class_ 参数搜索有指定CSS类名的tag

    import re
    html_doc = """
                <a><b>text1</b><c class="hello"><div>text2</div></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    
    # 2. keyword 参数: 使用多个指定名字的参数可以同时过滤tag的多个属性, 通过 class_ 参数搜索有指定CSS类名的tag
    print(f'soup.find_all(class_=re.compile("or"), id="link3"): {soup.find_all(class_=re.compile("or"), id="link3")}')
    # 等价于 使用attrs字典参数
    print(f'soup.find_all(attrs=("class": re.compile("or"), "id" : "link3")): {soup.find_all(attrs={"class": re.compile("or"), "id" : "link3"})}')
    # 等价于 省略掉find_all方法
    print(f'soup(class_=re.compile("or"), id="link3"): {soup(class_=re.compile("or"), id="link3")}',end="\n\n")
    # 有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:
    # print(f'soup.find_all(data-foo="value"): {soup.find_all(data-foo="value")}')
    
    # 按CSS搜索: 按照CSS类名搜索tag时, 可以分别搜索tag中的每个CSS类名
    print(f'soup.find_all("c", class_="hello")"):{soup.find_all("c", class_="hello")}',end="\n\n")
    
    输出结果:
    ---
      soup.find_all(class_=re.compile("or"), id="link3"): [<c class="world" id="link3"><div>text3</div></c>]
    soup.find_all(attrs=("class": re.compile("or"), "id" : "link3")): [<c class="world" id="link3"><div>text3</div></c>]
    soup(class_=re.compile("or"), id="link3"): [<c class="world" id="link3"><div>text3</div></c>]
    
    soup.find_all("c", class_="hello")"):[<c class="hello"><div>text2</div></c>]
    
  • string参数:通过 string 参数可以搜搜文档中的字符串内容, 见NavigableString小节。

    import re
    html_doc = """
                <a><b>text1</b><c class="hello"><div>text2</div></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    print(f'soup.find_all(string="text1") : {soup.find_all(string="text1")}')
    print(f'soup.find_all(string=["text1","text2"]) : {soup.find_all(string=["text1","text2"])}',end="\n\n")
    
    输出结果:
    ---
    soup.find_all(string="text1") : ['text1']
    soup.find_all(string=["text1","text2"]) : ['text1', 'text2']
    
  • recursive参数:调用tagfind_all()方法时,Beautiful Soup会检索当前tag的所有子孙节点, 如果只想搜索tag的直接子节点,可以使用参数 recursive=False(???) 。

    import re
    html_doc = """
                <a><b>text1</b><c class="hello"><div>text2</div></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    print(f'soup.html.find_all("c",recursive=True): {soup.html.find_all("c",recursive=True)}')
          print(f'soup.html.find_all("c",recursive=False): {soup.html.find_all("c",recursive=False)}',end="\n\n")  #返回[], 不知道recursive=False有什么用
    
    输出结果:
    ---
    soup.html.find_all("c",recursive=True): [<c class="hello"><div>text2</div></c>, <c class="world" id="link3"><div>text3</div></c>]
    soup.html.find_all("c",recursive=False): []
    
  • limit参数:一段简单的文档: 当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果,效果与SQL中的limit关键字类似。 使用​find_all方法并设置​limit=1参数不如直接使用​find()​ `方法,下面这两行代码是等价的。

    import re
    html_doc = """
                <a><b>text1</b><c class="hello"><div>text2</div></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    print(f'soup.find_all("c", limit=1) : {soup.find_all("c", limit=1)}')
    #find_all limit=1等价于使用find
    print(f'soup.find("c") : {soup.find("c")}', end="\n\n")
    
    输出结果:
    ---
    soup.find_all("c", limit=1) : [<c 
    class="hello"><div>text2</div></c>]
    soup.find("c") : <c class="hello"><div>text2</div></c>
    
  • find_all()可省略:

    import re
    html_doc = """
                <a><b>text1</b><c class="hello"><div>text2</div></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    print(f'soup.find_all("c") : {soup.find_all("c")}')
    print(f'soup("c") : {soup("c")}')
    
    输出结果:
    ---
    soup.find_all("c") : [<c class="hello"><div>text2</div></c>, <c class="world" id="link3"><div>text3</div></c>]
    soup("c") : [<c class="hello"><div>text2</div></c>, <c class="world" id="link3"><div>text3</div></c>]
    
b)其他find_xxx()方法
  • find_parents()find_parent(): 通过 .parents 属性对当前tag父辈节点的tag节点进行迭代,搜索方法的参数使用同find_all

    html_doc = """
                <a><b>text1</b><c><c class="hello"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
    div_tag = soup.c.div
    '''find_parents() 和 find_parent(): 通过 `.parents` 属性对当前`tag`父辈节点的`tag`节点进行迭代'''
    print(f'div_tag.find_parent(): {div_tag.find_parent("c")}')
    print(f'div_tag.find_parents(): {div_tag.find_parents("c")}',end="\n\n")
    
    输出结果
    ---
    div_tag.find_parent(): <c class="hello"><div>text2</div></c>
    div_tag.find_parents(): [<c class="hello"><div>text2</div></c>, <c><c class="hello"><div>text2</div></c></c>]
    
  • find_next_siblings()find_next_sibling()通过.next_siblings属性对当前tag的后面兄弟tag节点进行迭代, 搜索当前节点后面的兄弟节点,搜索方法的参数使用同find_all

    html_doc = """
                <a><b>text1</b><c><c class="hello"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
    div_tag = soup.c.div
    
    '''find_next_siblings() 合 find_next_sibling(): 通过 .next_siblings 属性对当前tag的后面兄弟tag节点进行迭代'''
    b_tag = soup.b
    print(f'b_tag.find_next_sibling(): {b_tag.find_next_sibling("c")}')
    print(f'b_tag.find_next_siblings(): {b_tag.find_next_siblings("c")}',end="\n\n")
    
    输出结果
    ---
    b_tag.find_next_sibling(): <c><c class="hello"><div>text2</div></c></c>
    b_tag.find_next_siblings(): [<c><c class="hello"><div>text2</div></c></c>, <c class="world" id="link3"><div>text3</div></c>]
    
  • find_previous_siblings()find_previous_sibling(): 通过 .previous_siblings 属性对当前tag的前面兄弟tag节点进行迭代, 搜索当前节点前面的兄弟节点,搜索方法的参数使用同find_all

    html_doc = """
                  <a><b>text1</b><c><c class="hello"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                  <div data-foo="value">foo!</div>
                  """
      soup = BeautifulSoup(html_doc)
      # soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
      div_tag = soup.c.div
    
      '''find_previous_siblings() 和 find_previous_sibling(): 通过 .previous_siblings 属性对当前tag的前面解析的兄弟tag节点进行迭代'''
    c_tag = soup.c
    print(f'c_tag.find_previous_sibling(): {c_tag.find_previous_sibling("b")}')
    print(f'c_tag.find_previous_siblings(): {c_tag.find_previous_siblings("b")}',end="\n\n")
    
    输出结果
    ---
    c_tag.find_previous_sibling(): <b>text1</b>
    c_tag.find_previous_siblings(): [<b>text1</b>]
    
  • find_all_next()find_next()通过.next_elements属性对当前tag的之后的tag和字符串进行迭代。

    html_doc = """
                  <a><b>text1</b><c><c class="hello"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                  <div data-foo="value">foo!</div>
                  """
      soup = BeautifulSoup(html_doc)
      # soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
      div_tag = soup.c.div
    
      '''find_all_next() 和 find_next(): 通过.next_elements属性对当前tag的之后的 tag 和字符串进行迭代'''
    print(f'c_tag.find_next(): {c_tag.find_next("text3")}')
    print(f'c_tag.find_all_next(): {c_tag.find_all_next("text3")}')
    print(f'c_tag.find_all_next(): {c_tag.find_all_next("e")}',end="\n\n")
    
    输出结果
    ---
    c_tag.find_next(): None
    c_tag.find_all_next(): []
    c_tag.find_all_next(): [<e>text4</e>]
    
  • find_all_previous()find_previous(): 通过.previous_elements属性对当前tag的之前的 tag 和字符串进行迭代。

    html_doc = """
                  <a><b>text1</b><c><c class="hello"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e>text4</e></a>
                  <div data-foo="value">foo!</div>
                  """
      soup = BeautifulSoup(html_doc)
      # soup = BeautifulSoup(BeautifulSoup(html_doc).prettify())
      div_tag = soup.c.div
    
      '''find_all_previous() 和 find_previous(): 通过.previous_elements属性对当前tag的之前的 tag 和字符串进行迭代'''
    print(f'c_tag.find_previous(): {c_tag.find_previous("text1")}')
    print(f'c_tag.find_all_previous(): {c_tag.find_all_previous("text1")}')
    print(f'c_tag.find_all_previous(): {c_tag.find_all_previous("b")}',end="\n\n")
    
    输出结果
    ---
    c_tag.find_previous(): None
    c_tag.find_all_previous(): []
    c_tag.find_all_previous(): [<b>text1</b>]
    
c)css选择器

对于熟悉CSS选择器语法的人,直接使用css选择器相较于直接使用find_xxx()会比较方便

  • 通过tag标签逐层查找:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # 通过tag标签逐层查找
    print(f'soup.select("html a b") : {soup.select("html a b")}',end="\n\n")   #不能写成soup.select("html.a.b")
    
    输出结果
    ---
    soup.select("html a b") : [<b>text1</b>]
    
  • 找到某个tag标签下的直接子标签:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # 找到某个tag标签下的直接子标签
    print(f'soup.select("a > c > c") : {soup.select("a > c > c")}',end="\n\n")
    
    输出结果
    ---
    soup.select("a > c > c") : [<c class="hello" id="link2"><div>text2</div></c>]
    
  • 按id查询某个a标签下的直接子标签:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # 按id查询某个a标签下的直接子标签
    print(f'soup.select("a > #link3") : {soup.select("a > #link3")}',end="\n\n")
    
    输出结果
    ---
    soup.select("a > #link3") : [<c class="world" id="link3"><div>text3</div></c>]
    
  • 按class查询:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    # 按class查询某个c标签
    print(f'soup.select("c .hello") : {soup.select("c .hello")}')
    
    # 按class查询某个a标签下的直接子标签
    print(f'soup.select("a > .hello") : {soup.select("a > .hello")}',end="\n\n")
    
    输出结果
    ---
    soup.select("c .hello") : [<c class="hello" id="link2"><div>text2</div></c>]
    soup.select("a > .hello") : [<e class="hello">text4</e>]
    
  • 同时用多种CSS选择器查询元素:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    
    #同时用多种CSS选择器查询元素
    # 分别查找class =.hello和.world的标签,查找为[]
    print(f'soup.select(".hello,.world") : {soup.select("#hello,#world")}')
    # 分别查找id=link2和link3的标签,,查找多个标签(因为id具有唯一性,而class没有)
    print(f'soup.select("#link2,#link3") : {soup.select("#link2,#link3")}',end="\n\n")
    
    输出结果
    ---
    soup.select(".hello,.world") : []
    soup.select("#link2,#link3") : [<c class="hello" id="link2"><div>text2</div></c>, <c class="world" id="link3"><div>text3</div></c>]
    
  • 通过是否存在某个属性来查找:

    html_doc = """
                <a><b>text1</b><c><c class="hello" id="link2"><div>text2</div></c></c><c class="world" id="link3"><div>text3</div></c><e class="hello">text4</e></a>
                <div data-foo="value">foo!</div>
                """
    soup = BeautifulSoup(html_doc)
    #通过是否存在某个属性来查找
    print(f'soup.select("c[class=hello]") : {soup.select("c[class=hello]")}')
    
    输出结果
    ---
    soup.select("c[class=hello]") : [<c class="hello" id="link2"><div>text2</div></c>]
    

4)文档修改

a)直接赋值修改
  • 修改tag的名称和属性

    '''1、修改tag的名称和属性'''
    soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
    tag = soup.b
    origin_tag = copy.copy(tag)
    
    tag.name = "blockquote"
    tag['class'] = 'verybold'
    tag['id'] = 1
    print(f"origin_tag : {origin_tag}, tag : {tag}",end="\n\n")
    
    输出结果
    ---
    origin_tag : <b class="boldest">Extremely bold</b>, tag : <blockquote class="verybold" id="1">Extremely bold</blockquote>
    
  • 通过.string修改tag中的内容

    '''2、通过.string修改tag中的内容'''
    soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
    tag = soup.b
    origin_tag = copy.copy(tag)
    
    tag.string = "Extremely light"
    print(f"origin_tag : {origin_tag}, tag : {tag}",end="\n\n")
    
    输出结果
    ---
    origin_tag : <b class="boldest">Extremely bold</b>, tag : <b class="boldest">Extremely light</b>
    
b)append追加内容(标签/文本)
  • tag中追加字符串内容

    '''3、向tag中添加内容 append()'''
    soup = BeautifulSoup("<a>Foo</a>")
    origin_soup = copy.copy(soup)
    
    soup.a.append("Bar")
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"soup.a.contents : {soup.a.contents}")  #a标签有两个直接的子节点(两个NavigableString对象):['Foo', 'Bar']
    
    #soup.a.append("Bar")等价于 new_str = NavigableString("Bar"), soup.a.append(new_str)
    new_str = NavigableString("Tar")
    soup.a.append(new_str)
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"soup.a.contents : {soup.a.contents}")  # a标签有两个直接的子节点(两个NavigableString对象):['Foo', 'Bar']
    
    # soup.a.append("Bar")等价于 soup.a.append(soup.new_string("Nar",NavigableString)),注意soup对象才有new_string方法,tag对象没有
    # soup.a.new_string("Nar",NavigableString)   #TypeError: 'NoneType' object is not callable
    new_str = soup.new_string("Nar",NavigableString)
    soup.a.append(new_str)
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"soup.a.contents : {soup.a.contents}", end="\n\n")  # a标签有两个直接的子节点(两个NavigableString对象):['Foo', 'Bar']
    
    输出结果
    ---
    origin_soup : <html><body><a>Foo</a></body></html>, soup : <html><body><a>FooBar</a></body></html>
    soup.a.contents : ['Foo', 'Bar']
    origin_soup : <html><body><a>Foo</a></body></html>, soup : <html><body><a>FooBarTar</a></body></html>
    soup.a.contents : ['Foo', 'Bar', 'Tar']
    origin_soup : <html><body><a>Foo</a></body></html>, soup : <html><body><a>FooBarTarNar</a></body></html>
    soup.a.contents : ['Foo', 'Bar', 'Tar', 'Nar']
    
  • tag中追加注释

    '''4、创建一段注释(字符串带<!--xxx-->),再使用append追加到该节点内的尾部:soup.new_string("Nice to see you.", Comment)'''
    soup = BeautifulSoup("<a>Foo</a>")
    # comment = soup.a.new_string("Nar",Comment)  #TypeError: 'NoneType' object is not callable
    comment = soup.new_string("This is a comment",Comment)
    soup.a.append(comment)
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"soup.a.contents : {soup.a.contents}", end="\n\n")  # a标签有两个直接的子节点(两个NavigableString对象):['Foo', 'Bar']
    
    输出结果
    ---
    origin_soup : <html><body><a>Foo</a></body></html>, soup : <html><body><a>Foo<!--This is a comment--></a></body></html>
    soup.a.contents : ['Foo', 'This is a comment']
    
  • tag中追加一个tag标签

    '''5、BeautifulSoup.new_tag() 创建一个tag,再使用append追加到该节点内的尾部'''
    soup = BeautifulSoup("<b></b>")
    original_tag = copy.copy(soup.b)
    tag = soup.b
    new_tag = soup.new_tag("a", href="http://www.example.com")
    tag.append(new_tag)
    print(f"original_tag : {original_tag}, tag : {tag}")
    print(f"tag : {tag.contents}", end="\n\n")  # a标签有两个直接的子节点(两个NavigableString对象):['Foo', 'Bar']
    
    输出结果
    ---
    original_tag : <b></b>, tag : <b><a href="http://www.example.com"></a></b>
    tag : [<a href="http://www.example.com"></a>]
    
c)insert指定位置插入(标签/文本)
  • 按位置插入到tag标签

    '''6、tag.insert() 按位置插入到标签内'''
    # Tag.insert() 方法与 Tag.append() 方法类似,区别是不会把新元素添加到父节点 .contents 属性的最后,
    # 而是把元素插入到指定的位置.与Python列表总的 .insert() 方法的用法相同
    soup = BeautifulSoup("<b>Hello world <i>Ni hao</i></b>")
    original_tag = copy.copy(soup.b)
    tag = soup.b
    tag.insert(1, "Nar_test")
    print(f"original_tag : {original_tag}, tag : {tag}")
    print(f"original_tag.contents : {original_tag.contents}, tag.contents : {tag.contents}", end="\n\n")
    
    输出结果
    ---
    original_tag : <b>Hello world <i>Ni hao</i></b>, tag : <b>Hello world Nar_test<i>Ni hao</i></b>
    original_tag.contents : ['Hello world ', <i>Ni hao</i>], tag.contents : ['Hello world ', 'Nar_test', <i>Ni hao</i>]
    
  • insert_before()方法在当前tag或文本节点前插入内容

    '''7、insert_before() 方法在当前tag或文本节点前插入内容'''
    soup = BeautifulSoup("<a><b>Hello world <i>Ni hao</i></b></a>")
    original_soup = copy.copy(soup)
    soup.a.b.insert_before("Nar_test")  #在b标签前insert_before
    print(f"original_soup : {original_soup}, soup : {soup}")
    print(f"original_soup.contents : {original_soup.contents}, soup.contents : {soup.contents}", end="\n\n")
    
    soup = BeautifulSoup("<a><b>Hello world <i>Ni hao</i></b></a>")
    original_soup = copy.copy(soup)
    # content_str = soup.b.string  #None,因为b标签内还有标签,String必须是最后一层
    content_str = soup.a.b.contents[0]  #在b标签内文本前insert_before
    content_str.insert_before("Nar_test")  #如果直接使用soup.b.string会报错:'NoneType' object has no attribute 'insert_before'
    print(f"original_soup : {original_soup}, soup : {soup}")
    print(f"original_soup.contents : {original_soup.contents}, soup.contents : {soup.contents}", end="\n\n")
    
    输出结果
    ---
    original_soup : <html><body><a><b>Hello world <i>Ni hao</i></b></a></body></html>, soup : <html><body><a>Nar_test<b>Hello world <i>Ni hao</i></b></a></body></html>
    original_soup.contents : [<html><body><a><b>Hello world <i>Ni hao</i></b></a></body></html>], soup.contents : [<html><body><a>Nar_test<b>Hello world <i>Ni hao</i></b></a></body></html>]  
    
    original_soup : <html><body><a><b>Hello world <i>Ni hao</i></b></a></body></html>, soup : <html><body><a><b>Nar_testHello world <i>Ni hao</i></b></a></body></html>
    original_soup.contents : [<html><body><a><b>Hello world <i>Ni hao</i></b></a></body></html>], soup.contents : [<html><body><a><b>Nar_testHello world <i>Ni hao</i></b></a></body></html>]
    
  • insert_after()方法在当前tag或文本节点后插入内容(同insert_before()

d)clear & extract & decompose:移除内容/移除标签
  • Tag.clear()方法移除当前tag内的字符串内容

    '''8、Tag.clear() 方法移除当前tag内的字符串内容'''
    soup = BeautifulSoup("<b>Hello world <i>Ni hao</i></b>")
    origin_soup = copy.copy(soup)
    tag = soup.i
    tag.clear()  #移除当前tag的内容
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"origin_soup.contents : {origin_soup.contents}, soup.contents : {soup.contents}", end="\n\n")
    
    输出结果
    ---
    origin_soup : <html><body><b>Hello world <i>Ni hao</i></b></body></html>, soup : <html><body><b>Hello world <i></i></b></body></html>
    origin_soup.contents : [<html><body><b>Hello world <i>Ni hao</i></b></body></html>], soup.contents : [<html><body><b>Hello world <i></i></b></body></html>]
    
  • Tag.extract()方法将当前tag移除文档树,并作为方法结果返回

    '''9、PageElement.extract()方法将当前tag移除文档树,并作为方法结果返回'''
    soup = BeautifulSoup("<b>Hello world <i>Ni hao</i></b>")
    origin_soup = copy.copy(soup)
    tag = soup.i
    res = tag.extract()  # 移除当前tag节点,返回值为该tag节点
    print(f"res : {res}")
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"origin_soup.contents : {origin_soup.contents}, soup.contents : {soup.contents}", end="\n\n")
    
    输出结果
    ---
    res : <i>Ni hao</i>
    origin_soup : <html><body><b>Hello world <i>Ni hao</i></b></body></html>, soup : <html><body><b>Hello world </b></body></html>
    origin_soup.contents : [<html><body><b>Hello world <i>Ni hao</i></b></body></html>], soup.contents : [<html><body><b>Hello world </b></body></html>]
    
  • Tag.decompose()方法将当前节点移除文档树并完全销毁,返回结果为None

    '''10、Tag.decompose() 方法将当前节点移除文档树并完全销毁'''
    soup = BeautifulSoup("<b>Hello world <i>Ni hao</i></b>")
    origin_soup = copy.copy(soup)
    tag = soup.i
    res = tag.decompose()  # 移除当前tag节点,返回值为None
    print(f"res : {res}")
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    print(f"origin_soup.contents : {origin_soup.contents}, soup.contents : {soup.contents}", end="\n\n")
    
    输出结果
    ---
    res : None
    origin_soup : <html><body><b>Hello world <i>Ni hao</i></b></body></html>, soup : <html><body><b>Hello world </b></body></html>
    origin_soup.contents : [<html><body><b>Hello world <i>Ni hao</i></b></body></html>], soup.contents : [<html><body><b>Hello world </b></body></html>]
    
e)wrap & unwrap:包装和解包
  • Tag.replace_with()方法移除文档树中的某段内容,并用新tag或文本节点替代它(标签替换成另一个标签或文本

    '''11、PageElement.replace_with()方法移除文档树中的某段内容,并用新tag或文本节点替代它(标签替换成另一个标签或文本)'''
    soup = BeautifulSoup("<b>Hello world <i>Ni hao</i></b>")
    origin_soup = copy.copy(soup)
    b_tag = soup.b
    
    new_tag = soup.new_tag("c")
    new_tag.string = "example.net"
    b_tag.i.replace_with(new_tag)
    print(f"origin_soup : {origin_soup}, soup : {soup}", end="\n\n")
    
    输出结果
    ---
    origin_soup : <html><body><b>Hello world <i>Ni hao</i></b></body></html>, soup : <html><body><b>Hello world <c>example.net</c></b></body></html>
    
  • Tag.wrap()方法可以对指定的tag元素进行包装,并返回包装后的结果

    '''12、PageElement.wrap()方法可以对指定的tag元素进行包装,并返回包装后的结果'''
    soup = BeautifulSoup("<p>I wish I was bold.</p>")
    origin_soup = copy.copy(soup)
    soup.p.string.wrap(soup.new_tag("b"))  #在p标签内的String外, 包装一个<b></b>
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    # <b>I wish I was bold.</b>
    soup.p.wrap(soup.new_tag("div")) #在p标签外,包装一个<div></div>
    print(f"origin_soup : {origin_soup}, soup : {soup}", end="\n\n")
    
    输出结果
    ---
    origin_soup : <html><body><p>I wish I was bold.</p></body></html>, soup : <html><body><p><b>I wish I was bold.</b></p></body></html>
    origin_soup : <html><body><p>I wish I was bold.</p></body></html>, soup : <html><body><div><p><b>I wish I was bold.</b></p></div></body></html>
    
  • Tag.unwrap()方法与 wrap()方法相反,该方法常被用来进行标记的解包

    '''12、Tag.unwrap()方法与 wrap() 方法相反,该方法常被用来进行标记的解包'''
    soup = BeautifulSoup("<div><p><a>I wish I was bold.</a></p></div>")
    origin_soup = copy.copy(soup)
    soup.p.unwrap()  # 移除掉p标签,但是不移除标签内的其他标签或文本
    print(f"origin_soup : {origin_soup}, soup : {soup}")
    
    输出结果
    ---
    origin_soup : <html><body><div><p><a>I wish I was bold.</a></p></div></body></html>, soup : <html><body><div><a>I wish I was bold.</a></div></body></html>
    

3、Selenium基础介绍

参考

  • Selenium入门指南

  • Selenium Python Tutorial - GeeksforGeeks

  • UI自动化告别手动下载驱动(使用WebDriverManager 自动下载管理driver)

  • Python3 网络爬虫(六):Python 使用 selenium 模拟登陆淘宝

  • Jack Cui博客

1)安装浏览器驱动

参考安装浏览器驱动 | Selenium,UI自动化告别手动下载驱动(使用WebDriverManager 自动下载管理driver)

大多数机器会自动更新浏览器, 但不会自动更新驱动程序。 为了确保为浏览器提供正确的驱动程序,一种较为灵活的解决方法是通过安装webdriver-manager来实现(也可以手动配置chromedriver.exe驱动的环境变量)。

  • 先安装webdriver-manager第三方库:

    pip install webdriver-manager -i https://pypi.tuna.tsinghua.edu.cn/simple/
    
  • 再通过webdriver-manager实现浏览器驱动(Chrome/Edge driver)的自动更新

    # selenium 4的写法
    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service as ChromeService
    from webdriver_manager.chrome import ChromeDriverManager
    from webdriver_manager.microsoft import EdgeChromiumDriverManager
    
    #自动更新chrome驱动
    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))   
    #自动更新Edge驱动
    driver = webdriver.Edge(service=ChromeService(EdgeChromiumDriverManager().install()))  
    

2)第一个Selenium脚本

参考编写第一个Selenium脚本 | Selenium

  • 假设要自动化测试的网页是:Web form

  • 要进行的操作是:输入内容到文本框,点击提交,并输出响应结果

  • Python代码需要完成如下7个步骤的操作:

    1. 使用驱动实例开启会话

    2. 在浏览器上执行操作, 导航到一个网页

    3. 请求浏览器信息(可有可无)

    4. 建立等待策略

    5. 查找元素,并完成表单填写和提交操作

    6. 在响应页面中获取响应结果

    7. 结束会话

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager
from selenium.webdriver.common.by import By

def test_eight_components(driver):

    #Step1: 使用驱动实例开启会话
    # driver = webdriver.Chrome()
    # driver = webdriver.Edge()
    driver = driver

    #Step2: 在浏览器上执行操作, 导航到一个网页
    driver.get("https://www.selenium.dev/selenium/web/web-form.html")

    #Step3: 请求浏览器信息,包括窗口句柄、浏览器尺寸/位置、cookie、警报等
    title = driver.title
    print(f'title == "Web form" : {title == "Web form"}')

    #Step4: 建立等待策略: 因为我们希望在尝试定位元素之前, 确保该元素位于页面上, 并且在尝试与该元素交互之前, 该元素处于可交互状态.
    # 隐式等待很少是最好的解决方案, 但在这里最容易演示
    driver.implicitly_wait(0.5)   #更多的等待策略参考 https://www.selenium.dev/zh-cn/documentation/webdriver/waits/

    #Step5: 发送命令,查找元素: 大多数Selenium会话中的主要命令都与元素相关, 如果不先找到元素, 就无法与之交互
    text_box = driver.find_element(by=By.NAME, value="my-text")  #Text input
    submit_button = driver.find_element(by=By.CSS_SELECTOR, value="button")  #点击按钮

    #Step6: 操作元素:通常对于一个元素, 只有少数几个操作可以执行
    text_box.send_keys("Selenium")   #模拟文本框输入
    submit_button.click()  #模拟按钮点击

    #Step7: 在响应页面中查找元素并获取元素信息
    message = driver.find_element(by=By.ID, value="message")
    value = message.text
    print(f'title == "Received" : {title == "Web form"}')

    #Step8: 结束会话
    driver.quit()

if __name__ == '__main__':
    # 自动更新驱动 参考https://www.cnblogs.com/bao-yan/p/16480026.html
    # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  #自动更新chrome驱动
    driver = webdriver.Edge(service=ChromeService(EdgeChromiumDriverManager().install()))  # 自动更新Edge驱动
    test_eight_components(driver=driver)

Note:

  • 如果不想自动打开浏览器,可以使用如下参数option.headless = True设置webdriver对象(参考Python使用Selenium Webdriver爬取网页所有内容))

    from selenium.webdriver import ChromeOptions
    
    option = ChromeOptions()
    # 设置浏览器为无头模式,使用过程中不会弹出浏览器页面
    option.headless = True
    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()),options=option)  #自动更新chrome驱动
    

3)常用方法

参考Selenium Python Tutorial - GeeksforGeeks

a)find_element()
  • find_element_by_id()selenium3的写法,selenium4写法如下:

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''selenium3: find_element_by_id()'''
    try:
        id_el = driver.find_element(by=By.ID,value="newsFieldWrap")
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按元素的ID属性查询元素 - driver.find_element(by=By.ID,value) : {id_el}")
        print(f"查询元素内容 : {id_el.text}",end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按元素的ID属性查询元素 - driver.find_element(by=By.ID,value) : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="7ca2b278-bcaa-4dd6-8b89-7bd9fea46f33")>
    查询元素内容 : LINE FRIENDS旗下创意互动品牌COLLER全新上市,探索品牌出圈新范式
    
    1 分钟前
    ST天马:228日起撤销公司股票交易其他风险警示
    
    2 分钟前
    三五互联:控股孙公司拟增资扩股引入投资者
    
    2 分钟前
    科德数控业绩快报:2022年度归母净利润6039.46万元 下降17.12%2 分钟前
    友发集团:部分经销商增持1.48%股份 增持完毕
    
    7 分钟前
    首个大湾区大型旅行团抵达香港迪士尼
    
    7 分钟前
    富创精密:2022年净利润2.4亿元,同比增长89.71%17 分钟前
    日媒:芯片行业加倍看好新加坡作为生产中心
    
  • find_element_by_name()selenium3的写法,selenium4写法如下:

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_name()'''
    try:
        name_el = driver.find_element(by=By.NAME,value="description")
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f'按元素的name属性查询元素 - driver.find_element(by=By.NAME,value="description") : {name_el}')
        print(f"查询元素内容 : {name_el.text}",end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按元素的name属性查询元素 - driver.find_element(by=By.NAME,value="description") : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="593edc0c-c3d8-43f3-8c70-76ab71339e01")>
    查询元素内容 : 
    
  • find_element_by_xpath()selenium3的写法,selenium4写法如下:

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_xpath()'''
    try:
        xpath = '//*[@id="eo-app"]/div[2]/div/div[1]/div[1]/div/div[1]/div[2]/div[1]/a/p' #xpath是基于XML的树状结构,可作为小型的查询语言,在网页中Copy即可
        xpath_el = driver.find_element(by=By.XPATH, value=xpath)
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按元素的xpath查询元素 - driver.find_element(by=By.XPATH, value=xpath) : {xpath_el}")
        print(f"查询元素内容 : {xpath_el.text}",end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按元素的xpath查询元素 - driver.find_element(by=By.XPATH, value=xpath) : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="5e158422-da4a-4292-a76c-1f4461b84385")>
    查询元素内容 : 比亚迪逆天改命,王传福赌赢了
    
  • find_element_by_link_text()selenium3的写法,selenium4写法如下(注意标签必须是<a></a>的元素,才能成功,参考https://www.jianshu.com/p/9006ba8478e6):

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_link_text()'''
    #参考 https://www.jianshu.com/p/9006ba8478e6
    try:
        # linkText_el = driver.find_element(by=By.LINK_TEXT, value="行业洞察")
        linkText_el = driver.find_element(by=By.LINK_TEXT, value="消费生活")  #用 find_element_by_link_text 方式定位,标签必须是<a></a>的元素,才能成功。
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按a标签的linkText查询 - driver.find_element(by=By.linkText_el, value=linkText_el) : {linkText_el}")
        print(f"查询元素内容 : {linkText_el.text}",end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按a标签的linkText查询 - driver.find_element(by=By.linkText_el, value=linkText_el) : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="36ab6667-de33-44d5-8418-e7b846024970")>
    查询元素内容 : 消费生活
    
  • find_element_by_partial_link_text()selenium3的写法,selenium4写法如下:

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_partial_link_text()'''
    try:
        partial_linkText_el = driver.find_element(by=By.PARTIAL_LINK_TEXT, value="行业")  #标签必须是<a></a>的元素,才能成功
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按a标签的PARTIAL_LINK_TEXT查询 - driver.find_element(by=By.PARTIAL_LINK_TEXT, value=partial_linkText_el) : {partial_linkText_el}")
        print(f"查询元素内容 : {partial_linkText_el.text}", end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按a标签的PARTIAL_LINK_TEXT查询 - driver.find_element(by=By.PARTIAL_LINK_TEXT, value=partial_linkText_el) : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="408eeab4-e8ac-4205-8d8d-7b06bfe41c68")>
    查询元素内容 : 行业洞察
    
  • find_element_by_tag_name()selenium3的写法,selenium4写法如下:

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_tag_name()'''
    try:
        tag_el = driver.find_element(by=By.TAG_NAME, value="a")  #标签必须是<a></a>的元素,才能成功
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按标签名TAG_NAME查询 - driver.find_element(by=By.TAG_NAME, value='a') : {tag_el}")
        print(f"查询元素内容 : {tag_el}", end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按标签名TAG_NAME查询 - driver.find_element(by=By.TAG_NAME, value='a') : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="9d9ecea3-5da3-453d-a145-17f26ed93282")>
    查询元素内容 : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="9d9ecea3-5da3-453d-a145-17f26ed93282")>
    
  • find_element_by_class_name()selenium3的写法,selenium4写法如下(注意对于多个class_name,只能输入一个class_name, 参考https://blog.csdn.net/A_under_taker/article/details/109482165):

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_class_name()'''
    #参考 https://blog.csdn.net/A_under_taker/article/details/109482165
    try:
        # class_el = driver.find_element(by=By.CLASS_NAME, value="common-new-info eo-bottom-dashed")  #Message: no such element: Unable to locate element: {"method":"css selector","selector":".common-new-info eo-bottom-dashed"} (Session info: chrome=101.0.4951.54)
        class_el = driver.find_element(by=By.CLASS_NAME, value="common-new-info")  #对于多个class_name(比如common-new-info eo-bottom-dashed),只能输入一个class_name,
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f"按元素CLASS_NAME属性查询元素 - driver.find_element(by=By.CLASS_NAME, value="") : {class_el}")
        print(f"查询元素内容 : {class_el.text}", end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按元素CLASS_NAME属性查询元素 - driver.find_element(by=By.CLASS_NAME, value=) : {class_el}
    查询元素内容 : 汽车出行
    比亚迪逆天改命,王传福赌赢了
    2023-02-24 16:26
    
  • find_element_by_css_selector()selenium3的写法,selenium4写法如下(多个属性组合定位参考https://blog.csdn.net/qq_35861801/article/details/108125082):

    driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))  # 自动更新chrome驱动, 获取浏览器对话实例
    driver.get("https://www.iyiou.com/")  #输入网址,发起请求
    driver.implicitly_wait(3)  #隐式等待3s
    
    '''find_element_by_css_selector()'''
    #参考 https://blog.csdn.net/qq_35861801/article/details/108125082
    try:
        #多个属性组合定位
        css_selector_el = driver.find_element(by=By.CSS_SELECTOR, value='div[class="eo-briefing-time"]')  #标签必须是<a></a>的元素,才能成功
        driver.implicitly_wait(3)  # 隐式等待3s
        print(f'按标签名CSS_SELECTOR查询 - driver.find_element(by=By.CSS_SELECTOR, value="div[class="eo-briefing-time"]") : {css_selector_el}')
        print(f"查询元素内容 : {css_selector_el}", end="\n\n")
    except Exception as e:
        print(e)
    
    输出结果
    ---
    按标签名CSS_SELECTOR查询 - driver.find_element(by=By.CSS_SELECTOR, value="div[class="eo-briefing-time"]") : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="c6ab1f93-aa24-4c8f-b746-37b7be438644")>
    查询元素内容 : <selenium.webdriver.remote.webelement.WebElement (session="7199bfa3f8ef10ab5749f3982ab29123", element="c6ab1f93-aa24-4c8f-b746-37b7be438644")>
    
b)find_elements()

可以一次性查询多个元素并以列表的形式返回,使用方法同find_element()

c)获取/设置 cookies 或 token

参考python + selenium token登录,python+selenium 通过添加cookies或token解决网页上验证码登录问题

  • 获取并设置cookie

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    
    driver = webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.get("https://www.dydata.io/")  # 镝数聚
    
    #获取cookies
    cookies = driver.get_cookies()  #type=list
    print(f"cookies = {cookies}")
    
    #添加cookies
    for cookie in cookies:
        driver.add_cookie(cookie) #提交所有cookie 
    
    # 将session_id装到cookie中
    driver.add_cookie({
        "domain": "www.dydata.io",
        "name": "session_id",
        "value": driver.session_id,
        "path": '/',
        "expires": None
    })
    
  • 获取并设置token

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    
    driver = webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.get("https://www.dydata.io/")  # 镝数聚
    
    #获取token
    token = driver.execute_script(f'window.localStorage.getItem("token"')
    print(f"token = {token}")
    
    #设置token
    token = 'eyJxxxDk54Q'
    js = f'window.localStorage.setItem("token", "{token}")'
    driver.execute_script(js)
    driver.get('https://www.iyiou.com/')  # 网址是登录后的主页网址
    
d)下拉滚动条

参考

  • 【selenium】下拉滚动条_selenium下拉框滚动条

  • python+selenium 滚动条/内嵌滚动条循环下滑,判断是否滑到最底部_selenium 判断滑动鼠标到底

4)模拟登录淘宝

参考Python3 网络爬虫(六):Python 使用 selenium 模拟登陆淘宝,Selenium被检测为爬虫,怎么屏蔽和绕过

a)selenium自动开启新的浏览器(登录失败)
import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains

# 使用 Selenium 的 ActionChains 方法,拖动滑块至最右边
def slide_action(driver, action_chains, xpath):
    # 点击登录后会出现滑块
    try:
        # 出现验证码,滑动验证
        slider = driver.find_element(By.XPATH, xpath)
        if slider.is_displayed():
            # 拖拽滑块
            action_chains.drag_and_drop_by_offset(slider, 258, 0).perform()
            time.sleep(0.5)
            # 释放滑块,相当于点击拖拽之后的释放鼠标
            action_chains.release().perform()
    except (Exception):
        print('未出现登录验证码')


#参考 https://blog.csdn.net/c406495762/article/details/106757531
if __name__ == '__main__':

    driver = webdriver.Chrome()
    driver.get("https://www.taobao.com")  #访问淘宝
    driver.implicitly_wait(1) #隐式等待1s

    '''***************************  模拟登录淘宝  *******************************'''
    '''Step1: 模拟登录点击'''
    # 通过xpath查找元素, 并点击登录
    login_xpath = r'//*[@id="J_SiteNavLogin"]/div[1]/div[1]/a[1]'
    # driver.find_element_by_xpath(xpath).click()    # 如果报了AttributeError: ‘WebDriver‘ object has no attribute ‘find_element_by_xpath,
    # 参考https://blog.csdn.net/m0_52818006/article/details/126283288
    driver.find_element(By.XPATH,login_xpath).click()  #模拟登录点击

    '''Step2: 输入用户名和密码'''
    username_xpath = '//*[@id="fm-login-id"]'
    password_xpath = '//*[@id="fm-login-password"]'
    login_click_xpath = '//*[@id="login-form"]/div[4]/button'
    slide_xpath = "//span[contains(@class, 'btn_slide')]"

    driver.find_element(By.XPATH,username_xpath).send_keys("15xxx2230")
    driver.find_element(By.XPATH,password_xpath).send_keys("1046xxx88aA")
    # driver.find_element(By.XPATH,login_click_xpath).click()

    # 使用 Selenium 的 ActionChains 方法,拖动滑块至最右边
    action_chains = ActionChains(driver)

    #selenium会被taobao的指纹识别算法检测到, window.navigator.webdriver=True, 但即使修改了该特征也不能避开指纹识别
    try:
        slide_action(driver, action_chains, slide_xpath)  #可能会在点击前出现滑块
        driver.find_element(By.XPATH,login_click_xpath).click()   #点击登录
        slide_action(driver, action_chains,slide_xpath)  #可能会在点击后出现滑块
        driver.find_element(By.XPATH, login_click_xpath).click()  # 点击登录
    except(Exception):
        print("登录处理出错")
        pass

    time.sleep(10)

在输入完用户名和密码,在点击登录之前,会有一个滑动条,有博主说滑动条可以使用SeleniumActionChains方法来实现,但是我在使用时,虽然能通过xpath能获取到元素,但是在调用action_chains.drag_and_drop_by_offset(slider, 258, 0).perform()时会报错:{ElementNotInteractableException}Message: element not interactable: [object HTMLSpanElement] has no size and location

而且即使手动滑动滚动条也不能登录,主要原因是taobao有反Selenium爬虫的指纹识别算法,可以检测出当前的调用者是人工的还是selenium,如果是自动调用的,在控制台中window.navigator.webdriver=True即使修改了该特征也不能避开指纹识别

上面的代码识别效果如下:
在这里插入图片描述

b)selenium绑定用户开启的浏览器(登录成功)

参考Selenium被检测为爬虫,怎么屏蔽和绕过,Selenium连接已存在的浏览器1 | 技术九柄剑

上面的方法不成功的主要原因是Taobao反爬虫算法Taobao可以通过检测浏览器window.navigator.webdriver=True等特征来判断此时是否存在爬虫, 通常我们可以使用 sannysoft来检测浏览器是否是由自动化爬虫工具(selenium、puppeter、playwright等)自动开启

目前较好的解决方法是让selenium连接手动开启的浏览器,不让其自动开启

  1. 首先通过对chrome.exe快捷方式中配置chrome的启动参数,命令如下:

    chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\selenium\ChromeProfile"
    

    配置方式如下:
    在这里插入图片描述

  2. 配置好了远程调试地址127.0.0.1:9222之后,接着使用如下代码让selenium 连接手动打开的浏览器

    import time
    
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.common.by import By
    from selenium.webdriver import ActionChains
    from code003_taobao_login_test import slide_action
    
    if __name__ == '__main__':
    
       sec = 1
       #selenium不通过浏览器驱动自动打开浏览器,而是连接手动打开的浏览器
       chrome_options = Options()
       chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")   #通过设置参数,让selenium登录手动打开的浏览器
       driver = webdriver.Chrome(options=chrome_options)
    
       print(driver.title)
    
       driver.get("https://www.taobao.com")  #访问淘宝
       driver.implicitly_wait(3) #隐式等待3s
    
       '''***************************  模拟登录淘宝  *******************************'''
       '''Step1: 模拟登录点击'''
       # 通过xpath查找元素, 并点击登录
       login_xpath = r'//*[@id="J_SiteNavLogin"]/div[1]/div[1]/a[1]'
       # driver.find_element_by_xpath(xpath).click()    # 如果报了AttributeError: ‘WebDriver‘ object has no attribute ‘find_element_by_xpath,
       # 参考https://blog.csdn.net/m0_52818006/article/details/126283288
       driver.find_element(By.XPATH,login_xpath).click()
       time.sleep(sec)  #模拟登录点击,注意需要等待一定时间后才可以跳转到登录页面
       # driver.implicitly_wait(1) #隐式等待1s
    
       '''Step2: 输入用户名和密码'''
       username_xpath = '//*[@id="fm-login-id"]'
       password_xpath = '//*[@id="fm-login-password"]'
       login_click_xpath = '//*[@id="login-form"]/div[4]/button'
       slide_xpath = '//*[@id="nc_1__scale_text"]/span'
    
       driver.find_element(By.XPATH,username_xpath).send_keys("1560***230")
       time.sleep(sec)
    
       driver.find_element(By.XPATH,password_xpath).send_keys("104***8aA")
       time.sleep(sec)
    
       # 使用 Selenium 的 ActionChains 方法,拖动滑块至最右边
       action_chains = ActionChains(driver)
       #selenium会被taobao的指纹识别算法检测到, window.navigator.webdriver=True, 但即使修改了该特征也不能避开指纹识别
       try:
           slide_action(driver, action_chains, slide_xpath)
           time.sleep(sec)  #可能会在点击前出现滑块
           driver.find_element(By.XPATH, login_click_xpath).click()
           time.sleep(sec)   #点击登录
           slide_action(driver, action_chains,slide_xpath)
           time.sleep(sec)  #可能会在点击后出现滑块
           driver.find_element(By.XPATH, login_click_xpath).click()
           time.sleep(sec)  # 点击登录
       except(Exception):
           print("登录处理出错")
           pass
    

    其中使用的slide_action()见4).a)。

    上面代码的实现效果如下(前提是需要手动开启浏览器,然后让selenium去连接):

    在这里插入图片描述

会发现手动开启的浏览器在登录账户时并不用滑动滚动条,并且登录成功后,如果不退出账户,下次即使手动关闭了浏览器,selenium再次访问taobao时会自动登录账号,主要原因是手动打开的浏览器中有session_id缓存数据,默认7天后账号登录失效。

5)其他问题

  • 对于多标签页来说,如果在使用driver.find_element(By.XPATH,xpath).click()时,xpath识别不了,抛出selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element的错误,其中大概率是当前driver调用的网页窗口并不存在该xpath 或者是当前页面没有完全加载出来,可以通过如下方法来修改,参考selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element

    #driver切换至指定标签页
    current_handle = driver.current_window_handle
    
    handles = driver.window_handles  #获取所有处理页,type=list[str]
    for handle in handles:
         driver.switch_to.window(handle)
         driver.close  #关闭当前标签页
         time.sleep(1)
         #处理逻辑
         ...
    
    #driver设置隐式等待
    time.sleep(1) 或者 driver.implicitly_wait(1)
    
  • 关闭当前标签页:参考Selenium基础之关闭页面和浏览器_selenium关闭浏览器_JaneMiss的博客-CSDN博客

    Selenium中有两种关闭浏览器的方式,这两种方式的区别在于:

    • close():关闭当前标签页

      driver.close()
      
    • quite():关闭浏览器,释放进程。

      driver.quite()
      
  • 获取当前标签页网址和的html内容:

    • 获取当前标签页网址:driver.current_url

    • 获取当前标签页html内容(设置延时即可获取动态html):

      driver.get("xxx.html")  # up主首页
      driver.implicitly_wait(10)  # 搜索等待时间设置长点
      html_doc = driver.page_source
      
  • seleniumBS4的结合:参考selenium和bs4的联合使用

  • selenium设置不自动开启浏览器:参考Selenium使用ChromeOptions启动参数 - 韩志超 - 博客园文章来源地址https://uudwc.com/A/0n0vW

    from selenium.webdriver import Chrome, ChromeOptions
    from selenium.webdriver.chrome.service import Service as ChromeService
    from webdriver_manager.chrome import ChromeDriverManager
    
    up_mainPage_url = 'https://space.bilibili.com/1641484091/?spm_id_from=333.999.0.0'# 加载动态网页
    option = ChromeOptions()
    # 设置浏览器为无头模式,使用过程中不会弹出浏览器页面
    option.headless = True
    driver = Chrome(service=ChromeService(ChromeDriverManager().install()), options=option)  # 自动更新chrome驱动
    driver.get(up_mainPage_url)
    

原文地址:https://blog.csdn.net/qq_33934427/article/details/131488496

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

h
上一篇 2023年07月02日 07:34
基于IDEA操作springboot项目构建docker镜像部署到云服务器并在idea把镜像推送到harbor仓库
下一篇 2023年07月02日 07:34