爬虫与网络编程基础-Task02:HTTP 协议与 Requests
0x00 Abstract
Requests 是一个 Python 公认的,优秀的第三方网络爬虫库,可以自动爬取 HTML 页面,自动进行网络请求的提交。所以本次任务首先来了解,学习 Requests 库以及 HTTP 协议。另外,由于Requests 库与 HTTP 协议的方法是一一对应的,所以会穿插着学习,以便更容易理解。
0x01 初探 Requests 库
Requests: 让 HTTP 服务人类 — Requests 2.18.1 文档 requests · PyPI
1. 安装
安装 requests
。
pip install requests
2. Requests 库的七个主要方法
方法 | 说明 |
---|---|
requests.request() | 构造一个请求,支持以下各方法的基础方法 |
requests.get() | 获取 HTML 网页的主要方法,对应于 HTTP 的 GET |
requests.head() | 获取 HTML 网页头信息的方法,对应于 HTTP 的 HEAD |
requests.post() | 向 HTML 网页提交 POST 请求的方法,对应于 HTTP 的 POST |
requests.put() | 向 HTML 网页提交 PUT 请求的方法,对应于 HTTP 的 PUT |
requests.patch() | 向 HTML 网页提交局部修改请求,对应于 HTTP 的 PATCH |
requests.delete() | 向 HTML 页面提交删除请求,对应于 HTTP 的 DELETE |
先来动动手:
import requests
r = requests.get("http://www.baidu.com") # GET
r.status_code # 检测请求状态码
200
r.encoding = 'utf-8' # 修改内容编码
r.text[:500] # 查看响应内容的字符串
'<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_'
下面仅先介绍最常使用的 requests.get()
方法,其余的方法待学习了 HTTP 协议之后,再对照着理解。
requests.get() 方法
具体参数:
r = requests.get(url, params, **kwargs)
url
:拟获取页面的 URL 链接。params
:URL 中的额外参数,字典或者字节流格式,可选。**kwargs
: 12 个控制访问的参数
通过 requests.get()
方法返回得到的 Response 对象具有以下属性:
属性 | 说明 |
---|---|
r.status_code | HTTP 请求的返回状态,若为 200 则表示请求成功 |
r.text | HTTP 响应内容的字符串形式,即,url 对应的页面内容 |
r.encoding | 从 HTTP header 中猜测的相应内容编码方式 |
r.apparent_encoding | 从内容中分析出的响应内容编码方式(备选编码方式) |
r.content | HTTP 响应内容的二进制形式 |
这五个属性是最常使用的五个属性。 r.status_code
获得的状态码,只有返回 200 表示请求成功,其他则为失败,如 404。
使用 requests.get()
方法获取信息的基本流程:
Case 通过 get 获取网页信息
import requests
r = requests.get("http://www.baidu.com")
r.status_code # 返回 200,确认响应成功
r.text[-300:] # 查看响应内容
'out Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使ç\x94¨ç\x99¾åº¦å\x89\x8då¿\x85读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>æ\x84\x8fè§\x81å\x8f\x8dé¦\x88</a> 京ICPè¯\x81030173å\x8f· <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n'
可以发现,返回的内容中有很多乱码。那么来看一下内容的编码方式。
r.encoding # 从 header 猜测编码
'ISO-8859-1'
r.apparent_encoding # 备选编码
'utf-8'
r.encoding = 'utf-8' # 使用备选编码替换当前编码方式
r.text[-300:] # 再次查看响应内容
' href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京 ICP 证 030173 号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n'
经过替换编码方式,原来的一些乱码变成了中文。变得具有可读性了。为什么会这样?那就需要理解 Response 的编码。
属性 | 说明 |
---|---|
r.encoding | 从 HTTP header 中猜测的相应内容编码方式 |
r.apparent_encoding | 从内容中分析出的响应内容编码方式(备选编码方式) |
r.encoding
:如果 header 中不存在 charset 字段,则默认编码为 ISO-8859-1。 就是当我们刚刚访问百度的时候,默认的编码。但这个编码并不能解析中文,所以查看内容时,会出现乱码。
r.apparent_encoding
:根据网页内容分析出的编码方式。 为了避免上述情况,Requests
库还提供了另一种备选编码方案,从 HTTP 的内容部分(而不是头部份),去分析内容可能的编码形式。严格来说 r.apparent_encoding
比 r.encoding
更加准确,因为前者是实实在在去分析内容,找到可能的编码;而后者只是从 header 的相关字段中提取编码。所以当后者不能正确解码时,需要使用前者——备用编码,来解析返回的信息。这也是为什么示例中替换编码方式后,即可正确解析出中文的原因。
3. 爬取网页的通用代码框架
注意 Requests
库有时会产生异常,比如网络连接错误、HTTP
错误异常、重定向异常、请求 URL
超时异常等等。所以我们需要判断 r.status_codes
是否为 200,在这里我们怎么样去捕捉异常呢?
这里我们可以利用 r.raise_for_status()
语句去捕捉异常,该语句在方法内部判断 r.status_code
是否等于 200,如果不等于,则抛出异常。
于是在这里我们有一个爬取网页的通用代码框架:
try:
r = requests.get(url, timeout=30) # 请求超时时间为 30 秒
r.raise_for_status() # 如果状态不是 200,则引发 HTTPError 异常
r.encoding = r.apparent_encoding # 配置编码
return r.text
except:
return "产生异常"
可见当 url
链接没有协议,产生错误时,抛出了异常。
通过这种框架,可以使我们的爬虫方法更加可靠,稳定。
0x02 学习 HTTP 协议
为了更好的理解 Requests 库中的这些方法,我们需要学习并了解 HTTP 协议。
HTTP 协议简介
HTTP,HyperText Transfer Protocol,超文本传输协议。
HTTP 是万维网的数据通信的基础。
HTTP 是一种基于“请求与响应”模式的,无状态的应用层协议。
- “请求与响应”:用户发起请求,服务器做相关响应。
- 无状态:第一次请求与第二次请求之间,没有关联。并且对于发送过的请求或响应都不进行保存。
- 应用层协议:该协议基于 TCP 协议。
HTTP 工作原理
HTTP 协议定义 Web 客户端如何从 Web 服务器请求 Web 页面,以及服务器如何把 Web 页面传送给客户端。HTTP 协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
- 客户端连接到 Web 服务器。
- 发送 HTTP 请求。
- 服务器接受请求并返回 HTTP 响应。
- 释放连接 TCP 连接。
- 客户端浏览器解析 HTML 内容。
在浏览器地址栏键入 URL,按下回车之后会经历以下流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址。
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接。
- 浏览器发出读取文件 (URL 中域名后面部分对应的文件) 的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器。
- 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器。
- 释放 TCP 连接。
- 浏览器将该 HTML 文本并显示内容。
URL
URL,Universal Resource Locator,统一资源定位符。
HTTP 协议采用 URL 作为定位网络资源的标识。
URL 遵守一种标准的语法,它由协议、域名、端口、路径名称、查询字符串、以及锚部分这六个部分构成,其中端口可以省略,查询字符串和锚部分为参数。具体语法规则如下:
protocol://host: port/pathname?query#fragment
在上述语法规则中,protocol
表示协议,host
表示主机名(域名或 IP 地址),port
表示端口,pathname
表示路径名称,query
表示查询字符串,fragment
表示锚部分。#
代表网页中的一个位置,作为页面 定位符
出现在 URL 中。其右面的字符,就是该位置的 标识符
(锚部分)。
统一资源定位符 将从因特网获取信息的几种基本元素包含在一个简单的地址中:
- 传输协议 - protocol
- 如 http/https/ftp 等。
- 服务器 - host
- 通常为域名:因为 IP 地址不好记,因此用域名美化,可以理解为 IP 的别名。使用域名 -> 通过 DNS 解析找到对应的 IP。
- 有时为 IP 地址:主机地址,每台电脑都有自己特定的标识,IPv4/IPv6。
- 端口号 - port (可省略):可以理解为窗口,交流的端口
- http:默认打开 80 端口
- https:默认打开 443 端口
- 路径 - path:访问主机分享文件的地址
- 以
/
字符区别路径中的每一个目录名称 - 文件路径:用户直接访问主机分享的文件
- 路由:目前比较流行的形式(后端监听地址访问事件,返回特定的内容)
- 以
- 查询参数 - query:帮助用户访问到特定的资源
- 格式
?name=value&name=value...
- GET 模式的表单参数,以
?
字符为起点,每个参数以&
隔开,再以=
分开参数名称与资料,通常以 UTF8 的 URL 编码,避开字符冲突的问题。
- 格式
- 锚点 - fragment:
#fragment
锚点,又叫命名锚记,是文档中的一种标记,网页设计者可以用它和 URL“锚”在一起,其作用像一个迅速定位器一样,可快速将访问者带到指定位置。- HTTP 请求不包括
#
:#
是用来指导浏览器动作的,对服务器端完全无用。
典型的统一资源定位符看上去是这样的:
http://zh.wikipedia.org:80/w/index.php?title=Special:%E9%9A%8F%E6%9C%BA%E9%A1%B5%E9%9D%A2&printable=yes
其中:
http
是协议;zh.wikipedia.org
是服务器;80
是服务器上的网络端口号;/w/index.php
是路径;?title=Special:%E9%9A%8F%E6%9C%BA%E9%A1%B5%E9%9D%A2&printable=yes
是查询参数。
大多数网页浏览器不要求用户输入网页中 http://
的部分,因为绝大多数网页内容是超文本传输协议文件。同样,80
是超文本传输协议文件的常用端口号,因此一般也不必写明。一般来说用户只要键入统一资源定位符的一部分就可以了。这只是浏览器中可以省略,写爬虫的时候,URL 中的传输协议是不可以省略的。
总结:URL 是通过 HTTP 协议存取资源的 Internet 路径,一个 URL 对应一个数据资源。
HTTP 协议对资源的操作
方法 | 说明 |
---|---|
GET | 请求获取 URL 位置的资源 |
HEAD | 请求获取 URL 位置资源的响应消息报告,即获取该资源的头部信息 |
POST | 请求向 URL 位置的资源后附加新的数据 |
PUT | 请求向 URL 位置存储一个资源,覆盖原 URL 位置的资源 |
PATCH | 请求局部更新 URL 位置的资源,即改变该处资源的部分内容 |
DELETE | 请求删除 URL 位置存储的资源 |
GET 与 POST 的区别:
- 原理区别:GET 请求获取资源,不会产生动作;POST 可能会修改服务器上的资源。因此 POST 用于修改和写入数据,GET 一般用于搜索排序和筛选之类的操作,目的是资源的获取,读取数据。
- 参数位置区别:GET 把参数包含在 URL 中,POST 通过 Request body 传递参数。所以 POST 可以发送的数据更大,GET 有 URL 长度限制;POST 能发送更多的数据类型(GET 只能发送 ASCII 字符)。
- POST 更加安全:不会作为 URL 的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中。
- POST 比 GET 慢:POST 在真正接收数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据。
- 浏览器请求 TCP 连接(第一次握手)
- 服务器答应进行 TCP 连接(第二次握手)
- 浏览器确认,并发送 POST 请求头(第三次握手,这个报文比较小,所以 HTTP 会在此时进行第一次数据发送)
- 服务器返回 100 Continue 响应
- 浏览器发送数据
- 服务器返回 200 OK 响应
PATCH 与 PUT 的区别: 假设 URL 位置有一组数据 UserInfo,包括 UserID,UserName 等 20 个字段。 需求:修改 UserName,其他不变。
- 采用 PATCH,仅向 URL 提交 UserName 的局部更新请求。
- 采用 PUT,必须将所有 20 个字段一并提交到 URL,未提交的字段将被删除。
因此 PATCH 的最主要好处是:节省网络带宽。
0x03 再探 Requests 库
HTTP 协议方法与 Requests 库方法是一一对应的。因此有了对 HTTP 协议方法的理解之后,我们再来学习 Requests 库中的其他方法。
Requests 库中的其他方法
request.head() 方法
>>> r = requests.head("http://httpbin.org/get")
>>> r.headers
{'Date': 'Sat, 19 Mar 2022 18:27:08 GMT', 'Content-Type': 'application/json', 'Content-Length': '307', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
>>> r.text
''
可以看到 r.text
为空,因此通过 head()
方法可以用很少的网络流量,来获得网页的概括信息。
requests.post () 方法
- 向 URL POST 一个字典,自动编码为 form。
>>> payload = {"key1": "value1","key2": "value2"}
>>> r = requests.post("http://httpbin.org/post", data = payload)
>>> print(r.text)
{ ...
"form": {
"key1": "value1",
"key2": "value2"
},
...
}
可见,当向 URL POST 一个字典或者键值对的时候,会默认存储在表单 form 的字段下。
- 向 URL POST 一个字符串,自动编码为 data。
>>>r = requests.post("http://httpbin.org/post", data = 'juanbudongle')
>>>print(r.text)
{ ...
"data": "juanbudongle",
"files": {},
"form": {}
}
requests.put() 方法
与 POST 类似,只是会覆盖原 URL 位置的资源
>>> payload = {"key1": "value1","key2": "value2"}
>>> r = requests.put("http://httpbin.org/post", data = payload)
>>> print(r.text)
{ ...
"form": {
"key1": "value1",
"key2": "value2"
},
...
}
requests.post ()
与 requests.put()
类似,上节末尾已经分析过两种方法的区别。
Requests 库的基础方法
requests.request()
是 Requests 库所有方法的基础方法,因此将其单列出来。或者说,库中其他方法的实现,都是基于 requests.request()
方法。
requests. request () 方法
requests.request(method, url, **kwargs)
method
:请求方式,对应 GET/PUT/POST 等 7 种。url
:拟获取页面的 URL 链接。**kwargs
: 控制访问的参数,共 13 个。
7 种请求方式如下:
**kwargs
控制访问的参数,一共有 13 个,均为可选项,下面依次讲解:
- params:字典或字节序列,作为参数增加到
url
中。 - data:字典,字节序或文件对象,作为 Request 的内容。 提交的键值对不直接存在
url
链接中,而是放在 Request 规定的存储位置。参考 requests post 方法 。 - json:json 格式的数据,作为 Request 的内容。
- headers:字典,HTTP 定制头。
- cookies:字典或 CookieJar,Request 中的 cookie。
- auth:元组,支持 HTTP 认证功能。
- files:字典类型,传输文件。
- timeout: 设定超时时间,秒为单位。
- proxies:字典类型,设定访问代理服务器,可以增加登录认证。 使用这个字段,可以有效隐藏用户爬取网页的源的 IP 地址信息,有效防止对爬虫的逆追踪。
- allow_redirects: True/False,默认为 True,重定向开关。
- stream:True/False,默认为 True,获取内容立即下载开关。
- verify:True/False,默认为 True,认证 SSL 证书开关。
- cert: 本地 SSL 证书路径。
0x04 Requests 实战
Case 1. 京东商品页面的爬取
对这个 手机商品页面 进行爬取。
>>> import requests
>>> url = 'https://item.jd.com/100029199502.html'
>>> r = requests.get(url)
>>> r.status_code
200
>>> r.encoding
'UTF-8'
>>> r.text
{'Date': 'Sat, 19 Mar 2022 18:27:08 GMT', 'Content-Type': 'application/json', 'Content-Length': '307', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
>>> r.text
"<script>window.location.href='https://passport.jd.com/new/login.aspx?ReturnUrl=http%3A%2F%2Fitem.jd.com%2F100029199502.html'</script>"
以上都是基本流程操作,我们发现返回的信息并不是商品页面。如果将这个返回的 URL 在浏览器中打开,会呈现一个登录页面。或者说京东阻止了这个爬虫,把我们重定向到了登陆页面。因为我们即使不登陆,在浏览器上还是可以访问的。
一般网页限制爬虫的方式有两种,一种是通过 Robots 协议,告知爬虫,哪些资源可以访问,哪些不可以;另一种是根据 HTTP 的 Header 中的 user-agent 来检测,判断这个 HTTP 访问是不是由爬虫发起的,对于爬虫的请求,网站是可以选择拒绝的。
由于 Requests
库返回的 Response
对象(也就是 requests.get()
返回的 r
),包含发出的 request
请求,因此我们可以查看之前对京东发出的请求到底是什么内容。
>>> r.request.headers # 查看发出的头信息
{'User-Agent': 'python-requests/2.27.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
# 可以发现,这里标明了我们是使用 'python-requests/2.27.1' 发出的请求
# 也就是说我们的爬虫诚实地告诉了服务器,这个请求是由一个爬虫产生的
# 因此,如果京东进行来源审查,并且不支持这种访问的时候,我们的访问会被拒绝
# 那么此时,我们就应该模拟浏览器来进行爬虫请求
>>> kv = {'user-agent': 'Mozilla/7.0'} # 构造一个键值对,指定 user-agent
>>> url = 'https://item.jd.com/100029199502.html'
>>> r = requests.get(url, headers = kv) # 修改 HTTP 头中的 user-agent
>>> r.status_code
200
>>> r.request.headers # 查看发出请求的头信息
{'user-agent': 'Mozilla/7.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
# 可以看到 user-agent 已经修改为我们键值对中的内容
>>> r.text[:600] # 查看返回内容
'<!DOCTYPE HTML>\n<html lang="zh-CN">\n<head>\n <!-- shouji -->\n <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n <title>【华为Mate X2】华为 HUAWEI Mate X2 5G全网通12GB+512GB墨黑素皮款 典藏版 麒麟芯片 超感知徕卡四摄 折叠屏 华为手机【行情 报价 价格 评测】-京东</title>\n <meta name="keywords" content="HUAWEIMate X2,华为Mate X2,华为Mate X2报价,HUAWEIMate X2报价"/>\n <meta name="description" content="【华为Mate X2】京东JD.COM提供华为Mate X2正品行货,并包括HUAWEIMate X2网购指南,以及华为Mate X2图片、Mate X2参数、Mate X2评论、Mate X2心得、Mate X2技巧等信息,网购华为Mate X2上京东,放心又轻松" />\n <meta name="format-detection" content="telephone=no">\n <meta http-equiv="mobile'
# 正常返回商品页面
以上是在 Python IDLE 中的测试,下面给出全代码:
import requests
url = 'https://item.jd.com/100029199502.html'
try:
kv = {'user-agent': 'Mozilla/7.0'}
r = requests.get(url, headers = kv)
r.raise_for_status()
r.encoding = r.apparent_encoding
print(r.text[:1000])
except:
print("Error")
总结:有的时候需要修改头信息 'user-agent',模拟浏览器向服务器提交 HTTP 请求。
Case 2. 百度搜索结果爬取
百度的搜索关键词接口: https://www.baidu.com/s?wd=keyword
这里要注意的是,简单的给 HTTP 头信息进行修改是没用的,需要去找一个真实的 User-Agent 修改到头信息中。
在浏览器中按下 F12:
随便选取一个 GET 请求,复制请求标头里的用户代理 User-Agent 信息。
>>> agent = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0'}
>>> kv = {'wd': 'Python'}
>>> r = requests.get (' https://www.baidu.com/s ', params=kv, headers=agent)
>>> r.request.url # 查看发出的 HTTP 请求的 URL
'https://www.baidu.com/s?wd=Python'
# 可以看到,搜素关键词添加到了后面
>>> r.request.headers
{'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
>>> r.status_code
200
>>> len(r.text) # 查看返回内容的长度
786376
# 这就说明返回成功了
# 修改 User-Agent 不正确时,返回的长度只有 227,即代表被百度服务器阻止访问了
总结:在实际应用时,可能会涉及各种伪装技巧,反反爬也是要考虑的一方面。
Case 3. 网络图片的爬取和存储
在国家地理的网站上随便找 一幅图,通过查看网页源码找到这幅图的 URL。
>>> import requests
>>> path = "city.jpg" # 给图片一个存储位置,存在当前目录下,命名为 city
>>> url = 'https://ngimages.oss-cn-beijing.aliyuncs.com/2022/03/16/fabe20d0-01cc-400d-ae07-8f56516a556a.jpg' # 源码中找到的图的 URL
>>> r = requests.get(url)
>>> r.status_code
200 # 成功
>>> with open(path, 'wb') as f: # 将图片以二进制形式写入文件
f.write(r.content)
完整代码:
import requests
import os
url = 'https://ngimages.oss-cn-beijing.aliyuncs.com/2022/03/16/fabe20d0-01cc-400d-ae07-8f56516a556a.jpg'
root = "E://pics//"
path = root + url.split('/')[-1] # .spilt 返回⼀个将字符串以 / 划分的列表,取 [-1],即最后一个(或者说倒数第一个),也就是 URL 中图片的名字
try:
kv = {'user-agent': 'Mozilla/5.0'}
if not os.path.exists(root): # 如果根目录不存在,则新建根目录
os.mkdir(root)
if not os.path.exists(path): # 如果文件不存在,则从网上爬取并存储
r = requests.get(url)
with open(path, 'wb') as f:
f.write(r.content)
print("文件保存成功")
else:
print("文件保存成功")
except:
print("爬取失败")
总结:在工程要求上,代码的可靠性与稳定性是非常重要的。因此哪怕是简单的代码,也要考虑可能出现的问题,并对这些问题做相关处理。
References
学习 HTTP 协议 ⭐⭐⭐ URL 锚点、fragment 锚部分、锚点定位的九点知识 ⭐⭐ 统一资源定位符 URL URL(统一资源定位符)各部分详解 Python 爬虫视频教程全集 | bilibili ⭐⭐⭐ Python 爬虫教程中转站 python3 requests 详解 http 请求中 get 和 post 方法的区别 python 爬虫-百度搜索结果爬取