简介

本篇文章将介绍使用 Python Flask 库搭建的后端程序,如何与前端程序进行数据通信。一方面介绍如何使用 Python requests 库发送 post 请求,另一方面介绍如何使用 JavaScript fetch 方法发送 post 请求。此外,详细地展示在 Flask 中如何接受数据,数据格式的转换与解码等。

requests.post

requests.post | requests docs

使用 requests 库的 post 方法进行数据传输时有非常多的可选参数,它的函数声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function) def post(
url: str | bytes,
data: _Data | None = None,
json: Any | None = None,
*,
params: _Params | None = ...,
headers: _HeadersMapping | None = ...,
cookies: CookieJar | _TextMapping | None = ...,
files: _Files | None = ...,
auth: _Auth | None = ...,
timeout: _Timeout | None = ...,
allow_redirects: bool = ...,
proxies: _TextMapping | None = ...,
hooks: _HooksInput | None = ...,
stream: bool | None = ...,
verify: _Verify | None = ...,
cert: _Cert | None = ...
) -> Response

常用的与数据传输相关的参数有 data、json 和 files,它们都有各自的应用场景和使用方式。下面将分别介绍如何在 requests.post 方法中使用这些参数,以及如何在 flask.request 中接收对应的数据。

requests.post(url, data=data)

首先介绍 data 参数,data 参数主要针对表单类型数据,如下是一个示例的使用 data 参数的 requests.post 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""client.py"""
import requests

url = "http://127.0.0.1:5000/receive"
data = {"name": "data", "content": 123} # Content-Type: application/x-www-form-urlencoded
response = requests.post(url=url, data=data, timeout=3)

print(f"response: {response},\n"
f"response.status_code: {response.status_code},\n"
f"type(response): {type(response)},\n"
f"response.json(): {response.json()},\n"
f"response.request.headers['Content-Type']: {response.request.headers['Content-Type']}")
# response: <Response [200]>,
# response.status_code: 200,
# type(response): <class 'requests.models.Response'>,
# response.json(): {'success': True},
# response.request.headers['Content-Type']: application/x-www-form-urlencoded

使用 requests.post(url, data=data) 方法的 data 参数传输数据,对应请求头中 Content-Type 字段的内容为 application/x-www-form-urlencoded,这表示数据为表单类型。

1
2
3
4
5
6
7
Host: 127.0.0.1:5000
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 21
Content-Type: application/x-www-form-urlencoded

flask.request.get_data()

在 Flask 服务端使用 request.get_data() 方法来获取 requests.post(url, data=data) 所发送的 data 数据,得到的数据将是原始字节流,不解析数据格式的 raw data,数据类型为 <class 'bytes'>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""server.py"""
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

raw_data = request.get_data()
# raw_data = request.data
print(f"raw_data: {raw_data},\ntype(raw_data): {type(raw_data)}")
# raw_data: b'name=data&content=123',
# type(raw_data): <class 'bytes'>
return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

flask.request.form

在 Flask 服务端使用 request.form 来获取 requests.post(url, data=data) 所发送的 data 数据,得到的将是处理后的表单数据,数据类型为 <class 'werkzeug.datastructures.structures.ImmutableMultiDict'>, 类似 Python 的字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""server.py"""
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

form = request.form
print(f"form: {form},\ntype(form): {type(form)}")
# form: ImmutableMultiDict([('name', 'data'), ('content', '123')]),
# type(form): <class 'werkzeug.datastructures.structures.ImmutableMultiDict'>

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

对于 ImmutableMultiDict 类型的对象,可以通过它的 get 方法来获取表单中的内容

1
value = request.form.get("key")

如何处理 <class ‘bytes’> 类型数据

对于原始字节流形式的 <class 'bytes'> 类型数据,需要先进行解码才能够使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""server.py"""
from flask import Flask, request, jsonify
from urllib.parse import parse_qs

app = Flask(__name__)

@app.route("/receive/", methods=["POST"])
def receive():
print(request.headers)

raw_data = request.get_data()
print(f"raw_data: {raw_data},\ntype(raw_data): {type(raw_data)}")
# raw_data: b'name=data&content=123',
# type(raw_data): <class 'bytes'>

'''decode bytes to string'''
decoded_data = raw_data.decode('utf-8')
print(f"decoded_data: {decoded_data},\ntype(decoded_data): {type(decoded_data)}")
# decoded_data: name=data&content=123,
# type(decoded_data): <class 'str'>

