爬虫基础以及Xpath、Beautiful Soup的应用
HTTP的基本原理
-
URI(Uniform Resource Identifier)和URL(Universal Resource Locator),以协议的链接方式访问任何互联网上的资源
-
HTTP(Hypyertext Transfer Protocol )和HTTPS(可以理解为HTTP的安全版本)
- HTTPS的安全基础是SSL,可以从英文全称Hypyertext Transfer Protocol Secure Socket Layer看出。
- HTTP(S)协议都属于计算机网络中的应用层协议,其下层是基于TCP协议实现的,TCP协议属于计算机网络中的传输层协议,包括建立连接时的三次握手和断开时的四次握手等过程
- By the way :谷歌从2017年1月推出的Chrome 56 开始,对位进行HTTPS加盟的网址亮出风险提示,即在地址栏的显著位置提醒用户"此网页不安全"。
HTTP过程:
在浏览器地址中输入一个URL,按下回车之后便可观察到对应的页面内容。实际上,这个过程是浏览器先向网站所在的服务器发送一个请求(request),网站服务器接收到请求后对其进行处理和解析,然后返回对象的响应(response),接着传回浏览器。
一些概念:
请求方法,用于标识请求客户端请求服务端的方式。常见的有两种GET,POST:GET,请求页面并返回页面。POST,大多数用于提交表单或上传文件,数据包含在请求体中。
GET和POST请求方法的区别
- GET请求中的参数包含在URL里面,数据可以在URL中看到;而POST请求的URL不会包含这些数据,数据都是通过表单形式传输的,会包含在请求中。
- GET请求提交的数据最多只有1024字节,POST方式则没有限制。
登录时一般需要提交用户名和密码,其中密码是敏感信息,如果使用GET方式请求,密码就会暴露在URL里面,造成密码泄露,所以这时最好以POST方式发送。上传文件时,由于文件内容比较大,因此也会选用POST方式。
Cookie:也常用复数形式cookies,这是网站为例辨别用户,进行会话跟踪而存储在用户本地的数据。它的主要功能是维持当前的会话。
- 输入用户名和密码成功登录某个网站后,服务器会用会话保存登录状态信息,之后每次刷新或请求该站点的其他页面,都会发现处在登录状态,这就是cookie的功劳。
User-Agent:做爬虫时如果加上此信息,可以伪装为浏览器。
Web网页基础
网页一般由三个部分组成,分别是HTML(超文本标记语言)、CSS(层叠 样式表)和JavaScript(活动 脚本语言)
HTML(Hypertext Markup Language):如用img标签表示图片、用video标签表示视频、用p标签表示段落,这些标签之间的布局常由布局标签div嵌套组合而成,各种标签通过不同的排列和嵌套形成最终的网页框架。
- HTML是整个网页的结果,是整个网站的框架。带有“<” “>”符号的都属于HTML的标签,并且标签都是成对出现的。
- <html>. .</html>:表示标记中间的元素是网页
- <body>. .</body>:表示用户可见的内容
- <div>. . </div>:表示框架
- <p>. .<\p>:表示段落
- <li>. .</li>:表示列表
- <img>. .<img>:表示图片
- <h1>. .</h1>:表示标题
- < a href= " ">. .</a>:表示 超链接
- CSS表示样式,
一个简单的HTML代码
<html>
<head>
<title>Python3网页爬取</title>
</head>
<body>
<div>
<p>Python爬虫 666</p>
</div>
<div>
<ul>
<li><a href = "http://c.biancheng.net">爬虫</a></li>
<li>数据清洗</li>
</ul>
</div>
</body>
</html>

