Categories

统计论文被引用的情况

GreaseMonkey昨天被导师叫过去分配了一个非常苦力的任务:统计他的论文被引用的情况。确切地说就是对于他的每篇论文,列出所有引用该论文的出版物,当然,所有的条目都要以标准的论文参考文献的格式给出详细信息来。因为这个毕竟是自己统计的,最后还要拿去图书馆查询、审核、盖章、等等等等。据说申请项目要用,看来这就是国内的现状吧?整天都忙这些事情,还有几个人能抽出空余时间去做正事呢?一直不明白何老师为啥要回国来。当然,从我自己的经历来看,很多事情其实也没有那么多的为什么,至于事情已经这样发生了,事后再要说个所以然出来,也并不是那么有必要的。

一开始我被这工作量给吓到了,那么多的论文,我怎么去找所有引用的啊?后来被告知只要包含 Google Scholar 上已经收录了的就可以了。不过大概一共有一千多 paper 吧,据说去年周 core 他们做得很辛苦。何老师给了我一个 Word 文档,说是之前的版本,其实只要在这基础上把更新的加进去就可以了。听起来好像工作量减少了许多,但是对于那种几百篇引用的论文,我去 Google Scholar 上找出来,然后每篇依次检查是否以前已经记录下来了,再决定要不要添加,看起来还不如直接无视之前的版本从头来呢。而且那个 Word 文档里面杂乱的格式,还有一些直接从 Google Scholar 上复制下来的还带着超链接的文本,对我来说颇有些不可忍受啊。然后何老师说,你去发动实验室的同学大家一起弄吧。-.-bb 大概这对我来说才是最困难的吧,那些认识但是却不是特别熟悉的人,就是你也不好意思去找人家帮忙,人家找你帮忙你也不好意思拒绝的那种,所以我也只有象征性地问问“最近超忙”的周 core ,他毕竟去年辛苦过一次了,于是还是不要拖他下水了。

不过其实我觉得这个任务本身并不是完全纯粹无聊的,或者说,其实本来就是很无聊的事情,但是却并不是坏事。就好龟仙人让孙悟空他们背着龟壳去跑步一样,虽然相当折磨人,但是却是必不可少的基本功;比较接近我自己的事例的话,比如说是小事后抄写汉字、做算数之类的。可是我也是后来才发现我还是一个相当爱偷懒的人,如果能有什么办法能让我少花点力气(也包括脑力)的话,我多半都会采用,后果大概就是现在脑袋里连九九乘法表都有一些空缺吧。所以后来我也经常提醒自己,麻烦事也得认真做。而关于统计引用这件事,也是如此吧。做研究的话,我想大致就是两点:发现问题和解决问题。两点都很难,不过,如果认真地去把统计这件事情做了,那么至少能够知道相同领域的人们大家都在做什么,解决一些什么样的问题,视野宽了之后看问题也就能更加全面一些,不能说一定会有什么显著的帮助,但是却是一次很不错的积累。至少并不是完全无聊的嘛,比如我整理了几篇,就看到竟然有不少人都在研究预测人的年龄的问题。

但是,江山易改,本性难移啊。在经历了内心痛苦的挣扎以数次关掉 OpenOffice 、打开终端,再关掉终端、打开 OpenOffice (Word 文档在 OpenOffice 下显示真是太难看了)之后,我还是决定先尝试一下。总之希望实现至少 80% 自动化。

要自动地获取数据,去解析网页上的文本来提取作者、会议期刊的名字之类的显然不太现实,不过我很早就发现 Google Scholar 提供了文献的 BibTeX 导入,只是默认这个功能没有打开,要到 preference 里去选一下,所以就需要 Cookie 支持了。正好 Python 的 urllib2 提供了 Cookie 的支持:

cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))