'''parse to form data'''
parsed_data = parse_qs(decoded_data)
print(f"parsed_data: {parsed_data},\ntype(parsed_data): {type(parsed_data)}")
# parsed_data: {'name': ['data'], 'content': ['123']},
# type(parsed_data): <class 'dict'>

'''convert to dict'''
parsed_data = {key: value[0] for key, value in parsed_data.items()}
print(f"parsed_data: {parsed_data},\ntype(parsed_data): {type(parsed_data)}")
# parsed_data: {'name': 'data', 'content': '123'},
# type(parsed_data): <class 'dict'>

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
1
2
3
4
5
6
7
8
9
"""client.py"""
import requests

url = "http://127.0.0.1:5000/receive"
data = {"name": "data", "content": 123} # Content-Type: application/x-www-form-urlencoded
response = requests.post(url=url, data=data, timeout=3)

print(f"response: {response}, type: {type(response)}, json: {response.json()}")
# response: <Response [200]>, type: <class 'requests.models.Response'>, json: {'success': True}

requests.post(url, json=json)

json 参数适合用于指定传输 dict 或者 json 类型的数据,如下是一个使用 json 参数的 requests.post 请求的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""client.py"""
import requests

url = "http://127.0.0.1:5000/receive"
json = {"name": "json", "content": 456} # application/json
response = requests.post(url=url, json=json, timeout=3)

print(f"response: {response},\n"
f"response.status_code: {response.status_code},\n"
f"type(response): {type(response)},\n"
f"response.json(): {response.json()},\n"
f"response.request.headers['Content-Type']: {response.request.headers['Content-Type']}")
# response: <Response [200]>,
# response.status_code: 200,
# type(response): <class 'requests.models.Response'>,
# response.json(): {'success': True},
# response.request.headers['Content-Type']: application/json

对应的请求头如下

1
2
3
4
5
6
7
Host: 127.0.0.1:5000
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 32
Content-Type: application/json

在 Flask 中使用 flask.request.get_json() 方法或者 flask.request.json 属性方法可以直接获得 json 类型结构的数据,对应于 python 中的 dict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""server.py"""
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

json = request.get_json()
# json = request.json
print(f"json: {json},\ntype(json): {type(json)}")
# json: {'name': 'json', 'content': 456},
# type(json): <class 'dict'>
return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

requests.post(url, files=files)

requests.post 方法的 files 参数用于发送文件数据到服务端,通常是用于上传文件的场景。files 参数需要传入一个字典,其中每个关键字是字段名,每个值是包含 (文件名,文件对象,文件类型) 的三元组。files 参数的基本格式如下:

1
files = {'field_name': (file_name, file_object, file_type)}
  • field_name 对应于 flask.request.files.keys() 中的关键字,以及 flask.request.files['field_name'].name 中的 name 字段;
  • file_name 对应于 flask.request.files['field_name'].filename 中的 filename 字段;
  • file_object 为文件的二进制流对象,例如通过 open('file.txt', 'rb') 打开的 <class '_io.TextIOWrapper'> 类型文件。
  • file_type 用于指定文件的类型,例如 text/plainimage/jpg 等,其对应于 flask.request.files['field_name'].context_type 中的 content_type 字段

如下是一个使用 files 参数的 requests.post 请求的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""client.py"""
import requests

file = open(file="./image.jpg", mode='rb')
url = "http://127.0.0.1:5000/receive"
files = {"file": ("image.jpg", file, "image/jpg")}
response = requests.post(url=url, files=files, timeout=3)
file.close()

print(f"response: {response},\n"
f"response.status_code: {response.status_code},\n"
f"type(response): {type(response)},\n"
f"response.json(): {response.json()},\n"
f"response.request.headers['Content-Type']: {response.request.headers['Content-Type']}")
# response: <Response [200]>, type: <class 'requests.models.Response'>, json: {'success': True}
# response: <Response [200]>,
# response.status_code: 200,
# type(response): <class 'requests.models.Response'>,
# response.json(): {'success': True},
# response.request.headers['Content-Type']: multipart/form-data; boundary=2350841a2ff6ebd663351868c181b2fe

在 Flask 中使用 flask.request.files 属性方法可以获得对应的 files 数据,使用 flask.request.files["field_name"] 可以获得对应文件的二进制流数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""server.py"""
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

files = request.files
file = request.files["file"]

print(f"files: {files},\n"
f"type(files): {type(files)}")
# files: ImmutableMultiDict([('file', <FileStorage: 'image.jpg' ('image/jpg')>)]),
# type(files): <class 'werkzeug.datastructures.structures.ImmutableMultiDict'>

