效率工具

用 QuillBot 改写来源材料后,常见的两个问题是:句子仍然和原文几乎一样,或者忘了注明出处

用 QuillBot 改写来源材料后,常见的两个问题是:句子仍然和原文几乎一样,或者忘了注明出处。 这篇做 sourceoverlap.py:逐段计算改写稿和来源的最长相同词串、三元组重合率,并检查是否有引用标记。

BINBHO · 图文教程

用 QuillBot 改写来源材料后,常见的两个问题是:句子仍然和原文几乎一样,或者忘了注明出处。 这篇做 source_overlap.py:逐段计算改写稿和来源的最长相同词串、三元组重合率,并检查是否有引用标记。

先看清成品

  • 示例的 4 段中:第 1 段有引用,但仍然连续 16 个词和原文相同,被报出;第 2 段原文放在引号里,通过;第 3 段是自己的内容,通过;第 4 段 9 个词相同且没有引用,报出 3 个问题。
  • 把最长串算法从完整的动态规划改为位置索引:20,000 词来源 × 30 段,耗时从 3.3 秒降到 0.20 秒;用 200 组随机数据和暴力算法对照,结果一致。
  • pytest 3 项通过。

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

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

做完之后你会得到什么?

成果 说明
source_overlap.py 逐段报告;有问题时退出码为 1
source.txt、draft.txt 示例来源和草稿(我自己写的)
test_overlap.py 3 项测试

需要准备哪些环境?

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

关键事实

事实 来源
改写他人的文字仍需要注明出处 Is using a paraphrasing tool plagiarizing?
QuillBot 有引用生成器(APA、MLA 等) QuillBot 官网工具列表

阈值(连续 8 个词、三元组重合 50%)是我定的自查标准,不是 QuillBot、学校或查重系统的标准。可以用 --run-max 和 --overlap-max 调整。

文件结构

66-quillbot-C-src/
├── source_overlap.py
├── source.txt
├── draft.txt
└── test_overlap.py

第 1 步:检查脚本

"""检查改写稿和来源文本还有多接近,以及每段有没有标注来源。
QuillBot 官方帮助(Is using a paraphrasing tool plagiarizing?)说明:改写别人的文字仍需要注明出处。
规则(阈值可调):
- 引号外和来源连续相同 ≥ 8 个词:需要加引号,或改成自己的表达
- 词三元组重合率 ≥ 50%:结构和原文太像
- 段落用到了来源,但没有引用标记:需要加引用
这是自查工具,不等同于 Turnitin 等查重系统的结果。"""
import argparse, json, re, sys

CITE = re.compile(r"\([A-Z][A-Za-z\-]+(?: et al\.)?,? \d{4}[a-z]?\)|\[\d+\]")
QUOTE = re.compile(r"[\"“][^\"”]*[\"”]")

def words(t): return re.findall(r"[a-z0-9]+(?:'[a-z]+)?", t.lower())

def longest_run(a, b):
    """a、b 两个词序列的最长公共连续片段。用 b 的词位置索引,只在相同词的位置上递推(比完整的 n×m 动态规划快)。"""
    pos = {}
    for j, w in enumerate(b): pos.setdefault(w, []).append(j)
    best, end, prev = 0, 0, {}
    for i, w in enumerate(a):
        cur = {}
        for j in pos.get(w, ()):
            k = prev.get(j - 1, 0) + 1; cur[j] = k
            if k > best: best, end = k, i + 1
        prev = cur
    return best, " ".join(a[end - best:end])

def trigram_overlap(a, b):
    A = {tuple(a[i:i + 3]) for i in range(len(a) - 2)}; B = {tuple(b[i:i + 3]) for i in range(len(b) - 2)}
    return len(A & B) / len(A) if A else 0.0

def check(source, draft, run_max=8, overlap_max=0.5):
    src = words(source); out = []
    for n, para in enumerate([p for p in re.split(r"\n\s*\n", draft.strip()) if p.strip()], 1):
        cited = bool(CITE.search(para)); unq = words(QUOTE.sub(" ", para))
        run, text = longest_run(unq, src); ov = trigram_overlap(unq, src)
        issues = []
        if run >= run_max: issues.append(f"引号外与来源连续 {run} 个词相同:“{text}”")
        if ov >= overlap_max: issues.append(f"词三元组重合 {ov:.0%},结构和原文太像")
        uses_source = run >= 4 or ov >= 0.15
        if uses_source and not cited: issues.append("用到了来源内容,但没有引用标记")
        out.append({"paragraph": n, "longest_run": run, "trigram_overlap": round(ov, 2), "cited": cited, "issues": issues})
    return out

