我对Google文档进行了反向工程,以播放任何文档的按键(2014年)

2020-12-27 07:28:05

如果您曾经在Google文档中输入过任何内容,现在就可以像电影一样播放它了-就像穿越时空,在写作时抬头看着自己。

之所以可行,是因为自2010年5月以来,用Google文档编写的每个文档都有一个修订历史记录,该修订历史记录可跟踪每个用户的每一次更改,时间戳的精确度可达到微秒。这些历史记录可供具有“编辑”权限的任何人使用;而且我编写了一个软件,可以查找,解码和重建任何给定文档的历史记录。

看到上面的小玩意儿吗?它就像一个视频播放器,但专门为写作而设计。这是我在距今大约四年前,即2010年圣诞节后的第二天开始工作的大西洋文章。该文章大约是我第一次(也是唯一一次)驾驶小型飞机。当时,我丝毫没有想过有一天我可以观看草稿的想法。但是,由于我碰巧在Google文档中写了这篇文章,因此我可以恢复所有击键。在上面,您可以看到第一段的最初不确定性。

这样做的好处是,在编写该视频时,我不必使用任何特殊软件。我当时使用的是普通的老式Google文档。为了向您展示我喜欢的这一段,我不必向您介绍整个文档(文档的全部39,154个修订版),我可以提取一些我认为很有趣的内容,并将其插入博客文章中。想象一下,一位高中英语老师可以做什么。想象一下,如果您在这里得到了Ta-Nehisi Coates的一件作品,而不是萨默斯(ol’Somers)的小努力,该怎么办。 (我一直想看看TNC的写作方式。如果他曾经使用过Google文档,现在可以了。)

为了制作嵌入内容,我使用了自己制作的名为草稿制作器(Draftback)的工具,我想我现在会启动它。借助Draftback,您可以播放和分析任何您自己的Google文档,或者就此而言,您有权编辑的任何Google文档。

(我与之交谈的每个人都感到惊讶,也许有些不安,发现他们每当与某人共享Google文档时,也会分享他们键入内容的详细记录。)

这是Draftback为几周前我正在撰写的文章自动生成的图表。它显示了我所做更改的时间轴,并在其下方显示了一个“图”,它告诉我这些修订在文档中的何处发生:图表越靠下,页面越靠下。一开始,我添加了成千上万个便笺词-这就是为什么文档如此之长如此之长的原因,以及为何编辑看起来稀疏的原因。然后您可以看到我进行了三遍不同的遍,第一遍专注于文章的顶部,并且速度很慢;而后面的则更快更远。文档和作者的视觉指纹。

如您所料,Google存储的数据令人难以置信。我们实际拥有的不仅仅是文档的粗略“视频”,而是每个字符的完整历史记录。 Draftback知道这一历史,并为每个角色分配了一个永久的唯一ID,这使人们可以做以前我从未真正完成过的工作。

例如,在这里,您可以看到我在输入简短文档。关注第一段:您不会看到它不是连续编写的,而是随着时间的推移通过一系列不连续的编辑而拼凑在一起的:我编辑了该段,然后做其他事情,然后回到段落,依此类推。我什至在一个段落中剪切并粘贴了一个短语。

由于Draftback具有每个角色的完整历史记录,并且即使在剪切和粘贴角色时也保留了该历史记录,因此可以选择一些文本并准确查看其来源。就像拥有文档的四维视图一样。

一直以来,我一直沉迷于您可能称之为写作的“考古学”:像约翰·麦克菲(John McPhee)的比尔·布拉德利(Bill Bradley)的个人资料(一种感觉,身在何处)或T. S.艾略特的《荒原》这样的东西。

我将阅读有关的内容:打字员中的艾略特是一本引人入胜的论文;约翰·麦克菲(John McPhee)读者的介绍很好,麦克菲(McPhee)自己的论文写作,《结构》和第4号草稿也很不错。我读过的最好的东西之一。

但是,如果您实际上可以看到这些家伙在工作呢?难道你不丢脸吗?

我担心大多数人都没有他们应有的好作家。一件事是,他们只是写的不够。另一个是他们没有意识到这应该很困难;他们以为优秀的作家才华横溢,而事实是,优秀的作家会变得优秀,好的程序员会变得优秀,任何好的事情都会变得优秀:碰上高峰。如果人们有生动的证据证明一位优秀的作家实际上花费了大部分时间为自己奋斗,也许人们会更好地理解这一点。

