Python爬虫自学

Python爬虫自学

前言

这是我自己学习Python爬虫的学习笔记,内容大部分来自上海交通大学出版的《Python语言程序设计实践教程》,欢迎大家一起交流在这里插入图片描述

参考博客:

Python爬虫教程(纯自学经历,保姆级教程)

BeautifulSoup 四大对象种类

爬虫:设置Request Header(请求头)

一,网络基础知识

1,网站访问流程

用户使用浏览器大致流程

网站访问流程

(1)在浏览器的地址栏中输入一个网页地址(这个地址称为“统一资源定位符”(uniform resource locator,URL)),浏览器程序会通过计算机发起一个网络访问请求,这个请求称为Request

(2)互联网上的网络设备会分析这个网页地址(如www.baidu.com),在众多注册过的服务器计算机中找到对应的百度Web服务器

(3)在用户输入时的网页地址中通常会指定访问的具体是哪个网页文件,服务器上的Web服务器程序会将与此文件相关的所有数据(文字、表格、动画等)返回给用户,这个过程称为响应,也称Response

(4)当用户的计算机收到这些数据,浏览器会将所有数据按网页预先设定的格式整合起来展示给用户。一次Web访问到此基本结束了

通常访问的网址包含以下几方面信息

网络协议://服务器主机:端口/文件路径?发送数据

如http://lib.njtech.edu.cn/list.php?fid=9

第一部分代表采用何种网络协议,它告诉浏览器该如何处理要打开的文件,http(超文本传输协议)是最常用的协议

第二部分代表Web服务器的域名或IP地址,根据这部分信息,网络设备才能在众多计算机中准确找到请求访问的那一台。少数情况下,有些域名后面要指明一个端口编号

第三部分代表用户请求访问的文件名即该文件在服务器上的存储位置

第四部分是用户发送到网页文件的数据,可能是一个用户名,也可能是一个查询关键字,等等,网页文件会处理这些数据并将处理结果返回给用户。

2,HTML文档的标签及属性

要想利用计算机程序获取网页数据,就需要对网页结构有一定的了解。日常看见的网页实际上是HTML文档,包含HTML标签和纯文本。

(1)HTML指的是hypertext markup language(超文本标记语言)

(2)HTML不是编程语言,而是一种标记语言

(3)标记语言是一套标记标签(markup tag)

(4)HTML使用标记标签来描述网页

简单网页显示

如图显示的简单网页对应的HTML脚本如下

<html>
<body bgcolor="#eeeeee">
    <style>
        .css1{background-color:yellow;color:green;font-style:italic;}
    </style>
    <h1 align="center">这里是标题行</h1>
    <p name="p1" class="css1">这里是第一段</p>
    <p name="p2" class="css1">这里是第一段</p>
    <img src="http://www.baidu.com/img/bd_logo1.png" style="width:200px;height:100px"></img>
    <a id='link' href="http://baidu.com">点我跳去百度</a>
</body>
</html>

脚本中那些由<>括起来的关键词就是标签,如<html>、<h1>、<p>等,标签通常是成对出现的,如“<h1 align="center">这里是标题行</h1>”中<h1>称为开始标签,</h1>称为结束标签。开始标签到结束标签之间的所有内容称为一个元素,文字”这里是标题行“就是这个元素的内容。标签可以具有属性,属性描述标签的各种信息和特征。属性总是出现在开始标签中,以键值对的形式展现,如name=‘p1’。

下面解释HTML文档常出现的标签及属性:

第1行的<html>称为文档类型标签,用来告知浏览器自身是HTML格式的文件

第2行的<body>称为网页主体标签,绝大部分网页内容包含在body标签内部。body标签属性bgcolor用来设置整体的背景颜色,bgcolor="#eeeeee"将网页背景设为浅灰色。

第3~5行设定了一个样式类css1,包含三种格式:文本设为绿色、背景设为黄色、字体为斜体。然后段落标签p1、p2的class属性都赋值为css1,使两个段落都具备了css1设定的样式。

第6行的<h1>称为标题标签,标签中的对齐属性align="center"用来将标题文字居中显示。标签元素“这里是标题行”是给用户看的内容。

第7行的<p>称为段落标签,标签中的名称属性name="p1"用来设置段落名称为“p1”,类属性class="css1"用来指定p1段落使用第四行中定义的css1样式。

第8行用<p>又定义了“p2”的第二个段落。

第10行的<img>称为图像标签,标签中的属性src指明图像文件的存储路径,属性style用以设定图像的显示大小。

第11行的<a>称为超链接标签,标签中的属性id规定了标签在文档的唯一标识,属性href指明了单击链接将跳转的URL。标签元素“点我跳去百度”是显示给用户看的内容。

