最近在研究东西时,经常利用知网来查看资料。非常感谢南农,我毕业这么久了,VPN 还能使用。不过在使用时,我发现 Zotero 不能正确识别网页上的文献信息了,只能以网页快照的形式保存到 Zotero 中。其实这个问题,我好几年前在研究生的时候,就已经发现了,当时也不知道怎么处理,也只能先用未代理的知网网页来获取文章的详细信息,然后再登录 VPN 连上知网,下载对应的文献,还是一个比较麻烦的过程。

1. 了解 Translator

要解决这个问题,首先要了解下 Zotero Translators,它能帮 Zotero 从各类网页上拉取对应的元数据(meta data),保存到 Zotero 中。Translators 有

  • Web translators:从网页上抓取信息,知网CNKI文献信息的抓取就是靠这个
  • Import translators:解析某个特定的格式,比如 RIS和 BibTex文献存储格式,保存到 Zotero 中
  • Export translators:将 Zotero 中的信息以某种格式导出
  • Search translators:从提供的标准 ID(如 DOI,PubMed ID ) ,查询并抓取对应的元数据

很明显知网对应的应该是一个 Web translator,对应代码是用 javascript 写的,主要部分为translator的元数据,类似一个字典,说明了该 translator 的一些信息,后面的代码需要有detectWebdoWeb。前者用于检测网页中信息,如果检测结果返回false,就不进行后续的抓取,可能直接使用网页快照的形式保存,如果发现有对应的数据或者发现是一个搜索结果页面,就可以进行下一步的抓取。其中搜索结果页面会包括多条文献记录,这时detectWeb返回的multiple。搜索结果会出现的页面如下,可以在上面选择对应的文献,添加到 Zotero 中。 detectWeb返回结果后,doWeb就会执行页面信息的抓取,下载对应的文件。

Zotero 中可以保存许多类型的条目,比如期刊文献,豆瓣图书,Youtube视频等许多信息,更全的列表可以查看这个列表,每个条目类型中又有各自的字段类型。detectWeb的一个功能就是返回对应网址所对应的条目类型,如果没有检测到对应的条目类型,就以网页快照(Snapshot)的形式来保存。我在知网使用 VPN 后,检查不到网页可能包括的具体条目类型,就用网页快照的形式来保存。

2. Scaffold 测试工具

为了能够更好的进行 translator 的开发,Zotero 有一个专门的测试工具叫 Scaffold。可以从 工具 -> Developer -> Translator Editor中打开,如果没找到这个选项,可以更新到最新的 Zotero版本。

第一次打开会让你选择一个存放 translator 的目录,你可以选择从官网的 Zotero translators下载解压后的目录。上面菜单的说明(详细可以参考 Scaffold页面的信息):

  • Load:列出目录中的 translator,你可以从中选择你需要修改的 translator,比如知网的CNKI,之后就会将对应的 translator 加载到 Scaffold中
  • Save:将 Scaffold 当前中的文件内容保存到对应的文件上
  • Save to Zotero:我目前没有发现有什么功能
  • Run detectWeb 和 Run doWeb: 分别用于测试 web translator 的detectWebdoWeb 函数
  • Run detectImport 和 Run doiImport:是用于其他 translator 的测试

下面的标签页的具体使用可以参考 参考 Scaffold 的说明。其中 Metadata页面的Traget 后面有个 Test Regex,需要你在Browser中输入一个网页,这里可以用一个知网某个文章的页面来进行测试。点击Text Regex,会利用前面的正则表达式,对网址的 URL 进行匹配,匹配上会返回 true,不匹配是false,使用正则的时候,注意一下特殊符号的反义。Code页面是主要的代码信息,具体的抓取流程和代码,肯定有detectWebdoWeb两个函数。Tests页面是 translator 后面附上的测试样本,而Testing上列出了Tests中的测试例子,选择某个样例,点击Run,运行Code中的代码并与Tests中列出的结果进行对比,有错误会提示,仔细对比下。点击下面的New Web可以将Browser中的页面添加到Testing中,点击Update会将相应的信息加载到Tests中。

Code中用的代码是 javascript,Zotero 自己也封装了一些方法,其中Z表示ZoteroZU表示Zotero.Utilities。常用的Z.debug()将信息打印到 Scaffold 的右侧,ZU.xpath(doc, xpath) 利用xpath语法从 HTML 网页中选择对应元素,还有ZU.doGetZU.doPost用来拉取数据。

3. 知网代理后不能识别的问题

