无网络数据传输方案
前言
- 在日常生活中,传输数据的方式有很多种。其中最普遍的就是网络传输,流媒体平台、局域网联机、面对面互传文件、蓝牙文件传输等都属于网络传输。也有一些无需网络的方式,比如常见的NFC,二维码等。但是NFC能够存储的数据量非常有限,在50字节左右。相比之下二维码容量更大,最大为2048字节,但是也不够存储音视频文件。
- 现在我们有三个问题:
- 用什么存储
- 如何编码文件
- 如何解码文件
- 先回答第一个,我们使用二维码存储数据(如果有其它更合适的也可以替换)。下面我们研究其它两个问题。
如何编码文件
- 在选择了二维码后,第二个问题就出现了,我们的二维码不足以编码文件,我们很自然可以想到拆分文件,每个片段生成一个二维码。这是传统网络传输会做的事情,在拆分后会出现片段乱序、片段缺失等问题,因此我们还需要一些校验信息和文件信息。这样我们就知道编码要怎么做了。
文件信息
- 我们可以在第一张二维码中存储文件信息,包括文件名、文件类型、文件大小、片段数等,我们还可以用一个signal字段区分文件信息二维码和文件内容二维码。
- 下面是文件信息二维码存储的内容:
{
'num': 0,
'signal': '<start>',
'content': file.name,
'filesize': file.size,
'n_chunks': n_chunks,
}
- 而文件片段存储的信息如下:
{
'num': idx,
'signal': '<content>',
'content': base64str
}
生成二维码
- 下面我们简单看一下二维码生成的代码。这里使用qrcode模块:
pip install qrcode
- 我们不能直接序列化带二进制内容的json,因此将二进制内容转换成base64,代码如下:
import json
import base64
import qrcode
chunk = {
'num': 0,
'signal': '<content>',
'content': base64.b64encode(b'xxx').decode('utf-8')
}
data = json.dumps(chunk)
qrcode.make(data).save('test.jpg')
对文件编码
- 下面我们对文件拆分并编码。这里我们每次读取固定大小的片段并编码成二维码:
import os
import json
import math
import qrcode
import base64
from tqdm import tqdm
# 文件信息
chunk_size = 1024 + 512
file_path = '111.mp3'
file_size = os.path.getsize(file_path)
chunks = math.ceil(file_size / chunk_size)
# 创建保存二维码的目录
os.makedirs('qrs', exist_ok=True)
qrcode.make(json.dumps({
'num': 0,
'signal': '<start>',
'content': '111.mp3',
'filesize': file_size,
'n_chunks': chunks
})).save(f'qrs/0.jpg')
with open(file_path, 'rb') as f:
pbar = tqdm(total=chunks)
while buffer := f.read(chunk_size):
qrcode.make(json.dumps({
'num': pbar.n + 1,
'signal': '<content>',
'content': base64.b64encode(buffer).decode('utf-8')
})).save(f'qrs/{pbar.n + 1}.jpg')
pbar.update(1)
编写UI
- 下面我们用streamlit编写一个简单的页面,streamlit的使用参见官网:streamlit.io/
- 代码如下:
# run with
# streamlit run app.py --server.enableXsrfProtection false
import json
import math
import uuid
import base64
from pathlib import Path
import qrcode
import streamlit as st
chunk_size = 1024 + 512
n_cols = 2
col_size = 350
tab1, tab2 = st.tabs(['上传文件', '下载文件'])
with tab1:
st.header('上传文件')
if file := st.file_uploader('上传文件进行编码'):
tmp_filename = f'{uuid.uuid4().hex}.{file.name.split(".")[-1]}'
# 创建目录
n_chunks = math.ceil(file.size / chunk_size)
dst_path = Path('qrs') / Path(file.name).stem
dst_path.mkdir(exist_ok=True, parents=True)
idx = 0
# 编码文件的meta信息
qrcode.make(
json.dumps({
'num': idx,
'signal': '<start>',
'content': file.name,
'filesize': file.size,
'n_chunks': n_chunks,
}, ensure_ascii=False)
).save(dst_path / f'{idx:08}.jpg')
idx += 1
pbar = st.progress(0 / n_chunks, text='Encoding File')
while buffer := file.read(chunk_size):
base64buffer = base64.b64encode(buffer).decode('utf-8')
qrcode.make(json.dumps({
'num': idx,
'signal': '<content>',
'content': base64buffer
})).save(dst_path / f'{idx:08}.jpg')
pbar.progress(idx / n_chunks)
idx += 1
st.success('Upload Success!')
with tab2:
st.header('下载文件')
dirs = Path('qrs').iterdir()
if d := st.selectbox('Choose the file', dirs):
cols = st.columns(n_cols)
for idx, file in enumerate(Path(d).glob('*.[jpJP][pnPN][gG]')):
cols[idx % n_cols].image(str(file), width=col_size)
cols[idx % n_cols].text(idx)
- 上面我们创建了两个tab,分别是上编码文件和下载文件。界面效果如下:
如何解码文件
- 正常情况下,我们不能通过扫描下载一个文件,扫码只返回一串字符串。如果想通过扫描下载文件,则需要编写一个特殊的客户端。在知道编码过程后,解码过程就简单了。
识别二维码
- 解码正常的做法是编写一个手机app,调用摄像头完成,这里为了方便就编写py脚本通过读取本地文件完成,我们先看识别二维码的操作。这里需要使用pyzbar模块:
pip install pyzbar
pip install opencv-python
- 识别代码如下:
import cv2
from pyzbar import pyzbar
img = cv2.imread('')
decoded = pyzbar.decode(img)[0].data
解码文件
- 下面就是从二维码中解码出文件。代码如下:
import json
import base64
from pathlib import Path
import cv2
from pyzbar import pyzbar
filename = ''
n_chunks = -1
contents = []
file_generator = Path('qrs/545bc9adcc8e48a88c45d4a1305b5e47').glob('*.jpg')
while file := next(file_generator):
if n_chunks != -1 and n_chunks == len(contents):
with open(filename, 'ab') as fp:
contents.sort(key=lambda x: int(x['num']))
for data in contents:
fp.write(data['content'])
break
img = cv2.imread(str(file))
data = json.loads(pyzbar.decode(img)[0].data)
if data['signal'] == '<start>':
filename = data['content']
n_chunks = data['n_chunks']
else:
data['content'] = base64.b64decode(data['content'].encode('utf-8'))
contents.append(data)
讨论
改进
- 在多数情况下,用二维码存储文件不是一个好的选择,存储一个2秒的音频需要大概70张二维码。占用的空间远比音频本身大,而且在编码解码过程要花费更多时间。对此我们有一些可以改进的地方。
- 1)压缩文件
- 第一件可以尝试做的事情就是压缩文件,比如图像可以采用JPEG格式。
- (2)动图存储
- 第二个尝试可以是将二维码序列转换成GIF,这样更多是方便管理二维码序列。
- (3)改进压缩算法
- 在我们的例子中,我们已经不能直接用常规二维码扫描算法得出文件结果,因此我们也可以不遵守二维码编码规则,定义一种压缩度更高的编码算法。具体可以参考:github.com/sz3/libcimb…
应用
- 从上面的结果来看,二维码传输方案似乎不是很实用,但是还是有一些新奇的用法的。
- 在纸质书中,我们的想法是只能传达文字、图像信息,当我们提到文字可以传递音频信息时,会想到下面的例子:
你干嘛~哎哟
鸡你太美
- 但是这种音频只存在我们的想象。现在利用二维码编码音频,打印到书本上,通过扫码我们就可以真正听到声音。同样我们还可以存储视频等信息。基于这一想法,我们可以做出许多有趣的东西。