name属性和id属性都是用来在HTML文档中标识某个标签元素的,都可以用于查找指定标签。其中id属性如同学生的学号,其值在HTML中是唯一的;而name属性如同学生姓名,其值可能重复。所以查找标签时优先使用id属性。class属性称为类属性,用于给标签设置某种样式。当多个标签采用同一样式时,可以新建一个命名的样式类,在这个样式类完成格式设定,然后将class属性设为该样式类即可。实际有很多标签可能没有id或name属性,如果有class属性,经常也会通过class属性查找标签。

3,HTML文档的标签结构

在前面的HTML脚本中,共使用了8个标签,其中有6个标签都是嵌套在body标签中的。在HTML文档中,标签嵌套很常见,有时会多达几十层。这种嵌套关系如果一个图表示出来就是一个树状结构。下面用一个包含表格的HTML文档作为示例。

含表格的HTML网页

其源码如下


<html><head>
    <title>网页标题</title>
</head> 
<body>
    <h2>金庸群侠传</h2>
    <table width ="400px" border ="1">
        <tr>
            <th>书名</th> <th>人物</th> <th>年份</th>
        </tr>
        <tr>
            <td>《射雕英雄传》</td><td>郭靖</td><td>1959</td>
        </tr>
        <tr>
            <td>《倚天屠龙记》</td><td>张无忌</td><td>1961</td>
        </tr>
        <tr>
            <td>《笑傲江湖》</td><td>令狐冲</td><td>1967</td>
        </tr>
        <tr>
            <td>《鹿鼎记》</td><td>韦小宝</td><td>1972</td>
        </tr>
    </table></body></html>

表格在HTML文档中以<table>,<tr>,<tr>,<td>等标签来表示。<table>代表表格,表示一行,代表标题行的一个单元格,代表数据行一个单元格。分析时可以将各级标签用下图所示的标签树加以描述,树形图层次关系反映各标签的嵌套关系。HTML标签结构图

概括的讲,HTML文档代码主要由三部分组成:标签及属性、文本、事件代码。最常见的标签大多充当数据文本的容器,规定文本、图片等内容的布局和格式;标签属性则对内容的显示样式进一步设定,如大小、颜色等。文本代表了网页实际上有意义的那部分数据,可能是文字或图片文件的路径等。爬取网页数据,实际上就是希望把这部分内容全部提取到本地计算机中进行处理。事件代码是指网页为了响应用户操作的那部分代码,称为客户端代码,通常是用JavaScript脚本语言编写并由用户浏览器执行的,如对用户填写的表单数据进行验证、对用户单击按钮的响应等。这三部分中,数据采集关心的是前两种。

4,查看HTML文档源码

通过浏览器打开网页后,在页面空白处右击,在弹出的菜单中点击“查看网页源码”(不同浏览器不同,IE为“查看源”),单击后弹出一个新的标签页显示当前网页源代码

右键菜单显示
短评网页源代码显示

对于采用数据动态加载技术的网页,单纯的查看源码是无法找到所需数据的,需要更高级的采集技术,不做论述(我也不会😂)。

很多网站源码动辄几百上千行,如何在源码中定位到自己所需的数据在的那部分标签代码呢?选中感兴趣的文本部分,然后右击,在弹出的菜单中单击“检查”(或“检查元素”)项。检查元素

二,requests库的使用

requests库需自行安装,在系统左下角搜索框内输入cmd,会找到一个应用“命令提示符”,打开后输入pip install requests,回车键执行即可完成安装。

1,请求网页

通过http协议请求网页有以下几种访问方式:get、post、delete、put、head、options。只介绍get、post两种方式。

1.get方式

get方式表示从指定的网页文件请求数据,是最常用的一种方式。当输入类似于“http://lib.njtech.edu.cn/list.php?fid=9”这样的URL时,服务器会直接返回这个网页无须用户再执行其他操作。

2.post方式

post方式表示指定的某个网页有待处理的数据源。例如,用户填写了一些表单并将表单数据提交到网页,服务器程序将对提交的数据做相关处理。这种情形很常见,如输入用户名和密码登录某网站、注册某网站账号或修改用户的个人资料,都涉及用户数据的提交。

函数功能说明
get(URL)根据给定的URL直接下载网页
post(URL,data={‘key’:‘value’})向给定的URL的网页提交字典类型的数据

相比较而言,get方式用的多,下面以百度为例演示get()函数的用法。

import requests
#使用get访问百度首页,函数返回对象赋值给r变量
r=requests.get('http://www.baidu.com')
print(r.text)#打印返回对象的内容

get函数示例结果

2,带头部参数的网页请求

