鸽园
  • Home
  • Categories
    • Coding
      • Python
      • C++
      • Shell
      • MATLAB
    • Products
      • Qzone2TG
  • About
  • GitHub
  • Donate

Published on 22/07/31

re.sub如何异步替换?

two white flying rockets during daytime
Async @spacex

这是 解析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。但如果存在较多表情码,异步的优势仍能显现。

  • Coding
  • Python
  • Qzone2TG
知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
Powered by Publii, Copyright (c) 2022-2026 JamzumSum