CSS(Cascading Style Sheets),即层叠样式表。”层叠“是指当HTML中引用了多样式文件,并且样式发生冲突时,浏览器能够按照层叠顺序处理这些样式。”样式“是指网页中的文字大小、颜色、元素间距、排列等格式。
CSS:是一种用来表现HTML或XML等文件样式的计算机语言;它主要用来设计网页的样式和美化网页,不仅可以静态地修饰网页,还可配合各种脚本语言动态地对网页各元素进行格式化。 CSS有助于实现负责任的Web设计。
#head_wrapper.s-ps-islite .s-p-top{
position:absolute;
bottom:40px;
width:100%;
height:181px;
}
上面代码为一个CSS样式。大括号前面是一个CSS选择器,意思是首先选择id为head_wrapper且class为s-ps-islite的节点,然后选择此节点内部的class为s-p-top的节点。大括号内部就是一条条样式规则。
在网页中,一般会统一定义整个网页的样式规则,并写入CSS文件中(.css)在HTML中,只需要用link标签即可引入写好的CSS文件。
Javascript的出现使得用户和信息之间不在只是一个浏览与显示的关系,还实现了一种实时、动态、交互的网页功能。
JavaSript通常也是以单独的文件形式加载的,后缀为js,在HTML中通script标签即可引入。如:<script src="jquery-2.1.0.js"></script>
Session和Cookie
Session,中文称之为会话,其本义是指有始有终的一些列动作、消息。例如打电话时,从拿起电话拨号到挂断电话之间的一系列过程就可以称为一个Session。Cookie,指某些网站为了鉴别用户身份、进行Session跟踪而存储在用户本地终端上的数据。Session在服务端,也就是网站的服务器,用来保存用户的Session信息;Cookie 在客户端,也可以理解为在浏览器端,有了Cookie,浏览器在下次访问相同网页时就会自动附带上它,并发送给服务器,服务器通过识别Cookie鉴定出是哪个用户在访问,然后判断此用户是否处于登录状态,并返回对应的响应。
代理的基本原理
代理实际上就是指代理服务器,英文叫做Proxy Server,功能是代网络用户取得网络信息。形象点说,代理是网络信息的中转站。当客户端正常请求一个网站时,是把请求发送给了Web服务器,Web服务器再把响应传回给客户端。设置代理服务器,就是在客户端和服务器之间搭建一座桥,此时客户端并非直接向Web服务器发起请求,而是把请求发送给代理服务器,然后代理服务器把请求发送给Web服务器,Web服务器返回的响应也是与代理服务器转发给客户端的。这样客户端同样可以正常访问网页,而且这个过程中Web服务器识别出的真实IP就不再是客户端的IP了,成功实现IP的伪装,这就是代理的 基本作用。
隐藏真实IP。上网者可以通过代理隐藏自己的IP,免受攻击。对于爬虫来说,使用不断更换的代理就是为了隐藏自身IP,防止自身的IP被封锁。
多线程和多进程的基本原理
在编写爬虫程序的时候,为了提高爬取效率,我们可能会同时运行多个爬虫任务,其中同样设计多进程和多线程。
- 多线程的含义。
进程可以理解为一个可以独立运行的程序单位,例如打开一个浏览器,就开启了一个浏览器进程。浏览器进程中,有的页面播放音乐,有的页面播放视频。这些任务同时运行,互不干扰。为什么能做到同时运行这么多任务呢?这便引出了线程的概念,一个任务就对应一个线程。进程是线程的集合。
多线程就是一个进程中同时执行多个线程。
- 并发与并行
计算机中运行一个程序,底层是通过处理器运行一条条指令来实现的。
处理器同一时刻只能执行一条指令**,并发(concurrency)**是指多个线程对应的多条指令被快速轮换地执行。
**并行(parallel)**同一时刻有多条指令在多个处理器上同时执行,必须依赖多个处理器。
爬取网页的requests包
网页的返回类型是str类型,但是它很特殊,是JSON格式。如果想直接解析返回结果,得到一个JSON格式的数据,可以直接调用json方法,它可以吧返回结果(JSON格式的字符串)转化为字典。

