不完美的紧急需求(上篇)

昨天从志强老哥那里接到了一个小的爬虫需求。

说是小需求,是还是有点虚的,然后,在我面试完,吃完饭,锻炼完,摸鱼完之后,我终于打开了宇宙第一IDE vscode,新建文件夹了。

在做了在做了.jpg

需求要点有两个:

1.根据wos上的检索结果下载paper的本体(pdf),并按照文献的顺序进行命名。

2.按照一定的要求,获取paper的信息,特别是摘要,然后合并成一个word的报告。

首先,需求明确,第一个需求

  1. “wos的检索结果”长什么样

  2. 利用什么东西下载文献

  3. 怎么下载,怎么批量下载

  4. 异常处理,下载不下来,访问不到等怎么处理

第二个需求:

  1. paper的哪些信息

  2. 从哪里获取paper的这些信息

  3. 报告的模板是什么样的

  4. 怎么生成结果信息,又怎么从网页信息做成报告文档?

首先,先看最原始的数据,wos的检索结果,这里给出我删掉了筛选条件后的结果表:

number PMID Pubmed title authors Journal year DOI

字段太长了就不展示,但是目前,从里面我们知道了一件事。

这里面没有我们最想要的下载链接,由于访问权限问题,wos能否导出下载连接我也无法确认,但是,至少这里面还有一个东西能实现这件事。

DOI号,理论上,通过这个编号,我们就一定能找到这篇文章,但是显然,杂志社千千万,我们不可能还要去各个杂志社的网站去下载pdf,那显然太复杂了。

所以我准备利用sci-hub,或者pubmed,两相权衡,我决定用sci-hub

这里就能看出sci-hub设计的强大了:

  1. 如果一个doi在sci-hub里没有,会直接返回无响应的状态码,这样我们就能够非常方便地用try…catch来控制在无文献时候的处理;

  2. 它没有多余的动态跳转,所有能下载的文献链接格式统一,相比之下,虽然pubmed访问文献的链接是统一的,但是下载仍然采用了动态生成的pdf链接,这对于批量下载极为不利,除非我们实现准备了杂志社的下载页面对照表(貌似这可以做啊)

  3. 它不需要什么特殊的访问权限,只要能访问互联网,而且不要大批量并发去抓取文献(类似ddos),基本都能够从上面下载文献,由此可见,这才是真正的remove barriers….相比之下NCBI这些都弱爆了…

虽然但是,我也发现,访问统一的NCBI-Pubmed页面,居然有我们需要的Abstract字段….

那么第一个需求的基本流程应该就厘清楚了:

使用sci-hub作为下载服务器-利用doi获得完整的下载链接-下载pdf。

首先目前可用的sci-hub地址需要自行查找,我这边貌似用的是sci-hub.st;

其二,需要观察sci-hub下载链接是真么样的,一个典型的例子:

https://sci-hub.yncjkj.com/10.1007/s00204-016-1824-6#

这里能很简单看出组成:

sci-hub-url+doi+”#”

这就非常合理了。我们能够用字符串的拼接很容易地将所有doi编号组合成下载地址;

其三,怎么访问这个地址,能够拿到得到web访问的返回,也就是pdf?那简单,在拼接好了完整的下载地址之后,肯定是requests.get即可.

这里介绍一个简单的小工具postman,可以很容易地看到返回的是什么。

那么我们把上面那个链接用get方法送过去,看看返回什么。

首先,最重要的,status code,由于上面的链接是已经确认sci-hub中存在的paper,所以status=200,这也是通用的成功get的返回状态。

其次,我们得到了一个看上去像是文本文档的东西,其实就是html页面了,这个页面如果由浏览器来渲染,那就是我们常见的网页,但这里我们不关心页面,我们只关心信息。

那么问题来了,为什么不是直接返回一个pdf文件?如果上面那个是下载链接,应该直接返回一个pdf文件不是么,但为什么仍然是一个html页面?因此,上面那个链接,只是能够访问到包含了真实下载链接在哪的一个网页,我们得自己找到真正的下载链接在哪里。

根据pastman返回的网页,按照最常见的想法,我们最后下载的东西是pdf,那么无论是html标签,或者是下载链接,里面肯定是带了pdf三个词的,所以直接全局搜索,搜索得到了两个:

1
2
3
4
5
6
7
8
9
10
11
<div id="buttons">
<button onclick = "location.href='https://twin.sci-hub.st/6037/d05ae80fb1486c3afcf9845f6ddf90f2/pendse2016.pdf?download=true'">↓ 下載</button>
</div>

...

<div id="article">
<embed type="application/pdf"
src="https://twin.sci-hub.st/6037/d05ae80fb1486c3afcf9845f6ddf90f2/pendse2016.pdf#navpanes=0&view=FitH"
id="pdf"></embed>
</div>

