最近这段时间将开始研究python在网络爬虫上的应用

既是大创结题项目的需求,同时也是为了拓宽自己的技术视野

本文作为学习笔记,同时也是编写爬虫程序过程中的经验总结

希望本阶段学习能让我熟练掌握网络爬虫,以后能将其用作其他网络安全领域的工具延申

因为是初次入门网络爬虫,那么在编写爬虫应用程序之前,很有必要搞清楚网络爬虫的原理

网络爬虫原理

URL

URI(Uniform Resource Identifier):统一标识符

URL(Uniform Resource Locator):统一资源定位符

URI分为URL和URN或同时具备locators 和names特性的一个东西

网络世界中更常见的还是URL,也就是平时浏览器中输入的web地址

但是URL也有它的格式规范

scheme://[username:password@]hostname[:port][/path][;parameters][?query][#fragment]

这里解释几个计网课上没有学到的概念

scheme(protocol):协议,例如http、https、ftp等

username:password:当URL需要用户名和密码才能访问时,可以将他们放在hostname前

parameters:参数,有时候我们需要向解析url的应用程序提供参数才能去访问资源,因为参数可能会定义传输格式

fragment:片段,有前端知识的同学应该对这个概念不陌生。片段可以作为单页面路由或HTML锚点,当你访问#锚点名时页面能够直接滚动到特定锚点的位置

Http

在计算机网络的相关协议中,网络爬虫主要涉及到的是应用层协议,如HTTP和HTTPS

虽然我们在爬虫时更关心的是响应体中返回的页面内容,但只有正确设置请求报文,才能让目标服务器对我们做出回应

请求头中比较重要的信息有以下几种

User-Agent:该请求头能向目标服务器说明客户端使用的操作系统、浏览器版本等信息。爬虫时加速该请求头可以让我们的爬虫程序伪装成浏览器。

Referer:标识该请求从那个页面发送过来,我们可以对该信息进行伪造来欺骗目标服务器

而响应头中也有一些重要信息

Last-Modified:用于指定资源的最后修改时间

Content-Encoding:用于指定响应内容的编码

Content-Type:文档类型,指定返回的数据是什么类型。例如text/html代表返回的是HTML文档,application/x-javascript代表返回JavaScript文件,image/jpeg代表返回图片

Set-Cookie:用于告诉浏览器需要将此内容放在Cookie中,下次请求时将Cookie带上

而响应体中的源代码和JSON数据则是我们需要爬取的内容

HTML DOM树

在HTML中,所有标签定义的内容都是节点,这些节点构成了HTML DOM树

DOM(Document Object Model),即文档对象模型,定义了访问HTML和XML文档的标准

html标签为一个根节点,而整个网站文档则是一个文档节点

节点中的文本时文本节点,节点中的属性是属性节点。同时,文档中的注释也对应一个注释节点。

节点树中的所有节点均可通过HTML DOM被JS访问、修改、创建和删除

节点之间会有父子和兄弟关系

XPath

简介

XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。

XML或HTML 文档是被作为节点树来对待的

页面中的所有元素基本都是节点,当然除节点外也有基本值

基本值是无父或无子的节点,例如文本或者是属性对应的值

语法

XPath 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。

XPath的常用表达式规则如下

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

这个表示和电脑文件系统中的路径表示非常相似

当然XPath中也有谓语、通配符和运算符的存在

谓语即方括号,和数组索引非常相似

//div[1]:选取所有div元素中的第一个元素

//div[last()]:选取所有div元素中的最后一个元素元素

//title[@lang='eng']: 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性

通配符有*和node()

*node()用于匹配任何元素节点,@*用于匹配任何属性节点

较为常用的运算符便是|and

|用于合并两个节点集,而and则能用于多属性匹配

//book | //cd 返回所有拥有 book 和 cd 元素的节点集

谓语中的多个属性条件间可以使用and来连接

通过几个复杂表达式我们可以更清晰地理解XPath

