Hom's Blog


Python:DOI的一个类

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__中完成.
  • 分解前缀和后缀 : 在初始化时会产生prefixsuffix属性.
  • 文件名化 : 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.?的情况..还有更多..
  • 获取对应文献的官方url : 通过doi网站实现, 可以通过geturlvalid_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.



◆ 本文地址: http://platinhom.github.io/2015/12/29/DOI-class/, 转载请注明 ◆

前一篇: Python:hashlib库
后一篇: 以正确的方式开源Python项目(ZZ)


Contact: Hom / 已阅读()
Source 类别: Coding  标签: Python