Pubmed爬虫+LLM消化的一些探索

如题。先前在朋友的博客中看到了一个利用大模型进行文献调研的研究(参见 《文献pdf改名&AI消化》 )。正好这一阵子有文献调研的需求,于是在此基础上进行了一些更深入的探索。

本文主要分为三个部分:①利用网络爬虫获取PubMed的论文全文内容;②通过prompt工程调用大模型,以json文档的形式返回消化结果;③pandas批量处理保存为Excel表格。下面是探索结果

一、利用网络爬虫获取PubMed的论文全文内容

此处参考前段时间我发布在GitHub上的一个项目: pubmed_spider: A spider program for downloading text from pubmed

众所周知,Pubmed是一个很全面的医学研究论文的数据库,并且每一篇论文都有唯一确定的PMID标识符来表示,因此我们可以通过PMID获得论文的信息页面。

除此之外,Pubmed还附带了一个PMC的数据库,其中存储着论文的全文,以PMCID标识符来表示。一些受受 NIH 资助的研究成果,以及开放获取的研究论文,会被PMC数据库收录,因此当我们知道一篇文章的PMCID时,我们就可以获得论文的全文。

PMCID与PMID之间存在一对一的映射关系。当我们知道一篇论文的PMID,我们就可以通过解析PubMed的页面,获得PMCID,进而获得全文。而Pubmed的搜索支持根据论文标题进行模糊匹配,因此当我们知道论文标题,我们也可以通过解析PubMed页面获得论文PMID。这就是 pubmed_spider 这个项目的原理。

(插一句话,pubmed_spider 是用户提供一系列论文标题,通过爬虫获得txt格式的论文全文内容,其最终目的服务于大模型的知识库RAG服务)

下面是具体代码:

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
import requests
from bs4 import BeautifulSoup
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0'}

# 定义函数,传入网址URL,返回页面上的全部文本内容。
def extract_page_content(url):
req = requests.get(url,headers=headers)
req.encoding = "utf-8"
txt = req.text
bs4obj = BeautifulSoup(txt,'lxml')
page_content = bs4obj.get_text().strip()
return page_content

# 传入PMCID,获取论文全文
def query_from_pmc(pmcid):
url = f"https://pmc.ncbi.nlm.nih.gov/articles/{pmcid}/"
text = libgeturl.extract_page_content(url)
if(text is None): text = ""
text = text.replace("\n","")
text = text.replace("```","")
text = text.replace('"""',"")
return text

# 传入PMID,获取论文摘要
def query_from_pm(pmid):
url = f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
text = libgeturl.extract_page_content(url)
if(text is None): text = ""
text = text.replace("\n","")
text = text.replace("```","")
text = text.replace('"""',"")
return text

通过PMID获取PMCID的代码逻辑稍微有些复杂,因为需要利用BeautifulSoup(bs4)库解析页面上的DOM元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_pmcid_by_pmid(pmid):
url = f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
req = requests.get(url,headers=headers)
req.encoding = "utf-8"
txt = req.text
bs4obj = BeautifulSoup(txt,'lxml')
# process bs4obj and extract pmcid
pmcid_list = []
# 这里的代码逻辑是,使用bs4寻找页面上所有样式表为identifiers的元素
all_ul_list=bs4obj.find_all("ul", class_=['identifiers'])
# 在获取这些元素以后,遍历各个元素,寻找是否存在属性data-ga-action为PMCID的元素
for li in all_ul_list:
all_link_list = li.find_all("a")
if(len(all_link_list)>0):
for link in all_link_list:
attrs = link.attrs
if("data-ga-action" in attrs):
if("PMCID" in attrs["data-ga-action"]):
#print(link.get_text())
pmcid = link.get_text().strip()
if(pmcid not in pmcid_list):
pmcid_list.append(pmcid)
break
return pmcid_list

二、通过prompt工程调用大模型

因为文献调研任务涉及到论文中的一些关键信息的提取,我改进了朋友博客文章中的那个prompt,要求以json文档的格式返回生成结果:

image.png

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
你是一位生物医学领域的文献总结大师,按照用户提出的格式总结上传的学术论文,用json输出内容。
除了json文档本身,请不要输出其他任何无关信息,包括对这篇论文或这份json文档的解释。

格式要求如下:

{"Title": "[请填写文章标题]",
"Population": "[列出文章中使用的人群队列名称或群体来源]",
"Species": "[列出文章中使用的实验动物物种,如`mus musculus` , 物种名尽可能使用拉丁名,如果没有合适的拉丁名就使用文献中使用的物种名原名]",
"Type": "[归纳一下文章的研究属于哪一种类型,如 `review`, `new algorithm`,`new technology`,`biological research`,`data source`等]",
"Method": "[列出论文中使用的群体遗传学方法或正选择压力检测方法。如果没有则以字符串None填充这个条目]",
"Tissues": "[列出论文中的研究涉及了哪些组织或细胞系,如`Liver`,`Brain`等]",
"Data": "[列出论文的数据可访问性部分给出的数据访问链接,或GEO等数据库的访问号。如果没有则以字符串None填充这个条目]",
"Code": "[列出论文的数据可访问性部分给出的代码链接,如Github存储库地址。如果没有则以字符串None填充这个条目]",
"Contribute": "[用简洁的语言总结论文的主要贡献]",
"Significance": "[说明这项研究对学术界或实际应用的意义]",
"Suggestion": "[列出作者提出的未来研究方向或建议]"
}