越来越多的网站注重对数据的保护,允许用户通过浏览器进行页面访问,但拒绝用户使用程序代码采集页面数据。例如用上面的方法去采集知乎网(http://www.zhihu.com)上的数据,会得到请求失败的信息(400 Client Error: Bad requests for url: http://www.zhihu.com)。那么网站是如何辨识用户通过浏览器还是程序代码访问呢?

用户使用浏览器访问一个页面时会发送一些请求数据,其中部分数据称为请求头部(headers),网站大多数是通过headers来分辨的。

为让网站服务器将爬虫程序理解为一次成功的浏览器访问,可以把类似的请求数据封装在一个参数中一起发给服务器,这样网站就会以为当前访问是浏览器的正常请求(没错,就是骗服务器)。requests库提供了一字典类型的headers参数,用来储存请求头的头部信息。在头部信息中,数据项很多,但不是所有项都有用,最常用的是User-Agent,其包含了用户浏览器类型、版本、以及操作系统的版本信息等。参考爬虫:设置Request Header(请求头)获取自己浏览器的User-Agent。

import requests

myHeaders={
    'user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.12151 SLBChan/30'
    }
url='http://www.zhihu.com'
r=requests.get(url,headers=myHeaders)
r.encoding='utf-8'
print(r.status_code)
print(r.text)

有些网站只加User-Agent还不能访问,可以尝试在headers字典里添加Cookie,甚至其他项,继续调试直到正常访问为止。

3,Response对象

调用一次get()函数就相当于发起了一次get方式的请求,requests库把用户请求数据封装为一个Request对象中;服务器会返回一个响应,requests库则把该响应的所有信息封装在一个Response对象中。在get方式访问百度示例中,这个Response对象就存入了变量r。

Response对象常用属性:

属性名称属性说明
status_code服务器响应状态码,整数类型,200代表请求成功,4XX或5XX代表出错
encoding服务器响应内容的编码方式,允许用户修改编码类型
text以字符串格式显示响应内容,依据头部信息中的编码约定对内容进行解码
content以字节(二进制)格式显示响应内容
headers以字典形式存储的服务器响应头部信息

Response对象的状态码属性status_code用一个整数反映了这次请求的结果状态,200表示请求成功;状态码4XX意味着用户发送的请求有问题,如403表示服务器拒绝请求,404表示服务器找不到请求的网页。5XX表示服务器处理请求时发生错误,如500表示服务器内部程序发生错误,503表示当前服务器无法提供服务,可能处于维护状态或停机

1.encoding属性的使用

HTML格式的网页文档本身对其内容是有一个编码设定的,这个原始的编码格式体现在Response对象的encoding属性上。有时候获取文档后出现很多看不懂的乱码,这时可以尝试将编码格式换为utf-8。

import requests
#使用get访问百度首页,函数返回对象赋值给r变量
r=requests.get('http://www.baidu.com')
r.encoding='utf-8'	#方法一,直接转换response对象编码格式
print(r.text)#打印返回对象的内容
r=requests.get('http://www.baidu.com')
print(r.content.decode('utf-8'))#方法二,对二进制内容以utf-8格式解码
2.content属性的使用

如果知道一张图片的URL,可以利用content属性下载并保存这张图片。在上述r.text打印出的网页代码中,可以发现一个标签里包含了百度logo图片的URL:<img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129>。采用以下代码可以将该图片下载保存,并重新命名为baidu.png。新建图片文件时open()函数采用了wb模式,即write+binary,以二进制形式写入图片数据

import requests
r=requests.get(http://www.baidu.com/img/bd_logo1.png)
path='D:\\learningFile\\python\\爬虫\\baidu.png'#下载文件保存的路径
with open(path,'wb') as file:		#以二进制+写模式新建文件
    file.write(r.content)
3.raise_for_status()方法的使用

为避免某一个或几个意外问题而导致程序终止,Response对象提供了一个raise_for_status()方法。此方法主要用于当请求访问某个网页出现异常时,允许程序及时捕捉到该异常并且跳过他,转而去执行下一个网页的采集。而对个别采集失败的网页,只需要查看一下出错日志,有针对性地重新处理即可。下面以采集《天龙八部》豆瓣短评(https://book.douban.com/subject/1255625/comments/)为例。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

分析URL发现,翻页时URL基本不变,只是参数start的值加20,也就是页数p=n时,start=20*p。因此可以利用range()函数循环采集前10页。

import time
import requests
url='https://book.douban.com/subject/1255625/comments/?start={}&limit=20&status=P&sort=new_score'
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',
    }
for p in range(0,11)
	try:
		url=url.format(int(page)*20)
    	r=requests.get(url,timeout=30,headers=header)#由于豆瓣有反爬虫机制,所以要加上headers参数
    	r.raise_for_status()
    	r.encoding='utf-8'
    	return r.text
	except Exception as ex:
    	print("第{}页采集错误,出错原因:{}。".format(page,ex))
    	return ""

三,BeautifulSoup库的使用

1,解析器选择

要从HTML文档中解析想要数据,首先根据该文档的源码创建一个BeautifulSoup对象,并选择一种解析器类型

import requests
import bs4
r=requests.get('http://www.baidu.com')
r.encongding='utf-8'
#新建beautifulSoup对象,赋值给soup对象
soup=bs4.BeautifulSoup(r.text,'html.parser')

BeautifulSoup对象有两个参数,第一个r.text是百度首页的文档源码,即要解析的对象;第二个html.parser是解析器类型,表示选择Python自带的解析器来解析前面的html文档

常用解析器比较

解析器参数写法优点缺点
Python标准库html.parser内置库,执行较快,解析HTML文档容错性好在旧版Python容错性不好
lxml html解析器lxml执行速度很快,解析HTML文档容错性好需安装C语言库
lxml xml解析器xml执行速度很快,支持解析XML格式文档需安装C语言库
html5libhtml5lib容错性最好,以浏览器方式解析文档,可生成HTML5格式的文档解析速度慢

推荐使用html.parser 和 lxml解析器。lxml解析器需事先安装,执行pip install lxml指令即可

2,BeautifulSoup的四种对象

BeautifulSoup能将HTML文档中复杂的源码映射为一个树形结构,并把整个文档中所有内容映射转化为四类对象:Tag,BeautifulSoup,NavigationString,Comment。

1.Tag对象(标签对象)

通俗点讲就是 HTML 中的一个个标签,例如

<title>The Dormouse's story</title>
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

上面的 title a 等等 HTML 标签加上里面包括的内容就是 Tag,下面我们来感受一下怎样用 Beautiful Soup 来方便地获取 Tags。

下面每一段代码中注释部分即为运行结果,以下图所示的HTML文档为例

在这里插入图片描述

# -*- coding: utf-8 -*-
import bs4
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body>
</html>
"""

#创建beautifulsoup对象
#也可以用打开本地的html文件来创建beautifulsoup对象,例如:
#soup=bs4.BeautifulSoup(open('BeautifulSoup示例Tag.html'),"lxml")
soup=bs4.BeautifulSoup(html,"lxml")

#格式化输出
print(soup.title)
print(soup.head)
print(soup.a)
print(soup.p)


#运行结果:
<title>The Dormouse's story</title>
<head><title>The Dormouse's story</title></head>
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>

创建BeautifulSoup对象后,文档的每一个标签自动转换为有一个Tag对象存放在树形结构的相应位置。

  1. 利用 soup加标签名轻松地获取这些标签的内容
  2. 它查找的是在所有内容中的第一个符合要求的标签
  3. 这些对象的类型是<class 'bs4.element.Tag'>

Tag对象有两个属性,即name,attrs,它们分别用来存储标签的类型及标签的属性。

#根据html新建的BeautifulSoup对象(html同上)
soup=bs4.BeautifulSoup(html,'html.parser')

print(soup.p)			#输出标签对象:<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
print(type(soup.p))		#输出对象类型:<class 'bs4.element.Tag'>
print(soup.p.name)		#输出标签p

print(soup.p.attrs)		#输出标签属性字典:{'class': ['title'], 'name': 'dromouse'}
print(soup.p['name'])	#输出标签name属性:dromouse
print(soup.p['class'])	#输出标签class属性:['title']
#运行结果
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<class 'bs4.element.Tag'>
p
{'class': ['title'], 'name': 'dromouse'}
dromouse
['title']

(标签的大多数属性是单值属性,但class属性是多值属性,因此获取class属性值得到的是一个列表)

attr获得的是字典对象,是可以修改和删除的

#-*-coding:utf-8-*-
from bs4 import BeautifulSoup
html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body>
</html>
"""

#创建beautifulsoup对象
#也可以用打开本地的html文件来创建beautifulsoup对象,例如:
#soup=bs4.BeautifulSoup(open('BeautifulSoup示例Tag.html'),"lxml")
soup = BeautifulSoup(html,"lxml")
#修改Tag对象的属性
soup.p['class']="newClassname"
print(soup.p)
#删除Tag对象的属性
del soup.p['class']
print(soup.p)

#运行结果:
<p class="newClassname" name="dromouse"><b>The Dormouse's story</b></p>
<p name="dromouse"><b>The Dormouse's story</b></p>
2.BeautifulSoup对象

表示的是一个文档的整体,可以看做HTML文档书的根或一个顶层节点,文档的所有标签及内容都是它的后代节点。因为BeautifulSoup对象对应的是整个文档,不是某种标签,所以它没有Tsg对象的那些属性。通常情况下,是从它开始向下搜索和遍历文档树的

#前面代码同上···
#创建beautifulsoup对象
soup = BeautifulSoup(html,"lxml")
#···
print(soup.name)
print(type(soup.name))
print(soup.attr)

#运行结果:
[document]
<type 'unicode'>
None
3.NavigableString对象

NavigableString称为可遍历的字符串对象,是用来操作那些包含标签内的字符串。在解析HTML文档时,真正关心的实际上是那些标签中包含的数据,而不是标签本身。这些有价值的数据可以通过NavigableString从标签中取出来,方法很简单,用.string即可,依旧以前面的网页为例

soup = BeautifulSoup(html,"lxml")

#获取标签内部文字
print(soup.p.string)
print(type(soup.p.string))

#运行结果:
The Dormouse's story
<class 'bs4.element.NavigableString'>
4.Comment对象

Comment对象是一个特殊类型的NavigableString对象。

如果标签内部的内容是注释,例如:<!-- Elsie -->。那么该NavigableSring对象会转换成Comment对象,并且会把注释符号去掉。

print(soup.a)
print(soup.a.string)
print(type(soup.a.string))

#运行结果:
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>
 Elsie 
<class 'bs4.element.Comment'>
————————+————————
版权声明:本文为CSDN博主「tyson Lee」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/chinaltx/article/details/86748757

如果我们需要获得comment类型的对象,需要先判断对象类型是comment还是NavigableString。

不过大多数情况下注释价值很小,并不需要关心他们。

接下来继续解析HTML文档树,有两种思路:一种是遍历文档树,一级一级地访问文档树节点;另一种是搜索文档树,基于目标数据所在标签的特征直接进行查找。

3,遍历文档树

HTML文档既然可以映射为一棵树,则各种嵌套的标签就可以转化为树的各层结点,基于这种结构,就可以对树进行遍历访问。遍历时可能出现三种路径:自上而下遍历、水平方向遍历、自下而上遍历。下面以“金庸群侠传”页面对应的标签书为例说明。

1.自上而下解析

从上级(外层)标签开始向内分析文档,即从树形结构的上层往下访问。此时,被上级标签直接包含的标签称为子节点(children、contents),子节点及其所有的下级统称为上级标签的后代节点(descendant)。例如,<tr>,是<table>标签的子节点,<td><tr>的子节点,<tr>,<td>都是<table>标签的后代节点。

使用soup.tr.contentssoup.tr.children两个属性都可以得到<tr>标签的所有子节点,但是contents属性返回的子节点列表,可以采用列表方式访问子节点元素;而children属性返回的是子节点所构成的生成器,还需要经过循环才能取出所有节点。soup.tr.descendants属性得到的同样是生成器,也需要通过循环获取<tr>的后代标签。

(1)使用contents属性输出标签所有子节点列表
from bs4 import BeautifulSoup,element
code='''<html><head>
    <title>网页标题</title>
</head> 
<body>
    <h2>金庸群侠传</h2>
    <table width ="400px" border ="1">
        <tr><th>书名</th> <th>人物</th> <th>年份</th></tr>
        <tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959年</td></tr>
        <tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961年</td></tr>
        <tr><td>《笑傲江湖》</td><td>令狐冲</td><td>1967年</td></tr>
        <tr><td>《鹿鼎记》</td><td>韦小宝</td><td>1972年</td></tr>
    </table></body></html>'''
soup=BeautifulSoup(code,'html.parser')
print(soup.table.contents)#输出<table>标签所有子节点
#运行结果
"""
['\n', <tr><th>书名</th> <th>人物</th> <th>年份</th></tr>, '\n', <tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959年</td></tr>, '\n', <tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961年</td></tr>, '\n', <tr><td>《笑傲江湖》</td><td>令狐冲</td><td>1967年</td></tr>, '\n', <tr><td>《鹿鼎记》</td><td>韦小宝</td><td>1972年</td></tr>, '\n']
"""

从输出结果可发现,换行符也算做子节点。

(2)对children属性做循环,输出标签的子标签
for child in soup.table.children:
    if type(child)!=element.NavigableString:#过滤标签之间的换行符
		print(child)        
#运行结果
"""
<tr><th>书名</th> <th>人物</th> <th>年份</th></tr>
<tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959年</td></tr>
<tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961年</td></tr>
<tr><td>《笑傲江湖》</td><td>令狐冲</td><td>1967年</td></tr>
<tr><td>《鹿鼎记》</td><td>韦小宝</td><td>1972年</td></tr>
"""
(3)对descendants属性做循环,输出标签的后代标签
for des in soup.table.descendants:
    if type(des) !=element.NavigableString:#过滤标签间的换行符
        print(des)
#运行结果
'''
<tr><th>书名</th> <th>人物</th> <th>年份</th></tr>
<th>书名</th>
<th>人物</th>
<th>年份</th>
<tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959年</td></tr>
<td>《射雕英雄传》</td>
<td>郭靖</td>
<td>1959年</td>
<tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961年</td></tr>
<td>《倚天屠龙记》</td>
...(省略部分)
'''

HTML文档源码中,标签间经常有换行符,而换行符属于NavigableSting类型对象,因此可用条件语句把换行符过滤掉,只留下Tag标签对象。比较children属性循环和descendants属性循环,children循环只输出了<table>标签的5个子标签<tr>内容;descendants循环不仅输出了直接下级<tr>标签,也循环输出了后代标签<td>

2.水平方向解析

在HTML文档代码中经常会看见很多相同类型的标签连续出现,用来展示一些数据列表和数据表格。此时,这些同类型标签之间是同级关系,因此可以把这些标签称为兄弟节点。使用next_siblings属性解析表格内的行标签非常方便,因为行与行之间就是兄弟关系。

from bs4 import BeautifulSoup,element
code='''<html><head>
    <title>网页标题</title>
</head> 
<body>
    <h2>金庸群侠传</h2>
    <table width ="400px" border ="1">
        <tr><th>书名</th> <th>人物</th> <th>年份</th></tr>
        <tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959年</td></tr>
        <tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961年</td></tr>
        <tr><td>《笑傲江湖》</td><td>令狐冲</td><td>1967年</td></tr>
        <tr><td>《鹿鼎记》</td><td>韦小宝</td><td>1972年</td></tr>
    </table></body></html>'''
soup=BeautifulSoup(code,'html.parser')
for child in soup.table.tr.next_siblings:#获取第一行向后的兄弟标签
    if type(child)!=element.NavigableString:#过滤标签之间的换行
        print(child)
#运行结果
<tr><td>《射雕英雄传》</td><td>郭靖</td><td>1959</td></tr>
<tr><td>《倚天屠龙记》</td><td>张无忌</td><td>1961</td></tr>
<tr><td>《笑傲江湖》</td><td>令狐冲</td><td>1967</td></tr>
<tr><td>《鹿鼎记》</td><td>韦小宝</td><td>1972</td></tr>

​ 从输出结果中容易发现,没有标题行,why?因为soup.table.tr获取的已经是表格第一行,也就是标题行,他不能把自己看作兄弟标签,所以next_siblings属性获取到的就是后面所有行的数据行。

3.自下而上解析

一个元素的直接上级对象称为父节点,对应的属性为parent。一个元素的所有上级对象则称为父辈节点,对应的属性为parents(复数形式,注意前面的content,contents,child,children,descendant,descendants)。抓取网页时查找父辈标签的情况很少,基本用法和上面两种差不多。

4,搜索文档树

现实中,一个复杂网页的节点的上下级关系可能多达几十层,所需的数据都深深藏在若干层标签之下,如果通过遍历文档树的方式去一级一级地查找目标数据所在的标签,需要话费很大的精力且容易出错。好在BeautifulSoup库提供了另外一种搜索文档树搜索的方法,能够快速地定位到目标数据所在标签附件(往往是距离目标数据不远的父辈标签),然后再利用局部的上下级节点关系,对这一块局部区域进行标签遍历,获取目标数据。

搜索过程中用的最多的方法就是find()函数和find_all()函数。这两个函数用法和参数相似,主要区别在于返回结果。find()函数返回满足查找条件的第一个对象,而find_all()函数则返回一个列表,列表元素为满足条件的所有对象。当没有找到符合条件的对象时,find()返回None,find_all()返回空列表,两个函数原型如下

find(name,attrs,recursive,text,**kwargs)
find_all(name,attrs,recursive,text,limit,**kwargs)

这两个函数参数较多且相似,实际上这些参数主要是起过滤器的作用。回想一下HTML文档源码中那些标签的特征,想想如何才能在众多标签中筛选出他们?可以从标签类型、标签标识熟悉(id)、名称属性(name)、类属性(class)等不同方面观察,如果发现目标标签具备了某种特征,就可以利用查找函数去获取它们。

有时想获取的数据位于一组同类标签中,或者目标标签不具备明显特征,那就向上观察他们的父标签;若还未找到特征,就继续查看父标签的上一级,这样由内而外的一层层分析通常会找到一个父辈节点标签具备某种特征。此时利用查找函数取出这个父辈标签的所有内容,然后就可以利用上一小节的遍历方法向下遍历解析,或者在取出的父辈标签上继续应用查找函数来获取后代标签,即可得到所想要的数据。

1.name参数

查找所有标签类型为name的对象。查找多个类型标签时,可以传入一个列表

from bs4 import BeautifulSoup
code="""
<html>
<body bgcolor="#eeeeee">
    <style>
        .css1{background-color:yellow;color:green;font-style:italic;}
    </style>
    <h1 align="center">这里是标题行</h1>
    <p name="p1" class="css1">这里是第一段</p>
    <p name="p2" class="css1">这里是第二段</p>
    <img src="http://www.baidu.com/img/bd_logo1.png" style="width:200px;height:100px"></img>
    <a id='link' href="http://baidu.com">点我跳去百度</a>
</body>
</html>"""
soup=BeautifulSoup(code,'html.parser')
print(soup.find_all('h1'))
#输出:[<h1 align="center">这里是标题行</h1>]
print(soup.find_all('h2'))
#输出:[]
print(soup.find_all('p'))
#输出:[<p class="css1" name="p1">这里是第一段</p>, <p class="css1" name="p2">这里是第一段</p>]
print(soup.find_all(['p','a']))
#输出:[<p class="css1" name="p1">这里是第一段</p>, <p class="css1" name="p2">这里是第一段</p>, <a href="http://baidu.com" id="link">点我跳去百度</a>]
2.**kwargs(关键字参数)

如果一个指定名字的参数不是find_all()函数内置的参数名,搜索时会把该参数名当做标签的属性来搜索。常见情况是:如果目标标签具备id属性,则优先采用id属性进行标签搜索,因为标签的id属性值在文档中大多是唯一的。

print(soup.find_all(id='link'))
#输出:print(soup.find_all(id='link'))
print(soup.find_all('h1',align="center"))
#输出:[<h1 align="center">这里是标题行</h1>]

需要注意标签的class属性,因为class本身就是Python语言中的关键字,所以采用class属性作为keyword参数搜索时,需要加一个下划线,写成class_。如果一个标签的class属性被赋予多个值时,此时只要有一个值匹配即认为满足条件。

soup2=BeautifulSoup('<p class="css1 css2"></p>','html.parser')
print(soup2.find_all(class_='css1'))
print(soup2.find_all(class_='css2'))
print(soup2.find_all(class_='css1 css2'))
#输出:以上三条语句都输出:
#[<p class="css1 css2"></p>]
3.attrs参数

attrs参数定义了一个字典参数来实现对标签属性的查找匹配。虽然对于大多数标签属性用keyword参数也能实现查找,但某些特殊情况,如HTML5格式文档中有一种data-foo属性,data-foo不符合Python标识符命名规则,是不能作为参数名的,此时需要attrs参数实现

>>>soup.find_all(attrs={'data-foo':'value'})
#输出:[]
>>>soup.find_all(attrs={'class':'css1'})
#输出:[<p class="css1" name="p1">这里是第一段</p>, <p class="css1" name="p2">这里是第二段</p>]
>>>soup.find_all('p',{'name':'p2','class':'css1'})
#输出:[<p class="css1" name="p2">这里是第二段</p>]
4.recursive参数

函数默认是查找当前对象的所有后代起点,如果只想搜索当前标签对象的子节点,无需查找子节点的后代,对recursive参数赋值为False即可。

5.text参数

text参数是针对文本内容的查找,通过它可以搜索文档的字符串内容。如果直接对text参数赋值进行查找的话,只有标签内的字符串与参数值完全不一样才算匹配。所以大多数情况下,对于文本字符串的搜索都是基于关键字的模糊查询,此时需要用到正则表达式,需要导入库re

>>>soup.find_all(text='这是第一段')
#输出:['这是第一段']
>>>soup.find_all(text='是')
#输出:[]
>>>import re
>>>soup.find_all(text=re.compile("是"))
#输出:['这里是标题行','这是第一段','这是第二段']
6.limit参数

find_all()方法默认返回对当前对象进行全部搜索的匹配结果,如果当前对象嵌套的后代标签很多,搜索会很慢。当不需要全部结果时,可以使用limi参数限制返回结果的数量,一旦匹配结果达到limit规定的数量就会停止搜索并立刻返回。

四,爬虫应用实例

前面学习requests库时,曾以豆瓣上《天龙八部》短评为例,讲解如何抓取网页源码,现在进一步实现从源码中获取真正想要的那部分评论,并以此生成一个词云图片。

为了让代码结构清晰,对代码进行拆分,将抓取网页源码、分析网页源码抽取评论内容、生成词云图片文件三部分功能分别定义成独立函数,有主程序进行调用。

豆瓣作为公共网站,对爬虫程序有一定的限制,所以在代码中需要增加休眠函数time.sleep(2),每爬取一页评论后暂停2秒,否则爬虫程序会被网站拒绝访问(可能会封号,然后你的电脑就不能进入这个网站)。很多成熟的网站都有类似限制,所以程序的爬去频率不可太频繁。

为了能够从HTML文档源码获取到评论内容,需要分析评论内容所在标签的特征,以便利用BeautifulSoup库进行筛选。选中网页上一段短评内容,然后利用浏览器的检查元素功能,可以查看到源码中这段评论所在的局部标签树结构

选中目标内容

局部标签树

图中的源码中有一组结构相同的列表项标签<li>,评论内容是位于<li>的一个后代标签<span>之内的。从<li>标签往上看,可发现一个上级标签具备属性id=‘comments’,所以可以考虑直接对HTML文档对象使用查询函数find()来获取这个div标签,然后再向下获取它内部的所有<li><p><span>标签及内容。

在前面已经知道豆瓣评论每页网址变化规律为start=20,40,60……因此可以用

'https://book.douban.com/subject/1255625/comments/?start={}&limit=20&status=P&sort=new_score'.format(int(page)*20)

page(=1,2……10)为变量循环

import time,requests,jieba
from bs4 import BeautifulSoup
from wordcloud import WordCloud

#函数1:爬取给定url的html文档
def getHtmlDoc(url,page,header):
    try:
        url=url.format(int(page)*20)
        r=requests.get(url,timeout=30,headers=header)#由于豆瓣有反爬虫机制,所以要加上headers参数
        r.raise_for_status()
        r.encoding='utf-8'
        return r.text
    except Exception as ex:
        print("第{}页采集错误,出错原因:{}。".format(page,ex))
        return ""

#函数2:获取给定html文档中的评论内容,返回评论列表
def getComment(html):
    comment=[]#该列表用于存放当前页面的所有评论
    soup=BeautifulSoup(html,'html.parser')
    div=soup.find('div',id='comments')
    
    #获取<div>标签内部的所有列表项标签<li>,再获取后代标签<p>、<span>
    for li in div.find_all('li',{'class':'comment-item'}):
        p=li.find('p',{'class':'comment-content'})
        text=p.span.string
        comment.append(text)
    return comment
    
#函数3:根据给定评论文件,利用jieba库分词后生成词云文件
#这里提供两种生成词云的方式,第一种是根据文本生成词云,第二种是根据字典生成词云
#第一种因为不好去除词云图片里的单个字,所以采用第二种
"""def creatWordCloud(fileName):
    with open(fileName,'r',encoding='utf-8') as file:
        text=file.read()
        ls_word=jieba.lcut(text)#利用jieba库对所有评论进行分词
        all_words=','.join(ls_word)#所有词语以逗号连接成一个长字符串
        wcloud=WordCloud(font_path=r'C:\Windows\fonts\simhei.ttf',
                         width=1000,height=800,
                         background_color='white',
                         max_words=200,
                         margin=2)
        wcloud.generate(all_words)
        #生成词云图片文件,主文件名同文本文件名
        fileCloud=fileName.split('.')[0]+'.png'
        wcloud.to_file(fileCloud)
"""
def creatWordCloud(fileName):
    with open(fileName,'r',encoding='utf-8') as file:
        text=file.read()
        counts={}#存放词语及其词频的字典
        ls_word=jieba.lcut(text)#利用jieba库对所有评论进行分词
        for word in ls_word:
            if len(word)==1:#排除单字
                continue
            else:
                rword=word
            counts[rword]=counts.get(rword,0)+1
        wcloud=WordCloud(font_path=r'C:\Windows\fonts\simhei.ttf',
                         width=1000,height=800,
                         background_color='white',
                         max_words=200,
                         margin=2)
        wcloud.fit_words(counts)
        #生成词云图片文件,主文件名同文本文件名
        fileCloud=fileName.split('.')[0]+'.png'#取fileName的前半部分和.png拼接
        wcloud.to_file(fileCloud)
#以下为主程序
url='https://book.douban.com/subject/1255625/comments/?start={}&limit=20&status=P&sort=new_score'
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',
    }
all_comment=[]    #存储全部评论的列表

for p in range(1,11):
    html=getHtmlDoc(url,str(p),headers)      #循环爬取前10页html文档
    page_comment=getComment(html)    #从html文档中抽取评论内容
    all_comment.extend(page_comment) #每页的评论列表添加到总列表中
    time.sleep(2)                    #每爬取一页暂停2s
    print('第{}页处理完成。'.format(p))

print('网页采集结束,开始写入文件、生成词云')
#评论列表全部写入文件
fileName='D:\\learningFile\\python\\爬虫\\4应用实例\\天龙八部评论.text'
with open(fileName,'w',encoding='utf-8') as file:
    file.writelines(all_comment)

#根据评论文件生成词云
creatWordCloud(fileName)
print('词云生成结束。')

最后运行得到的结果如下图

程序运行结果

天龙八部评论

在这里插入图片描述

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值