共计 4393 个字符,预计需要花费 11 分钟才能阅读完成。
明天在粉丝交换群外面,有个同学说他发现了 Requests
的一个 bug,并修复了它:
聊天记录中对应的图片为:
看到这个同学的截图,我大略晓得他遇到了什么问题,以及为什么会误认为这是 Requests 的 bug。
要解释这个问题,咱们须要首先明确一个问题,那就是 JSON 字符串的两种显示模式和 json.dumps
的ensure_ascii
参数。
假如咱们在 Python 外面有一个字典:
info = {'name': '青南', 'age': 20}
当咱们想把它转成 JSON 字符串的时候,咱们可能会这样写代码:
import json
info = {'name': '青南', 'age': 20}
info_str = json.dumps(info)
print(info_str)
运行成果如下图所示,中文变成了 Unicode 码:
咱们也能够减少一个参数ensure_ascii=False
,让中文失常显示进去:
info_str = json.dumps(info, ensure_ascii=False)
运行成果如下图所示:
这位同学认为,因为 {"name": "\u9752\u5357", "age": 20}
和{"name": "青南", "age": 20}
从字符串角度看,显然不相等。而 Requests 在 POST 发送数据的时候,默认是没有这个参数,而对 json.dumps
来说,省略这个参数等价于ensure_ascii=True
:
所以实际上 Requests
在 POST 含有中文的数据时,会把中文转成 Unicode 码发给服务器,于是服务器基本就拿不到原始的中文信息了。所以就会导致报错。
但实际上,并不是这样的。我经常跟群里的同学说,做爬虫的同学,应该要有一些根本的后端常识,才不至于被这种景象误导。为了阐明为什么下面这个同学的了解是谬误的,为什么这不是 Requests 的 bug,咱们本人来写一个含有 POST 的服务,来看看咱们 POST 两种状况的数据有没有区别。为了证实这个个性与网络框架无关,我这里别离应用 Flask、Fastapi、Gin 来进行演示。
首先,咱们来看看 Requests 测试代码。这里用 3 种形式发送了 JSON 格局的数据:
import requests
import json
body = {
'name': '青南',
'age': 20
}
url = 'http://127.0.0.1:5000/test_json'
# 间接应用 json= 的形式发送
resp = requests.post(url, json=body).json()
print(resp)
headers = {'Content-Type': 'application/json'}
# 提前把字典序列化成 JSON 字符串,中文转成 Unicode,跟第一种形式等价
resp = requests.post(url,
headers=headers,
data=json.dumps(body)).json()
print(resp)
# 提前把字典序列化成 JSON 字符串,中文保留
resp = requests.post(url,
headers=headers,
data=json.dumps(body, ensure_ascii=False).encode()).json()
print(resp)
这段测试代码应用 3 种形式发送 POST 申请,其中,第一种办法就是 Requests 自带的 json=
参数,参数值是一个字典。Requests 会主动把它转成 JSON 字符串。后两种形式,是咱们手动提前把字典转成 JSON 字符串,而后应用 data=
参数发送给服务器。这两种形式须要在 Headers 外面指明'Content-Type': 'application/json'
,服务器才晓得发上来的是 JSON 字符串。
咱们再来看看 Flask 写的后端代码:
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
return {'success': True}
@app.route('/test_json', methods=["POST"])
def test_json():
body = request.json
msg = f'收到 POST 数据,{body["name"]=}, {body["age"]=}'
print(msg)
return {'success': True, 'msg': msg}
运行成果如下图所示:
能够看到,无论应用哪种 POST 形式,后端都能接管到正确的信息。
咱们再来看 Fastapi 版本:
from fastapi import FastAPI
from pydantic import BaseModel
class Body(BaseModel):
name: str
age: int
app = FastAPI()
@app.get('/')
def index():
return {'success': True}
@app.post('/test_json')
def test_json(body: Body):
msg = f'收到 POST 数据,{body.name=}, {body.age=}'
print(msg)
return {'success': True, 'msg': msg}
运行成果如下图所示,三种 POST 发送的数据,都能被后端正确辨认:
咱们再来看看 Gin 版本的后端:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Body struct {
Name string `json:"name"`
Age int16 `json:"age"`
}
func main() {r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "running",})
})
r.POST("/test_json", func(c *gin.Context) {json := Body{}
c.BindJSON(&json)
msg := fmt.Sprintf("收到 POST 数据,name=%s, age=%d", json.Name, json.Age)
fmt.Println(">>>", msg)
c.JSON(http.StatusOK, gin.H{"msg": fmt.Sprintf("收到 POST 数据,name=%s, age=%d", json.Name, json.Age),
})
})
r.Run()}
运行成果如下,三种申请形式的数据完全相同:
从这里能够晓得,无论咱们 POST 提交的 JSON 字符串中,中文是以 Unicode 码的模式存在还是间接以汉字的模式存在,后端服务都能够正确解析。
为什么我说中文在 JSON 字符串外面以哪种模式显示并不重要呢?这是因为,对 JSON 字符串来说,编程语言把它从新转换为对象的过程(叫做 反序列化
),自身就能够正确处理他们。咱们来看下图:
ensure_ascii
参数的作用,仅仅管制的是 JSON 的显示款式,当 ensure_ascii
为True
的时候,确保 JSON 字符串外面只有 ASCII 字符,所以不在 ASCII 128 个字符内的字符,都会被转换。而当 ensure_ascii
为False
的时候,这些非 ASCII 字符仍然以原样显示。这就像是一个人化妆和不化妆一样,实质并不会扭转。现代化的编程语言在对他们进行反序列化的时候,两种模式都能正确辨认。
所以,如果你是用现代化的 Web 框架来写后端,那么这两种 JSON 模式应该是没有任何区别的。Request 默认的 json=
参数,相当于ensure_ascii=True
,任何现代化的 Web 框架都能正确辨认 POST 提交上来的内容。
当然,如果你应用的是 C 语言、汇编或者其余语言来裸写后端接口,那的确可能有所差异。可智商失常的人,谁会这样做?
综上所述,这位同学遇到的问题,并不是 Requests 的 bug,而是他的后端接口自身有问题。可能那个后端应用了某种弱智 Web 框架,它接管到的被 POST 发上来的信息,没有通过反序列化,就是一段 JSON 字符串,而那个后端程序员应用正则表达式从 JSON 字符串外面提取数据,所以当发现 JSON 字符串外面没有中文的时候,就报错了。
除了这个 POST 发送 JSON 的问题,以前我有个上司,在应用 Scrapy 发送 POST 信息的时候,因为不会写 POST 的代码,突发奇想,把 POST 发送的字段拼接到 URL 上,而后用 GET 形式申请,发现也能获取数据,相似于:
body = {'name': '青南', 'age': 20}
url = 'http://www.xxx.com/api/yyy'
requests.post(url, json=body).text
requests.get('http://www.xxx.com/api/yyy?name= 青南 &age=20').text
于是,这个同学得出一个论断,他认为这是一个广泛的法则,所有 POST 的申请都能够这样转到 GET 申请。
但显然,这个论断也是不正确的。这只能阐明,这个网站的后端程序员,让这个接口能同时兼容两种提交数据的形式,这是须要后端程序员额定写代码来实现的。在默认状况下,GET 和 POST 是两种齐全不同的申请形式,也不能这样转换。
如果这位同学会一些简略的后端,那么他立即就能够写一个后端程序来验证本人的猜测。
再来一个例子,有一些网站,他们在 URL 中可能会蕴含另外一个 URL,例如:
https://kingname.info/get_info?url=https://abc.com/def/xyz?id=123&db=admin
如果你没有根本的后端常识,那么你可能看不出下面的网址有什么问题。然而如果你有一些根本的后端常识,那么你可能会问一个问题:网址中的 &db=admin
,是属于https://kingname.info/get_info
的一个参数,跟 url=
平级;还是属于 https://abc.com/def/xyz?id=123&db=admin
的参数?你会纳闷,后端也会纳闷,所以这就是为什么咱们这个时候须要 urlencode 的起因,毕竟上面两种写法,是齐全不一样的:
https://kingname.info/get_info?url=https%3A%2F%2Fabc.com%2Fdef%2Fxyz%3Fid%3D123%26db%3Dadmin
https://kingname.info/get_info?url=https%3A%2F%2Fabc.com%2Fdef%2Fxyz%3Fid%3D123&db=admin
最初,以我的爬虫书序言中的一句话来作为总结:
爬虫是一门杂学,如果你只会爬虫,那么你是学不好爬虫的。