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

书接上文,我们已经得到了爬下来的论文的pdf,也进行了重命名,最后得到了一系列数字命名的pdf/txt,而且还有一个error.txt,能够看到那些失败的具体是什么原因。

那么现在需要实现第二个需求,获取这些文献的信息,abstract内容,然后按照一定的格式填入word。

同样,拆解一下需求,如果不依赖python,我准备怎么做这件事情?

1.根据doi/pubmed/title等信息,去pubmed搜索这篇paper,因为pubmed上,即便没有下载文献的权限,也能够获取到文献对应的标题、作者、发表时间、doi和abstract信息。

2.将这些信息复制到一个中间介质中,因为恰好我知道,word有所谓的模版和插入对象功能,我不需要逐个复制进去然后排版,当然,如果我不知道,那么我就会先给一个编号,然后获取信息,然后粘贴到word文档里,然后按照固定格式排版。

3.不断重复这个过程,直到导出的列表中所有的文章信息都获取完成,如果还有需要,可能需要精修之类的再手动操作。

那么分析数据流向
数据输入:从wos上获取的信息表,也就是表格数据,结构数据,具体而言,最终将会以dataframe/dict/json/list的形式存在

数据获取:从输入数据中取出一个标签数据,然后根据标签数据从一个对应的web服务器中获取对应的数据,然后储存为某种格式的中间数据。

数据输出,最终输出的,是结构化之后的文本,然后用某种api输出到word内,而且至少要保持每一次运行输出的内容最后要有一个分页符。

将上述过程循环,即可完成需求。

此外,还有一个数据持久化的问题,中间数据是否应该保存出来?目前看来是需要的,很简单,如果这种从web服务器上获取的数据不保存下来,那么一旦后续还有需求,需要增删改查当前的信息,那么难不成再从web服务器上获取一次?万一获取有问题不是血亏,因此,这种从别的地方,设定了一定的规则得到的中间数据,是应该进行一段时间的持久化的,那么就直接保存为csv就好了。这种有价值的中间数据还是应该持久化的。

那么,先实现爬信息的需求:

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
import requests
from scrapy import Selector

def get_keywords_abstract(url):
r = requests.get(url) #打开网页
if r.status_code != 200: #如果网页连接错误,就返回空字符串
print("Connection error: {}".format(url))
return "", ""
selector = Selector(text=r.text)
abstracts = selector.css('#enc-abstract p::text').extract() #把extract_first()改成extract(),抓取所有的文本片段
if len(abstracts):
abstract = (''.join(abstracts)).strip() #把文本片段连接起来
else:
abstract = "This paper has no abstract"

return abstract

import pandas as pd

#可以利用PMID来取摘要
articles = pd.read_csv('./paperlist.csv', sep=',')

#利用构成的网页链接取循环
urls = articles['URL']
abstract = pd.Series(index=articles.index)

for i, url in enumerate(urls):
abstract[i] = get_keywords_abstract(url)
print("Finish article: {}".format(i))

articles['abstract'] = abstract
articles.columns
articles.to_csv('./paper_with_abs.csv', sep=',', header=True)

这里使用了scrapy的selector了来选择css节点“#enc-abstract p::text”,并提取他内的文本,然后拼接起来,而异常处理也主要是处理网络error和缺少abstract的问题就行。

其中有个小定式:

1
2
for i, data in enumerate(iter,start = 0):
func(x)

这里用的是枚举变量enumerate,枚举接受一个可迭代对象,返回对象的索引和对应的数据,因此,就有一个特殊的用法了,利用enumerate可以同时获得“当前循环到的索引数和对应的数据”,不需要再单独在for循环内部提取索引。此外,上述循环也可以直接接收返回的枚举变量,它大概就类似于一个元组,第一个数据是当前循环到的索引,第二个就是对应的数据。而start参数可以调整循环开始的位置,不一定需要从一开始循环,可以从几乎任意的位置开始循环。这非常方便,至少比R方便多了,不需要额外再设定i,j这种索引值就能直接从可迭代对象里获取到可迭代对象的索引和对应的数据。