如上代码,t.text
返回的是一个HTML文档,这种情况下用r.json
就会解析错误,抛出JSONDecodeError异常。
利用正则表达式爬取内容
requests库可以获取网页的源代码,即HTML代码。但是我们真正想要的数据是包含在HTML代码之中的,此时可使用正则表达式来获取想要的信息。
正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检验一个串是否含有某种子串,将匹配的子串替换或者从某个串中取出符合某个条件的子串等,通过简单的方法可以实现强大的功能。
正则表达式re包
引入正则模块:import re
主要使用的方法是match(),从字符串的起始位置开始匹配正则表达式,如果匹配成功,就返回匹配成功的结果;否则就返回None;
匹配目标:可以使用括号()将想提取的子字符串括起来。()实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式依次对应每个分组,调用group方法传入分组的索引即可获取提取结果。
import re
# pattern为校验的规则
pattern = '^Hello\s(\d+)\sWorld'
# str为要进行校验的字符串
str = 'Hello 1234567 World_This is a Regex Demo'
result = re.match(pattern,str)
print(result)
# 如果result不为None, 则group()方法对result进行数据提取
print(result.group())#输出完整的匹配结果
print(result.group(1))#输出第一个被()包围的匹配结果
通用匹配 .*(贪婪):匹配任意的尽可能多的字符 .*?(非贪婪)匹配尽可能少的字符。
-
match()方法是从字符串开头的地方开始匹配,意味着一旦开头不匹配,整个匹配就失败了。
-
search方法是匹配时扫描整个字符串,然后返回第一个匹配成功的结果。也就是说正则表达式可以是字符串的一部分,在匹配时,search方法会依次以每个字符作为开头扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容
-
findall匹配的与正则表达式相匹配的全部字符串,而不是第一个,其结果返回的是列表类型
表示单字符匹配规则
字符 | 功能 |
---|---|
. | 匹配任意1个字符(除了\n和\r) |
[] | 匹配[]中列举的字符 |
\d | 匹配数字,即0-9 |
\D | 匹配非数字,即匹配不是数字的字符 |
\s | 匹配空白符,也就是空格和\tab |
\S | 匹配非空白符,\s取反 |
\w | 匹配单词字符,a~z、A~Z,0~9 |
\W | 匹配非单词字符,\w取反 |
验证手机号码是否符合规则(不考虑边界问题)
# 首先清楚手机号的规则
#1.都是数字; 2.长度为11; 3.第一位是1;4 第二位是35 678中的一位
import re
pattern = "1[35678]\d{9}"
phoneStr = "18230092223"
# 使用re.match()从头开始匹配文本,获得匹配结果,无法匹配时将返回None
result = re.match(pattern, phoneStr)
result.group()
表示数量的规则
字符 | 功能 |
---|---|
* | 匹配前一个字符出现0次、多次或者无限次,可有可无、可多可少 |
+ | 匹配前一个字符出现1次、多次或者无线次,知道出现一次 |
? | 匹配前一个字符出现1次或者0次,要么有1次,要么没有 |
{m} | 匹配前一个字符出现m次 |
{m, } | 匹配前一个字符至少出现m次 |
{m,n} | 匹配前一个字符至少出现m~n次 |
表示边界的规则
字符 | 功能 |
---|---|
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
\b | 匹配一个单词的边界 |
\B | 匹配非单词边界 |
import re
# 定义规则匹配 str ="单词 ve r"
# 1. 以字母开始,以r结尾
# 2.中间有空格符
# 3. ve 两边分别限定匹配单词边界
pattern = r"^\w+\s\bve\b\sr" # 字符串加上r表示原生字符串
str = "0o ve r"
result = re.match(pattern,str)
result.group()
匹配分组的规则
字符 | 功能 |
---|---|
| | 匹配左右任意一个表达式 |
(ab) | 将括号中字符作为一个分组 |
\num | 引用分组号num匹配到的字符 |
(?P) | 分组起别名 |
(?P = name) | 引用别名为name 分组匹配到的字符串 |
import re
# 匹配出0-100的数字
# 从左往右开始匹配
# 经过分析,可以将0 ~ 100分成三部分
# 1. 0 "0$"
# 2. 100 "100$"
# 3. 1-99 "[1-9]\d{0,1}$"
pattern = "0 $| 100 $|[1-9]\d{0,1}$"
result = re.match(pattern,"93")
result.group()
import re
#获取页面<h1>标签中的内容,爬虫的时候会用到
str = "<h1>hello world!</h1>"
pattern = r"<h1>(.*)</h1>" # 用()表示一个分组
result = re.match(pattern, str)
print(result.group()) #group()和group(0)匹配整个字符串
print(result.group(1))# group(1)匹配第一个分组()里面
网页数据的解析提取
对于网页的节点来说,可以定义id、class或其他属性,而且节点之间还有层次关系,在网页中可以通过Xpath或CSS选择器来定位一个或多个节点,然后调用相应方法获取该节点的正文内容或者属性,就可以提取我们想要的信息。
Beautiful Soup的使用
爬虫的requests和beautifulsoup4两个包:
通过requests模块可以很方便地发起HTTP请求。requests模块是第三方模块,利用requests对象的get()方法,对指定的url发起请求。
BeautifulSoup库提供了很多解析HTML的方法,可以帮助我们很方便地提取需要的内容。当成功爬取网页之后,就可以通过BeautifulSoup对象对网页内容进行解析。最开始一般写成这样的格式
res = requests.get('https://xxx.xxx.xx')
soup = BeautifulSoup(res,'html.parser')
在BeautifulSoup中,一般使用BeautifulSoup解析到的**Soup文档,可以使用find_all()、find()、select()**方法定位所需要的元素。find_all()可以获得列表、find()可以获得一条数据。select()根据选择器可以获得多条也可以获得单条数据。其中关键点在于,对于所需内容的精准定位,通过()内的语句来实现。
- 对于HTML中类的内容,可以通过class类名来进行定位
soup.select('.class')
- id在一个HTML中是唯一的,因此可以通过id名来寻唯一的内容
soup.select('#id')
应用1:爬出新浪文化网页中的栏目要闻的链接地址和标题
#了解标签信息来获取所需内容,导入相应的安装包
import requests
from bs4 import BeautifulSoup
# 获取网页数据要使用requests的url()方法
url = "http://cul.news.sina.com.cn/"
res = requests.get(url)
res.encoding = 'utf-8' # 正确的中文显示
# 检查HTTP响应状态,来确保能正常获取网页,若输出状态代码为200则为正常:
print(res.status_code)
#现在获取了网页数据,可以看看得到了什么:
# 下面命令会显示HTTP响应的全部内容,包括HTML代码和需要的文本数据信息
print(res.text)
soup = BeautifulSoup(res.text,'html.parser')
# 使用bs4对象的select()方法提取标签中的数据,此方法返回列表h4:
h4 = soup.select('.blk122')
#需要的链接和标题位于<a>标签下,需要通过循环获取所有<a>标签下的数据显示。
h5 =h4[0].select('a')
for link in h5:
print(link['href']) # 显示链接地址
print(link.text) # 显示标题

