无网络数据传输方案
前言
-
在日常生活中,传输数据的方式有很多种。其中最普遍的就是网络传输,流媒体平台、局域网联机、面对面互传文件、蓝牙文件传输等都属于网络传输。也有一些无需网络的方式,比如常见的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…
应用
-
从上面的结果来看,二维码传输方案似乎不是很实用,但是还是有一些新奇的用法的。
-
在纸质书中,我们的想法是只能传达文字、图像信息,当我们提到文字可以传递音频信息时,会想到下面的例子:
-
你干嘛~哎哟 鸡你太美
-
但是这种音频只存在我们的想象。现在利用二维码编码音频,打印到书本上,通过扫码我们就可以真正听到声音。同样我们还可以存储视频等信息。基于这一想法,我们可以做出许多有趣的东西。