web请求过程剖析

服务器渲染

服务器直接整合数据和html文件,并返回

客户端渲染

服务器先返回一个html骨架,之后请求再次返回需要的数据,在浏览器客户端整合成页面(页面源代码没有数据)。

通过Network实时查看服务器对请求的返回,可以抓包到数据。

HTTP协议

HTTP协议,Hyper Text Transfer Protocol(超文本传输协议),服务器和浏览器之间的数据交互遵守的就是HTTP协议。

请求:

1
2
3
请求行 -> 请求方式(get/post)  请求URL地址   协议
请求头 -> 服务器要使用的附加信息
请求体 -> 一般是请求参数(例如搜索框输入的东西)

响应:

1
2
3
状态行 -> 协议 状态码(404等)
响应头 -> 放一些客户端要使用的附加信息(例如安全密钥,加密数据等)
响应体 -> 服务器返回的真正的客户端要使用的内容(HTML, json等)

请求头中的常见重要内容:

1、User-Agent:请求载体的身份标识(用什么发送的请求)

2、Referer:防盗链( 这次请求是从哪个页面来的?反爬使用)

3、cookie:本地字符串数据信息(用户登录信息,反爬的token)

响应头的重要内容:

1、cookie:本地字符串数据信息(用户登录信息,反爬的token)

2、token字样的各种反爬和反攻击的字符串

请求方式:

1、GET:显示提交

2、POST:隐式提交

Requests

用requests发送请求,用GET的方式发送请求。

1
2
import requests
resp = requests.get(Url)

关于User-Agent的反爬:

有时候网页服务器会检测你的身份标识(User-Agent),来判断你是正常浏览器访问还是通过爬虫手段。可以先正常访问一下浏览器,然后复制正常访问时的身份标识,创建一个字典来伪造身份

1
2
3
headers = {
"User-Agent": "your message"
}

这个信息可以通过,检查 -> 网络 -> 查看发送成功的请求,然后查看他的请求头,里面有你的User-Agent信息。

1
resp = requests.get(Url, headers = headers)

获取后直接进行替换headers。

指定字符集

request模块默认的编码字符集是UTF-8,对于有些用GBK编码的网站,爬出数据后可能会是乱码,我们可以更改指定的编码字符集:

1
2
#resp = requests.get(URL)
resp.encoding = "GBK"

案例:百度搜索框搜索

通过百度等搜索引擎搜索,发送的请求基本都是GET(查看页面请求头可知),那只要在程序中发送GET请求就好了。实现过于简单不赘叙。

案例:百度翻译单词

通过请求调试发现,百度翻译的数据结果来源于sug页面,在Payload的data项下可以看到请求的内容,比如我输入一个dog,data下就会出现kw : dog,这个kw就是keyword。

所以通过修改sug的kw,然后来进行翻译内容爬取:

1
2
3
4
5
6
7
url = "https://fanyi.baidu.com/sug"
data = {
"kw" : "dog"
}
#发送post请求,发送的数据必须存放在字典里,通过data参数进行传递
resp = requests.post(url, data = data)
print(resp.json()) #将服务器放回的内容直接处理成json

案例:豆瓣电影排行榜

电影排行榜的渲染,属于上述的客户端渲染,html框架和数据是分开响应的(框架一般不变而排行榜内容可能会变),此时我们需要的电影数据就不是从网页源代码中寻找。

通过抓包,筛选出XHR格式的响应,里面存储着用于和html框架整合的数据。

一个URL中,问号分割网址和参数,问号后面的是参数类型及其数值。在写程序时,对于较长的URL,可以通过重新封装参数来分离参数和网址地址。

(这里放一张图片)

1
2
3
4
5
6
7
URL = "your website"
#重新封装参数
param = {
#参数
}
#独属于get请求的,发送参数的选项是params,不同于POST请求的data
resp = requests.get(url = url, params = param)

补充:记得在完成请求和响应后,在程序的最后,关掉responds,防止过多的请求挂载着,使得访问服务器堵塞:

1
resp.close()

正则匹配Re模块

正则库re

运用python自带的正则库

1
import re

设置一个匹配对象,预加载正则表达式

1
member = re.compile(" ")

在单引号内写正则表达式。

还可以传入第二个参数,用来指定额外的条件。

1
2
3
member = re.compile(r'', re.I)  #忽略大小写
member = re.compile('.*?', re.DOTALL) #句点匹配符匹配换行符,或者用re.S
member = re.compile('.*?', re.VERBOSE) #忽略正则表达式字符串中的空白符和注释

有关正则的python方法

search()

找到第一个匹配的字符串并返回一个Match对象

1
2
3
4
member = re.compile(r'123')
mo = member.search('123456')
print(mo.group())
#123

match

从头开始匹配,类似于正则表达式开头的^。