下面是论文的标题和摘要页:
(-abstract-)


下面是论文的正文:
(-content-)

prompt中的 (-abstract-) 字段和 (-content-) 字段,需要在实际调用大模型API时替换为论文的摘要内容( query_from_pm )和论文全文( query_from_pmc ) 。

上面的json格式模板仅作为一个示例,用于群体遗传学的调研 (因此, Population, Species, Method, Data, Code这几个字段对我们来说很重要)。在处理其他类型研究时,可以根据需要修改json的字段内容。

三、pandas批量处理保存为Excel表格

假设我们已经下载好了所有论文的PMID信息和标题信息(例如,通过PubMed检索页面导出),并保存为文件 csv-articles-set.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
34
35
36
37
38
39
40
41
42
43
import pandas as pd
import libqwen

# 读入文件
article_df = pd.read_csv("csv-articles-set.csv")

# 首先循环下标,获取文章的pmid和pmcid,
# 然后调用爬虫模块,获取文章的文本内容
# 之后调用LLM模块,获取文章摘要和其他必要信息
# 最后调用json和pd模块,将结果处理和合并为表格
res_df = pd.DataFrame()
error_msg_dt = {}
for i in range(article_df.shape[0]):
print(i)
record = article_df.iloc[i,:]
pmid = record["PMID"]
pmcid = record["PMCID"]
text_ab = query_from_pm(pmid)
text_ct = query_from_pmc(pmcid)
message = prompt
message = message.replace("(-abstract-)",text_ab)
message = message.replace("(-content-)",text_ct)
libqwen.hist_msg = [] # must empty it mannually.
res = libqwen.chat(message)
#print(res)
res1 = res.replace("```json","").replace("```","").strip()
try:
dt1 = json.loads(res1)
df1 = pd.DataFrame(dt1,index=[0])
res_df = pd.concat([res_df,df1],axis=0)
res_df = res_df.reset_index(drop=True)
# 每处理完成一篇,以parquet中间格式临时存储结果,防止程序中途发生意外停止运行。
res_df.to_parquet("LLM-process-result.parquet", compression="brotli") # 更高压缩 率选brotli
except Exception as e:
error_msg_dt[i] = e
pass

## 读取最终的文件,并转换为Excel格式
df1 = pd.read_parquet("LLM-process-result.parquet")
df_merge_and_combined = article_df[["PMID","Title","Publication Year","Journal/Book","DOI"]]
df_merge_and_combined.columns = ["PMID","Title-1","Publication Year","Journal/Book","DOI"]
df_merge_and_combined = df_merge_and_combined.join(df1)
df_merge_and_combined.to_excel("LLM-process-result.xlsx")

其中,libqwen.py 的内容如下(在之前的LLM命令行聊天软件的基础上改的,因此显得很凑合。主要是需要这里的 chat() 函数接口,通过这个接口获得LLM处理的返回值)

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
import requests,json,datetime,os,sys,platform

API_KEY = "sk-82b3bfa35dce4e93877d7a54841d157d"
headers = {
'Content-Type': 'application/json',
'Accept' : 'application/json',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Authorization': f"Bearer {API_KEY}"
}

def get_response_with_stream(messages):
data_payload_dt = {
"model":'qwen-long',
"stream":True,
"messages":messages
}
payload = json.dumps(data_payload_dt)
url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
response = requests.request("POST", url, headers=headers, data=payload, stream=True)
full_content = ""
for line in response.iter_lines():
txt = line.decode("UTF-8")
txt = txt.strip()
left_trunc_pos = txt.find('{')
left_trunc_pos = left_trunc_pos if(left_trunc_pos>0) else 0 # 防止找不到左大括号返回-1这个数字,导致后续处理出错
txt = txt[left_trunc_pos:] # 按左大括号位置进行截断。包含左大括号
if(txt=="data: [DONE]"): continue
try:
obj = json.loads(txt)
response_msg = obj["choices"][0]["delta"]["content"]
full_content += response_msg
except:
full_content += txt
return full_content

# 与ChatBot交互的方法
hist_msg = [] # history of messages
def chat(msg):
global hist_msg
hist_msg.append({"role":"user","content":msg})
message = get_response_with_stream(hist_msg)
if(len(message)==0): message="I don't understand this question." # 如果因为各种原因导致对话出错,此时message是空值。为了避免后续对话出现异常,此时人为给对话message赋值。
hist_msg.append({"role":"assistant","content":message})
return message

四、效果展示与注意事项

我们以一个小文献集为例,这一批文献是从PubMed上以 “Population genetics” 为关键词检索到的,部分内容如下:

image.png

通过上面的大模型消化,我们最终得到的成品如下表所示,其中的 Populations, Species, Data, Code等列是我们所关心的。

image.png

另外,关于LLM处理的准确性问题,经过实测,qwen-long的准确性还是比较好的。同期我也测试过百度ERNIE的系列模型,发现模型的幻觉问题比较严重,因此不建议使用ERNIE系列的模型。如果需要更准确的处理结果,大家也可以切换其他模型API,例如deepseek、gemini等。另外,这一工具在文献初筛的时候用比较好,筛选完以后最好还是得再自己精读一下比较稳妥。

以上。