这就是为什么我想要类似Draftback之类的原因。我有一个我无法撼动的印象:您会得到一个人物,他的作品易于理解,简洁,无争议,风格良好,最重要的是典型的写作:即,某个人以某种形式写作,而这种写作就是那里的工作不是报告,而是说出话,如果我们只有他们的关键设备和语言范围,我们会怎么想……像AO这样的人斯科特(Scott)为《纽约时报》(New York Times)评论电影,并且做得如此出色,有时我会看电影,以便阅读他的评论。

所以你得到了A.O.斯科特(Scott)用Google文档编写,然后发布其中的完整播放和摘录的片段,为每位影迷,每位有抱负的作家和每位高中英语老师写的最热门的歌曲—当然是带注释的导演评论风格。国家。

这一切始于5年前在hacker News上的pg亲自撰写的一篇奇怪的帖子:我在2009年看到的最令人惊讶的事情是Etherpad提供的。 pg因他的论文而出名,在这里您可以看到他写了一篇,退格和全部。这是一种感觉。当时,这是有史以来最大的Hacker News故事之一。

看起来像这样(这实际上是一个稍晚一些的高级版本;当Google收购Etherpad时,原来的版本在etherpad.com上被删除了。稍后再讨论。)全部是一个带有顶部滑块和一个滑块的文档。大播放按钮,显示每个修订。您可以播放整个历史,从头到尾。很简单。

我记得看到过这种回放,并认为可能会更好。我想要更多信息:pg何时暂停,暂停了多长时间?他到底删除了多少?与其他作家相比如何?如果我看到一个我真的很喜欢的句子,该怎么办?

因此,我决定构建一个名为Jimbopad的东西。我对Jimbopad竟如此简单感到惊讶。您实际上并不需要那么多代码来回放某人写的记录。您只需要一个文本区域和某种跟踪差异的方法即可。这是回放UI的样子,这是使JavaScript成为可能的JavaScript(单击突出显示的代码位进行注释):

就其本身而言,这很简单,实际上比Etherpad更好。 Etherpad的问题在于,为了增强其播放功能,实际上它在每个刻度上都存储了文档的完整快照。因此,如果您有一个1MB的文本文件(例如,您正在撰写7,500字的文章),则每次击键都会在磁盘上转储另外一个兆。 Jimbopad是专门为回放而设计的-我不必担心实时协作,这是Etherpad的存在理由和重大价值主张-每次修订之间仅存储了“增量”,因此大约有1,000 x所需存储空间减少。

这就是为什么如果要进行“版本控制”以进行写作,则必须记录所有内容。对于作者来说,您必须使其琐碎,失败并退回到以前的状态而变得微不足道。他们的每半个序曲都必须保存-因为每个半个序曲,就像每个“提交”一样,可能都有他们想回到的单词。

一旦我制作了Jimbopad(这是该程序可能最简单的),我就想要更好的东西。那就是我着手构建Draftback 1.0的时候。您可以在这里看到它的外观。

据我所知,这是编写回放的最新技术。当然,您已经有了滑杆。但是,您还可以获得这些漂亮的绿色和红色,可以准确地显示每个修订中的更改。您会自动滚动到文档更改的部分(巨大的创新)。而且您可以进入“实际速度”播放模式,与观看不断的机器人轰鸣声相比,我认为以某种方式,它更加亲密和有趣。 (它的功能是,如果修订之间的延迟足够长,就会出现一个问题,并说“作者盯着太空30分钟。”)您甚至可以搜索词组,然后仅过滤包含该词组的修订。

但是仍然存在很多问题。 “搜索”过滤器确实很幼稚:它所做的只是查找修订版,其完整呈现的文本包括该短语,然后过滤掉所有其他内容。这很有用,但是我真正想要的是短语或句子的“家谱”;我想知道句子的各个部分从何而来,而这正是我现在所看到的原子单位。使用diff-match-patch方法甚至是不可能的。

也许更大的问题是没有优秀的作家会使用该程序。到目前为止,我的“编辑器”只是一个简单的文本区域,它要求您使用Markdown编写。最终我想到了这句咒语:“ A.O。 Scott永远不会使用降价促销”,“ A.O。斯科特永远不会使用降价促销。”