这两个长得都挺像下载链接,那么直接用postman再get一下就行了,最后发现第二个是真正的下载链接,只需要对它进行get方法,就能直接收到pdf文件,那么基本锁定了我们的目标,那么我们只需要根据它的div或者字段拿到就行,注意看这里这个字段是包裹在什么html标签中,往外看一层,它的标签是:

1
2
3
4
5
6
7
<div id="article">
<embed
type="application/pdf"
src = urllink
id = "pdf">
</embed>
</div>

那么很清楚的,这里是包在id为article的css层(或者干脆点就叫DIV元素)里面,里面又有一个类型为type,文本内容为src,id为pdf的css嵌入对象,我们就是要拿到这个嵌入对象,既然说了这是css嵌入层,那么我们就可以用css嵌入层选取,然后构建下载链接的对象,再用get进行请求就能下载到pdf的二进制文件,然后开一个文件写进去就行,那么综上,整个代码应该差不多是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import requests
from bs4 import BeautifulSoup
import os

path = "./input/paperlist1"

if os.path.exists(path) == False:
os.mkdir(path) #创建保存下载文章的文件夹

f = open("./input/paperlist1/doi.txt", "r", encoding="utf-8") #存放DOI码的.txt文件中,每行存放一个文献的DOI码,完毕须换行(最后一个也须换行)

head = {\
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36'\
} #防止HTTP403错误
for line in f.readlines():
line = line[:-1] #去换行符
url = "https://sci-hub.st/" + line
try:
download_url = ""
r = requests.get(url, headers = head)
r.raise_for_status() #如果这里status出现问题就直接跳走了,一般是doi不存在。
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "html.parser")
#download_url = "https:" + str(soup.div.ul.find(href="#")).split("href='")[-1].split(".pdf")[0] + ".pdf" #寻找出存放该文献的pdf资源地址(该检索已失效)
if soup.iframe == None:
if "https" in soup.embed.attrs["src"]:
download_url = soup.embed.attrs["src"]
else:
download_url = "https:" + soup.embed.attrs["src"]
else:
if "https" in soup.iframe.attrs["src"]:
download_url = soup.iframe.attrs["src"]
else:
download_url = "https:" + soup.iframe.attrs["src"]
print(line + " is downloading...\n --The download url is: " + download_url)
download_r = requests.get(download_url, headers = head)
download_r.raise_for_status()
with open(path + line.replace("/","_") + ".pdf", "wb+") as temp:
temp.write(download_r.content)
except:
with open("error.txt", "a+") as error:
error.write(line + " occurs error!\n")
with open(path + line.replace("/","_") + ".txt", "w") as temp:
temp.write("Can't Download!")

else:
download_url = ""
print(line + " download successfully.\n")
f.close()

这个代码非常简单,也非常脆弱,比如下载期间的断线等异常情况没有处理,爬虫没有做随机时间设置,容易被封ip,对下载不了的文献也只是简单处理了一下,但没有记录失败原因,因为有可能并不是因为没有文章而失败,也可能是因为网络波动导致失败;

但是,至少这个代码能成功运行下载到这些文献。只需要提供一个doi的文件即可。

在这个脚本中,额外生成了一个error.txt,里面会记录哪些paper,此外,为了下一步进行命名,我们将下载失败的文章也创建了一个txt文件。(当然其实从后面的脚本来看,也可以不需要创建新的txt,可能只是为了好看。)

拿到paper之后,重命名就容易多了,就按照匹配规则,直接批量给某个文件夹的文件命名罢了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import xlrd

dirpath = './' #输出文件夹
datapath = './namesheet.xlsx' #命名表路径

x1 = xlrd.open_workbook(datapath) #读取excel
sheet1 = x1.sheet_by_name("Sheet1") #读取sheet1

idlist = sheet1.col_values(0) #存放第一列
xylist = sheet1.col_values(1) #存放第二列

file_names = os.listdir(dirpath)

for i in file_names:
id = i[:-4] #截取前18位
back = i[-4:]
if id in idlist:
xy = xylist[idlist.index(id)]
#print(xy)
os.renames(os.path.join(dirpath, i), os.path.join(dirpath, str(xy) + back)) #重命名
else:
print("Not Here!")

这里因为是特殊需要,要命名成论文的顺序.pdf/txt,这里可以通过替换namesheet.xlsx这个文件中的键值对实现任意的命名。当然,目前的脚本有改善的余地,比如既然命名规则是固定的,那么就不需要额外创造这个namesheet文件,直接用上面输出pdf时候的doi组合成名字,然后直接用字典存键值对就行,甚至也可以在上面输出pdf的时候加上循环,完成从下载到命名一步操作。

ok,到这里为止,第一个需求基本解决了,第二个需求稍微复杂一点,但实现思路类似,留在下一篇博客来讲吧。