1. 中药靶点研究的数据获取困境与破局思路
做中药网络药理学的人,几乎都绕不开TCMSP这个数据库。它全称是Traditional Chinese Medicine Systems Pharmacology Database and Analysis Platform,是目前中药系统药理学领域引用率最高的数据源之一,收录了大量中药材的化学成分、ADME参数以及对应的靶点蛋白信息。但凡你要做"中药-成分-靶点-通路"这类网络分析,第一步就是把目标中药的靶点基因列表拿到手。
问题就出在这个"拿到手"上。TCMSP的网页界面设计得比较传统,查询单味中药时,你需要手动选择药材名、点击搜索、等待结果加载、找到靶点信息区域、逐条复制粘贴到本地文件。如果只做一两味药,忍一忍也就过去了。但实际课题里动辄十几味、几十味药做对比分析,甚至要做复方中所有药材的合并靶点集,手动操作就变成了纯粹的体力消耗。我见过有同学复制粘贴了一整个下午,最后发现漏了两味药的数据,又得从头核对。
更麻烦的是,TCMSP的靶点信息展示方式并不总是规整的表格。有些药材的靶点列表嵌在详情页里,有些需要展开折叠区域,有些字段的命名在不同页面之间还有细微差异。手动整理时,格式不统一、基因名大小写混乱、重复项遗漏等问题层出不穷。这些看似琐碎的细节,到了下游做Venny图取交集、做STRING网络构建的时候,就会变成致命的错误来源。
所以,用脚本自动化完成这个抓取和整理过程,本质上解决的是三个层面的问题:效率问题(批量处理替代逐条操作)、准确性问题(程序化提取避免人为遗漏和格式错误)、可复现性问题(脚本留存下来,换一批药材或者换一个时间点重新跑,结果一致)。R语言在这个场景下是一个很自然的选择,因为做网络药理学的人后续大概率要用R做统计分析、画图,整个流程用同一套工具链,数据格式的衔接最顺畅。
这篇文章面向的是有基本R语言操作经验、正在做或准备做中药网络药理学研究的同学。如果你还没装R和RStudio,文章里也会提到环境准备的关键点。核心目标很明确:给你一套能直接跑通的R脚本方案,把TCMSP上目标中药的靶点基因批量抓下来,整理成干净的、可以直接进入下游分析的数据表。
2. 整体方案设计与技术选型考量
2.1 为什么选R而不是Python或其他工具
这个问题的答案其实很实际。做中药网络药理学的主流工具链里,R的出场率极高——clusterProfiler做富集分析、igraph做网络可视化、limma做差异表达,这些包都是R生态里的。如果你的数据抓取环节也用R完成,那么从原始数据到分析结果之间就不需要跨语言转换,数据框的列名、类型、编码方式从头到尾保持一致,省去了很多格式对齐的麻烦。
另一个考虑是R的rvest包在网页解析方面的成熟度。rvest是Hadley Wickham团队开发的,语法设计跟dplyr一脉相承,用管道操作符串联起来非常直观。对于TCMSP这种结构相对固定的页面,rvest配合CSS选择器或者XPath就能精准定位到目标元素。相比之下,Python的BeautifulSoup虽然也很强大,但如果你本身R更熟,为了抓数据专门去搭Python环境、学一套新语法,投入产出比不划算。
当然,如果你的团队统一用Python做数据分析,那用requests+BeautifulSoup或者selenium也是完全可行的。工具选择的核心原则是:跟你下游分析的工具链保持一致,减少数据流转环节的摩擦。
2.2 抓取策略:直接请求还是模拟浏览器
TCMSP的页面加载方式决定了抓取策略的选择。经过实际测试,TCMSP的药材详情页和靶点列表页大部分内容是服务端渲染的,也就是说,你直接向目标URL发送HTTP请求,返回的HTML里就包含了靶点信息。这意味着不需要动用Selenium或者Playwright这类浏览器自动化工具,用httr发请求、rvest解析HTML就够了。
但这里有几个关键细节需要注意。第一,TCMSP的查询接口可能对请求频率有限制,如果你在循环里不加延时地连续请求几十次,有可能会被临时限制访问。第二,某些药材的靶点数据是通过AJAX异步加载的,直接请求页面URL拿不到完整内容,需要找到实际的API接口地址。第三,请求头里的User-Agent字段最好设置成常见浏览器的标识,避免被识别为异常流量。
我的建议是采用"先探测、后批量"的策略:先用一两味药做测试,确认请求能正常返回数据、解析规则能正确提取目标字段,再扩展到全量药材列表。这样即使中途遇到问题,排查范围也可控。
2.3 数据整理的核心逻辑
抓取只是第一步,真正决定数据能不能用的,是整理环节。TCMSP返回的靶点信息通常包含以下几类字段:靶点名称(可能是基因符号或蛋白名称)、靶点ID(如UniProt ID)、以及与该靶点相关的成分信息。不同药材的数据结构可能略有差异,比如有的药材靶点列表里包含多个成分对应同一靶点的情况,有的则是去重后的靶点集合。
整理逻辑需要明确几个决策点:是否保留成分-靶点的对应关系(如果后续要做成分-靶点网络,就需要保留;如果只做靶点集合分析,可以去重)、基因名统一用哪种命名规范(推荐用HGNC官方基因符号,全大写)、多味药的靶点如何合并(是取并集还是保留药材来源标记)。这些决策没有标准答案,取决于你的研究设计,但脚本里需要把这些选项做成可配置的参数,方便不同课题复用。
3. 环境准备与核心工具包配置
3.1 R与RStudio的安装要点
如果你还没有R环境,直接去R语言官网下载对应操作系统的安装包。Windows用户注意选择正确的版本(64位系统选x64),安装路径建议不要包含中文和空格,避免后续包安装时出现路径解析问题。RStudio是IDE,不是必须的,但强烈建议装上,代码编辑、变量查看、绘图预览的体验比原生R控制台好太多。
安装完R之后,RStudio要单独下载安装。两者的关系是:R是引擎,RStudio是驾驶舱。你可以在RStudio里切换不同版本的R,也可以在不打开RStudio的情况下直接用R命令行运行脚本,但日常开发肯定是用RStudio更顺手。
3.2 核心R包的安装与加载
这个项目需要用到几个核心包,各自的分工如下:
| 包名 | 用途 | 安装命令 |
|---|---|---|
| httr | 发送HTTP请求,获取网页内容 | install.packages("httr") |
| rvest | 解析HTML,提取目标元素 | install.packages("rvest") |
| xml2 | rvest的底层依赖,处理XML/HTML | install.packages("xml2") |
| dplyr | 数据框操作,清洗整理数据 | install.packages("dplyr") |
| stringr | 字符串处理,正则匹配 | install.packages("stringr") |
| purrr | 函数式编程,批量循环处理 | install.packages("purrr") |
| readr | 读写CSV等格式文件 | install.packages("readr") |
安装时如果遇到某个包下载失败,大概率是网络问题,可以尝试换CRAN镜像源。在RStudio里可以通过Tools > Global Options > Packages > CRAN mirror来切换,国内用户建议选离自己地理位置近的镜像。
安装完成后,在脚本开头统一加载:
library(httr) library(rvest) library(xml2) library(dplyr) library(stringr) library(purrr) library(readr)注意:如果你的R版本较老,某些包可能要求更高的R版本。建议至少使用R 4.0以上版本,避免因为版本兼容性问题卡在安装环节。
3.3 工作目录与文件结构规划
在开始写代码之前,先把项目文件夹结构规划好。我习惯的结构是这样的:
TCMSP_Scraping/ ├── scripts/ │ └── tcmsp_scraper.R # 主脚本 ├── data/ │ ├── raw/ # 原始抓取结果 │ └── processed/ # 清洗后的数据 ├── output/ │ └── logs/ # 运行日志 └── README.md # 项目说明在R脚本里用setwd()设置工作目录,或者更推荐的做法是用RStudio的Project功能,把整个文件夹作为一个项目打开,这样工作目录会自动设定,脚本的可移植性也更好。
4. 核心抓取逻辑的逐层拆解
4.1 理解TCMSP的页面结构与请求方式
要抓数据,先得搞清楚数据在哪里。打开TCMSP网站,搜索一味药材(比如"当归"),进入详情页后,你会看到几个信息区块:药材基本信息、成分列表、靶点信息、相关疾病等。靶点信息通常以列表或表格形式呈现,每个靶点包含名称、ID等字段。
关键问题是:这些靶点数据是直接嵌在详情页的HTML里,还是通过单独的接口异步加载的?判断方法很简单:在浏览器里右键查看页面源代码(不是"检查元素",是"查看网页源代码"),搜索靶点名称中的关键词。如果能在源代码里找到,说明是服务端渲染,直接请求页面URL即可;如果找不到,就需要打开浏览器的开发者工具,在Network面板里找XHR请求,定位实际的API地址。
根据我的实际测试,TCMSP的靶点数据大部分情况下是包含在详情页HTML中的,但页面结构可能随时间调整。所以脚本里需要把CSS选择器或XPath路径做成可配置的变量,方便页面改版时快速调整。
4.2 构建请求函数:稳健地获取页面内容
直接写一个函数来封装请求逻辑,把超时设置、重试机制、请求头伪装都考虑进去:
fetch_page <- function(url, max_retries = 3, delay = 2) { for (i in seq_len(max_retries)) { tryCatch({ response <- GET( url, user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"), timeout(30) ) if (status_code(response) == 200) { return(content(response, "text", encoding = "UTF-8")) } else { message(sprintf("请求返回状态码 %d,第 %d 次重试", status_code(response), i)) } }, error = function(e) { message(sprintf("请求出错:%s,第 %d 次重试", e$message, i)) }) Sys.sleep(delay * i) # 递增延时,避免频繁请求 } return(NULL) }这个函数的设计要点:重试机制应对临时网络波动,递增延时避免触发频率限制,User-Agent伪装降低被拦截概率,超时设置防止单次请求卡死整个脚本。这些细节在批量抓取时非常重要,因为只要有一味药请求失败,整个流程就可能中断。
4.3 解析靶点信息:定位与提取
拿到HTML内容后,用rvest解析。假设靶点信息在一个带有特定class的表格或列表中,提取逻辑大致如下:
parse_targets <- function(html_content, herb_name) { page <- read_html(html_content) # 定位靶点信息区域(选择器需要根据实际页面结构调整) target_nodes <- page %>% html_nodes(".target-list li") # 示例选择器 if (length(target_nodes) == 0) { warning(sprintf("%s 未找到靶点信息,请检查选择器", herb_name)) return(NULL) } targets <- target_nodes %>% html_text(trim = TRUE) %>% str_trim() # 提取基因名和靶点ID(根据实际文本格式调整正则) result <- data.frame( herb = herb_name, target_raw = targets, stringsAsFactors = FALSE ) return(result) }这里最关键的是选择器的准确性。你需要用浏览器的开发者工具,定位到靶点列表所在的HTML元素,记下它的class或id,然后填入html_nodes()。如果页面结构复杂,可能需要多层嵌套选择,比如".herb-detail .target-section table tbody tr"这样的路径。
实操心得:选择器不要写得太"死"。比如不要用
nth-child(3)这种依赖位置的选择器,因为页面微调就可能失效。优先用有语义的class名或id。如果实在没有合适的class,用XPath结合文本内容定位也是一种办法。
4.4 批量循环:遍历药材列表
单味药的抓取逻辑跑通后,扩展到批量就简单了——把药材名列表读进来,用purrr::map_dfr()循环处理:
herb_list <- c("当归", "黄芪", "人参", "甘草", "川芎") all_targets <- map_dfr(herb_list, function(herb) { message(sprintf("正在抓取:%s", herb)) # 构建查询URL(需要根据TCMSP的实际URL规则调整) query_url <- sprintf("https://tcmsp-e.com/herb_detail.php?herb=%s", URLencode(herb)) html_content <- fetch_page(query_url) if (is.null(html_content)) { warning(sprintf("%s 抓取失败,跳过", herb)) return(NULL) } result <- parse_targets(html_content, herb) Sys.sleep(1.5) # 每味药之间间隔1.5秒 return(result) })map_dfr()的好处是自动把每次返回的数据框按行合并,省去了手动rbind()的麻烦。如果某味药返回NULL,它会自动跳过,不会导致整个循环崩溃。
5. 数据清洗与格式标准化
5.1 基因名规范化处理
TCMSP返回的靶点名称格式可能不统一,有的用基因符号(如"PTGS2"),有的用蛋白名称(如"Prostaglandin G/H synthase 2"),有的还带有物种前缀。下游分析通常需要统一的基因符号,所以清洗环节要做几件事:
第一,去除多余空白和特殊字符。用str_trim()去掉首尾空格,用str_replace_all()清理不可见字符。
第二,统一大小写。人类基因符号的规范是全大写,所以用str_to_upper()统一转换。
第三,处理别名。有些靶点可能有多个别名,需要映射到官方符号。这一步如果数据量不大,可以手动维护一个映射表;如果数据量大,可以考虑用org.Hs.eg.db等注释包来做ID转换。
clean_targets <- all_targets %>% mutate( target_clean = target_raw %>% str_trim() %>% str_to_upper() %>% str_replace_all("[^A-Z0-9-]", "") # 只保留字母、数字和连字符 ) %>% filter(!is.na(target_clean), target_clean != "")5.2 去重与多药合并策略
如果研究设计是"多味药的靶点取并集",那么清洗后需要去重:
unique_targets <- clean_targets %>% distinct(target_clean, .keep_all = TRUE)但如果需要保留"哪味药对应哪些靶点"的信息(比如做药材-靶点二分网络),就不能简单去重,而是要用列表列(list-column)或者长格式来保留对应关系:
herb_target_summary <- clean_targets %>% group_by(herb) %>% summarise( target_count = n_distinct(target_clean), targets = paste(unique(target_clean), collapse = "; ") )这种汇总表在写论文时特别有用,可以直接放进补充材料。
5.3 输出为下游可用的格式
清洗完的数据建议同时输出两种格式:CSV用于Excel查看和手动核对,RDS用于R脚本之间的数据传递(RDS能完整保留数据类型和结构,读取速度也更快)。
write_csv(clean_targets, "data/processed/tcmsp_targets_clean.csv") saveRDS(clean_targets, "data/processed/tcmsp_targets_clean.rds")注意:CSV文件用Excel打开时,如果基因名里包含类似日期格式的字符串(比如"MAR1"可能被识别为日期),会出现显示错误。这是Excel的自动类型推断导致的,不是数据本身的问题。可以用
read_csv()在R里查看,或者在Excel里用"数据导入向导"指定列类型为文本。
6. 常见问题与排查技巧实录
6.1 请求被拒绝或返回空内容
这是最常见的问题。表现是fetch_page()返回NULL,或者返回的HTML里找不到靶点信息。排查思路按以下顺序进行:
第一步,检查URL是否正确。手动在浏览器里打开同样的URL,看能否正常访问。如果浏览器能打开但R不行,说明是请求头或频率问题。
第二步,检查请求频率。如果连续快速请求多次后被拒绝,说明触发了频率限制。解决办法是增大Sys.sleep()的间隔,或者把请求分散到不同时间段执行。
第三步,检查页面结构是否变化。如果URL能访问、频率也不高,但就是解析不到数据,很可能是TCMSP改版了,CSS选择器失效。这时候需要重新用开发者工具定位目标元素,更新选择器。
6.2 中文编码问题
TCMSP的页面编码可能是UTF-8,也可能是GBK。如果抓下来的中文显示为乱码,需要在content()函数里指定正确的编码:
content(response, "text", encoding = "UTF-8")如果UTF-8不行,试试"GBK"或"GB2312"。判断方法:看返回的HTML源码里<meta charset="...">标签的值。
6.3 靶点名称格式不一致
不同药材的靶点列表可能用不同的命名体系。有的用基因符号,有的用UniProt ID,有的用蛋白全称。这种情况下,单纯靠字符串清洗无法统一,需要做ID映射。推荐的做法是:先把所有靶点名称收集起来,去重后得到一个唯一列表,然后手动或半自动地映射到统一的基因符号。如果靶点数量在几百个以内,手动核对虽然费时但最可靠;如果上千个,可以考虑用注释包批量转换,但转换后一定要抽样验证准确性。
6.4 脚本运行中断后的断点续跑
批量抓取几十味药时,如果跑到一半因为网络问题中断,重新跑一遍会浪费时间。解决办法是在循环里加入"已抓取则跳过"的逻辑:
if (file.exists(sprintf("data/raw/%s.rds", herb))) { message(sprintf("%s 已存在,跳过", herb)) next }每味药抓取成功后立即保存为单独的RDS文件,这样即使中断,下次运行也能从断点继续。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 返回NULL | URL错误或网络不通 | 浏览器手动访问同一URL | 修正URL,检查网络 |
| 返回403/429 | 请求频率过高 | 查看status_code | 增大延时,降低频率 |
| 解析结果为空 | 选择器失效 | 查看页面源代码 | 更新CSS选择器 |
| 中文乱码 | 编码不匹配 | 查看meta charset | 指定正确encoding |
| 基因名混乱 | 命名体系不统一 | 抽样查看原始数据 | 做ID映射转换 |
| 脚本中途崩溃 | 单次请求异常 | 查看错误信息 | 加tryCatch和重试 |
7. 完整脚本结构与可复用性设计
7.1 脚本的模块化组织
把整个流程拆成几个独立的函数模块,每个模块负责一个明确的任务:fetch_page()负责请求,parse_targets()负责解析,clean_targets()负责清洗,save_results()负责输出。主流程用一个main()函数串联起来。这样的好处是:调试时可以单独测试某个模块,修改时也只影响对应的部分,不会牵一发而动全身。
main <- function(herb_list, output_dir = "data/processed") { # 1. 批量抓取 raw_data <- batch_fetch(herb_list) # 2. 解析 parsed_data <- parse_all(raw_data) # 3. 清洗 cleaned_data <- clean_targets(parsed_data) # 4. 保存 save_results(cleaned_data, output_dir) # 5. 输出摘要 print_summary(cleaned_data) return(cleaned_data) }7.2 参数配置的外部化
把药材列表、URL模板、选择器、延时时间这些容易变化的参数,统一放在脚本开头的配置区域,或者单独写一个config.R文件。这样换一个课题时,只需要改配置,不用动核心逻辑。
# ========== 配置区域 ========== config <- list( herb_list = c("当归", "黄芪", "人参"), base_url = "https://tcmsp-e.com/herb_detail.php", target_selector = ".target-list li", request_delay = 1.5, max_retries = 3, output_dir = "data/processed" ) # ==============================7.3 日志记录与运行监控
批量任务最好加上日志记录,把每味药的抓取状态、耗时、靶点数量都写进日志文件。这样出问题时可以快速定位是哪一步、哪味药出的错。
log_message <- function(msg, log_file = "output/logs/scraping_log.txt") { timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S") write(sprintf("[%s] %s", timestamp, msg), file = log_file, append = TRUE) }7.4 扩展到其他类似数据库的思路
这套"请求-解析-清洗-输出"的框架,不仅适用于TCMSP,换成其他中药数据库(如BATMAN-TCM、SymMap等)或者类似的生物信息学数据库,只需要修改URL模板和解析选择器,核心逻辑完全可以复用。这也是为什么建议把代码模块化——一次投入,多处受益。
8. 我踩过的坑与实操建议
说几个实际做的时候容易忽略的点。
第一,不要等到脚本写完才测试。我的习惯是每写一个函数就单独跑一次,用一两味药的数据验证输出是否符合预期。等全部写完再跑,一旦出错,排查范围太大。
第二,抓取前先手动确认目标数据的位置。打开开发者工具,用元素选择器点一下靶点列表,看看它的HTML结构是什么样的。这一步花五分钟,能省掉后面半小时的调试。
第三,保存原始HTML。解析失败时,如果有原始HTML留存,可以离线调试解析逻辑,不用反复请求服务器。我通常会把每味药的原始HTML存一份到data/raw/html/目录下。
第四,注意TCMSP的更新。数据库会不定期更新,药材的靶点列表可能变化。如果你的研究有明确的时间节点要求,建议在脚本里记录抓取日期,并在论文方法部分注明数据获取时间。
第五,基因名映射要留痕。如果做了ID转换或别名映射,一定要把原始名称和转换后名称的对应关系保存下来。审稿人问起来的时候,你能说清楚每一个基因是怎么来的。
最后分享一个提高效率的小技巧:如果你要抓的药材数量很多,可以把药材列表分批,比如每批10味药,跑完一批检查一下日志和输出,确认没问题再跑下一批。这样即使某一批出了问题,也不会影响已经完成的部分。