而且也有 MozillaCookieJar 可以直接读取 Firefox 的 cookie ,我立即想偷一下懒,不过打开 Firefox 的 profile 文件夹才想起来 Firefox 3 的 Cookie 文件从 plain text 换成 sqlite 数据库了,不能直接这样读了。不过印象中之前倒是看到 Mithro 贴过在 Python 中读取 Firefox 3 的 cookie 的代码,在 Google Reader 里很快就找到了:Reading Firefox 3.x cookies in Python 。不过在那之前我手工把 Firefox 的 Cookie 复制出来加到 Python 的 urllib2 的 headers 里面,发现出来的结果并不是想要的,不知道 Google Scholar 是不是针对不同的 User-Agent 或者类似的东西会用不同的 Cookie 之类的,不过既然我已经可以添加自动的 Cookie 支持了,那我就在爬虫启动的时候去那个 Preference 页面提交一下表单好了,这样也不用依赖 Firefox 的 Cookie 了。

不过接下来我发现 Google (至少是 Google Scholar )的网页如果不是故意做成那样子的话,真是一点章法都没有。那个 Preference 页面的 form 是用 GET 而不是 POST 来提交的,倒是为爬虫的作者省了不少事情。不过,似乎有一些选项除了存在 Cookie 中之外,也会体现在后续的 query URL 中,而返回的 HTML 页面结果就彻底不说了,本来我还想偷懒用 BeautifulSoup 来解析,然而我发现 BeautifulSoup 基本上就只能解析出 htmlhead 以及 body 这么几个标签,得到一个几乎为空的 DOM 树,可见浏览器要把这样的页面显示出来,容错性得要多大呢!

所以我很快就决定放弃 BeautifulSoup ,改用正则表达式直接分析。发现正则表达式几乎是我用得最多的一个但是也几乎是每次用都要去查文档的一个功能,实在是没有办法,常用的几个正则表达式引擎 grep 、Emacs、VIM、Ruby、Python 几乎都有一些略微不同之处——包括正则表达式的书写语法,还有如何用正则表达式进行查询、替换以及结果的形式还有如何获取 capturing group 的内容等等。经过尝试,使用下面这个正则表达式可以匹配出文章以及其引用次数和所有引用文章的页面链接:

    pat = re.compile(r'">%s</a></span>' % re.escape(title) + \
            r'(?:.*?<a class=fl href="([^"]+)">' + \
            r'Cited by (\d+)</a>)?(.*?Import into BibTeX)', 
            re.DOTALL | re.IGNORECASE)

我想让结果尽量精确,所以在搜索的时候 keyword 使用用引号括起来的完整论文标题,后面再附上了作者的名字,比如:

"Face Recognition Using Laplacianfaces" Xiaofei He

在匹配的时候也要求标题精确匹配,当然我希望自动处理的时候就要做到完全精确,所以如果无法精确匹配就会引发 Not Found 错误,而如果有多个精确匹配的时候就会引发 Ambiguity 错误,这些出错的文档会被收集起来并在最后结果中列出来,最后手工处理就好了——事实上两种错误我都遇到了。不过要精确匹配还需要一些工作:

  • Google 会在结果中对匹配到的单词进行加粗,也就是加上 <b> 标签,按理说如果标题整个精确匹配的话,只会有一个大的 <b> 包围起来,不过如果标题里有诸如破折号之类的情况的话,也会出现在标题中间被加上 <b> 的情况。
  • 有些字符以 HTML Entity 的形式出现,论文标题里最常见的应该是单引号了,会以 &#39; 的形式出现。

因此在进行匹配之前我还对 HTML 进行了一点预处理,分别是滤掉加粗的标签和还原 HTML Entity 。最后就是抓取 BibTeX 了,对于引用页面来说,抓取里面出现的所有 BibTeX 链接内容就可以了,我下面这个函数来完成:

def get_bibtex(page):
    pat = re.compile(r'<a class=fl href="([^"]+)"' + \
            r'[^>]*>Import into BibTeX')
    matches = pat.findall(page)
    return [retrieve(bib) for bib in matches]

不过还要抓取论文自己的 BibTeX ,这里我直接用了 get_bibtex 函数,不过给他的文本被限制在当前论文的这一段了,这也是我在之前那个正则表达式里最后一个 Capturing Group (.*?Import into BibTeX) 里得到的内容的作用。

