效率工具

QuillBot 长文处理:按字数限制分块与合并

QuillBot 免费版每次最多改写 125 个词。长文章一段段手动数词又慢又容易出错。 这篇做 chunker.py:把长文按句子切成不超过上限的块,按编号保存;改写完放回原处后,再按原来的段落结构合并。

BINBHO · 图文教程

QuillBot 免费版每次最多改写 125 个词。长文章一段段手动数词又慢又容易出错。 这篇做 chunker.py:把长文按句子切成不超过上限的块,按编号保存;改写完放回原处后,再按原来的段落结构合并。

先看清成品

  • essay.txt(182 个词,2 段)在上限 40 时切成 7 块;合并后词序列和原文完全一致(same words: True)。
  • 修复了缩写断句的 bug:Dr.、Prof.、e.g.、U.S.、approx. 不再被当成句子结尾。
  • pytest 7 项通过。

合规提醒:QuillBot 官方帮助中心明确说明没有提供任何 B2B 或 B2C API,所以本文不自动化操作网页,也不使用非官方接口;本地脚本只处理你自己的文本,粘贴和改写都在官网或官方扩展里手动完成。改写不等于原创:官方帮助(Is using a paraphrasing tool plagiarizing?)说明,改写他人的文字仍要注明出处。学术用途请遵守学校的规定。不共享账号。本文不列价格。

运行说明:本文没有登录或使用 QuillBot。本地脚本和测试都在 box 上真实运行过(Python 3.13)。示例文章、“改写稿”都是我自己写的;改写稿是我手写的、故意带错误的版本,不是 QuillBot 的真实输出。

做完之后你会得到什么?

成果 说明
chunker.py split 切块,生成 001.txt… 和 _layout.txt(记录段落位置)
chunker.py join 合并改写结果,缺块时报错
test_chunker.py 7 项测试

需要准备哪些环境?

项目 说明
Python 3.9 以上(box 上是 3.13),只用标准库;测试需要 pip install pytest
QuillBot 网页版 https://quillbot.com 或官方扩展

关键事实

事实 来源
免费版改写每次最多 125 个词,Premium 不限 免费版和 Premium 的区别
免费版只有 Standard 和 Fluency 两种模式 同上
免费版摘要最多 1,200 个词 What’s the difference between a free and a Premium account?
没有 API Does QuillBot offer an API?

QuillBot 怎样计算词数没有公开(例如带连字符的词算几个),所以默认上限设为 120,留出余量。

文件结构

66-quillbot-A-src/
├── chunker.py
├── essay.txt
└── test_chunker.py

第 1 步:切块脚本

"""把长文按句子切成不超过 N 个词的块,方便分批粘贴进 QuillBot 改写器(免费版每次最多 125 个词,见官方帮助中心)。
默认上限 120:QuillBot 计算词数的方式没有公开,留一点余量。改写完把结果按编号放回 out/,再用 join 合并。"""
import argparse, os, re, sys

SENT = re.compile(r"(?<=[.!?])[\"')\]]*\s+(?=[A-Z0-9\"'(\[])")

ABBR = {"dr.", "prof.", "mr.", "mrs.", "ms.", "st.", "vs.", "etc.", "approx.", "no.", "fig.", "al.", "jr.", "sr.", "inc.", "ltd.", "co."}

def words(s): return len(s.split())

def sentences(para):
    """先按标点切,再把以缩写结尾的片段(Dr.、e.g.、U.S.、单个首字母 J.)和下一段接回去。"""
    out = []
    for piece in SENT.split(para):
        if out:
            last = out[-1].split()[-1].lower()
            if last in ABBR or re.fullmatch(r"(?:[a-z]\.){2,}|[a-z]\.", last):
                out[-1] += " " + piece; continue
        out.append(piece)
    return out

def split_long(sentence, limit):
    """单句超过上限时,按分号、逗号再切;仍然太长就硬切。"""
    parts, cur = [], ""
    for piece in re.split(r"(?<=[;,])\s+", sentence):
        cand = (cur + " " + piece).strip()
        if words(cand) <= limit: cur = cand; continue
        if cur: parts.append(cur)
        while words(piece) > limit:
            w = piece.split(); parts.append(" ".join(w[:limit])); piece = " ".join(w[limit:])
        cur = piece
    if cur: parts.append(cur)
    return parts