实际使用

当我们想使用XPath选择页面中的某个节点时,如果页面层次结构太过复杂,往往很难定位

在实际编写爬虫时,获取XPath表达式的最佳途径是从浏览器控制台的元素界面获取

以edge浏览器为例,右键元素中某一节点可以将其xpath表达式导出

爬虫流程

一个爬虫程序的流程分为获取网页、提取信息和保存数据

获取网页

该步骤的关键是构造一个请求并发送给服务器,然后接收到响应并对其进行解析

实现方法便是python中的众多HTTP 请求库,如urlibrequests,在编写ctf的web题目脚本时就经常会用到,算得上是老熟人了🤣

提取信息

获得源代码后,就需要从中间提取我们想要的数据

我们可以通过正则表达式进行匹配,也可以利用网页节点属性、CSS选择器或者XPath来提取网页信息

涉及到的库有Beautiful Souppyquerylxml

保存数据

一般在项目中,爬虫爬取的数据会被保存到数据库中

所以我们也需要掌握一定的数据库知识

非常规HTML代码

有时候网页返回的可能是JSON字符串或者二进制数据,我们可以将其解析、转换成相应的目标文件格式

当然也可能遇到源代码非常简略,页面信息并不在HTML代码中,而是靠JavaScript渲染的情况

此时需要我们抓取JS文件,并在本地使用一些库进行模拟渲染

相关库的基础用法

requests

由于requests,是对urllib的再次封装,即urllib的升级版,所以直接学习requests即可

安装

pip3 install requests

请求方法

request库中最核心的便是请求方法

方法 描述
delete(url, args) 发送 DELETE 请求到指定 url
get(url, params, args) 发送 GET 请求到指定 url
head(url, args) 发送 HEAD 请求到指定 url
patch(url, data, args) 发送 PATCH 请求到指定 url
post(url, data, json, args) 发送 POST 请求到指定 url
put(url, data, args) 发送 PUT 请求到指定 url

而这些请求方法的返回对象即为页面的响应信息

我们可以用一个变量来接收返回对象

r = requests.get('https://rickliu.com')

而请求方法中的各种参数则可以用来完善发送的请求报文

请求数据

Get请求中传递的URL参数可以通过函数中的关键字参数params实现

下面是get请求传递数据较为标准的格式

1
2
3
4
5
data = {
'name':'rickliu',
'key':1024
}
r = requests.get('http://rickliu.com',params=data)

该方法与直接在url中添加url参数的效果相同

等价于r = requests.get('http://rickliu.com?name=rickliu&key=1024')

Post请求如果需要传递表单数据,则是通过关键字参数data

我们可以向data传入字符串、字典或者元组,分别对应不同的Post请求需求

1
2
3
4
5
6
7
8
9
10
# 网页中常见的表单可以通过传递字典给data,字典中的键值对会被自动编码成表单形式
payload = {
'name':'rickliu',
'key':1024
}
# 当表单的name属性为数组,我们可以通过将元组传递给data来向同一个键传多个值
payload = (('key',1024),('key',2048),('key',4096))
# 如果payload为字符串,则会直接传递而不会被编码
payload = 'where is flag'
r = requests.get('http://rickliu.com',data=payload)

当然,如果页面限制接收json格式的内容,特征为响应头中Content-Type:application/json,那么post传递的数据也可以传给关键词参数json,传递的数据会被自动编码

1
2
3
4
5
payload = {
'name':'rickliu',
'key':1024
}
r = requests.get('http://rickliu.com',json=payload)

实际场景中,我们选用json或者data,则需要通过对页面抓包查看Content-Type响应头的值来决定

请求头

通过给关键词参数headers传字典,我们可以定义一些请求头的值

字典中的键值对对应不同的请求头和值

用法示例如下

1
2
3
4
5
headers = {
'flag':'flag{R1ck}',
'Accept-Encoding':'gzip,deflate'
}
r = requests.get('http://rickliu.com',headers=headers)