信息抓取下来之后按照论文分别存储到文件中,整个脚本粗略就是如此了。经过我以往的经验,爬虫后期处理不管是简单还是复杂,最好都是先把数据抓取下来再考虑后期的处理,这样分离开来比较容易处理错误一些,毕竟网络环境差异,这是最容易出错的一个环节,等数据到了本地,要怎么样来处理,就是做实验也方便了许多。整个脚本再加上一些稍微人性化一点的 log 信息(虽然是命令行的,但是 UI 还是很重要啊),经过小范围的调试之后发现效果不错。不过回想一下这样抓似乎有些暴力,于是我设定脚本每从 Google 下载一个页面就休息一秒钟。把脚本启动起来,然后就去赶公交车回寝室了。心想着等回到寝室的时候应该就抓完了吧。

结果回到寝室一看,发现程序出错了,网络错误,Service Unavailable 。赶紧打开 Google Scholar ,发现跳转到了 sorry.google.com 一个让输入验证码的页面,说也许我可以杀杀毒什么的。输入验证码之后,可以访问了,不过脚本还是不行,发现关闭浏览器之后再次打开也要重新输入验证码。看来是做到 session 里的信息?或者用了某些 Javascript ?其实之前用 Ruby on Rails 的时候了解了一点 Session 和 Cookie ,但是并没有细究其底层到底是如何实现的,但是 HTTP 本身是无状态的协议,Session 肯定也该是在 Cookie 的基础上实现的吧?

不过那时我也不想深究这些问题了,把 sleep 的时间改为了 60 秒,放到 DH 的服务器上去跑,这次应该不会有什么问题了,只是抓取的效率太慢了而已,不过明天早上起来应该可以了。另外一个就是我发现学校 VPN 代理和 VPN 用的大概是不同的出口,所以还有一个出口可以用,于是我把要抓取的论文分成两份,本地和 DH 同时抓。

后台抓数据去了,我再来琢磨如何把数据给展示出来,本来一直想着怎么在 Python 里分析 BibTeX ,其实 BibTeX 的格式倒是挺简单,但是就是不同的条目里面的信息完整性相差太大了,有的很完整,有的着只有标题和作者。但是出门去坐公交车的时候,突然觉得既然 LaTeX 里面能直接用 \cite 生成引用并在末尾列出标准格式的参考文献列表,那么应该有办法直接在当前位置生成类似于参考文献列表的那些内容。一开始我想也许有个什么命令可以直接展开 BibTeX 的条目,但是最后搜索了一下,发现有另一个方法。实际上在运行 bibtex 命令的时候,它会搜集 TeX 文件里引用到的那些参考文献,并把参考文献的列表生成出来,放在一个 .bbl 文件里,而 latex 命令其实就是把那个 .bbl 直接放到文档末尾的(当然还有索引序号维护等工作),所以我只要生成一个 .bib 文件包含所有 BibTeX 的信息,然后生成一个 .tex 文件并在里面引用所有的论文,之后就可以用 bibtex 命令得到 .bbl 文件,里面就包含了每篇论文的格式化好的 TeX 格式的代码,而又了它们,我就可以随心所欲地生成列表了!Perfect! 😀

挺简单的代码,也折腾了不少时间,主要是 Google 的 BibTeX 也是自动生成的,其中有一条有错误,导致 latex 编译失败,而 latex 又以错误信息毫无用处著称,我甚至都看不出出错的是哪行。结果折腾到比较晚才睡。还有就是要对结果按照给定的一个标题列表排一下序,抓取的时候是没有按照顺序存储,这样也方便我直接把任务分配到几个机器上去跑。结果第二天一大早起来,发现数据果然已经抓完了。立马把数据拷贝到一起,生成了最终 TeX 文件并编译成 PDF 发给了导师。然后就去赶公交车去实验室了。