我坚信您需要一个漂亮干净的所见即所得(WYSIWYG)编辑器,以使人们使用您的书写软件。

我看了很多选择,最终我花了钱买了一个叫做Redactor的东西。是的:绝望中我实际上购买了RTF技术。我花了200美元买了一个Javascript文件。

Redactor实际上是一个很好的编辑器,它具有如此强大的API,确实很容易破解,但最终仍使用contentEditable,contentEditable最终造成了很大的麻烦。这是我在该编辑器上工作时的一些TODO和注意事项:

WYSYWIG控制按钮有时无法反映状态。切换开关无法正确切换。

§最后真正提供了标题的内容:有关如何对Google Docs的diff数据结构和渲染器进行反向工程的说明,该系统实际上可能是为实时协作而开发的,又称为“操作转换”,也无所事事与“写作考古学”

面对我的灌篮是Google的这篇博客文章,他们在文章中解释了为什么他们取消了Docs的contentEditable方法,并从头开始构建了一个全新的渲染引擎。

使用Google文档时,实际上并没有在您认为要输入的位置输入内容。您正在屏幕外的iFrame中输入文本区域,然后通过postMessage API将这些事件发送到您看到的“编辑界面”,其作用类似于绘制光标。 (您在Google文档上的光标实际上不是光标,而是2像素宽的div!)

我以此为依据,不仅证明contentEditable已注定要失败,还证明Google是唯一拥有胆识和技术实力来进行疯狂体操的人,他们需要在浏览器中构建类似于Word的内容。我想如果我不能击败他们,我会加入他们的行列。

我首先尝试为Docs构建一个实际的插件。我玩了他们的示例代码,然后浏览了文档。我试图查看是否有一个钩子可以告诉我用户何时更改了文档。回想一下,我真正需要的只是一个钩子,一个diff-match-patch库和一个存储增量的地方。