这里代理我用是学校的 VPN,代理后的网址类似这样https://kns-cnki-net-s.vpn2.njau.edu.cn/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2015&filename=NYSB201509001&uid=thisisafakeuidhashstringsW4IQMovwHtwkF4VYPoHbKxJw!!&v=MDcxMTBoMVQzcVRyV00xRnJDVVJMT2VaK2RvRnkzbFZicklLelRZYkxHNEg5VE1wbzlGWllSOGVYMUx1eFlTN0Q=,没有代理的网址是https://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2015&filename=NYSB201509001&v=MDMzOTVUcldNMUZyQ1VSTE9lWitkb0Z5M2xWYi9BS3pUWWJMRzRIOVRNcG85RlpZUjhlWDFMdXhZUzdEaDFUM3E=,可以看到二者开头不一样,代理后变成了https://kns-cnki-net-s.vpn2.njau.edu.cn。如果修改了 translator 中的网址匹配正则,应该可以正常识别了。修改后的匹配规则是^https?://kns,非常直白。把浏览器和 Zotero 关闭再重新打开,跳到对应页面,能正常显示 CNKI 的信息,连图标都不是网页快照的样子了。不过在保存过程中出现了问题。打开 Zotero Connector浏览器插件的选项里面可以看到出现的错误信息

从CNKI.js中查看包含这个网址的代码,发现这是一段用于从知网上获取 refworks 格式引文信息的代码:

function getRefworksByID(ids, next) {
    var postData = "";
    for (var i=0, n=ids.length; i<n; i++) {
        postData += ids[i].dbname + "!" + ids[i].filename + "!0!0,";
    }
    postData = "formfilenames=" + encodeURIComponent(postData);

    ZU.doPost('http://epub.cnki.net/kns/ViewPage/viewsave.aspx?TablePre=SCDB', postData, function() {
        ZU.doPost(
            'http://epub.cnki.net/KNS/ViewPage/SaveSelectedNoteFormat.aspx?type=txt',
            'CurSaveModeType=REFWORKS',
            function(text) {
                //fix item types
                text = text.replace(/^RT\s+Dissertation\/Thesis/gmi, 'RT Dissertation')
                    //Zotero doesn't do well with mixed line endings. Make everything \n
                    .replace(/\r\n?/g, '\n')
                    //split authors
                    .replace(/^(A[1-4]|U2)\s*([^\r\n]+)/gm, function(m, tag, authors) {
                        var authors = authors.split(/\s*[;,,]\s*/); //that's a special comma
                        if (!authors[authors.length-1].trim()) authors.pop();

                        return tag + ' ' + authors.join('\n' + tag + ' ');
                    });

                next(text);
            }
        );
    });
}

代码中的 HTTP 网址是http://epub.cnki.net/kns/ViewPage/viewsave.aspx?TablePre=SCDB,在错误信息里面显示,被加上了代理的后缀。这里为什么会出现这样的问题,我认为可能是在 javascript 里面对跨域请求(CSRF)是有严格限制的,尤其是进行 POST 请求的时候。这个问题,按照我自己的理解,用各种不同方法尝试了好几天,上面这个函数是用于获取 refworks 格式的引文信息,而我发现这个引文信息也可以从下图红框的链接中得到 再点击 Refworks,出现了想要的结果 对应的网页为https://kns-cnki-net-s.vpn2.njau.edu.cn/kns/ViewPage/viewsave.aspx?displayMode=Refworks,打开浏览器的开发工具,可以看到点击 Refworks 时往服务器那发送了什么数据 从上面表里看到,非空的字段有hid_kLogin_headerUrlhid_KLogin_FooterUrlformfilenamesCookieName。其中formfilenames字段是由数据库名和文件名拼成的,而文件名和数据库名一般可以从 URL 中获取到,translator 中有一个函数处理:

