爬虫与网络编程基础-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_codeHTTP 请求的返回状态,若为 200 则表示请求成功
r.textHTTP 响应内容的字符串形式,即,url 对应的页面内容
r.encoding从 HTTP header 中猜测的相应内容编码方式
r.apparent_encoding从内容中分析出的响应内容编码方式(备选编码方式)
r.contentHTTP 响应内容的二进制形式

这五个属性是最常使用的五个属性。 r.status_code 获得的状态码,只有返回 200 表示请求成功,其他则为失败,如 404。 |600

使用 requests.get() 方法获取信息的基本流程: |500

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>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使ç\x94¨ç\x99¾åº¦å\x89\x8då¿\x85读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>æ\x84\x8fè§\x81å\x8f\x8dé¦\x88</a>&nbsp;京ICPè¯\x81030173å\x8f·&nbsp; <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>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京 ICP 证 030173 号&nbsp; <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_encodingr.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 请求/响应的步骤:

  1. 客户端连接到 Web 服务器。
  2. 发送 HTTP 请求。
  3. 服务器接受请求并返回 HTTP 响应。
  4. 释放连接 TCP 连接。
  5. 客户端浏览器解析 HTML 内容。

在浏览器地址栏键入 URL,按下回车之后会经历以下流程:

  1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址。
  2. 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接。
  3. 浏览器发出读取文件 (URL 中域名后面部分对应的文件) 的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器。
  4. 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器。
  5. 释放 TCP 连接。
  6. 浏览器将该 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

其中:

  1. http 是协议;
  2. zh.wikipedia.org 是服务器;
  3. 80 是服务器上的网络端口号;
  4. /w/index.php 是路径;
  5. ?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 () 方法

  1. 向 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 的字段下。

  1. 向 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 种请求方式如下: |500

**kwargs 控制访问的参数,一共有 13 个,均为可选项,下面依次讲解:

  • params:字典或字节序列,作为参数增加到 url 中。 |700
  • data:字典,字节序或文件对象,作为 Request 的内容。 |700 提交的键值对不直接存在 url 链接中,而是放在 Request 规定的存储位置。参考 requests post 方法
  • json:json 格式的数据,作为 Request 的内容。 |700
  • headers:字典,HTTP 定制头。
  • cookies:字典或 CookieJar,Request 中的 cookie。
  • auth:元组,支持 HTTP 认证功能。
  • files:字典类型,传输文件。 |700
  • 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: |800

随便选取一个 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 爬虫-百度搜索结果爬取