findall()

找到所有匹配的字符串并返回一个列表

1
2
3
4
member = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
member.findall('Cell: 415-555-9999 Work: 212-555-0000')

#['415-555-9999', '212-555-0000']

finditer

匹配字符串中所有内容,返回一个迭代器(Match对象组),用for循环一个一个提出出来后.group( )

1
2
3
it = member.finditer(r" ")
for i in it:
print(i.group())

sub()

找到匹配的字符串并用第一个参数替换

1
2
3
4
name = re.compile('Agent\s\w+')
mo = name.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
print(mo)
#CENSORED gave the secret documents to CENSORED.

从正则匹配出的字符串中提取需要内容:

group()groups()

当使用括号分隔正则表达式时,group()可以指定返回第几部分,groups()返回一个元组

1
2
3
4
5
6
phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')
mo = phoneNumRegex.search('My number is 415-555-4242.')
mo.group(1) #'415'
mo.group(2) #'555-4242'
mo.group(0) #'415-555-4242'
mo.group() #'415-555-4242'

或者,我们可以给每一部分的匹配内容命名,设定一个组别,然后把组名传入给.group("name")来提取数据。

1
2
#格式:    (?P<组名>正则式子)
#打印: print(mo.group("组名"))

举个例子:

1
2
3
4
5
6
7
8
9
string = "<div class='西游记'><span id='10010'>中国联通</span></div>"

obj= re.compile(r"<span id='(?P<id>\d+)'>(?P<name>\w+)</span>", re.S)

result = obj.search(s)