def main(argv=None):
    ap = argparse.ArgumentParser(); ap.add_argument("source"); ap.add_argument("draft")
    ap.add_argument("--run-max", type=int, default=8); ap.add_argument("--overlap-max", type=float, default=0.5)
    a = ap.parse_args(argv)
    res = check(open(a.source, encoding="utf-8").read(), open(a.draft, encoding="utf-8").read(), a.run_max, a.overlap_max)
    for r in res:
        print(f"第 {r['paragraph']} 段:最长相同 {r['longest_run']} 词,三元组重合 {r['trigram_overlap']:.0%},引用 {'有' if r['cited'] else '无'}")
        for i in r["issues"]: print("   -", i)
    return 1 if any(r["issues"] for r in res) else 0

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

第 2 步:示例来源和草稿

来源:
Spaced repetition is a learning technique in which material is reviewed at gradually increasing intervals. Studies have found that learners who space their practice over several days remember more after a month than learners who study the same material in a single session. The effect is strongest for factual knowledge such as vocabulary, dates, and definitions.

草稿:
Reviewing material at gradually increasing intervals is known as spaced repetition. Learners who space their practice over several days remember more after a month than learners who cram (Kim, 2022).

According to Kim, the benefit is largest for facts like vocabulary and dates, and "the effect is strongest for factual knowledge" (Kim, 2022).

In my own class, I tried weekly review quizzes, and students said the quizzes helped them prepare for the final exam.

The technique works best for factual knowledge such as vocabulary, dates, and definitions.

第 3 步:测试

import random, source_overlap as S

SRC = open("source.txt", encoding="utf-8").read(); DRAFT = open("draft.txt", encoding="utf-8").read()

def brute(a, b):
    best = 0
    for i in range(len(a)):
        for j in range(len(b)):
            k = 0
            while i + k < len(a) and j + k < len(b) and a[i + k] == b[j + k]: k += 1
            best = max(best, k)
    return best

def test_fast_matches_bruteforce():
    random.seed(1)
    for _ in range(200):
        a = [random.choice("abcd") for _ in range(random.randint(0, 30))]; b = [random.choice("abcd") for _ in range(random.randint(0, 30))]
        assert S.longest_run(a, b)[0] == brute(a, b)

def test_planted_paragraphs():
    r = S.check(SRC, DRAFT)
    assert r[0]["longest_run"] == 16 and r[0]["cited"]            # 抄了长句:有引用也要报
    assert r[1]["issues"] == []                                     # 引号内的原文不计入
    assert r[2]["issues"] == []                                     # 自己的内容
    assert "用到了来源内容,但没有引用标记" in r[3]["issues"]

def test_quotes_excluded():
    q = 'He wrote "learners who space their practice over several days remember more after a month" (Kim, 2022).'
    assert S.check(SRC, q)[0]["issues"] == []

第 4 步:运行

$ python source_overlap.py source.txt draft.txt
第 1 段:最长相同 16 词,三元组重合 57%,引用 有
   - 引号外与来源连续 16 个词相同:“learners who space their practice over several days remember more after a month than learners who”
   - 词三元组重合 57%,结构和原文太像
第 2 段:最长相同 2 词,三元组重合 0%,引用 有
第 3 段:最长相同 1 词,三元组重合 0%,引用 无
第 4 段:最长相同 9 词,三元组重合 64%,引用 无
   - 引号外与来源连续 9 个词相同:“for factual knowledge such as vocabulary dates and definitions”
   - 词三元组重合 64%,结构和原文太像
   - 用到了来源内容,但没有引用标记
$ python -m pytest -q
3 passed

怎么检查结果?

  • 第 1 段说明:即使有引用,大段照搬也要加引号或重写。
  • 第 2 段说明:引号内的原文不计入相同词串。
  • 处理完所有问题后再运行一次,退出码应为 0。

调试示例

算法太慢。 第一版用标准的 n×m 动态规划求最长公共词串。测试规模为 20,000 词的来源加 30 段、每段 150 词,耗时 3.3 秒;按这个速度,书籍规模的来源要半分钟以上。

改进:先给来源建立“词 → 出现位置”的索引,只在相同词的位置上递推,耗时降到 0.20 秒。

为了确认改动没有改变结果,测试 test_fast_matches_bruteforce 用 200 组随机序列,和逐位置比较的暴力算法对照,结果全部一致。

常见问题

现象 原因 处理
有引用仍然报错 大段照搬原文 加引号,或用自己的话重写
自己的段落被报“用到了来源” 和来源有通用短语 调整阈值,或忽略这条提醒
引用格式没被识别 只支持 (Author, 2020) 和 [1] 两种 按需修改 CITE 正则
查重系统结果和本工具不同 算法和比对库不同 以学校指定的系统为准

模板规则

  • 来源的原句要么加引号,要么彻底重写。
  • 用了来源的段落必须带引用。
  • 本工具只用于自查,结果不能代替正式查重。

项目资源

用途 地址
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
Is using a paraphrasing tool plagiarizing? https://help.quillbot.com/hc/en-us/articles/35854878046615-Is-using-a-paraphrasing-tool-plagiarizing

改写之后再查一遍和来源的距离,并补上引用,才是负责任地使用改写工具。