re.sub如何异步替换?
这是
解析Qzone3TG系列的第二篇。代码见 aioqzone-feed/api/emoji.py
Motivation
现有如下需求:
这是一段[em]e100[/em]富文本[em]e101[/em],是emotion_cgi_msgdetail_v6的返回[em]102[/em]
需要将上述文本中的表情码替换为可读的文字或 emoji。可以直观判断需要使用正则表达式。正则很容易编写:\[em\]e(\d+)\[/em\],然后通过 re.sub 的 repl 参数实现查询调用:
def sync_query(eid: int) -> str | None: ...
re.sub(r"\[em\]e(\d+)\[/em\]", lambda m: sync_query(int(m.group(1))) or m.group(0), text)
然而实际情况往往与预期不同。例如,上述查询函数并非同步函数。
事实上,由于 QzEmoji 的 async 分支使用 sqlalchemy + aiosqlite,异步查询是笔者的本意。那么,如何让 re.sub 支持一个异步的替换函数呢?
Solution
实际上,无法让 re.sub 直接支持异步的 repl。但可以自行实现一个 sub 函数。
r, tasks = [], []
base = 0
for i, m in enumerate(pattern.finditer(text)):
fr, to = m.span(0)
r.append(text[base:fr]) # save un-replaced string as is
task = asyncio.create_task(repl(m))
task.add_done_callback(lambda t, idx=i: r.__setitem__(idx, r[idx] + t.result()))
tasks.append(task)
base = to
r.append(text[base:])
使用 re.finditer 找出所有表情码的位置。对于每个表情码 $e_i = text[from_i, to_i]$,其 $from_i$ 到上一个表情码的 $to_{i-1}$ 之间的串 $p_i = text[to_{i-1}, from_i]$ 无需修改,直接保留即可,记作 $p_i$;每个表情码对应一个异步查询任务,其完成回调为 $p_i$ 追加查询结果,即 $p_i += query(e_i)$。最后,最后一个表情码之后可能也存在无需修改的文字 $p_{n+1} = text[to_n:]$。
需要将所有生成的任务保存起来,然后执行 asyncio.wait(tasks) 等待所有任务结束。随后将 $p_0, … p_{n+1}$ 拼接即可:
if tasks: await asyncio.wait(tasks)
return "".join(r)
后记
关于性能,笔者并未进行过验证。然而根据经验推测,当文本中仅有一两个表情码时,异步 sub 的速度不及 re.sub。但如果存在较多表情码,异步的优势仍能显现。