print(result.group()) #结果:<span id='10010'>中国联通</span>
print(result.group("id")) # 结果:10010 # 获取id组的内容
print(result.group('name")) # 结果:中国联通 # 获取name组的内容

python读写JSON数据

读(load):

1
data = json.load(load_f)

写(dump):

1
json.dump(data, dump_f)

总结如图:

方法 作用
json.dumps() 将python对象编码成json字符串
json.loads() json字符串解码成python对象
json.dump() 将python中的对象转化成json储存到文件中
json.load() 将文件中的json的格式转化成python对象提取出来

提取HTML内嵌的子页面(超链接)

当我们用正则爬取内嵌在html代码里的数据时,有时候要提取出子页面的URL,这些URL一般被放在超链接中,和一串文字相呼应,在html中超链接的格式是这样的:

1
<a href = 'URL' title = "浮动标题">一串文字</a>

用a标签来表示超链接。

bs4解析

html标记语言

html作为一种超文本标记语言,大体上有以下两种标签格式:

两端闭合

1
<标签 属性 = ”属性值“>被标记的内容</标签>

自带闭合

1
<标签 />

用Beautiful Soup解析数据

安装bs4模块

1
pip install bs4

把页面源代码交给Beautiful Soup进行处理,生成bs对象

1
page = BeautifulSoup(resp.text, "html.parser") #指定为html解析器

从bs对象中查找数据

find(标签, 属性 = 值)

find_all(标签, 属性 = 值)

关键字冲突问题:

当html的属性和python语法的关键字冲突时,可能会出现语法错误,例如:

1
2
3
4
5
table = page.find("table", class = "hq_table") #calss是python的关键字
#上述代码必定报错,class冲突了,可以用下划线解决歧义
table = page.find("table", class_ = "hq_table")
#或者用字典的方式传入属性-值对
table = page.find("table", attrs = {"class" : "hq_table"})

通过find和find_all函数,我们可以嵌套寻找标签内的内容,比如先找到A标签的内容,然后再在A标签里面找到B标签的内容,最后通过.text函数拿到被标签标记的内容

当我们想要A标签里的属性值时,我们可以用get函数获取:

1
member = A.get('属性')

X path解析

安装模块和基础解析

x path是在XML文档中搜索内容的一门语言,兼容html(html是xml的一个子集)

1
pip install lxml

使用x path模块:

1
2
3
4
5
6
7
8
9
form xpath import etree

xml = """...""" #内容

tree = etree.XML(xml) #生成一个对象
tree = etree.parse("A.html") #解析文件并生成一个对象
tree = etree.HTML(resp.text) #解析HTML源码并生成一个对象

result = tree.xpath() #使用xpath功能

在x path解析里,用层级嵌套来寻找内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml = """
<book>
<name>名字</name>
<nick>东西1</nick>
<author>
<nick, id = "1">周</nick>
<nick, id = "2">陶</nick>
<nick, id = "3">王</nick>
<div>
<nick, id = "4">林</nick>
</div>
<span>
<nick, id = "5">刘</nick>
</span>
author>
"""
result = tree.xpath("/book") #/表示层级关系,第一个/是根节点
#一个迭代器指向book
result = tree.xpath("/book/name/text()") #text()拿文本
#['名字']
result = tree.xpath("/book//nick/text()") #//表示后代,之间有任意路径
#['东西1','周','陶','王','林','刘']
result = tree.xpath("/book/author/*/nick/text()") # *是通配符,代表任意节点
#['林','刘']

由上述例子:

text(),用该函数提取文本

/代表层级关系

*代表通配符,能代表任意节点

//代表任意后代,即任意层节点

节点索引

1
2
3
4
5
6
7
8
<body>
<li><a href = " ">one</a></li>
<li><a href = " ">two</a></li>
<li><a href = " ">three</a></li>
<li><a href = "one"></a></li>
<li><a href = "two"></a></li>
<li><a href = "three"></a></li>
</body>

对于上述的html,当我们想提取其中<li>标签下的内容时,会一次性提取三个,我们可以用索引来只提取某个。注意:索引从1开始

1
2
3
tree = etree.parse("A.html")
result = tree.xpath("/body/li[1]/a/text()")
#['one']

而当我们想提取某个固定属性值的节点的内容,比如上述例子中的href为one的节点时:

1
2
result = tree.xpath("/body/li[@href = "one"]/a/text()")
#['小']

用符号@加上属性 = 属性值,来索引特定的内容。

@符号用来代表属性,使用@属性来拿到属性值

.句点号在x path解析里代表当前节点,在相对查找时,一般用./加相对路径。

由此,通过循环提取出href内容:

1
2
3
4
5
6
li_list = tree.xpath("/body/li") #先查找到所用li节点
for li in li_list:
print(li.xpath("./a/@href")) #通过相对查找提取出a中的href
#['one']
#['two']
#['three']

注:通过检查元素能快速复制x path,右键 -> copy -> copy x path

requests进阶

处理cookie 登录网站

有些网址的数据必须要在登录后才会出现马,想要爬取这些数据,我们就要模拟登录的过程。

登录的过程实际上是从服务器得到一串cookie,之后客户端带着cookie去请求服务器时,服务器就会发送对应cookie的数据到客户端,简化后就是:

1、登录 -> 得到cookie

2、带着cookie -> 请求内容

使用session进行请求,使得第一个请求到第二个请求之间的cookie不会丢失。

1
2
3
4
5
6
7
8
9
#生成一个会话对象
session = requests.session()
#登录
url_login = "//login"
url_for_data = " "
data = {用户名和密码}
session.post(url, data = data)
resp = session.get(url_for_data) #两个操作在同一个会话对象上进行!!!

除了会话,在network里直接抓包请求头里的cookie,然后用字典的方式传入headers,同样实现了一样的效果,实际上会话操作也是带着有cookie的请求头去请求到data的。

防盗链Referer

防盗链:回溯本次请求的上一级请求是什么

防盗链是为了防止URL的不正常访问顺序存在的,例如URL2中存在防盗链URL1,那么当我们访问URL2时,就会对它进行一个溯源,若发现URL2的访问不源自于URL1,那么就会报错。

应对这个问题,简单的只需要将Referer的信息传入headers里就好了。

代理

代理:通过第三方的IP去发送请求

提高爬虫效率

多线程

第一种写法:

导入多线程的线程类:

1
form threading import Thread

之后就可以开始编写自己的线程子类了。

1
2
3
4
5
6
7
8
9
10
11
from threading import Thread

class Mythread(Thread):
def run(self):
for i in range(100):
print("hello!")
if __name__ == '__main__':
t = Mythread()
t.start()
for i in range(100):
print("world!")

继承线程类后,改写run函数,将里面改写成要执行的内容。然后start函数提示线程开始工作(只是提醒可以开始工作了,具体工作时间由CPU决定)

在main函数创建一个线程对象,然后和main函数里的print一起工作:

1
2
3
4
5
6
7
8
world!
world!hello!
hello!
world!
hello!

world!hello!
hello!

就会出现这种异步的打印结果,说明多线程成功实现了。

第二种写法:

1
2
3
def func():
...
t = Thread(target = func) #创建线程并给线程安排任务

多进程

1
from multiprocessing import process

python里的写法和多线程基本一致。

线程池(和进程池)

线程池:一次性开辟一些线程,直接给线程池提交任务,线程任务的调度交给线程池来完成。

1
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

开辟一个线程池:

1
2
3
4
5
6
7
8
9
def func(name):
for i in range(100):
print(name)

if __name__ == '__main__':
with ThreadPoolExecutor(50) as t:
for i in range(100):
t.submit(func, name = f"线程{i}")
print("Over!!")

这里创建了一个50个线程的线程池,提交func任务,由线程池来进行调度,部分结果如下:

1
2
3
4
5
6
7
8
9
线程98
线程97线程94
线程90

线程98
线程97线程94


线程94

协程

协程:当程序遇见了IO操作之类的阻塞时,可以选择性的切换到其他任务上。

在单线程的情况下,实现多任务异步操作,进行任务之间的切换实现CPU的无缝工作。

python实现多任务异步

带有async关键字的函数是一个协程函数,协程对象可以实现多任务异步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import asyncio
import asyncio
import time

async def fun1():
print("hello")
await asyncio.sleep(2)
print("hello")

async def fun2():
print("world")
await asyncio.sleep(3)
print("world")

async def fun3():
print("!!")
await asyncio.sleep(4)
print("!!")


async def main():
tasks = [fun1(), fun2(), fun3()]
await asyncio.wait(tasks)

if __name__ == '__main__':
t1 = time.time()
asyncio.run(main())
t2 = time.time()
print(t2 - t1)

#输出结果大约为4秒钟,在异步条件下由最长的睡眠4秒决定程序运行时间,说明程序是并发运行。

asyncio.run():运行协程函数。(一般用来运行最高层级的入口点 “main()” 函数)

await:用于挂起阻塞的异步调用接口,一般放在协程对象前面

asyncio.wait():并发地运行传入的可迭代对象,比如我们传入一个协程函数列表

新版python异步协程方法wait的改动

python3.8后,协程对象要手动包装成task对象,方法如下

1
2
3
4
5
tasks = [
asyncio.create_task(fun1()),
asyncio.create_task(fun2()),
asyncio.create_task(fun3())
]

在上述例子中,要将协程对象fun1封装,然后再放到task列表里。

aiohttp模块实现异步请求

在之前的程序中,request.get()是同步操作的请求,要实现异步操作的请求,就要用到aiohttp模块。

创建一个aiohttp.ClientSession()对象,相当于requests模块的requests对象。

1
2
3
4
5
6
7
async def aiodownload(url):
name = "yours"
async with aiohttp.ClientSession() as session: #创建一个异步对象session
async with session.get(url) as resp: #相当于resp = session.get(url)
#等待请求后,写入到一个文件
with open(name, mode = "wb") as f:
f.write(await resp.content.read()) #读取内容是异步的,需要await挂起

这个是一个大致的模板,然后将这个异步函数传入URL放到tasks列表里,用asyncio.wait()启动,再用asyncio.run()运行就大功告成了。

Selenium

selenium是一款自动化测试工具,可以打开浏览器然后像人一样去操作浏览器,程序员因此可以直接从selenium中直接提取网页数据。

1
pip install selenium

然后需要安装浏览器驱动,我的浏览器用的是Edge,所以直接去Edge官网下载浏览器驱动。

下载地址

下载完后,将压缩包解压到python解释器所在的文件夹。

1
2
3
4
5
6
7
8
from selenium.webdriver import Edge
import time

web = Edge()

web.get("http://www.baidu.com")
time.sleep(10)

用这个代码测试一下,如果能正常打开浏览器然后进入百度就说明可以使用了,不过这时候窗口顶部,会出现一行提示受自动化工具控制的字样,这个字样会影响我们爬取资源,之后再解决。

xpath提取数据

selenium工具一般使用xpath来寻找各种元素:

1
2
web = Edge()
web.find_element_by_xpath(" ")

导入键盘模块可以模拟键盘输入:

1
2
3
from selenium.webdriver.common.keys import Keys
web.find_element_by_xpath("搜索框的xpath").send_keys("input", Keys.ENTER)
#输入后回车

窗口切换

1
2
3
4
5
6
7
#selenium中不会主动切换窗口,需要我们手动切换
#这里代表切换到窗口排列顺序中最后一个窗口的位置
web.switch_to.window(web.windows_handles[-1])
#在新窗口中提取内容
content = web.find_element_by_xpath("").text
#关闭窗口
web.close()

下拉列表select

1
2
3
4
5
6
7
8
9
10
from selenium.webdriver.support.select import Select
#定位到下拉列表
sel_el = web.find_element_by_xpath("")
#对元素进行包装,包装成下拉菜单
sel = Select(sel_el)
#让浏览器进行调整选项
for i in range(len(sel.options)):
#按照索引进行切换
sel.select_by_index (i)

除了根据索引切换,还能根据value值和文本text进行切换,如select_by_visible_text()select_by_value

配置无头不显示浏览器

我们正常在使用selenium的时候会打开一个浏览器窗口,配置一下浏览器能够使窗口不显示。

1
2
3
4
5
from selenium.webdriver.edge.options import Options
opt = Options()
opt.add_argument("--headless")
opt.add_argument("--disable")
web = Edge(options = opt)

拿到页面Elements

Elements是经过数据加载以及js等执行后,产生的html内容(大部分时不是页面源代码)

1
print(web.page_source)

验证码