应用:爬取"豆瓣读书Top 250"第一页内容,并存入douban.csv文件中
查看网页源代码(F12 or 右键-> 检查)

代码展示
import pandas as pd
import requests
import re
from bs4 import BeautifulSoup
def getcontent(url):
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"} # 纠结半天原来是headers 写错了改为自己的浏览器的headers
data = requests.get(url, headers=headers)
print(data)
soup = BeautifulSoup(data.text, 'lxml')
div = soup.find("div", attrs={'id': 'content'})
print(soup.find("div", attrs={'id': 'content'}))
tables = div.find_all("table")
price = []
date = []
nationality = []
nation = []
bookname = []
link = []
score = []
comment = []
num = []
author = []
for table in tables:
bookname.append(table.find_all("a")[1]['title'])
link.append(table.find_all("a")[1]["href"])
score.append(table.find("span", class_="rating_nums").string)
people_info = table.find_all("span")[-2].text
people_num = re.findall(r'\d+', people_info)
num.append(people_num[0])
navistr = (table.find("p").string)
infos = str(navistr.split("/"))
infostr = str(navistr)
s = infostr.split("/")
if re.findall(r'\[', s[0]):
w = re.findall(r'\s\D+', s[0])
author.append(w[0])
else:
author.append(s[0])
price_info = re.findall(r'\d+\.\d+', infos)
price.append(price_info[0])
date.append(s[-2])
nationality_info = re.findall(r'[[](\D)[]]', infos)
nationality.append(nationality_info)
for i in nationality:
if len(i) == 1:
nation.append(i[0])
else:
nation.append("中")
dataframe = pd.DataFrame(
{'书名': bookname, '作者': author, '国籍': nation, '评分': score, '评分人数': num, '出版时间': date,
'价格': price, '链接': link})
dataframe.to_csv("douban.csv", index=False, encoding='utf-8-sig', sep=',')
if __name__ == '__main__':
url = "https://book.douban.com/top250?icn=index-book250-all"
getcontent(url)
部分解析:
可以源码看出,所有信息都在一个div中,这个div下有25个table,其中每个table都是独立的信息单元,书名可以直接在节点中的title中提取:
bookname.append(table.find_all("a")[1]['title'])
评价人数用正则表达式提取:
people_info = table.find_all("span")[-2].text
people.append(re.findall(r'\d+',people_info))
再看以下两种国籍信息:
<p class = "pl">[中] 曹雪芹 著 / 人民文学出版社/ 1996-12/59.70元</p>
<p class = "pl"> 余华 / 作家出版社/2012 -8 -1/20.00元</p>
提取两种情况下的作者信息:
s= infostr.split("/")
if re.findall(r'\[',s[0]):
w =re.findall(r'\s\D+',s[0])
author.append(w[0])
else:
author.append(s[0])
除去国籍[中]两边的中括号
nationality_info = re.findall(r'[[](\D)[]]',infos)
nationality.append(nationality_info)
#其中,有国籍的都写出了,但是没写出的发现都是中国,所以把国籍为空白的改写为”中“;
for i in nationality:
if len(i) == 1:
nation.append(i[0])
else:
nation.append("中")
结果:

Xpath的使用
利用xpath爬取”豆瓣读书Top250“的所有内容,并存入doubanl.csv文件
Xpath全称是XML Path Language,即XML路径语言。Xpath使用路径表达式来选取XML文档中的节点或者节点集。这些路径表达式和在常规的计算机文件系统中看到的表达式非常相似。
应用 :利用xpath爬取”豆瓣读书Top250“的所有内容,并存入doubanl.csv文件
首先,我们要利用浏览器插件ChroPath来获取书名、图书的评分、评论人数以及简介的xpath。以书名来说,源代码中找到书名,右击,在弹出的快捷键菜单中选择Copy->Copy Xpath选项,以书名红楼梦为例,获取书名的xpath为:`
//*[@id="content"]/div/div[1]/div/table[1]/tbody/tr/td[2]/div[1]/a
但是需要注意的是浏览器复制的xpath只能做参考,因为浏览器经常会在自己里面增加多余的标签,需要手动把这个标签删除,整理为:
//*[@id="content"]/div/div[1]/div/table[1]/tr/td[2]/div[1]/a
同样获取图书的评分、评论人数、简介,结果如下:
//*[@id="content"]/div/div[1]/div/table[1]/tr/td[2]/div[2]/span[2]
//*[@id="content"]/div/div[1]/div/table[1]/tr/td[2]/div[2]/span[3]
//*[@id="content"]/div/div[1]/div/table[1]/tr/td[2]/p[1]
一共有10页,每页25本,代码如下:
import csv
import requests
from lxml import etree
# 获取每页地址
def getUrl():
for i in range(10):
url = 'https://book.douban.com/top250?start={}'.format(i * 25)
for item in urlData(url):
write_to_file(item)
print('成功保存"豆瓣图书Top250"第{}页的数据!'.format(i + 1))
# 数据存储到CSV文件
def write_to_file(content):
# 'a'追加模式
with open('douban1.csv', 'a', encoding='utf-8', newline='') as f:
fieldnames = ['name', 'score', 'comment', 'info']
# 利用csv包中的DictWriter()函数将字典格式数据存储到CSV文件中
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writerow(content)
# 获取每页数据
def urlData(url):
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"}
html = requests.get(url, headers=headers).text
res = etree.HTML(html)
trs = res.xpath('//*[@id="content"]/div/div[1]/div/table/tr')
for tr in trs:
yield {
'name': tr.xpath('./td[2]/div/a/text()')[0].strip(),
'score': tr.xpath('./td[2]/div/span[2]/text()')[0].strip(),
'comment': tr.xpath('./td[2]/div/span[3]/text()')[0].replace('(', '').replace(')', '').strip(),
'info': tr.xpath('./td[2]/p[1]/text()')[0].strip()
}
if __name__ == '__main__':
getUrl()