最终会输出一份csv文档,csv文档中增加了abstract字段,里面的内容就是抓取到的摘要或者无摘要的缺省。

输出一份csv之后,我们就有机会把csv变成word了。

在从csv向word转换的过程中,有一个小技巧,可以借助docx包,将预先设置好的word模板中对应的模板文本进行变量的替换即可。这是个非常方便的技巧,平时也可以用来将大量的表格内容直接替换并形成word文档,当然,理论上使用word里面的一个叫信件的模板替换也能实现类似的效果,只不过复杂一点而已。

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
import os
import pandas as pd
import docx

def porcess_word(number,title,author,year,abstract):
'''主处理函数,使用docx进行文字替换,容易出现格式丢失,需要注意'''
doc1=docx.Document('../paperinfo.docx') #读取文件
list_row = ['NNNN','TTTT','AAAA001','YYYY','AAAA002']#word中需要替换的参数
list_replace=[number,title,author,year,abstract]#读取Excel的参数
for kk in range(len(list_row)):#通过循环进行逐个参数的替换
text_row= list_row[kk]
text_replace = list_replace[kk]
for p in doc1.paragraphs:
if text_row in p.text:
inline = p.runs
for i in range(len(inline)):
if text_row in inline[i].text:
text = inline[i].text.replace(str(text_row), (str(text_replace) + "\n"))
inline[i].text = text

doc1.save('%s.doc'%(number))#最终按照序号和姓名的格式保存文件


if __name__ == "__main__":

file_path = "./merge" #指定路径
os.chdir(file_path)
excel_file = pd.read_csv('../paper_with_abs.csv') #读取Excel数据
#excel_file['year'] = pd.to_datetime(excel_file['year'])#进行日期格式转换
for i in range(len(excel_file)): #逐行进行数据处理
data_temp=excel_file.loc[i]
#number,title,author,year,abstract
number = data_temp['number']
title = data_temp['title']
author = data_temp['authors']
year = data_temp['year']
abstract = data_temp["abstract"]

porcess_word(number,title,author,year,abstract)#调用处理函数

然后,这样的做法会将每一个条目生成一个word文档,我们还需要将他们合并,这个就很简单了。

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
from docx import Document
from docxcompose.composer import Composer
from natsort import natsorted #需要使用自然数大小排序

def main(files,final_docx):
new_document = Document()
composer = Composer(new_document)
for fn in files:
addbreak = Document(fn)
addbreak.add_page_break()

composer.append(addbreak)
composer.save(final_docx)

if __name__ == "__main__":
mergedir = "./merge"
os.chdir(mergedir)

files = natsorted(os.listdir())
new_doc='./paperinfomerged.docx'

main(files,new_doc)

这样就能直接将文件夹内的全部的word文档合并为一个文件了,而且符合预期的格式,而且每一次合并之后,手动添加了一个page_break分页符,保证长文档合并的时候不会引发文档格式的混乱。

至此,需求基本完成,但是仍然留有一些不足:

  1. 有相当的文献没法下载,其一是缺少权限,其二是sci-hub也并没有收录全部的paper。当然,理论上如果有办法获取到每篇paper本身的下载链接,那肯定可以更方便的下载,这只能期待wos本身能直接导出下载链接了。

  2. sci-hub页面解析有个很有意思的问题,有时候用css选择的方式会获得错误的链接,或者缺少“https:”前缀导致下载失败。这是在我测试单个文件下载的时候发现的,但是用xpath和selenium就没有这个问题。

  3. word的版式依赖于模板文件中设置好的格式,在python内操作文本内容的格式很复杂,当然,如果文档超过1000个之类的,那还是得去研究怎么在代码内调整字体,加粗等细节了……

  4. 目前只是实现了这个需求,代码结构还是比较乱的,理论上这两个需求能直接打包连接成一个完整的小程序,而且也缺少一些user friendly的信息输出,比如文献下载成功与失败的数量,失败原因分类,abstract获取的情况统计,缺少这些东西。后面找时间重构一下就行(在做了.jpg)