事实证明老是在屋里呆着思维会闭塞的,虽然赶公交车十分痛苦,但是出门透透气确实有好处,外面的风一吹我就想起来昨天还在考虑的一个分页的问题到最后还是被我给忘记了,结果如果论文引用次数大于 100 的话,就最多只收录了 100 篇。于是到实验室立马 fix 了这个问题,不过抓取数据又是漫长的等待,等了一会有些受不了了,就把每个页面的等待时间设置为了 20 秒,这样稍微快一些了,可是没过一会儿就 Service Unavailable 了,难道是我间隔时间太规律了?可是 Google 未免也太小气了!就算判断出来我是 bot ,20 秒才访问一次都要禁止啊?太过分了!我还想,我们大家相互忍让一下,这事就完美解决了,看来还不能善罢甘休了。

于是我一边再把时间设置为 60 秒,放到 DH 上去跑,作为最后保底用,一方面想办法怎么突破那个验证码的障碍。一个方法是弹出验证码手工输入一下,不过这个应该当作所有其他方法都不行的时候才采用的吧,因为我甚至连它如何从 scholar.google.com 跳转到 sorry.google.com 的都不清楚,更不知道它在验证的时候有没有用到 javascript 之类的东西(有不少 WordPress 的放 spam 插件就是用类似的方法来实现的)。所以我先尝试用 Firefox 来输入验证码,然后用之前提到的代码从 Python 里读取 Firefox 的 Cookie 。

经过一个小脚本的测试,发现这个方法失败了,我也不知道问题出在哪里,不过这个时候我终于想到了另一个办法:用 GreaseMonkey 。根据我以前的一些经验(如果你不知道 GM ,我曾经写过一个简短的介绍),GreaseMonkey 做某些事情非常方便,但是也有一些不太好的地方,比如使用“裸” Javascript 写代码很麻烦,而其位置的特殊性似乎也不能直接用 Firebug 进行调试,所以调试比较痛苦,还有就是文档有点陈旧了。不过既然 GM 如此 popular 而且社区似乎挺大,缺乏文档似乎说不过去,于是我又搜索了一下,打算评估一下可行性。

之前曾经使用过一点 Prototype.js 这个 Javascript 库,很方便,不过最近似乎 jQuery 越来越火了,于是也趁机尝试一下吧,正好看到有在 GM 脚本里加载 jQuery 的方法。再有就是 GreaseMonkey 本身的文档了,搜索到一个 wiki ,点过去看到的页面让我坚定了信心,GM 准能行!不过那个 wiki 的的页面并不是 full of useful documents ,而是那个 Apache 的经典页面(估计当时正在修改配置或者升级之类的吧,不过现在已经修复了):

works

我第一次觉得这句话这么有哲理!其实仔细回想一下我并不需要什么 GM 的特殊功能,如果所有内容都可以从 Google Scholar 的网站上得到的话,就不会有 cross-domain AJAX 的问题,直接用标准 AJAX 的 API 就可以了。于是大致扫描了一下 jQuery 的 Tutorial ,也算是再回忆一下 Javascript 的语法。(这里我突然想到一点,于是稍微跑一下题。对于学习语言来说的话,我觉得一开始肯定是要看语法规则之类的,但是那应该是作为一个大致框架或者引导作用,后续学习或者长时间不用了再复习的时候则主要用看例子的办法比较有效。类比到机器学习里的话,现在 learn by example 应该已经取代早期的 AI 成为主导了,不过就好像一来就给你一堆例子让你自己去抽象规则一样,纯粹的 learn by example 似乎还是比较困难的,所以适当地加入一些先验知识以及启发式规则应该是很有效的方法,而不应该受到排斥。当然如果目的就是理论研究那又另当别论了。)

准备妥当之后就开工了,不过我发现 VIM 编辑 javascript 缩进太难看了,VIM 默认支持的文件类型非常多,但是如果是 Emacs 支持的格式的话,缩进一般都比 VIM 漂亮很多。不过 Emacs 这里似乎拿 java-mode 来充数了,虽然也比 VIM 那个要好些,不过我还是想更浮云一点。于是再去网上搜索了一番,找到了那个看起来非常 promising 的 js2-mode ,下下来用了一下,发现很奇怪地每次回车都给我缩进到老远的地方还自动插入一些引号,不过我现在没空去管它为什么会出怎么奇怪的 bug 了,再找了另一个 javascript.el ,总算用起来舒坦了。

