改写最大的风险是意思变了却没发现:12% 变成 21%,“不应该”变成“应该”,may 被删掉,引用标记丢了。 这篇做
drift_check.py:对比原文和改写稿中的数字、否定词、情态动词、专有名词、引用标记和冻结词,列出所有差异。
先看清成品
- 我手写的“坏改写稿”里埋了 5 处错误,全部被找出:12%→21%、not 丢失、may 丢失、[3] 丢失、冻结词 in person 被改。
- 修复了两类误报:句首大写词被当成专有名词;引用中的数字被重复计算。
- pytest 5 项通过。
合规提醒:QuillBot 官方帮助中心明确说明没有提供任何 B2B 或 B2C API,所以本文不自动化操作网页,也不使用非官方接口;本地脚本只处理你自己的文本,粘贴和改写都在官网或官方扩展里手动完成。改写不等于原创:官方帮助(Is using a paraphrasing tool plagiarizing?)说明,改写他人的文字仍要注明出处。学术用途请遵守学校的规定。不共享账号。本文不列价格。
运行说明:本文没有登录或使用 QuillBot。本地脚本和测试都在 box 上真实运行过(Python 3.13)。示例文章、“改写稿”都是我自己写的;改写稿是我手写的、故意带错误的版本,不是 QuillBot 的真实输出。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
drift_check.py |
列出差异;有差异时退出码为 1 |
original.txt、paraphrase.txt |
示例文本 |
test_drift.py |
5 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.9 以上(box 上是 3.13),只用标准库;测试需要 pip install pytest |
| QuillBot | 网页版 https://quillbot.com 或官方扩展 |
关键事实
| 事实 | 来源 |
|---|---|
| Freeze Words 可以锁定某些词语,改写时不被修改 | Paraphrasing Tool 帮助目录(What is Freeze Words in Quillbot?) |
| 改写结果中可能有错误,需要检查 | Paraphrasing Tool 帮助目录(Why are there errors in my Quillbot results?) |
| 同义词滑块越高,改动越大 | What is the Synonym Slider in Quillbot? |
建议:改写前先在 QuillBot 中用 Freeze Words 锁定术语,改写后再用 --freeze 传入同一份列表核对。
文件结构
66-quillbot-B-src/
├── drift_check.py
├── original.txt
├── paraphrase.txt
└── test_drift.py
第 1 步:检查脚本
"""对比原文和改写稿,找出“意思可能被改了”的地方:数字、否定词、情态动词、专有名词、引用标记、冻结词。
改写工具会换词换句式,但这些元素一变,意思往往就变了。QuillBot 的 Freeze Words 功能可以锁定词语不被改写,
这里的 --freeze 用同样的列表做事后核对。只在本地运行,不调用任何服务。"""
import argparse, json, re, sys
from collections import Counter
NUM = re.compile(r"(?<![\w.])\d+(?:[.,]\d+)*\s?%?")
NEG = {"not", "no", "never", "none", "nobody", "nothing", "neither", "nor", "without", "cannot", "hardly", "rarely"}
MODAL = {"may", "might", "must", "should", "can", "could", "will", "would", "shall"}
CITE = re.compile(r"\([A-Z][A-Za-z\-]+(?: et al\.)?(?:,| and [A-Z][A-Za-z\-]+,) \d{4}[a-z]?\)|\[\d+(?:[,–-]\d+)*\]")
STOP_CAPS = {"The", "A", "An", "In", "On", "At", "However", "This", "That", "These", "Those", "It", "We", "They", "Some", "Others", "Most", "By", "For", "But", "And", "Our", "If", "When", "While", "As", "Many"}
def toks(t): return re.findall(r"[A-Za-z]+(?:'[a-z]+)?", t)
def feats(t):
w = [x.lower() for x in toks(t)]
neg = Counter(x for x in w if x in NEG) + Counter("not" for x in w if x.endswith("n't"))
# 专有名词:只算不在句首的大写词(句首大写只是语法要求)
caps = Counter(m.group(1) for m in re.finditer(r"(?<![.!?]\s)(?<!^)(?<![\[(])\b([A-Z][a-zA-Z]+(?:\s[A-Z][a-zA-Z]+)*)", t)
if m.start() > 0 and m.group(1).split()[0] not in STOP_CAPS)
no_cite = CITE.sub(" ", t) # 引用里的数字([3]、2021)单独统计,不算两次
return {"numbers": Counter(n.replace(" ", "") for n in NUM.findall(no_cite)), "negations": neg,
"modals": Counter(x for x in w if x in MODAL), "names": caps, "citations": Counter(CITE.findall(t))}
def compare(orig, para, freeze=()):
a, b = feats(orig), feats(para); rep = {}
for k in a:
miss, add = a[k] - b[k], b[k] - a[k]
if miss or add: rep[k] = {"missing": sorted(miss.elements()), "added": sorted(add.elements())}
lost = [f for f in freeze if f.lower() not in para.lower()]
if lost: rep["frozen"] = {"missing": lost, "added": []}
return rep
def main(argv=None):
ap = argparse.ArgumentParser(); ap.add_argument("original"); ap.add_argument("paraphrase")
ap.add_argument("--freeze", default="", help="逗号分隔,必须原样保留的词语")
a = ap.parse_args(argv)
rep = compare(open(a.original, encoding="utf-8").read(), open(a.paraphrase, encoding="utf-8").read(),
[f.strip() for f in a.freeze.split(",") if f.strip()])
label = {"numbers": "数字", "negations": "否定词", "modals": "情态动词", "names": "专有名词", "citations": "引用", "frozen": "冻结词"}
for k, v in rep.items():
print(f"[{label[k]}] 原文有、改写稿没有:{v['missing']};改写稿新增:{v['added']}")
print("未发现差异" if not rep else f"{len(rep)} 类差异,请逐条核对")
return 1 if rep else 0
if __name__ == "__main__":
sys.exit(main())
第 2 步:示例文本
原文:
In 2019, most of the 48 teams met in person at least three times a week (Rivera, 2021). Managers said written updates should not replace meetings entirely. Teams using Notion reported 12% fewer missed deadlines, but the sample may be too small to generalize [3].
改写稿(我手写,故意带错误):
Back in 2019, the majority of the 48 teams gathered face to face three or more times weekly (Rivera, 2021). According to managers, written updates should replace meetings entirely. Teams on Notion saw 21% fewer missed deadlines, and the sample is too small to generalize.
第 3 步:测试
import drift_check as D
O = open("original.txt", encoding="utf-8").read(); P = open("paraphrase.txt", encoding="utf-8").read()
def test_planted_drifts():
r = D.compare(O, P, ["missed deadlines", "in person"])
assert r["numbers"] == {"missing": ["12%"], "added": ["21%"]}
assert r["negations"]["missing"] == ["not"] and r["modals"]["missing"] == ["may"]
assert r["citations"]["missing"] == ["[3]"] and r["frozen"]["missing"] == ["in person"]
assert "names" not in r # 调试前:句首的 Managers/According/Back 被误当成专有名词
def test_citation_digits_not_double_counted():
assert "numbers" not in D.compare("See [3] and (Lee, 2020).", "As shown in [3] (Lee, 2020).")
def test_identical_and_reworded_ok():
assert D.compare(O, O) == {}
assert D.compare("Sales did not grow in 2020.", "In 2020, sales didn't grow.") == {}
def test_name_change():
r = D.compare("The study used data from Statistics Canada.", "The study used data from a national agency.")
assert r["names"]["missing"] == ["Statistics Canada"]
def test_cli_exit(tmp_path):
assert D.main(["original.txt", "paraphrase.txt"]) == 1
assert D.main(["original.txt", "original.txt"]) == 0
第 4 步:运行
$ python drift_check.py original.txt paraphrase.txt --freeze "missed deadlines,in person"
[数字] 原文有、改写稿没有:['12%'];改写稿新增:['21%']
[否定词] 原文有、改写稿没有:['not'];改写稿新增:[]
[情态动词] 原文有、改写稿没有:['may'];改写稿新增:[]
[引用] 原文有、改写稿没有:['[3]'];改写稿新增:[]
[冻结词] 原文有、改写稿没有:['in person'];改写稿新增:[]
5 类差异,请逐条核对
$ python -m pytest -q
5 passed
怎么检查结果?
- 每一条差异都回到原文核对:是有意修改,还是改写出了错。
- 原文和改写稿完全相同时,输出“未发现差异”,退出码为 0。
didn't和did not被视为相同(测试test_identical_and_reworded_ok)。
调试示例
第一版有两类误报。 第一次运行时多报了两条:
[数字] 原文有、改写稿没有:['12%', '3'];改写稿新增:['21%']
[专有名词] 原文有、改写稿没有:['Managers'];改写稿新增:['According', 'Back']
3来自引用标记[3],在“引用”一类里已经报过一次。修复:提取数字前先去掉引用标记。- Managers、According、Back 只是句首的大写字母,并不是专有名词。修复:只统计不在句首的大写词。
修复后正好剩下埋入的 5 处错误。
仍然存在的局限: 数字写成英文单词的情况不检查。原文 "at least three times" 被改成 "three or more times",意思其实变了,但工具没有报出来。数字词需要人工核对。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 报了专有名词差异 | 改写时换掉了机构名或人名 | 确认是否允许替换 |
| 数字差异但意思没变 | 例如 50% 被写成 half | 人工确认 |
| 否定词丢失 | 改写把否定句改成了肯定句 | 重点核对,这往往会让意思反过来 |
| 冻结词丢失 | QuillBot 中没有设置 Freeze Words | 改写前先锁定 |
模板规则
- 术语、数字、引用在改写前就用 Freeze Words 锁定。
- 改写后先跑一遍检查,再人工通读。
- 否定词和情态动词的差异优先处理。
项目资源
| 用途 | 地址 |
|---|---|
| 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 |
| Paraphrasing Tool 帮助目录 | https://help.quillbot.com/hc/en-us/sections/35854128340375-Paraphrasing-Tool |
改写后跑一遍检查,花不了一分钟,却能避免把错误的数字或相反的意思交出去。