DOI是文献会议论文等文章的网上注册”ID”. 一个ID对应一篇文献, 十分好的用于区分文献. DOI由doi.org组织维持, 关于DOI的细节就不说了..
以下这个类包括了DOI对象, 包括验证, 标准化, 查找用的正则表达式, 分解等等.
- 正则表达式:
DOI.pdoi
, 类属性, 定义了查找doi的正则表达式, 使用就是rout=obj.pdoi.search(text);if rout: out=rout.group()
返回标准的DOI. 缺点是如果DOI和一堆文字连在一起就不好区分了..比较难以避免. - 标准化 : 理论上需要使用正则提取一下, 懒了, 直接
10.
判断位置再小写一下就算了 ╮(╯▽╰)╭. 在构造函数__new__
中完成. - 分解前缀和后缀 : 在初始化时会产生
prefix
和suffix
属性. - 文件名化 : DOI往往含有window中不支持的特殊符号,例如典型的
/
,还有<>:;
等. 这里把文件名化标准是:- 小写化, 去除两端空格
/
变成@
- 数字, 字母,
+()._-
不变保持原状 - 其余的特殊符号进行url的quote变化(变成
%2f
类似的url替代符),注意原来的@
也会替换掉,而看到文件名的@
实际是\
- 使用
quote()
和unquote()
方法进行变换
- 分解 : 这个是为了保存东东的, 将doi文件名化后, 将后缀分解为5个一组, 每组一个文件夹. 这样可以避免用前缀保存相应doi对应文件时一个文件夹上百万文件. 可以自定义别的长度, 默认是5. 有些已知的bug:
- window下
abcd.
的文件夹最后一个.
会被去掉(第一个点不会被去掉),所以相应要rstrip('.')
. 所以可能出现4个长度. - window下有保留名如con, aux这些词不能用于文件夹和文件名命名, 例如不能创建叫
Con.1
的文件夹. 比较极端情况, 但出现过..解决办法是对于符合的pattern将.
quote掉..这里只替换掉con.?
的情况..还有更多..
- window下
- 获取对应文献的官方url : 通过doi网站实现, 可以通过
geturl
和valid_doiorg
方法实现, 后者可以禁止重定向, 减少加载时间,会更快, 但获取的url 可能是个需要跳转的url, 例如PNAS的文献. 获取的链接地址会保存到obj.url
属性. - 获取文献标题 : 通过crossref的api实现.
- 是否有open access : 这个比较难, 只能收集一些前缀和通过doaj来判断.
- 获取bib和ris格式 : 通过修改header的Access实现, 有
getbibtex
,getendnote
方法对应 - 获取文献引用格式化输出 : 通过修改header的Access实现, 有
getbibliography
方法对应, 可以指定style和local. style参考csl (默认apa),locale参考这里. 总介绍参考CN
doi.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Last Update: 2016.1.28 - 4:00AM
'''DOI class '''
import os,sys
import re,difflib,requests,urllib2
TIMEOUT_SETTING=30
################ DOI class ############################
class DOI(str):
'''A class for standard DOI.
It should contain "10." and / or @, Else: it will be blank
if doi is quote, it will unquote when generate'''
pdoi=re.compile("(?:\:|\\b)(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?![\|\"&\'<>])\\S)+)(?:\+?|\\b)")
def __new__(self,doi=""):
'''Should be 10.*/ or 10.*@
Normalize(lower,strip)
Unquote it (@ to /)'''
if "10." in doi:
doi=doi.lower().strip()
if ("/" in doi):
return str.__new__(self,doi)
elif("@" in doi):
return str.__new__(self,requests.utils.unquote(doi.replace('@','/')))
else:
return str.__new__(self,"")
else:
return str.__new__(self,"")
def __init__(self,doi=""):
'''Generate prefix/suffix'''
str.__init__(self)
tmp=self.split('/',1)
self.prefix=tmp[0]
if (len(tmp)>1):
self.suffix=tmp[1]
else:
self.suffix=""
#article object
self.crjson=None
self._url=""
@property
def url(self):
'''Get URL property'''
if not self._url:
self._url=self.geturl()
return self._url
def quote(self,doi=None):
'''Quote it to file name format'''
if (not doi):
return requests.utils.quote(self,'+/()').replace('/','@')
else:
return requests.utils.quote(doi,'+/()').replace('/','@')
def unquote(self, doi):
'''Only for outer string'''
return requests.utils.unquote(doi.replace('@','/'))
def diff(self,doi):
'''Compare two doi string'''
if (isinstance(doi,DOI)):
return difflib.SequenceMatcher(None, self, doi).ratio()
else:
return difflib.SequenceMatcher(None, self, DOI(doi)).ratio()
def decompose(self, url=True, outdir=True, length=5, doi=None):
'''Decompose quoted doi to a list or a url string at most length for each
Note that dir name can't end with '.', it will be delete here.
Default, decompose to a outdir name
If url, output url string (containing quoted doi name)
If outdir, output string for directory of doi'''
if (not doi):
suffix=self.quote(self.suffix)
lens=len(suffix)
if (lens<=length):
if outdir:
return self.prefix+"/"
if (url):
return self.prefix+"/"+self.prefix+"@"+suffix
else:
return [self.prefix,suffix]
layer=(lens-1)/length
dirurl=[self.prefix]
for i in range(layer):
item=suffix[i*length:(i+1)*length].rstrip('.')
if (item[:4].lower() == 'con.'):
item=item.replace('.','%2E',1)
dirurl.append(item)
if outdir:
return "/".join(dirurl)+"/"
if (url):
return "/".join(dirurl)+"/"+self.prefix+"@"+suffix
else:
dirurl.append(suffix[(lens-1)/length*length:])
return dirurl
else:
return DOI(doi).decompose(url=url,outdir=outdir,length=length)
def geturl(self,doi=None):
'''Get the doi article url'''
if (not doi):
doi=self
if (self._url): return self._url
else:
doi=self.unquote(doi)
r=requests.get("http://dx.doi.org/"+doi,timeout=TIMEOUT_SETTING)
if (r.status_code is 200):
self._url=r.url
return r.url
else:
return ""
def valid_doaj(self,doi=None):
'''Valid the DOI is Open Access by DOAJ'''
doi = self.unquote(doi) if doi else self
r=requests.get('https://doaj.org/api/v1/search/articles/doi:'+doi,timeout=TIMEOUT_SETTING)
return r.json().get('total',0)>0
def valid_doiorg(self,doi=None,geturl=False):
'''Valid DOI is OK in dx.doi.org'''
doi = self.unquote(doi) if doi else self
r=requests.get("http://dx.doi.org/"+doi,allow_redirects=geturl,timeout=TIMEOUT_SETTING)
if (geturl and r.status_code is 200): self.url=r.url
return (r.status_code != 404)
def gettitle(self,doi=None):
'''Get the doi title, may be faster than valid_crossref'''
doi = self.unquote(doi) if doi else self
if not self.crjson: self.getcrossref()
if (self.crjson):
return self.crjson.get('message','{}').get('title',[''])[0]
print "Error doi (DOI.gettile)! "+doi
return ""
def getcrossref(self,doi=None):
'''Return the json result of crossref api'''
doi = self.unquote(doi) if doi else self
r=requests.get("http://api.crossref.org/works/"+doi,timeout=TIMEOUT_SETTING)
if (r.status_code is 200):
self.crjson=r.json()
return self.crjson
print "Error doi (DOI.gettile)! "+doi
self.crjson={}
return self.crjson
def getbibtex(self,doi=None):
'''Get the bibtex result *.bib file context'''
doi = self.unquote(doi) if doi else self
header={'Accept':'application/x-bibtex'}
r=requests.get("http://dx.doi.org/"+doi,headers=header,timeout=TIMEOUT_SETTING)
if (r.status_code is 200):
return r.text
print "Error doi (DOI.gettile)! "+doi
return ""
def getendnote(self,doi=None):
'''Get the endnote result *.ris file context'''
doi = self.unquote(doi) if doi else self
header={'Accept':'application/x-research-info-systems'}
r=requests.get("http://dx.doi.org/"+doi,headers=header,timeout=TIMEOUT_SETTING)
if (r.status_code is 200):
return r.text
print "Error doi (DOI.gettile)! "+doi
return ""
def getbibliography(self,style="",locale="",doi=None):
'''Get the bibliography with given style'''
doi = self.unquote(doi) if doi else self
typestr="text/x-bibliography"
if (style):
typestr=typestr+"; style="+style
if locale:
typestr=typestr+"; locale="+locale
header={'Accept':typestr}
r=requests.get("http://dx.doi.org/"+doi,headers=header,timeout=TIMEOUT_SETTING)
if (r.status_code is 200):
return r.text
print "Error doi (DOI.gettile)! "+doi
return ""
def freedownload(self,doi=None):
doi = self.unquote(doi) if doi else self
opprefix=['10.1371','10.3390',"10.3389","10.1186", "10.1093"]
if (self.prefix in opprefix):
return True
return self.valid_doaj()
Update: the bibtex, endnote, bibliography methods.