到这里似乎磨了太久的刀了,于是我赶紧开始写。期间遇到了不少问题,一个是 javascript 或者 AJAX 本身的特殊性——它是异步的。这让我想起了很久以前用 boost::asio 的经历,非常不习惯。本来想把所有的任务一股脑串起来,但是又不知道 Javascript 有没有尾递归,到时候万一超过最大递归限制了就不好了。当然最直接的方法还是直接在顶层并行地把所有任务分发出去,也就是分发 N 个论文的任务,然后每个任务再分发 M 个获取引用的 BibTeX 的任务,虽然觉得 Firefox 肯定会做调度,但是还是觉得这样似乎有点太暴力了,而且我也不清楚这样最后的结果如何整合在一起呢,这些异步的回调函数会在不同的线程里执行吗?需要对共享变量加锁之类的吗?似乎没有在 Javascript 里听说过类似的东西,看来我对相关的东西还是了解太少了。所以最后还是选择了最简单的办法:把 AJAX GET 方法强制串行化,回到了和 Python 脚本一样的执行流程。

另一个问题是正则表达式,由于我前面提到的语法不兼容性,所以表达式要做稍许修改,特别是诸如 . 是否会匹配到换行符之类的地方。另一个让我吃苦头的就是 Javascript 里的用正则表达式寻找所有匹配的方法:

var myRe = /ab*/g;
var str = "abbcdefabh";
var myArray;
while ((myArray = myRe.exec(str)) != null) {
  var msg = "Found " + myArray[0] + ".  ";
  msg += "Next match starts at " + myRe.lastIndex;
  print(msg);
}

在一个循环里重复地调用 exec ,看来上次匹配的信息保存在了正则表达式里了吧。可是我发现自己得到的结果是无限循环。折腾了好久终于发现原来自己少了 g (global) 选项,所以一直都只匹配第一个了。原来正则表达式的 global 选项还有这用处。然后就是调试了很久的 . 匹配换行的问题,我觉得加了 m (multiline) 选项应该就可以了,但是好像又不行,最后我干脆在预处理的时候把所有的换行符都替换成了空格,让整个 HTML 成为一个长行(反正 Google 生成的文件本身换行就是乱七八糟的)。

最后是处理 UI ,虽然我 HTML/CSS/Javascript 一直都是半吊子,但是无疑这是我用过的做 GUI 最方便的工具。这里我也不需要什么华丽的 UI 了,用 jQuery 很方便地在 DOM 树上添加了一些文本用来说明,然后添加了一个大文本框用来输入要搜集的论文的标题列表,以及一个按钮用来启动搜集过程。大概就是这个样子:

getcites_ui

输入作者和标题列表之后点击下面的 Fetch 按钮,就开始工作了。执行过程会把当前正在抓取的文章以及状态在后面列出,另外,由于 jQuery 的便利,我还小小地浮云了一下,在每次更新状态的时候让浏览器以自动往下滚屏。最后的结果就像这个样子,出错的文章一般就只有几篇了,所以直接列出来并醒目地标出错误原因,而得到的所有 BibTeX 因为在 Javascript 里面不能像 Python 那样直接访问本地文件,所以也在页面上显示出来,为了方便 Ctrl+ACtrl+C 复制,我把它放到了一个文本框里:

getcites_fetching

得到的 BibTeX 存放的方式和 Python 脚本是一样的:每篇论文首先是自己的 BibTeX ,然后紧接着所有引用到它的论文的 BibTeX 。不过这里因为所有的 BibTeX 放在一起了,所以不同的论文相关的 BibTeX 之间用标记隔开了。得到的结果用一个小脚本打散一下存放到各个文件中,就可以继续沿用之前的生成最终 PDF 的脚本了。

我还担心在抓取的过程中 Google 会再要求我重新输入验证码,所以特地处理了网络错误,不过似乎 Google 在输入了验证码之后就在这个浏览器的 session 都直接放行了。终于,在万事俱备,只欠启动的时候,Google 又来了最狠的一招:

google_403