print(f"file: {file},\n"
f"type(file): {type(file)},\n"
f"file.name: {file.name},\n"
f"file.filename: {file.filename},\n"
f"file.content_type: {file.content_type}")
# file: <FileStorage: 'image.jpg' ('image/jpg')>,
# type(file): <class 'werkzeug.datastructures.file_storage.FileStorage'>,
# file.name: file,
# file.filename: image.jpg,
# file.content_type: image/jpg

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

如何处理 FileStorage 类型的对象

使用 FileStoreage.read() 方法可以将 FileStorage 类型的对象转换成 bytes 类型的字节流数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""server.py"""
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

files = request.files
print(f"files: {files},\ntype(files): {type(files)}")
# files: ImmutableMultiDict([('file', <FileStorage: 'image.jpg' (None)>)]),
# type(files): <class 'werkzeug.datastructures.structures.ImmutableMultiDict'>

file = files["file"]
print(f"file: {file},\n"
f"type(file): {type(file)}")
# file: <FileStorage: 'image.jpg' ('image/jpg')>,
# type(file): <class 'werkzeug.datastructures.file_storage.FileStorage'>,

file_bytes = file.read()
print(f"file_bytes: {file_bytes},\n"
f"type(file_bytes): {type(file_bytes)}")
# file_bytes: b'\xff\xd8\xff...\xe0C\xff\xd9',
# type(file_bytes): <class 'bytes'>

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

data 和 json 的区别

data 与 json 参数的区别:

  • data 发送的是表单数据,通常编码为 application/x-www-form-urlencoded
  • json 发送的是 JSON 数据,自动编码为 application/json

使用 data 参数传输 int 和 list 类型的数据时,服务端调用 request.form 得到的都是 str 类型数据

1
2
3
4
5
6
7
8
9
10
11
"""client.py"""
...
response = requests.post(url=url, data={"num": 123, "array": [1, 2, 3]})

"""server.py"""
...
form = request.form
print(f"form: {form},\ntype: {type(form['num'])}, {type(form['array'])}")
# form: ImmutableMultiDict([('num', '123'), ('array', '1'), ('array', '2'), ('array', '3')]),
# type: <class 'str'>, <class 'str'>
...

使用 request.form.getlist(“key”) 方法可以得到 list 类型,但其中的元素依然是 str 类型

1
2
3
4
5
6
7
8
9
...
form = request.form
print(f"form: {form},\n"
f"form.get: {form.get('num')}, type: {type(form.get('num'))},\n"
f"form.getlist: {form.getlist('array')}, type: {type(form.getlist('array'))}")
# form: ImmutableMultiDict([('num', '123'), ('array', '1'), ('array', '2'), ('array', '3')]),
# form.get: 123, type: <class 'str'>,
# form.getlist: ['1', '2', '3'], type: <class 'list'>
...

使用 json 参数传输 int 和 list 类型的数据时,服务端调用 request.json 得到的将是对应的类型数据

1
2
3
4
5
6
7
8
9
10
11
"""client.py"""
...
response = requests.post(url=url, json={"num": 123, "array": [1, 2, 3]})

"""server.py"""
...
json = request.json
print(f"json: {json},\ntype: {type(json['num'])}, {type(json['array'])}")
# json: {'num': 123, 'array': [1, 2, 3]},
# type: <class 'int'>, <class 'list'>
...

此外不能同时使用 data 和 json 参数,如果同时指定 data 和 json 参数,requests.post 会忽略 data 参数,只处理 json。

手动指定 Content-Type

如果使用 data 并希望发送 JSON 数据,需要手动设置 Content-Type 并将数据序列化:

1
2
3
4
5
import json
url = "..."
payload = {"key1": "value1", "key2": "value2"}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(payload), headers=headers)

Python 如何处理 bytes 类型数据

针对文本类型数据

1
2
3
file_bytes = file.read()
decoded_bytes = file_bytes.decode('utf-8')
print(decoded_bytes)

针对图片类型数据

针对图片类型数据,可以通过 numpy.frombuffer 将数据转换成 ndarray 格式,再通过 cv2.imdecode 对数据进行解码,转换成图片对应的 ndarray 类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"""server.py"""
from flask import Flask, request, jsonify
import numpy as np
import cv2

app = Flask(__name__)

@app.route("/receive", methods=["POST"])
def receive():

files = request.files
print(f"files: {files},\ntype(files): {type(files)}")
# files: ImmutableMultiDict([('file', <FileStorage: 'image.jpg' (None)>)]),
# type(files): <class 'werkzeug.datastructures.structures.ImmutableMultiDict'>

file = files["file"]
print(f"file: {file},\n"
f"type(file): {type(file)}")
# file: <FileStorage: 'image.jpg' ('image/jpg')>,
# type(file): <class 'werkzeug.datastructures.file_storage.FileStorage'>,

file_bytes = file.read()
print(f"file_bytes: {file_bytes},\n"
f"type(file_bytes): {type(file_bytes)}, len(file_bytes): {len(file_bytes)}")
# file_bytes: b'\xff\xd8\xff...\xe0C\xff\xd9',
# type(file_bytes): <class 'bytes'>, len(file_bytes): 8824

image = np.frombuffer(file_bytes, np.uint8)
print(f"image.shape: {image.shape}, type(image): {type(image)}, image.dtype: {image.dtype}")
# image.shape: (8824,), type(image): <class 'numpy.ndarray'>, image.dtype: uint8
image = cv2.imdecode(image, cv2.IMREAD_COLOR) # BGR
print(f"image.shape: {image.shape}, type(image): {type(image)}, image.dtype: {image.dtype}")
# image.shape: (193, 261, 3), type(image): <class 'numpy.ndarray'>, image.dtype: uint8

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

注意上面代码中 file_bytes 的长度为 8824,转换成 np.ndarray 格式后的 shape 也为 (8824,),解码后得到的图片形状却为 (193, 261, 3),两者的数据类型都是 np.uint8,所以两者的大小并不匹配,193×261×3 = 151118 ≠ 8824。这是因为 8824 对应的是图片压缩之后的数据,其比实际的图片大小要小,通过 cv2.imdecode 方法解码后还原得到未经压缩的像素矩阵,它的大小要比压缩后的数据流更大。

除了使用 cv2,还可以使用 python 的 io 库 + Pillow 对图片进行解码

1
2
3
4
5
6
7
8
9
10
11
...
bytes = io.BytesIO(file_bytes)
print(f"bytes: {bytes.getvalue()}, type(bytes): {type(bytes)}")
# bytes: b'\xff\xd8\xff...\xe0C\xff\xd9', type(bytes): <class '_io.BytesIO'>
image = Image.open(bytes)
print(f"image.size: {image.size}, type(image): {type(image)}")
# image.size: (261, 193), type(image): <class 'PIL.JpegImagePlugin.JpegImageFile'>
image = np.array(image)
print(f"image.shape: {image.shape}, type(image): {type(image)}, image.dtype: {image.dtype}")
# image.shape: (193, 261, 3), type(image): <class 'numpy.ndarray'>, image.dtype: uint8
...

JavaScript

使用 fetch 方法发送 post 请求

前端的 HTML 和 JavaScript 代码,通过下面的命令调用

1
python -m http.server --bind 0.0.0.0 5001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- import javascript -->
<script src="script.js"></script>
<title>Transfer File</title>
</head>
<body>
<input type="file" id="fileInput" accept="image/*" onchange="transferFile(event)"/>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// script.js
const url = "http://127.0.0.1:5000/receive"

function transferFile(event) {
// Get the file from <input .../> tag
console.log(event.target)
// <input type="file" id="fileInput" accept="image/*" onchange="transferFile(event)">
const imageFile = event.target.files[0];
console.log(imageFile)
// File {name: 'dog.jpg', ..., size: 8824, type: "image/jpeg"}

// Construct a FormData object
const formData = new FormData();
formData.append('imgfile', imageFile);

// Send a post request
fetch(url, {method: 'POST', body: formData})
.then(response => response.json()) // reponse -> response.json() -> jsondata
.then(jsondata => console.log(jsondata)) // log the response
.catch(error => console.error('Error:', error));
}

后端的 Python + Flask 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"""server.py"""
from flask import Flask, request, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route("/receive", methods=["POST"])
def receive():
print(request.headers)

files = request.files
print(f"files: {files},\n"
f"type(files): {type(files)}")
# files: ImmutableMultiDict([('imgfile', <FileStorage: 'dog.jpg' ('image/jpeg')>)]),
# type(files): <class 'werkzeug.datastructures.ImmutableMultiDict'>

file = files["imgfile"]
print(f"file: {file},\n"
f"type(file): {type(file)},\n"
f"file.name: {file.name},\n"
f"file.filename: {file.filename},\n"
f"file.content_type: {file.content_type}")
# file: <FileStorage: 'dog.jpg' ('image/jpeg')>,
# type(file): <class 'werkzeug.datastructures.FileStorage'>,
# file.name: imgfile,
# file.filename: dog.jpg,
# file.content_type: image/jpeg

return jsonify({"success": True})

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)