乐趣区

关于python3.x:为什么爬虫工程师应该有一些基本的后端常识

明天在粉丝交换群外面,有个同学说他发现了 Requests 的一个 bug,并修复了它:

聊天记录中对应的图片为:

看到这个同学的截图,我大略晓得他遇到了什么问题,以及为什么会误认为这是 Requests 的 bug。

要解释这个问题,咱们须要首先明确一个问题,那就是 JSON 字符串的两种显示模式和 json.dumpsensure_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_asciiTrue的时候,确保 JSON 字符串外面只有 ASCII 字符,所以不在 ASCII 128 个字符内的字符,都会被转换。而当 ensure_asciiFalse的时候,这些非 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

最初,以我的爬虫书序言中的一句话来作为总结:

爬虫是一门杂学,如果你只会爬虫,那么你是学不好爬虫的。

退出移动版