这个 403 Forbidden 和并不是像之前那样跳转到 sorry.google.com 之后的 Service Unavailable 页面,而是由 scholar.google.com 直接给出的,并且没有说输入验证码了就可以通过了——因为根本没有输入验证码的框。一方面我超级无语加沮丧,另一方面我又感叹今天怎么尽遇到神奇的事情?之前看到那句“It works!”我还觉得是得到神人指引呢,看来还是道高一尺,魔高一丈,Google 非得等到我万事俱备了,才使出那招“东风破”。这难道就功亏一篑了?我重启了 Firefox ,换了台机器,也不行。只有希望它按照承诺“restore your access as quickly as possible”了,于是很无聊地看 Google Reader 。

最后也不知过了多久恢复了访问,好像也不是太久。于是点了 fetch 按钮,看着屏幕慢慢地向下滚动。心里觉得大概还会再次被 ban 掉吧,就算不是刚才的完全 ban 掉,大概也需要重新输入验证码吧,所以可能需要这样弄好几次才能把资料收集完,总之尽量速战速决。在停在一篇有接近 300 篇引用的论文的时候,我就一直紧张地盯着屏幕,最后看到它顺利通过,也算松了一口气。

出乎我意料的是,几分钟过后,竟然顺利地把所有论文都处理完了——当然紧接着在上 Google Scholar 就又是那个 403 Forbidden 的页面了,不过难道是之前的请求太频繁了 Google 只好先把它们一股脑全处理了,才发现应该 403 ?不过不管怎样,真是非常赞!大致检查了一下结果,保存下来之后,再去看 DH 上的那个脚本,还在第二篇论文挣扎呢,立马就把它结束掉了。

又等待了一段时间,Google Scholar 恢复之后,把几篇出错的论文的 BibTeX 手工保存下来。不过其实还是有一些瑕疵的,比如 Google 的 BibTeX 里会有“Conference on”这样的东西,不知道为何它在“on”这里断掉了,不过 Google 似乎也是直接从 ACM 等网站直接抓的 BibTeX 吧,好像在那边看到就是这样了。不过这些地方稍微处理一下就好了,比如:

sed -i 's/Conference on},$/Conference},/' allbib.txt

最后生成出 PDF ,发现引用竟然有 2k+ ,幸好没有手工做啊。发给何老师,他又希望要 Word 版本,好编辑一些。那好吧,倒是有几个办法,比如从 TeX 转换为 RTF ,然后导入 Word ,或者转换为 HTML 再导入 Word 。最后选择了 RTF 的办法,得到了 Word 文档,又给他发过去,然后说:“就是格式稍微有点乱了。”结果何老师很看起来很严重的样子问我:“格式哪里乱了?我怎么没有看出来?”结果我说不如 PDF pp 了,他狂汗。 ^_^bb

最后再应他的要求区分了一下自引和他引,这个在已经有数据的情况下自然是很容易的事情了,最后的 PDF 截个图放在这里吧,也算我狐假虎威炫耀一下(我也觉得何老师是非常牛的,不过也没有具体概念,问 Roxxane ,她也说“非常牛”,还是个模糊的概念。不过想想就算我比较熟悉的方面,其实也是差不多,你说 Ritchie 有多牛呢?还是不能拿尺子量的东西。)

allcites

不过最让我欣慰的是,何老师对结果还是比较满意的。因为在最初加入这个实验室的时候给何老师写过一篇比较长的 Email (发现我真是写起来就没个完啊,又发现何老师这么忙人果真是没有认真看 Email 的习惯),说明了自己之前接触的东西一直都是 engineering 方面的,然后在找他聊天的时候他就说做 research 并不是“从网上找几段代码来粘贴到一起”,还给了我一些建议,不过我大概就对这句话印象比较深刻吧。我想我也想借此机会让他看看程序员的处事方式吧,因为我觉得 engineering 当然也并不是“从网上找几段代码来粘贴到一起”的事的。 😉

回头我会把相关的脚本整理出来,也许会有其他人有用吧。今天太晚了,先休息了,明天俱乐部还要去灵峰探梅呢,可不能睡过了。 😀

13 comments to 统计论文被引用的情况