无网络数据传输方案

前言

  • 在日常生活中,传输数据的方式有很多种。其中最普遍的就是网络传输,流媒体平台、局域网联机、面对面互传文件、蓝牙文件传输等都属于网络传输。也有一些无需网络的方式,比如常见的NFC,二维码等。但是NFC能够存储的数据量非常有限,在50字节左右。相比之下二维码容量更大,最大为2048字节,但是也不够存储音视频文件。
  • 现在我们有三个问题:
  1. 用什么存储
  2. 如何编码文件
  3. 如何解码文件
  • 先回答第一个,我们使用二维码存储数据(如果有其它更合适的也可以替换)。下面我们研究其它两个问题。

如何编码文件

  • 在选择了二维码后,第二个问题就出现了,我们的二维码不足以编码文件,我们很自然可以想到拆分文件,每个片段生成一个二维码。这是传统网络传输会做的事情,在拆分后会出现片段乱序、片段缺失等问题,因此我们还需要一些校验信息和文件信息。这样我们就知道编码要怎么做了。

文件信息

  • 我们可以在第一张二维码中存储文件信息,包括文件名、文件类型、文件大小、片段数等,我们还可以用一个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…

应用

  • 从上面的结果来看,二维码传输方案似乎不是很实用,但是还是有一些新奇的用法的。
  • 在纸质书中,我们的想法是只能传达文字、图像信息,当我们提到文字可以传递音频信息时,会想到下面的例子:
你干嘛~哎哟
鸡你太美
  • 但是这种音频只存在我们的想象。现在利用二维码编码音频,打印到书本上,通过扫码我们就可以真正听到声音。同样我们还可以存储视频等信息。基于这一想法,我们可以做出许多有趣的东西。