超时时间

超时时间是用来防止服务器响应太慢而等待太长时间

毕竟爬虫的吞吐量还是很大的,如果等待太久会增加很大的时间开销

关键词参数timeout即为超时时间设置

我们可以给请求的连接和读取阶段3分别设置超时时间,或者直接设定总的超时时间

1
2
3
4
# 设置总的超时时间
r = requests.get('http://rickliu.com',timeout=0.1)
# 连接和读取分别设置为1秒和5秒
r = requests.get('http://rickliu.com',timeout=(1,5)))

返回对象

请求方法的返回为一个Response对象

该对象的属性包含许多响应体的信息

下面是一些常用属性

status_code:响应状态码

encoding:响应头部字符编码

headers:响应头

cookies:cookies信息

text:响应体内容(str类型)

history:请求历史

lxml

安装

pip3 install lxml

etree

而在进行解析前,可以先使用etree.HTML对HTML文本进行初始化

在初始化过程中,会自动对HTML文本修正,例如补全标签的闭合,并自动添加body、html节点

lxml一般通过XPath来对页面中的元素进行选择

调用xpath方法即可进行数据提取,函数中的参数即为XPath表达式

例如我想获取我的博客下头像的图床链接,层次结构如下

我们可以用如下爬虫程序

1
2
3
4
5
6
7
import requests
from lxml import etree

r = requests.get('https://rickliu.com')
html = etree.HTML(r.text)
result = html.xpath('//*[@id="sidebar-menus"]/div[1]/img/@src')
print(result[0])

Beautiful Soup

安装

Beautiful在解析时依赖解析器,它除了支持Python标准库中的HTML解析器外,还支持一些第三方库(比如lxml)

所以在安装时需要一并将解析器安装

1
2
pip install bs4
pip install lxml

解析器

以下为Beautiful Soup 支持的部分解析器

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, ‘html.parser’) python内置的标准库,执行速度适中 Python3.2.2之前的版本容错能力差
lxml HTML解析器 BeautifulSoup(markup, ‘lxml’) 速度快、文档容错能力强 需要安装C语言库
lxml XML解析器 BeautifulSoup(markup ‘xml’) 速度快,唯一支持XML的解析器 需要安装C语言库
html5lib BeautifulSoup(markup, ‘html5lib’) 最好的容错性、以浏览器的方式解析文档、生成HTML5格式的文档 速度慢,不依赖外部拓展

在初始化的BeautifulSoup时,将第二个参数设为需要的解析器即可

1
2
3
from bs4 import BeautifulSoup

soup = BeautifulSoup('<p>Hello world</p>', 'lxml')

节点选择器

beautifulsoup库比较有特定的节点选择器有遍历文档树搜索文档树,当然也可以使用非常贴合前端开发逻辑的CSS选择器,或者使用lxml解析器的Xpath

遍历文档树

遍历文档树即使用父子、兄弟等节点间的关联关系来选择特定节点

直接子节点.contents属性或者.children属性

contents返回的是列表对象,而children返回的则是列表迭代器

子孙节点.descendants属性,返回生成器对象

.descendants可以通过递归循环返回所有子孙节点

生成器(generator)

在 Python 中,使用了 yield 的函数被称为生成器(generator)

yield 是一个关键字,用于定义生成器函数,生成器函数是一种特殊的函数,可以在迭代过程中逐步产生值,而不是一次性返回所有结果。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

当在生成器函数中使用 yield 语句时,函数的执行将会暂停,并将 yield 后面的表达式作为当前迭代的值返回。

然后,每次调用生成器的 next() 方法或使用 for 循环进行迭代时,函数会从上次暂停的地方继续执行,直到再次遇到 yield 语句。这样,生成器函数可以逐步产生值,而不需要一次性计算并返回所有结果。

调用一个生成器函数,返回的是一个迭代器对象。

