<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么爬蟲(chóng)工程師應(yīng)該有一些基本的后端常識(shí)?

          共 7489字,需瀏覽 15分鐘

           ·

          2021-06-25 03:05

          劇照:眷思量

          作者:kingname

          來(lái)源:未聞Code

          今天在粉絲交流群里面,有個(gè)同學(xué)說(shuō)他發(fā)現(xiàn)了Requests的一個(gè) bug,并修復(fù)了它:

          聊天記錄中對(duì)應(yīng)的圖片為:

          看到這個(gè)同學(xué)的截圖,我大概知道他遇到了什么問(wèn)題,以及為什么會(huì)誤認(rèn)為這是 Requests 的 bug。

          要解釋這個(gè)問(wèn)題,我們需要首先明白一個(gè)問(wèn)題,那就是 JSON 字符串的兩種顯示形式和json.dumpsensure_ascii參數(shù)。

          假設(shè)我們?cè)?Python 里面有一個(gè)字典:

          info = {'name''青南''age'20}

          當(dāng)我們想把它轉(zhuǎn)成 JSON 字符串的時(shí)候,我們可能會(huì)這樣寫(xiě)代碼:

          import json
          info = {'name''青南''age'20}
          info_str = json.dumps(info)
          print(info_str)

          運(yùn)行效果如下圖所示,中文變成了 Unicode 碼:

          我們也可以增加一個(gè)參數(shù)ensure_ascii=False,讓中文正常顯示出來(lái):

          info_str = json.dumps(info, ensure_ascii=False)

          運(yùn)行效果如下圖所示:

          這位同學(xué)認(rèn)為,由于{"name": "\u9752\u5357", "age": 20}{"name": "青南", "age": 20}從字符串角度看,顯然不相等。而 Requests 在 POST 發(fā)送數(shù)據(jù)的時(shí)候,默認(rèn)是沒(méi)有這個(gè)參數(shù),而對(duì)json.dumps來(lái)說(shuō),省略這個(gè)參數(shù)等價(jià)于ensure_ascii=True

          所以實(shí)際上Requests在 POST 含有中文的數(shù)據(jù)時(shí),會(huì)把中文轉(zhuǎn)成 Unicode 碼發(fā)給服務(wù)器,于是服務(wù)器根本就拿不到原始的中文信息了。所以就會(huì)導(dǎo)致報(bào)錯(cuò)。

          但實(shí)際上,并不是這樣的。我常常跟群里的同學(xué)說(shuō),做爬蟲(chóng)的同學(xué),應(yīng)該要有一些基本的后端常識(shí),才不至于被這種現(xiàn)象誤導(dǎo)。為了說(shuō)明為什么上面這個(gè)同學(xué)的理解是錯(cuò)誤的,為什么這不是 Requests 的 bug,我們自己來(lái)寫(xiě)一個(gè)含有 POST 的服務(wù),來(lái)看看我們POST 兩種情況的數(shù)據(jù)有沒(méi)有區(qū)別。為了證明這個(gè)特性與網(wǎng)絡(luò)框架無(wú)關(guān),我這里分別使用Flask、Fastapi 、Gin 來(lái)進(jìn)行演示。

          首先,我們來(lái)看看Requests 測(cè)試代碼。這里用3種方式發(fā)送了 JSON 格式的數(shù)據(jù):

          import requests 
          import json 

          body = {
              'name''青南',
              'age'20
          }
          url = 'http://127.0.0.1:5000/test_json'

          # 直接使用 json=的方式發(fā)送
          resp = requests.post(url, json=body).json() 
          print(resp)

          headers = {
              'Content-Type''application/json'
          }

          # 提前把字典序列化成 JSON 字符串,中文轉(zhuǎn)成 Unicode,跟第一種方式等價(jià)
          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)

          這段測(cè)試代碼使用3種方式發(fā)送 POST 請(qǐng)求,其中,第一種方法就是 Requests 自帶的json=參數(shù),參數(shù)值是一個(gè)字典。Requests 會(huì)自動(dòng)把它轉(zhuǎn)成 JSON 字符串。后兩種方式,是我們手動(dòng)提前把字典轉(zhuǎn)成 JSON 字符串,然后使用data=參數(shù)發(fā)送給服務(wù)器。這兩種方式需要在 Headers 里面指明'Content-Type': 'application/json',服務(wù)器才知道發(fā)上來(lái)的是 JSON 字符串。

          我們?cè)賮?lái)看看 Flask 寫(xiě)的后端代碼:

          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 數(shù)據(jù),{body["name"]=}{body["age"]=}'
              print(msg)
              return {'success'True'msg': msg}

          運(yùn)行效果如下圖所示:

          可以看到,無(wú)論使用哪種 POST 方式,后端都能接收到正確的信息。

          我們?cè)賮?lái)看 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 數(shù)據(jù),{body.name=}{body.age=}'
              print(msg)
              return {'success'True'msg': msg}

          運(yùn)行效果如下圖所示,三種 POST 發(fā)送的數(shù)據(jù),都能被后端正確識(shí)別:

          我們?cè)賮?lái)看看 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 數(shù)據(jù),name=%s, age=%d", json.Name, json.Age)
                  fmt.Println(">>>", msg)
                  c.JSON(http.StatusOK, gin.H{
                      "msg": fmt.Sprintf("收到 POST 數(shù)據(jù),name=%s, age=%d", json.Name, json.Age),
                  })
              })
              r.Run()
          }

          運(yùn)行效果如下,三種請(qǐng)求方式的數(shù)據(jù)完全相同:

          從這里可以知道,無(wú)論我們 POST 提交的 JSON 字符串中,中文是以 Unicode 碼的形式存在還是直接以漢字的形式存在,后端服務(wù)都可以正確解析。

          為什么我說(shuō)中文在 JSON 字符串里面以哪種形式顯示并不重要呢?這是因?yàn)椋瑢?duì) JSON 字符串來(lái)說(shuō),編程語(yǔ)言把它重新轉(zhuǎn)換為對(duì)象的過(guò)程(叫做反序列化),本身就可以正確處理他們。我們來(lái)看下圖:

          ensure_ascii參數(shù)的作用,僅僅控制的是 JSON 的顯示樣式,當(dāng)ensure_asciiTrue的時(shí)候,確保 JSON 字符串里面只有 ASCII 字符,所以不在 ASCII 128個(gè)字符內(nèi)的字符,都會(huì)被轉(zhuǎn)換。而當(dāng)ensure_asciiFalse的時(shí)候,這些非 ASCII 字符依然以原樣顯示。這就像是一個(gè)人化妝和不化妝一樣,本質(zhì)并不會(huì)改變。現(xiàn)代化的編程語(yǔ)言在對(duì)他們進(jìn)行反序列化的時(shí)候,兩種形式都能正確識(shí)別。

          所以,如果你是用現(xiàn)代化的 Web 框架來(lái)寫(xiě)后端,那么這兩種 JSON 形式應(yīng)該是沒(méi)有任何區(qū)別的。Request 默認(rèn)的json=參數(shù),相當(dāng)于ensure_ascii=True,任何現(xiàn)代化的 Web 框架都能正確識(shí)別 POST 提交上來(lái)的內(nèi)容。

          當(dāng)然,如果你使用的是 C 語(yǔ)言、匯編或者其他語(yǔ)言來(lái)裸寫(xiě)后端接口,那確實(shí)可能有所差別??芍巧陶5娜?,誰(shuí)會(huì)這樣做?

          綜上所述,這位同學(xué)遇到的問(wèn)題,并不是 Requests 的 bug,而是他的后端接口本身有問(wèn)題??赡苣莻€(gè)后端使用了某種弱智 Web 框架,它接收到的被 POST 發(fā)上來(lái)的信息,沒(méi)有經(jīng)過(guò)反序列化,就是一段 JSON 字符串,而那個(gè)后端程序員使用正則表達(dá)式從 JSON 字符串里面提取數(shù)據(jù),所以當(dāng)發(fā)現(xiàn) JSON 字符串里面沒(méi)有中文的時(shí)候,就報(bào)錯(cuò)了。

          除了這個(gè) POST 發(fā)送 JSON 的問(wèn)題,以前我有個(gè)下屬,在使用 Scrapy 發(fā)送 POST 信息的時(shí)候,由于不會(huì)寫(xiě)POST 的代碼,突發(fā)奇想,把 POST 發(fā)送的字段拼接到 URL 上,然后用 GET 方式請(qǐng)求,發(fā)現(xiàn)也能獲取數(shù)據(jù),類(lèi)似于:

          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

          于是,這個(gè)同學(xué)得出一個(gè)結(jié)論,他認(rèn)為這是一個(gè)普遍的規(guī)律,所有 POST 的請(qǐng)求都可以這樣轉(zhuǎn)到 GET 請(qǐng)求。

          但顯然,這個(gè)結(jié)論也是不正確的。這只能說(shuō)明,這個(gè)網(wǎng)站的后端程序員,讓這個(gè)接口能同時(shí)兼容兩種提交數(shù)據(jù)的方式,這是需要后端程序員額外寫(xiě)代碼來(lái)實(shí)現(xiàn)的。在默認(rèn)情況下,GET 和 POST 是兩種完全不同的請(qǐng)求方式,也不能這樣轉(zhuǎn)換。

          如果這位同學(xué)會(huì)一些簡(jiǎn)單的后端,那么他立刻就可以寫(xiě)一個(gè)后端程序來(lái)驗(yàn)證自己的猜想。

          再來(lái)一個(gè)例子,有一些網(wǎng)站,他們?cè)?URL 中可能會(huì)包含另外一個(gè) URL,例如:

          https://kingname.info/get_info?url=https://abc.com/def/xyz?id=123&db=admin

          如果你沒(méi)有基本的后端知識(shí),那么你可能看不出上面的網(wǎng)址有什么問(wèn)題。但是如果你有一些基本的后端常識(shí),那么你可能會(huì)問(wèn)一個(gè)問(wèn)題:網(wǎng)址中的&db=admin,是屬于https://kingname.info/get_info的一個(gè)參數(shù),跟url=平級(jí);還是屬于https://abc.com/def/xyz?id=123&db=admin的參數(shù)?你會(huì)疑惑,后端也會(huì)疑惑,所以這就是為什么我們這個(gè)時(shí)候需要 urlencode 的原因,畢竟下面兩種寫(xiě)法,是完全不一樣的:

          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

          最后,以我的爬蟲(chóng)書(shū)序言中的一句話來(lái)作為總結(jié):

          爬蟲(chóng)是一門(mén)雜學(xué),如果你只會(huì)爬蟲(chóng),那么你是學(xué)不好爬蟲(chóng)的。

          Python貓技術(shù)交流群開(kāi)放啦!群里既有國(guó)內(nèi)一二線大廠在職員工,也有國(guó)內(nèi)外高校在讀學(xué)生,既有十多年碼齡的編程老鳥(niǎo),也有中小學(xué)剛剛?cè)腴T(mén)的新人,學(xué)習(xí)氛圍良好!想入群的同學(xué),請(qǐng)?jiān)诠?hào)內(nèi)回復(fù)『交流群』,獲取貓哥的微信(謝絕廣告黨,非誠(chéng)勿擾?。?/span>~


          還不過(guò)癮?試試它們




          Python進(jìn)階:探秘描述符的工作原理

          為什么數(shù)據(jù)庫(kù)不應(yīng)該使用外鍵?

          揭開(kāi)Python對(duì)象的神秘面紗!

          Python 算法模板庫(kù),Pythonista 找工作利器

          Python 小技巧:如何實(shí)現(xiàn)操作系統(tǒng)兼容性打包?

          Python 為什么要有 pass 語(yǔ)句?


          如果你覺(jué)得本文有幫助
          請(qǐng)慷慨分享點(diǎn)贊,感謝啦!
          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本成人三级91精品电影 | 中文字幕在线色 | 3344gc在线观看免费下载视频 | 向井蓝无码精品一区二区 | 免费看18禁 |