def chunk(text, limit=120):
    chunks = []
    for para in re.split(r"\n\s*\n", text.strip()):
        cur = ""
        for s in sentences(para.replace("\n", " ").strip()):
            for p in (split_long(s, limit) if words(s) > limit else [s]):
                cand = (cur + " " + p).strip()
                if words(cand) <= limit: cur = cand
                else: chunks.append(cur); cur = p
        if cur: chunks.append(cur)
        chunks.append("")                       # 段落分隔
    return [c for c in chunks[:-1]]

def main(argv=None):
    ap = argparse.ArgumentParser(); sub = ap.add_subparsers(dest="cmd", required=True)
    s = sub.add_parser("split"); s.add_argument("src"); s.add_argument("--limit", type=int, default=120); s.add_argument("--out", default="chunks")
    j = sub.add_parser("join"); j.add_argument("dir"); j.add_argument("--out", default="joined.txt")
    a = ap.parse_args(argv)
    if a.cmd == "split":
        os.makedirs(a.out, exist_ok=True); n = 0; layout = []
        for c in chunk(open(a.src, encoding="utf-8").read(), a.limit):
            if c == "": layout.append("PARA"); continue
            n += 1; open(f"{a.out}/{n:03}.txt", "w", encoding="utf-8").write(c + "\n"); layout.append(f"{n:03}")
            print(f"{n:03}  {words(c):>3} 词  {c[:60]}…")
        open(f"{a.out}/_layout.txt", "w").write("\n".join(layout) + "\n")
        print(f"共 {n} 块,上限 {a.limit} 词")
    else:
        layout = open(f"{a.dir}/_layout.txt").read().split(); paras, cur, missing = [], [], []
        for item in layout:
            if item == "PARA": paras.append(" ".join(cur)); cur = []; continue
            p = f"{a.dir}/{item}.txt"
            if not os.path.exists(p): missing.append(item); continue
            cur.append(open(p, encoding="utf-8").read().strip())
        paras.append(" ".join(cur))
        if missing:
            print("缺少这些块的改写结果:", ", ".join(missing)); return 1
        open(a.out, "w", encoding="utf-8").write("\n\n".join(paras) + "\n"); print("写入", a.out)
    return 0

if __name__ == "__main__":
    sys.exit(main())

第 2 步:示例文章(我自己写的)

Remote work changed how small teams communicate. In 2019, most of the teams we surveyed met in person at least three times a week; by 2023, that number had fallen to once a week. Managers reported that written updates became more important, while spontaneous hallway conversations largely disappeared. Some teams responded by scheduling short daily check-ins. Others relied on shared documents and asynchronous comments, which worked well for focused tasks but poorly for resolving disagreements quickly.

The survey also asked about tools. Teams that used a single shared workspace reported fewer missed deadlines than teams that spread their work across many apps. However, the difference was small, and the sample of 48 teams is not large enough to draw firm conclusions. Further research should track the same teams over a longer period, compare different industries, and measure outcomes such as employee retention, customer satisfaction, and the time it takes new members to become fully productive in their roles, because these effects may only appear after several months of consistent practice and careful observation by researchers who understand the context of each organization.

第 3 步:测试

import os, pytest
import chunker as C

def test_abbreviations_not_split():
    # 调试时发现:第一版在 Dr. / Prof. / e.g. / U.S. / approx. 后面都断句了
    t = "We interviewed Dr. Smith and Prof. Lee about it. Many tools, e.g. Slack, were used by the U.S. Army in 2020. Results varied by approx. 12 percent. It ended."
    assert C.sentences(t) == ["We interviewed Dr. Smith and Prof. Lee about it.",
                              "Many tools, e.g. Slack, were used by the U.S. Army in 2020.",
                              "Results varied by approx. 12 percent.", "It ended."]

@pytest.mark.parametrize("limit", [20, 40, 120])
def test_limit_and_roundtrip(limit, tmp_path):
    text = open("essay.txt", encoding="utf-8").read()
    chunks = C.chunk(text, limit)
    assert all(C.words(c) <= limit for c in chunks if c)
    assert " ".join(c for c in chunks if c).split() == text.split()