下面是一个简单的示例,展示了生成器函数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def countdown(n):
while n > 0:
yield n
n -= 1

# 创建生成器对象
generator = countdown(5)

# 通过迭代生成器获取值
print(next(generator)) # 输出: 5
print(next(generator)) # 输出: 4
print(next(generator)) # 输出: 3

# 使用 for 循环迭代生成器
for value in generator:
print(value) # 输出: 2 1

直接父节点.parent属性

父辈节点.parents属性

.parents 属性可以递归得到元素的所有父辈节点

兄弟节点.next_sibling后一个兄弟节点,.previous_sibling前一个兄弟节点;.next_siblings后面所有兄弟节点,.previous_siblings前面所有兄弟节点

搜索文档树

搜索文档数通过name、正则表达式、属性和条件方法等筛选节点

搜索文档数的核心为find_all()find()方法

筛选条件作为参数传给这两个方法,而返回值即为筛选到的节点

其中find_all返回所有满足筛选条件的节点,而find则是返回第一个匹配结果

当然,搜索文档树也融合了节点关联的思想

使用find_parents()find_parent()可以直接选择筛选结果的父亲节点

使用find_next_siblings()find_next_sibling()可以直接选择筛选结果后面的兄弟节点

使用find_previous_siblings()find_previous_sibling()可以直接选择筛选结果前面的兄弟节点

name筛选

name筛选即通过给find_all()中的第一个参数name传参字符串或字符串列表,可以选中标签名与相应字符串完整匹配的标签

例如find_all('h')会选中<h>标签但不会选中<html>

find_all(["a","b"])会选中<a><b>标签

正则表达式

根据参数中传进的正则表达式来匹配标签名

1
2
3
4
5
import re
for tag in soup.find_all(re.compile("^b")):
print(tag.name)
# body
# b

例如上面的^b会匹配所有b开头的标签,所以返回了<body><b>

属性筛选

如果想要通过筛选属性值来搜索,可以通过方法中的参数attrs,也可以通过传非内置参数

  1. 如果传给find_all()的参数不是搜索内置的参数名,搜索时会把该参数当作标签的属性来搜索

    例如给其传参idsoup.find_all(id='R1ck'),则会搜索所有id为R1ck的标签

    给参数名传的值可以是字符串、正则表达式、列表、True

    给参数传True会匹配所有包含该属性的标签,无论属性值是什么。

  2. 给attrs传字典,也可以实现属性值筛选

    字典中的键为属性,值为属性值

条件方法筛选

条件方法筛选,即给find_all()的name参数或属性参数传入实现了特定条件的方法名

该方法只能接收一个参数,如果该方法传给name,则接收参数为标签节点,如果传给属性,则接收参数为属性

返回值必须为布尔变量

方法中可以实现复杂的筛选

例如下面的例子,能够筛选出所有包含class属性却不包含id属性的标签

1
2
3
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
soup.find_all(has_class_but_no_id)

CSS选择器

.select()方法传入CSS选择器语法的字符串即可使用CSS选择器

具体的CSS选择器介绍可以参考以下文档

CSS 选择器 - CSS:层叠样式表 | MDN (mozilla.org)

CSS 元素选择器 (w3school.com.cn)

提取节点信息

.string可以获取节点包含的文本,即两标签之间的文本

.attrs会返回包含所有属性及属性值的字典,如果属性有多值,则会在值的位置返回列表

.name可以获取节点的名称

selenium

在复杂登录场景下,或者是严格反爬机制下,可以使用selenium库,但是爬取速度较慢

安装

pip install selenium

浏览器驱动

由于需要模拟浏览器进行相关操作,所以需要下载对应浏览器版本的驱动

这里以Chrome为例

在本地Chrome浏览器的设置>关于中找到浏览器版本

在下面的驱动下载网站中找到对应版本的驱动

https://googlechromelabs.github.io/chrome-for-testing/#stable

一般保证版本号前三个数字相同就行