事实证明,他们没有为自己的文档公开此类事件。 (“当用户更改...电子表格中任何单元格的值时,onEdit触发器会自动运行。”但这就是事情开始变得非常有趣的时候。

我决定只打算在Google文档上编写一个Chrome扩展程序,并且每次进行更改时都要捕获呈现的HTML。当然,用户必须安装Chrome扩展程序,但这非常简单,当他们使用文档时,他们几乎不会注意到我的扩展程序在那里。感觉就像是无缝的透明体验。

所以我所做的就是看了一下Web检查器,发现了我关心的DOM。我发现所有实际内容都具有这些类,例如kix-page和kix-lineview和kix-wordhtmlgenerator-word-node。 (Google的Docs编辑界面和渲染引擎的代号为“ Kix”。)我认为可以在Chrome扩展程序中执行以下操作:

我以为我很聪明,但是在测试这段代码时,我发现有时它会丢失文档的很大一部分。我发现Google会按需呈现页面:如果您加载一个99页的文档,尽管看起来可以立即向下滚动,但是直到您滚动这些页面后,它们的实际文本才会生成进入视野。

在这一点上,我有点愚蠢。我试图对混淆的,缩小的客户端编辑器代码进行反向工程,以便可以找到任何渲染功能。我想如果能找到一些钩子,就可以欺骗编辑者以为我滚动了整个文档。这样,我的diff-match-patch工具将在每个修订版中使用完整文档。

我的想法是,如果Docs编辑器/渲染代码全部是Javascript,即使看起来像这样的80,000行代码,我也必须能够弄清楚它是如何工作的:

我试图通过在各处抛出断点来做到这一点。我会在代码中搜索不会混淆的短语(例如innerHTML),并在其旁边添加一个断点。然后,在UI中进行操作,看看是否达到了断点。然后,我将检查调用堆栈,看看周围有什么值。我发现了类似的东西,例如,如果您在控制台中键入P.j.zb.rx()之类的东西并运行它,那么您将“重做”任何最后的操作。我花了几天时间这样做。实际上,在一个周末,我花了很多时间盯着缩小版的Docs Javascript,以至于我确实患上了眼溃疡。

您是否听说过NASA如何花费数年时间和数千万美元开发可以在太空,水下和上下颠倒书写的钢笔,而俄国人却只带了一支铅笔的故事?它显然是伪造的(太空笔比铅笔安全得多,俄罗斯人也想要一支铅笔),但它说明了一点。这是解决我的渲染问题的“俄罗斯人带铅笔”解决方案。再次,单击突出显示的行以查看说明情况的注释:

不用说,我对这种解决方案并不满意。而且我在眼溃疡的时候看到了一些奇怪的东西。有一次,我单击了Chrome检查器中的“来源”标签,然后开始查看“网络”标签。每当我输入内容时,我都会注意到这些/ save调用:

负载看起来非常多汁。例如,在这里,我在文档开头的句子结尾处键入句点:

这似乎足够解析:类型(ty)插入(is)的“命令”,其中“插入开始索引”(ibi)为24,字符串(s)为“。”。现在我们用煤气做饭。

在这一点上,我认为我的Chrome扩展程序可能很笨。我要做的就是拦截这些“保存”请求并将它们存储在某个地方。稍后,我可以弄清楚如何使用它们来重建文档。只要有人从编辑一开始就安装了我的扩展程序,并且从未在没有扩展程序的浏览器中进行过任何更改,我就应该有足够的能力来完成Docs可以做的一切。 (我认为,与通过这些保存调用发送到服务器的数据相比,文档获取的文档数据完全不多;因此,这些数据必须足以呈现所有内容。)

这些似乎并不难理解。您有一个看起来像“多重”或捆绑操作的内容,然后在其中包含其他操作的列表:一些插入和一些删除。对于插入,您具有要添加的字符串;对于删除,指示您删除内容的索引。我构建了一个调试工具,可以逐步浏览这些修订的列表,以查看渲染的文档和我用来表示其的关键字符数组的转储:

数据是如此简单,以至于几乎可以建议构建器和渲染器的实现。您有一个字符数组,并在其中插入和删除字符。设置文本格式时,您只是将选项的哈希传递给一系列字符。我的文档构建器的整体外观如下所示。实际上,它的主要作用是为多个变量赋予易于理解的名称:

渲染器也非常简单。 (对于较大的文档,到目前为止,我还没有渲染样式,因为它需要很多额外的工作才能改善用户体验。)它的工作原理是这样的。我们有两个级别:段落和跨度。为了弄清楚要包装的样式,我们查看每个字符并说“您算出的样式是什么?”根据其属性的哈希值。然后我们说“那些样式等于您之前角色的样式吗?”如果是这样,我们继续跨度。如果没有,我们将创建一个新的跨度。

当然,除了大的关键之外,如果您不必安装Chrome扩展程序来捕获这些/ save请求,那不是很好吗?

我正在与Genius的老板讨论此事,他建议我查看Docs中的标准“修订历史”菜单-也许他们的所有差异都在其中?

我以为他一定是错的,因为我记得Google曾经进行过相当粗略的更改:对于可能已成千上万次更改的文档,可能进行了数十次或最多一百次修订。但是我沉迷于他,并在翻阅“修订历史”菜单时保持“网络”选项卡处于打开状态。然后,我偶然遇到了/ load调用。它具有如下所示的URL:

嗯,我想知道当您更改开始和结束参数以覆盖更大的范围时会发生什么?您是否会偶然获得文档的完整修订历史记录?

有两个复杂的问题–一个是您不能只说“将修订版1加载到无穷大”(或-1):您必须指定实际的上限。我的第一个建议是进行二进制搜索-如果您得到500响应,则知道您的响应过高,因此降低了上限。如果您获得200,则您处于范围内,因此您增加了下界;停下来,直到>上。

而且,当然,需要构建一个可以按比例工作的渲染器,包括具有成千上万修订版本的文档,其中每个修订版本都有数百页长。 (为此,主要技巧是在每个修订版本的所在地周围计算一个“窗口”,并且仅在该窗口内进行重型渲染。)然后制作人们想要使用的UI。并且找到一种方法来代表其他Google用户使用这些未公开的API,而无需让他们提供其凭据。

值得一提的是,当Google构建此用于存储一系列细微变化的文档的系统时,他们可能并没有考虑回放。 他们这样做的原因可能与Etherpad这样做的原因相同,这是为了促进实时协作。 您可以快速,可靠地做到这一点的唯一方法 ......