def test_split_join_cli(tmp_path):
    out = tmp_path / "c"
    C.main(["split", "essay.txt", "--limit", "40", "--out", str(out)])
    assert C.main(["join", str(out), "--out", str(tmp_path / "j.txt")]) == 0
    j = (tmp_path / "j.txt").read_text(encoding="utf-8")
    assert j.count("\n\n") == 1 and j.split() == open("essay.txt", encoding="utf-8").read().split()

def test_join_reports_missing(tmp_path):
    out = tmp_path / "c"; C.main(["split", "essay.txt", "--limit", "40", "--out", str(out)])
    os.remove(out / "003.txt")
    assert C.main(["join", str(out), "--out", str(tmp_path / "j.txt")]) == 1

def test_very_long_sentence_hard_split():
    s = " ".join(["word"] * 300) + "."
    assert [C.words(c) for c in C.chunk(s, 125)] == [125, 125, 50]

第 4 步:在 QuillBot 里逐块改写(手动)

  1. 打开 chunks/001.txt,复制,粘贴到 Paraphraser 左侧。
  2. 改写后检查每一句,把结果覆盖保存回 001.txt。
  3. 对每个块重复以上步骤,然后运行 join。

(本文没有实际操作这一步。)

第 5 步:运行

$ python chunker.py split essay.txt --limit 40 --out c40
001   34 词  Remote work changed how small teams communicate. In 2019, mo…
002   22 词  Managers reported that written updates became more important…
003   20 词  Others relied on shared documents and asynchronous comments,…
004   26 词  The survey also asked about tools. Teams that used a single …
005   19 词  However, the difference was small, and the sample of 48 team…
006   37 词  Further research should track the same teams over a longer p…
007   24 词  because these effects may only appear after several months o…
共 7 块,上限 40 词
$ python chunker.py join c40 --out joined.txt
写入 joined.txt
$ python -m pytest -q
7 passed

怎么检查结果?

  • 每块的词数不超过上限。
  • 合并前删掉某一块,join 会报出缺少的编号,并返回退出码 1。
  • 第 007 块以小写的 because 开头:这是一个超长句按逗号切开的结果。改写时把 006 和 007 连起来读,确认意思连贯。

调试示例

1. 缩写被当成句子结尾。 第一版只用正则“句号 + 空格 + 大写字母”断句。用下面这句测试:

We interviewed Dr. Smith and Prof. Lee about it. Many tools, e.g. Slack, were used by the U.S. Army in 2020. Results varied by approx. 12 percent.

结果被切成 We interviewed Dr. / Smith and Prof. / Lee about it. / … 共 9 段。这些碎片如果分到不同的块里,QuillBot 改写的就是半句话。 修复方法:切完后检查每一段的最后一个词,如果是缩写表里的词、e.g./U.S. 这类带点的形式,或者单个首字母,就和下一段接回去。这个例子已加入回归测试。

2. 我数错了,不是程序错。 我一度以为第 002 块(22 词)应该能再装下一句,于是打印每句的词数:14 + 8 = 22,下一句 20 词,加上就超过 40。程序是对的。

常见问题

现象 原因 处理
粘贴后 QuillBot 提示超出字数 QuillBot 的计数方式和本工具不同 把 --limit 调小到 100
合并后段落没了 _layout.txt 被删除或修改 重新运行 split
某块以小写开头 超长句被按逗号切开 改写时连着前一块一起看
join 报缺块 某块的改写结果没保存 补上对应编号的文件

模板规则

  • 块的边界尽量落在句子上,不切断句子。
  • 保留 _layout.txt,合并时才能恢复段落。
  • 逐块核对意思(见 B 篇),再合并。

项目资源

用途 地址
Does QuillBot offer an API? https://help.quillbot.com/hc/en-us/articles/4541472549527-Does-Quillbot-Offer-an-API
免费版和 Premium 的区别(改写器) https://help.quillbot.com/hc/en-us/articles/35855733045143-What-is-the-difference-between-free-and-Premium-in-the-QuillBot-Paraphraser

切块解决的是字数限制的问题;改写后的意思还要自己逐句核对。