多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Arxiv论文批量导入NoteExpress:API+BibTeX半自动化方案

Arxiv论文批量导入NoteExpress:API+BibTeX半自动化方案 在科研圈里Arxiv和NoteExpress这两个名字几乎每天都会出现在研究生的屏幕上。Arxiv是“论文还没正式发表就得先上去看最新版”的速度来源NoteExpress则是写论文时整理参考文献的本地大管家。但这两个工具之间一直缺一座足够顺手的桥Arxiv每天更新量大很多方向一天就有几十篇新论文如果靠一篇一篇点进网页、下载PDF、再手动录入题录到NoteExpress那手机片刻都停不下来文献库也永远处在欠账状态。这篇博文就是来填这个坑的。我会用一个实测可跑的方案把“Arxiv论文批量导入NoteExpress”这件事从手动降维成半自动内容包括用Arxiv官方API抓元数据、生成BibTeX、批量下载PDF、再把所有东西塞进NoteExpress。流程跑通之后从一份论文ID列表到NoteExpress里的完整题录和PDF附件5分钟是够用的熟练以后还能更短。1. 为什么导入Arxiv论文这么费劲四条路线的对比分析1.1 手动录入的痛点能忍但没必要先聊聊我自己的使用场景。研究方向是计算机视觉Arxiv上的cs.CV分类每天新增几十篇每周上百篇。我养成的习惯是每周花一天把最近一周的相关论文过一遍挑出值得精读的。以前的做法非常原始在Arxiv网页上搜索关键词看到标题合适的论文就点进去浏览器自带翻译或者直接读摘要觉得可以就在NoteExpress里手动建一条题录把标题、作者、年份、期刊、页码一个个敲进去。接下来还得打开PDF下载页面另存为然后再回到NoteExpress里把PDF关联到刚才的那条题录上。这个流程单看一篇论文好像只需要两分钟。但乘以几十篇就是连续一两个小时的无脑重复操作。更要命的是手动录入特别容易出错。作者英文名漏打几个字母年份顺手敲错这些错误在写论文生成参考文献时都会原形毕露到时候再回头改数据库里的题录比新录一条还麻烦。我有一次帮同门整理导师布置的“近三年目标检测方向论文调研”一共需要整理两百多篇。当时我用的是在线检索加手动导入的方式搞了整整一个下午中间复制粘贴还出了好几次错最后靠Excel比对才把重复和错误的条目纠正过来。从那天起我就下定决心这类工作一定要找一个能批量处理的方案。1.2 NoteExpress自带的在线检索到底行不行NoteExpress的在线检索功能是很多中文数据库用户的救命稻草但我对它的评价是对中文数据库很友好对英文数据库部分友好对Arxiv这种纯预印本平台只能说“看版本、看运气”。我自己实测下来的感受是在NE的在线检索入口里能找到Arxiv的情况不算稳定。有的版本能搜到并返回结果有的版本干脆找不到这个库。就算找到了检索结果的加载速度也比较慢更关键的是它保存题录时一条一条处理完全谈不上“批量”。还有一个绕不过去的痛点在线检索只能拿到题录信息拿不到PDF。题录进了NE之后你还是得切到浏览器打开Arxiv页面去下载PDF然后再回到NE里手动关联。和“从Arxiv网页直接操作”相比省掉的只是录入那个步骤但下载和关联还是一点都没少。所以我的结论很明确如果只是偶尔导个一两篇NE在线检索完全够用。但如果你需要的是“批量导入”这条路可以直接pass它的模式决定了它不适合大规模操作。1.3 为什么最终选了“APIBibTeX”中转路线我们的真实需求其实很朴素给我一批Arxiv论文标识——可以是一串论文ID可以是一个关键词搜索结果也可以是一个作者名下的所有论文——我要它们全部进入NoteExpress最好连PDF都一起管好。顺着这个需求想最可靠的路径是什么我的答案是“标准化中转格式”。具体来说就是用Arxiv官方API把论文元数据抓下来整理成BibTeX文件再让NoteExpress去解析BibTeX。这个方案有三个别人给不了的优势第一Arxiv官方API返回的是结构化XML标题、作者、摘要、分类、PDF链接都是现成字段不需要自己去爬网页或者解析HTML稳定性和准确性都有保障。第二BibTeX是文献管理软件之间的“普通话”。NoteExpress自带BibTeX导入过滤器可以直接把BibTeX里的字段映射到NE题录的对应字段。不需要写数据库导入插件也不需要手动调整Excel。第三API返回结果里本身就有PDF链接下载PDF和生成BibTeX这两个动作可以放在同一个脚本里完成。一次循环题录和PDF全都备齐后面要做的事情就只剩下在NE里按一下导入按钮。对比之后你会发现其他路线本质上都是单篇处理而APIBibTeX这种中转方式唯一的“手动”环节就是导入时按下那个按钮。2. 动手前需要准备的东西2.1 软件和Python环境这个方案需要的工具很少我用到的就是这三样NoteExpress版本越新越好我自己用的是3.x系列。Python 3.8及以上不需要装额外的数据库也不需要服务器。命令行工具wget或curl用来批量下载PDF。Linux和Mac自带Windows用Git Bash或者PowerShell也能搞定。如果你电脑里没有Python问题也不大。整个方案的核心逻辑只是两个HTTP请求一个请求API拿XML一个请求PDF链接下文件。你用JavaScript、Go、甚至Shell脚本都能实现同样的效果。我选Python纯粹是因为它处理XML最顺手。如果愿意用第三方库的话装一个arxiv就够了命令是pip install arxiv。这个库把API请求、结果解析、限速这些都封装好了用它写出来的脚本更短。不想装库也行Python自带的urllib和xml.etree完全可以搞定只是代码会长一些我会在下面把两种方式都写出来。2.2 Arxiv官方API的入门用法Arxiv API只有一个入口地址:http://export.arxiv.org/api/query。所有请求都拼在URL参数里传过去,不需要认证,不需要API key这也让脚本写起来非常简单。常见的参数有这么几个search_query搜索表达式。比如all:electron表示在所有字段里搜electroncat:cs.CV表示计算机视觉分类au:John_Doe表示按作者检索。多个条件可以用 AND、OR 组合。start从第几条开始返回默认0。配合max_results可以做翻页。max_results单次返回的最大条数。官方没有严格限制但一次性拉几千条容易触发限流建议单次控制在100以内不够就翻页。sortBy和sortOrder排序字段我用得最多的是submittedDate按提交时间倒序排这样最新论文在前面。返回结果是Atom格式的XML根节点是feed每篇论文对应一个entry。论文编号可以从id节点里提取格式是http://arxiv.org/abs/2410.02644取最后一段2410.02644就是标准的Arxiv ID。标题在title节点作者是多个author节点下的name摘要在summary。有一点需要提醒API的搜索语法和Arxiv网页上的搜索框并不完全一致。同一个检索词网页和API返回的排序、匹配范围可能不同。我的习惯是在网页上先试好关键词和筛选条件再翻译成API参数这样能避免脚本跑完发现结果不对又重新拉数据。2.3 BibTeX基本格式NE认什么BibTeX可以理解为文献管理界的通用交换格式。它就是一个纯文本文件里面一条一条地列着文献信息每条以article或misc开头后面的花括号里是各种字段。下面是一条典型的Arxiv论文BibTeX条目article{2410.02644, title {这里写论文标题}, author {Doe, John and Smith, Jane}, journal {arXiv preprint arXiv:2410.02644}, year {2024}, eprint {2410.02644}, archivePrefix {arXiv}, primaryClass {cs.CV}, url {https://arxiv.org/abs/2410.02644}, abstract {这里写摘要} }字段的含义很容易看懂标题、作者、年份、URL、摘要。eprint字段是Arxiv特有的标识符archivePrefix指来源预印本平台“journal”字段在Arxiv预印本场景下一般填arXiv preprint arXiv:xxxx。NoteExpress在导入BibTeX时,就是用“导入过滤器”来解析这个文件。它会自动把title映射到题录的“标题”字段author映射到“作者”year映射到“年份”url映射到“URL”abstract映射到“摘要”。我在NE 3.2上实测过这些常用字段都能正确进来。至于primaryClass这类Arxiv专属字段NE可能会忽略这不影响核心使用。3. 完整实操从论文ID列表到NE题录加PDF3.1 第一步批量抓取元数据并生成BibTeX这一步是整个流程里技术含量最高的部分但其实也只是一个Python脚本的事。下面这个脚本实现了请求Arxiv API、解析返回的XML、生成BibTeX文件。import urllib.request import urllib.parse import xml.etree.ElementTree as ET import time ARXIV_API http://export.arxiv.org/api/query def arxiv_search(query, start0, max_results10): params { search_query: query, start: str(start), max_results: str(max_results), sortBy: submittedDate, sortOrder: descending } url ARXIV_API ? urllib.parse.urlencode(params) req urllib.request.Request(url) with urllib.request.urlopen(req) as resp: return resp.read().decode(utf-8) def parse_arxiv_xml(xml_text): ns { atom: http://www.w3.org/2005/Atom, arxiv: http://arxiv.org/schemas/atom } root ET.fromstring(xml_text) papers [] for entry in root.findall(atom:entry, ns): title entry.find(atom:title, ns).text.strip().replace(\n, ) summary entry.find(atom:summary, ns).text.strip().replace(\n, ) paper_id entry.find(atom:id, ns).text.strip().split(/abs/)[-1] authors [a.find(atom:name, ns).text for a in entry.findall(atom:author, ns)] published entry.find(atom:published, ns).text.strip() primary entry.find(arxiv:primary_category, ns) primary_class primary.get(term) if primary is not None else papers.append({ title: title, summary: summary, paper_id: paper_id, authors: authors, published: published, primary_class: primary_class }) return papers def bibtex_escape(text): text text.replace(\\, \\\\) text text.replace({, \\{).replace(}, \\}) text text.replace(, \\) text text.replace(%, \\%) text text.replace(#, \\#) text text.replace(_, \\_) return text def format_author(name): parts name.split() if len(parts) 1: return parts[0] return parts[-1] , .join(parts[:-1]) def papers_to_bibtex(papers): entries [] for p in papers: authors and .join(format_author(a) for a in p[authors]) year p[published][:4] entry farticle{{{p[paper_id]}, title {{{bibtex_escape(p[title])}}}, author {{{authors}}}, journal {{arXiv preprint arXiv:{p[paper_id]}}}, year {{{year}}}, eprint {{{p[paper_id]}}}, archivePrefix {{arXiv}}, primaryClass {{{p[primary_class]}}}, url {{https://arxiv.org/abs/{p[paper_id]}}}, abstract {{{bibtex_escape(p[summary])}}} }} entries.append(entry) return \n\n.join(entries) if __name__ __main__: query cat:cs.CV AND submittedDate:[202401010000 TO 202403312359] xml_text arxiv_search(query, max_results20) papers parse_arxiv_xml(xml_text) with open(arxiv_papers.bib, w, encodingutf-8) as f: f.write(papers_to_bibtex(papers)) print(f共生成 {len(papers)} 条题录)这个脚本里藏着几个容易踩的坑。第一个坑是特殊字符转义。很多论文标题里有下划线、百分号、井号这些符号直接写入BibTeX会导致解析出错。我在bibtex_escape里做了处理把_、%、、#等字符转义成LaTeX安全的写法。第二个坑是作者名格式。Arxiv API返回的是“John Doe”这种“名在后姓在后”的顺序而BibTeX标准格式要求“Doe, John”这种“姓在前名在后”的写法format_author函数就是为了处理这个转换。第三个坑是编码。写文件时一定要加encodingutf-8否则后面导入NE时中文或特殊符号容易乱码。脚本运行完当前目录下会多出一个arxiv_papers.bib文件打开看看里面就是标准BibTeX数据了。3.2 第二步基于BibTeX批量下载PDFBibTeX文件已经包含每篇论文的Arxiv ID了,把ID提取出来拼PDF链接,就可以批量下载。下面是配套的下载脚本import re import os import time import urllib.request def download_pdfs(bib_filearxiv_papers.bib, out_dirpapers): os.makedirs(out_dir, exist_okTrue) with open(bib_file, encodingutf-8) as f: content f.read() ids re.findall(reprint \{([^}])\}, content) print(f找到 {len(ids)} 篇论文) for paper_id in ids: pdf_path os.path.join(out_dir, f{paper_id}.pdf) if os.path.exists(pdf_path) and os.path.getsize(pdf_path) 0: print(f跳过已存在: {paper_id}) continue url fhttps://arxiv.org/pdf/{paper_id} print(f正在下载: {paper_id}) try: urllib.request.urlretrieve(url, pdf_path) print(f完成 - {paper_id}.pdf) except Exception as e: print(f下载失败 {paper_id}: {e}) time.sleep(3)这段代码的逻辑很简单但有三个细节我是在实际使用中总结出来的。第一是必须检查文件是否已存在。脚本会反复跑不检查的话每次都会重新下载一遍浪费流量不说频繁请求还会被Arxiv临时限流。第二是下载间隔至少3秒。我最早写脚本时没有加延时连续下载到第七八篇就开始报403后来把间隔调到3秒才稳定。第三是文件名统一用论文ID.pdf不要用自定义的中文名或长标题名。这样后面导入NE时通过文件名就能快速找到对应的PDF文件也不会因为文件名太长相容性出问题。3.3 第三步把BibTeX导入NoteExpress题录和PDF都准备好了接下来就是NE的主场。导入BibTeX的操作其实很简单我一步步说清楚打开NoteExpress新建一个数据库或者打开你正在用的常用库。点击菜单栏“文件”-“导入题录”。在导入对话框里“导入文件”选择刚生成的arxiv_papers.bib“过滤器”选择BibTeX。不同NE版本显示名称略有差异有的叫“BibTeX”有的叫“BibTeX - 通用”找个带BibTeX字样的一般不会错。“导入选项”选“插入新题录”点击“导入”完成。导入完成后千万不要直接关窗口。先看右下角状态栏里的导入数量确认和BibTeX里的条目数一致。然后随便点开几条题录检查标题、作者、年份、摘要、URL这些核心字段有没有正确进入。摘要偶尔会丢这个问题我在后面“常见问题”部分细说。还有一个小技巧导入后题录类型默认可能是“期刊论文”但Arxiv预印本更准确的类型是“电子文献”或“预印本”。如果你比较在意题录类型的准确性可以全选这些题录右键“批量修改题录类型”统一改成你想要的。这一步不影响参考文献格式,但能让文献库更规范。3.4 第四步把PDF批量关联到题录导入题录只是完成了第一步把PDF挂到对应题录上才算真正搞定。这一步我试过好几种方式实际效果差异很大。先说NE自带的“批量添加附件”。在题录列表里选中多篇论文右键选“添加附件”或“批量添加附件”然后选择包含PDF的文件夹。NE会自动尝试把文件夹里的PDF和题录匹配匹配依据主要是DOI和外部标识符。问题在于Arxiv论文一般没有DOINE不太认识eprint这个标识符所以自动匹配的成功率并不高。我实测下来文件名是2410.02644.pdf这种格式时它能匹配上一些但总有漏网之鱼比例还不少。所以我的最终方案是手动拖拽补齐。在题录列表里选中一条题录右侧附件区会出现一个空白附件面板直接把papers文件夹里对应的PDF拖进去就行。对于一周几十篇论文来说手动拖拽大概也就花几分钟完全可以接受。为了减少手动配对的难度我在下载PDF时指定的文件名就是论文ID。这样当我在文件夹里找对应文件时只需要看题录里的URL末尾或eprint字段就能快速定位到文件名不用猜、不用读标题、不用复制粘贴。3.5 懒人版用arxiv库写一个自动化脚本如果你不想看API解析和XML处理的细节用arxiv这个第三方库可以直接简化掉一半代码。下面是我常用的懒人版本import arxiv import os import time def download_paper(eprint_id, out_dirpapers): os.makedirs(out_dir, exist_okTrue) pdf_path os.path.join(out_dir, f{eprint_id}.pdf) if os.path.exists(pdf_path) and os.path.getsize(pdf_path) 0: return False url fhttps://arxiv.org/pdf/{eprint_id} os.system(fwget -O {pdf_path} {url}) time.sleep(3) return True def main(): search arxiv.Search( querycat:cs.CV AND all:segmentation, max_results20, sort_byarxiv.SortCriterion.SubmittedDate ) results list(search.results()) with open(papers_auto.bib, w, encodingutf-8) as f: for res in results: paper_id res.entry_id.split(/abs/)[-1] authors and .join(a.name for a in res.authors) f.write(farticle{{{paper_id}, title {{{res.title}}}, author {{{authors}}}, journal {{arXiv preprint arXiv:{paper_id}}}, year {{{res.updated.year}}}, eprint {{{paper_id}}}, archivePrefix {{arXiv}}, url {{https://arxiv.org/abs/{paper_id}}} }} ) download_paper(paper_id) if __name__ __main__: main()这个脚本短了很多arxiv库把API调用和解析都封装好了直接遍历结果就能拿到标题、作者、更新年份等字段。运行完目录下会同时出现papers_auto.bib和papers文件夹然后按照前面3.3和3.4的步骤导入NE、关联PDF即可。4. 常见问题与排查技巧实录4.1 导入后字段丢失或乱码怎么办导入BibTeX后最常遇到的问题是摘要字段为空。我遇到过两次一次是NE版本较老BibTeX过滤器不支持abstract字段的映射另一次是BibTeX文件里摘要含有换行符和特殊字符解析时被截断了。解决办法是先检查BibTeX文件本身有没有问题用文本编辑器打开看摘要是否完整如果文件没问题就考虑升级NE版本或者导入后在NE里选中缺失摘要的题录手动补录。还有乱码问题。如果BibTeX文件是用encodingutf-8保存的理论上不会乱码但前提是NE能正确识别文件编码。导入时如果发现中文乱码把BibTeX文件用“另存为 UTF-8 无BOM”格式再试一次通常能解决。4.2 PDF下载失败或网络异常的排查下载PDF时报404通常是个别论文ID有问题。有些论文被作者撤稿或删除了Arxiv页面上已经不存在这时候404是正常的。解决方案是跳过该ID不影响其他论文下载。报403是比较常见的限流信号。Arxiv对单IP的请求频率有限制连续快速下载会被临时封禁。解决办法很简单把time.sleep里的间隔加大到5秒以上等一段时间后再重试。如果403一直出现说明IP可能被临时封了等几小时或一天再继续。还有一种情况是网络环境本身无法正常访问arxiv.org表现为所有连接都超时。这种情况不要折腾脚本了换个能正常访问外网学术站点的网络环境再跑。如果当前网络受限建议在可正常访问的网络环境下执行脚本。4.3 附件匹配不上的补救办法这个问题的根源在于NE不认识eprint这个标识符所以自动匹配PDF的成功率不稳定。如果你试了“批量添加附件”后发现很多题录没挂上PDF补救方案有两个第一个方案是检查BibTeX里有没有放url字段。有些NE版本在批量匹配附件时会参考URL字段如果URL里包含arxiv.org/abs/链接匹配成功率会提高。如果当初生成BibTeX时漏了这个字段可以在NE里手动补上URL然后重新试一次批量添加附件。第二个方案就是回归手动拖拽。选中题录打开发附件面板把对应PDF拖进去。我最终实际采用的是这个方案虽然听起来不够“自动”但胜在完全可控不会出现匹配错乱的情况。4.4 常见问题速查表问题可能原因解决办法导入后题录数量不对BibTeX文件语法错误用文本编辑器检查是否有未闭合的花括号标题出现LaTeX命令特殊字符未转义生成BibTeX时用bibtex_escape处理摘要字段为空NE过滤器版本不支持abstract升级NE或导入后手动补录PDF下载404论文ID错误或论文被撤访问Arxiv页面确认编号是否有效PDF下载403请求频率过高增加下载间隔到5秒以上稍后重试批量关联附件失败NE不识别eprint ID手动拖拽PDF到附件区导入后作者名颠倒BibTeX作者格式有误检查format_author是否按“姓, 名”生成中文内容乱码文件编码不是UTF-8另存为UTF-8无BOM格式5. 效率进阶建议与避坑心得5.1 把“一次性导入”变成“每周例行”脚本跑通了别把它当成一次性工具用完就扔。对我来说这套流程最大的价值在于它能把文献跟踪变成每周固定的例行工作。每周一早上花十分钟跑一遍脚本把上一周的Arxiv新论文抓下来导入NE。导入后在NE里建一个“本周新论文”的文件夹把本次导入的题录全部移动进去。然后随手翻一翻标题和摘要觉得值得细看的就打上标签“重点关注”。等周末有空时只需要在这个文件夹里按标签筛选就能快速找到需要精读的论文读完顺手在NE的笔记功能里写几句想法。一周几十条新论文实际需要花的时间不长但文献库从此不会再欠账写论文时想找某一篇看过的论文一分钟内就能定位到。5.2 更灵活的检索与筛选技巧Arxiv API的search_query支持很多组合除了最常见的关键词搜索下面几个是我反复在用的写法cat:cs.CV AND cat:cs.AI希望论文同时出现在计算机视觉和人工智能分类下的交集中适合做交叉领域调研。au:Jitendra_Malik按作者搜索注意空格要用下划线。想跟踪某位学者的动态时非常管用。all:deep learning AND submittedDate:[202401010000 TO 202403312359]关键词加时间窗口只抓某个时间段内的论文。ti:image segmentation限定在标题里搜比全字段搜索精准得多适合筛选主题非常明确的论文。如果你在Arxiv网页上用的也是这套检索词那转换成API参数时基本只需要把空格替换成加号、把时间格式改成YYYYMMDDHHMM这种纯数字格式就行。5.3 我踩过的几个坑最后分享几个我自己在这套方案上踩过的坑都比较有代表性。第一个坑是下载PDF时没加延时。最早写的脚本是一篇接一篇连续下载速度看起来很快但到第七八篇时开始大量返403。后来我把间隔加到3秒又在失败时自动等待10秒重试一次才彻底稳定下来。第二个坑是导入BibTeX后没有检查摘要字段。当时导入了上百条题录只看了数量和标题没注意摘要全是空的。等到后来想按摘要做筛选时才发现问题只能一篇篇去Arxiv页面重新找额外浪费了很多时间。所以导入后一定要抽查几篇确认字段完整再继续。第三个坑是PDF文件名用了中文。有一批论文标题很长我图省事直接用标题前几个字当文件名结果同步到另一台电脑时文件名乱码NE里的附件全部失效。后来所有自动下载的文件全部改成eprint_id.pdf这个问题再也没有出现过。第四个坑也和网络环境有关。有一阵子在单位网络环境下跑脚本下载时经常莫名超时我以为是Arxiv封了我的IP换了家里的网络就好了。后来才明白是单位网络的稳定性问题不是Arxiv的问题。所以遇到连续性下载失败先考虑是不是网络环境本身不稳定。这套流程我用了大概一年最大的变化不是省了多少时间而是文献库终于能够跟上Arxiv的更新速度了。以前一周不整理就积累几十篇待处理现在每周跑一遍脚本基本当天就能把该入库的入库、该读的读完。整个过程里我觉得最难的不是写脚本而是第一次把NE的BibTeX导入和PDF批量关联这两个环节跑通跑通之后就再也回不去手动一条条录入了。如果你也有批量导入Arxiv论文到NoteExpress的需求照着上面三步走第一次搭环境花半小时之后就是稳定复用。祝各位科研顺利文献管理不再成为负担。
返回列表