function getIDFromURL(url) {
    if (!url) return;
    var dbname = url.match(/[?&]dbname=([^&#]*)/i);
    var filename = url.match(/[?&]filename=([^&#]*)/i);
    Z.debug('getIDFromURL', dbname, filename)
    if (!dbname || !dbname[1] || !filename || !filename[1]) return;
    return {dbname: dbname[1], filename: filename[1], url: url};
}

网址https://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2015&filename=NYSB201509001&v=MDMzOTVUcldNMUZyQ1VSTE9lWitkb0Z5M2xWYi9BS3pUWWJMRzRIOVRNcG85RlpZUjhlWDFMdXhZUzdEaDFUM3E=的数据库名为CJFDLAST2015,文件名为NYSB201509001,数据库名主要看前面4个英文字母,数据库主要用于区别是期刊文章,硕博论文等类型,具体可以参考getTypeFromDBName函数。POST 请求有多种形式,从https://kns-cnki-net-s.vpn2.njau.edu.cn/kns/ViewPage/viewsave.aspx?displayMode=Refworks网址中含有=,怀疑可能是属于application/x-www-form-urlencoded类型,提交的表单数据会按照 key1=val1&key2=val2 这样的形式进行转码。这里我用 Python 试了下,把按照不同参数组装得到的 url,可以用requests.get来获取数据,结果也是正常的。后面又修改 translator 中获取 refworks 的代码

function getRefworksByID(ids, next) {
    var postData = "";
    for (var i=0, n=ids.length; i<n; i++) {
        postData += ids[i].dbname + "!" + ids[i].filename + "!1!0,";
    }
    postData = "formfilenames=" + encodeURIComponent(postData);
    postData += '&hid_kLogin_headerUrl=/KLogin/Request/GetKHeader.ashx%3Fcallback%3D%3F';
    postData += '&hid_KLogin_FooterUrl=/KLogin/Request/GetKHeader.ashx%3Fcallback%3D%3F';
    postData += '&CookieName=FileNameS';

    ZU.doGet('https://kns.cnki.net/kns/ViewPage/viewsave.aspx?displayMode=Refworks' + '&' +  postData, 
        function(text) {
            var parser = new DOMParser();
            var html = parser.parseFromString(text, "text/html")
            var text = ZU.xpath(html, "//table[@class='mainTable']//td")[0].innerHTML;
            var text = text.replace(/<br>/g, '\n');
            text = text.replace(/^RT\s+Dissertation\/Thesis/gmi, 'RT Dissertation');
            text = text.replace(/^(A[1-4]|U2)\s*([^\r\n]+)/gm, 
                    function(m, tag, authors) {
                        var authors = authors.split(/\s*[;,,]\s*/); //that's a special comma
                        if (!authors[authors.length-1].trim()) authors.pop();
                        return tag + ' ' + authors.join('\n' + tag + ' ');
                    }
                );
            next(text);
        }
    );
}

获取的结果是一个网页的文本,不是一个 HTML document 的对象,我需要将文本转换为 HTML document 对象,这里用了DOMParser重新解析了下,再用xpath选择 refworks 对应的元素信息。和原来的抓取代码相比,我这个代码中的文章摘要信息是不全的,可能是有字数限制。

这样修改了网址的匹配正则,以及 refworks 的获取代码。知网的信息在使用 VPN 的时候,是可以正常使用的,但是文章摘要信息是不全的。后面我又花了点时间尝试并与论坛上的人交流了下,做了后面的改进。

4. CNKI.js 的改进

在解决代理问题的过程中,我发现 Zotero 解析知网上的 refworks 是有问题。知网上的 CN段处的信息CN 11-3342/S,后面这串数字和代码是国内统一连续出版物号。不是 refworks 中CN表示的callNumber引用次数的信息。关于为什么代码一定要使用 refworks 格式,我认为是 refworks 中保留的信息比较多,这些信息不一定能直接从文章详情页中获取,而且 refworks 在 Zotero 中已经有一个解析工具translator.setTranslator('1a3506da-a303-4b0a-a1cd-f216e6138d86'),可以方便地解析获取相应的信息。

Zotero 中保存的知网条目是没有网页快照等其他信息,更别说PDF和CAJ文件了。不过从代码里看,作者应该有相应的准备,可能没有知网的账号来进行测试吧,毕竟账号这么贵。Zotero 中文献条目的网页快照和文章pdf下载等信息是保存在条目中的attachments字段中,该字段是一个列表,列表中的元素是一个字典,具体类似

"attachments": [
    {
        "title": "中小企业融资方式的比较与选择",
        "mimeType": "text/html",
        "snapshot": true
    },
    {
        "title": "Full Text CAJ",
        "mimeType": "application/caj",
        'url': cajurl
    },
    {
        "title": "Full Text PDF",
        "mimeType": "application/pdf",
        'url': pdf
    }
]

mimeType这个信息,我上网查了下,没有 CAJ 文件对应的,我这写了 application/caj,用起来也没问题,就是缩略图和网页快照一样。 这里我加了一段代码用于自动下载文章,当然是你登录了知网的情况下

// get pdf download link
function getPDF(doc) {
    var pdf = ZU.xpath(doc, "//a[@name='pdfDown']");
    return pdf.length ? pdf[0].href : false;
}

// caj download link, default is the whole article for thesis.
function getCAJ(doc, itemType) {
    // //div[@id='DownLoadParts']
    if (itemType == 'thesis') {
        var caj = ZU.xpath(doc, "//div[@id='DownLoadParts']/a");
    } else {
        caj = ZU.xpath(doc, "//a[@name='cajDown']");
    }
    return caj.length ? caj[0].href : false;
}

// add pdf or caj to attachments, default is pdf
function getAttachments(doc, item) {
    var attachments = [{
        url: item.url,
        title: item.title,
        mimeType: "text/html",
        snapshot: true
    }];
    var pdfurl = getPDF(doc);
    var cajurl = getCAJ(doc, item.itemType);
    // Z.debug('pdf' + pdfurl);
    // Z.debug('caj' + cajurl);
    var loginUser = ZU.xpath(doc, "//input[@id='loginuserid']");
    // Z.debug(doc.body.innerHTML);
    // Z.debug(loginUser[0].value);
    // Z.debug(loginUser.length);
    if (loginUser.length && loginUser[0].value) { 
        if (pdfurl) {
            attachments.push({
                title: "Full Text PDF",
                mimeType: "application/pdf",
                url: pdfurl
            });
        } else if (cajurl) {
            attachments.push({
                title: "Full Text CAJ",
                mimeType: "application/caj",
                url: cajurl
            });
        }
    }

    return attachments;
}

默认情况下会有一个网页快照,同时如果有 PDF 的话,会优先 PDF。学位论文的情况下,会把整本论文的CAJ下载下来。这里我用了是否出现登录账号的用户名来判断是否登录知网,只有在登录之后才可以下载 PDF 或 CAJ。不过在后续测试的时候发现,这个登录信息是由JS来生成的,在网页加载后,由JS根据cookies中的信息来生成这个用户登录信息。不知道为什么Zotero好像没有识别出JS加载后的页面。后面又花了点时间比较了登录前面网页的区别,最后发现在登录后,会有一个UID的加密字符串,未登录UID为空。

我发现的最后一个问题是,如果期刊是网络首发,比如下面这篇文章

从 URL 中获取不到对应的 dbnamefilename,不过我从网页中导出/参考文献这里发现了一些信息

<a class="icon icon-output" href="javascript:void(0);" onclick="
        SubTurnExport('//kns.cnki.net/kns/ViewPage/viewsave.aspx','CAPJLAST!XBZW20191009000!1!0')
      "><i></i>导出/参考文献
</a>

这里清清楚楚的写上了数据库和文件名'CAPJLAST!XBZW20191009000!1!0',我写了下面的函数来处理这个字符串,修改了getIDFromPage

function getIDFromRef(doc, url) {
    var func = ZU.xpath(doc, '//div[@class="link"]/a')[0].onclick + '';
    var tmp = func.split(',')[1].split('!');
    // Z.debug(func + tmp[0].slice(1));
    return { dbname: tmp[0].slice(1), filename: tmp[1], url: url };
}

function getIDFromPage(doc, url) {
    return getIDFromURL(url)
        || getIDFromURL(ZU.xpathText(doc, '//div[@class="zwjdown"]/a/@href'))
        || getIDFromRef(doc, url);
}

最终修改后的CNKI_custom.js,有兴趣的可以参考下。

我之前试着把相关的更新提交到 Zotero 官方库中,不过人家提了一些问题。这些问题下面我会说到。根据这些问题,我又做了些修改,具体的内容在下面。

5. 与 Zotero 论坛交流的结果

我之前有将相关问题发到 Zotero 的论坛上,他们认为我直接修改 URL 的正则匹配项是不提倡的做法。正确地做法是通过 Zotero Connector 中的代理设置来实现网页的重定向。在 Firefox 和 Chrome 安装了插件的话,可以右键插件图标找到选项信息,下图左边是 Firefox,右边是 Chrome。

进去之后,选择Proxies,下面是我按照论坛中的提示做了修改,人家觉得我们学校的 VPN 前缀比较奇怪。我也不知道有啥奇怪,我也不懂。

https://kns-cnki-net-s.vpn2.njau.edu.cn 代理之后的网址是类似这样的,%h应该是https://kns-cnki-net,后面是%p是正常的网址后缀。论坛上那人特别提示我

%h-s.vpn2.njau.edu.cn/%p for the scheme and another with %h.s.vpn2.njau.edu.cn/%p. Make sure "Automatically associate new hosts" is enabled for both and "Automatically convert between dots and hyphens in proxied hostnames" is enabled for the first one

设置后当你登录知网的时候,会在页面上显示代理的提示信息

直接修改匹配URL不是一个优雅的做法,后面又重新根据上面的结果,把最终的 CNKI translator整理出来,可以用这个结果来替换Zotero原有CNKI.js。

6. 写在最后

花了一下午时间,把这一周修改的过程记录了下来,主要是想把这个过程分享出去。如果没记错,Zotero 是我 2013 年左右开始使用,中间还试了一下 Mendely,后来还是用回 Zotero,还是有一些感情的。而且在使用过程中,也发现它的强大之处。Zotero 的文档也非常丰富,不过可能因为刚赶上 5.0 版本的大更新,有些文档还未及时更新。论坛中也反馈的问题也能及时得到回复,你可以把相关的问题以提交 Bug 的形式提交,并得到一个 Debug ID,开发人员可以在后台通过这个 ID 来获取相应的信息,这个也是非常方便的。Zotero 不仅仅是一个文献管理器,也还是其他互联网资源的管理工具,或许其他人写新的 translator 的时候,希望我这个笔记能有一些帮助。