使用用户脚本为拥有 24 年历史的政府网站添加键盘快捷键


原文:Adding Keyboard Shortcuts to a 24 Year Old Government Website with Userscripts

背景 (Background)

在过去的一年里,我一直在清理美国食品药品监督管理局(FDA)的 510k 数据库的数据[1]

该数据库收录了 510k 项目[7]的申请,包含 FDA 的审批流程,几乎所有(99%)供人使用的医疗器械都采用这一程序。[2]

在 archive.org[3] 上的搜索显示,这个网站至少从 2000 年 10 月 18 日就存在了。事实上,我们甚至可以看到它当年的样子。顶上配有官方的 Comic Sans 字体设计的标志。

这是截至 2024 年的网站。令人惊讶的是,在过去的 24 年里,它的外观竟然没什么变化。

从那时起,其主界面几乎没什么变化,更像一个简单时代的活化石。它的样式相当简陋,并且页面都是服务器渲染的。除了用于日期选择器的一些代码,它几乎不包含任何 JavaScript。

根据 .cfm 文件扩展名,它似乎是由一个 1995 年的构建工具 Adobe ColdFusion 构建的[4]

数据录入

为了清理数据,我使用该数据库的搜索功能按名称查找医疗器械。

然而,数据中存在一些问题,减慢了我的速度。

器械和公司名称没有标准化,可能存在缩写、首字母缩略词或常见的拼写错误。该网站的搜索功能不提供模糊字符串匹配,因此查找一个器械通常需要反复试错。我的工作流程是点击搜索输入框,输入一个名称(可能会输入几次),然后用鼠标高亮文本并将其复制到另一个程序中。

这个过程感觉非常低效。每次搜索,我都必须将手从鼠标和键盘之间多次移动。

我手动搜索了数千个器械,因此优化流程的每一步都是值得的。此外,写代码比手动录入数据更有趣,这给了我一个有趣的休息机会。

我的目标是扩展网站的功能,以便我可以在不离开键盘的情况下完成大部分任务。

用户脚本

什么是用户脚本?用户脚本[5]实际上是一个用 JavaScript 编写的程序,旨在为网站提供原始开发者意图之外的附加功能。

在这个案例中,是为了给 FDA 的 510k 数据库网站提供键盘快捷键。

我想要实现以下任务的快捷键:

  • 打开搜索页面
  • 聚焦到“器械名称”的搜索输入框
  • 复制器械的 510K ID 号码

ViolentMonkey

有许多支持用户脚本的浏览器扩展,但我使用的是一个名为 ViolentMonkey 的工具。它是更流行的扩展 TamperMonkey 的开源替代品。

这个工具提供了一种在不同网站上运行自定义 JavaScript 的好方法。它提供了一个浏览器内的 JavaScript 编辑器,也允许用户从各种用户脚本库安装其他人的脚本。

幸运的是,由于网站使用的是非常简单的 HTML,因此编写这些快捷键的代码非常简单。

快捷键

ViolentMonkey 通过它的快捷键扩展[6],使得注册快捷键变得非常容易。通过在头部(header)添加这一行代码,我可以轻松注册快捷键:

1
// @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1

打开搜索页面

这是最容易写的快捷键了。我们只需将位置(location)设置为搜索页面的 URL。当按下 Ctrl + Alt + N 时,该标签页会被重定向到搜索页面。

1
2
3
4
VM.shortcut.register("ctrl-alt-n", () => {
location.href =
"https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfPMN/pmn.cfm";
});

聚焦搜索输入框

打开搜索页面后,我想要聚焦到器械名称输入字段。

这是此输入框的 HTML 代码。开发人员很有帮助地给了它一个 ID:DeviceName,所以我们可以使用 document.getElementById() 在页面上找到它。

1
`<input type="text" name="DeviceName" id="DeviceName" size="20" maxlength="20">

这是用户脚本。我们找到该元素,然后使用 focus() 将我们的浏览器焦点放在它上面。

1
2
3
4
VM.shortcut.register("ctrl-alt-s", () => {
const input = document.getElementById("DeviceName");
input.focus();
});

复制器械 ID

最后一个快捷键稍微复杂一些,因为它必须处理两种情况。在网站上提交搜索表单后,下一个页面可能以两种方式渲染:

  1. 如果只有一个结果,网站会显示该结果的详细信息,包括 510k 编号。
  2. 如果有多个结果,网站会显示一个表格,其中包含每个 510k 编号以及指向每个提交详情的链接。

在我们的代码中,我们检查 URL 中是否存在字符串 ?ID=,该字符串仅出现在单结果详情页面上。

1
2
3
4
5
if (location.href.includes("?ID=")) {
// 复制详情页中的ID
} else {
// 复制表格中的结果
}

不幸的是,显示器械 510k 编号的 HTML 元素没有使用 id 属性。因此,我们需要使用该元素的 xpath(XML Path Language)

xpath 描述如何从树的根部(文档的起点)走到树上某个特定成员(某个节点)的 “地址” 或 “导航路线”。如果元素在页面上移动,xpath 将不再准确。幸运的是,由于该页面是服务器模板化(server templated)的 HTML,该元素在页面上并不会真正移动。如果这是一个现代的 JavaScript 网页应用,我们就要使用一种不同的方法。

我们可以使用 Firefox 方便的开发者工具中的“复制 xpath”选项来快速找到这个值。

现在我们可以使用 window.navigator.clipboard.writeText() 将该节点的 innerText 值复制到我们的剪贴板。

目前我们有了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (location.href.includes("?ID=")) {
// 复制详情页中的ID
var xpath =
"/html/body/div[3]/maxamineignore/div[2]/div[2]/span[2]/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table/tbody/tr/td/table/tbody/tr[2]/td";
// 根据提供的 XPath 路径,在文档对象模型(DOM)树中查找并选择匹配的节点
var deviceId = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue.innerText;
window.navigator.clipboard.writeText(deviceId);
} else {
// 复制表格中的最后一个结果
}

现在我们处理有多个响应的情况。有时我们对同一个器械有多个 510k 提交。我武断地使用最旧的一个值作为打破僵局的规则,所以我的脚本也会这样做。

与之前类似,我们使用表格的 xpath 在文档中找到它。然后我们找到表格的最后一行,并获取它的第三个子元素,即包含 510K 编号的列。一旦我们有了这一列,我们就获取它的第一个子元素,并将其文本写入剪贴板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
} else {
// 复制表格中的最后一个结果
var xpath = '/html/body/div[3]/maxamineignore/div[2]/div[2]/span[2]/table[2]/tbody/tr/td/table/tbody';
var table = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
var tableLength = table.children.length;
var lastRow = table.children[tableLength-1]
// 从最后一个元素获取ID
var deviceId = lastRow.children[2].firstChild.text;
window.navigator.clipboard.writeText(deviceId);
}

最后,我们将这段代码封装在 VM.shortcut.register() 的回调函数中,以获得我们的最终脚本。现在,当我按下 Ctrl + Shift + C 时,510k 编号会自动写入我的剪贴板,省去了我必须用鼠标手动高亮 510k 编号的麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
VM.shortcut.register("ctrl-shift-c", () => {
if (location.href.includes("?ID=")) {
// 复制详情页中的ID
var xpath =
"/html/body/div[3]/maxamineignore/div[2]/div[2]/span[2]/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table/tbody/tr/td/table/tbody/tr[2]/td";
var deviceId = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue.innerText;
window.navigator.clipboard.writeText(deviceId);
} else {
// 复制表格中的最后一个结果
var xpath =
"/html/body/div[3]/maxamineignore/div[2]/div[2]/span[2]/table[2]/tbody/tr/td/table/tbody";
var table = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
var tableLength = table.children.length;
var lastRow = table.children[tableLength - 1];
// 从最后一个元素获取ID
var deviceId = lastRow.children[2].firstChild.text;
window.navigator.clipboard.writeText(deviceId);
}
});

结论

如果你发现自己被网站上一些重复性的任务所困扰,我强烈建议尝试用用户脚本来自动化其中的一部分。

最酷的部分在于你可以自己动手解决这个问题,并为自己节省一些时间。

很难量化我在这里为自己节省了多少时间,但现在我减少了将手离开键盘的次数,我的工作流程无疑变得更轻松了。

脚注 (Footnotes)


  1. 1.https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfpmn/pmn.cfm ↩︎
  2. 2.https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10465388/ ↩︎
  3. 3.https://web.archive.org/web/20001015000000*/https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfpmn/pmn.cfm ↩︎
  4. 4.https://en.wikipedia.org/wiki/Adobe_ColdFusion ↩︎
  5. 5.https://en.wikipedia.org/wiki/Userscript ↩︎
  6. 6.https://github.com/violentmonkey/vm-shortcut ↩︎
  7. 7.510k 项目 是美国食品药品监督管理局(FDA)针对大多数医疗器械在美国上市前要求进行的一项审批流程 ↩︎