【生信】简单的 GDS (GEO DataSets) 查询统计工具

旧时模样 提交于 2020-11-18 00:05:16

背景

写了个 GDS 的查询工具,主要用于检索式检索结果的空间与数量分析,毕竟循证理念嘛,荟萃全球资源,自己是否已经做到了呢?这就需要对自己检索到的数据集的源头进行定位、统计和分析,通过喜闻乐见的 html 可视化报表形式呈现,以确定是否需要对检索式进行改进。

功能

根据检索式检索 GSE 数据集并批量查询元数据,统计 GSE 数据集所来自的国家和城市。高级功能正在逐步开发。

食用方式

  1. 我的仓库下载好项目文件。
  2. 任意文本编辑器打开 XML 文件进行编辑,按照示例格式填入自己的检索式,支持多条。需要注意的是浏览器有迭代次数的限制(比如3000),检索的结果数不宜过千,否则被浏览器检测到潜在的“无限循环”,可能会启动保护机制终止程序运行,得到空白网页。目前还没有研究解法。
xsltApplySequenceConstructor: A potential infinite template recursion was detected.
You can adjust xsltMaxDepth (--maxdepth) in order to raise the maximum number of nested template calls and variables/params (currently set to 3000).
  1. Chrome 浏览器关闭安全模式(chrome --allow-file-access-from-files --user-data-dir=[自己填个放数据的目录] --disable-web-security)后以 File 协议打开 XML 文件,批量查询国外网站的数据速度会很慢,请耐心等待浏览器加载完毕。(用的都是 NCBI 的官方数据,安全得很,不用担心)

原理

数据集查询

使用了 Entrez Programming UtilitiesESearch 接口,ESearch 在 GDS 中的用例参考官网这篇说明

定位

数据来自 GEO 官网的 Download GEO data 说明的 Construct a URL 部分所提供的接口,具体使用的是通讯贡献者(第一贡献者,contrib1)提供的地址信息作为数据集的来源。

数据加工方式

上述两个接口都提供了 XML 这个返回类型(ESearch 提供 xml 和 json 格式,Download GEO data 提供 text,html 和 xml 格式,取交集 xml 格式),于是用了“老掉牙”的 XSLT 来转换数据。用得越多掉的坑越多,但毕竟是 XML 的原生伴侣,利用内置函数库检索相当高效。唉,怎奈 json 后来居上了呢。然而不可否认的是,有内置检索功能与程序广泛支持,能够“Code once, run everywhere.” 的全栖式数据格式少之又少,XML 牛B!在此我会把使用 XML 的 legacy —— XSLT 遇到的新问题新发现以及解决方案一一记录。

新问题新发现

fn:escape-uri

这篇博客所列出的 xslt 数值函数与字符串函数很多都能用,唯独我急切需要的 escape-uri 函数使用后总是提示不存在这个函数。为什么?不知道!
为什么会需要这个函数?带有空格的检索式构建的指向 xml 文件的链接,通过 XSLT document() 函数加载会失败,空格需要转义(URL encode)为%20。而且链接两端包含空格,常见于格式化链接时 XML 文件中所使用到的缩进符,也会使 document() 函数加载失败。因此空格是不允许的。链接两旁的空格可以用 normalize-space 函数消除。链接的格式化字符串内部必须要紧凑,也就是在同一行,除了检索式以外不能包含空格。
无奈只能在网上找 xsl:template 实现,参考 Stack Overflow 上 Mads Hansen 提供的方案。核心代码如下,注意 template 依赖开头的几个 variable,一定要复制全:

<!-- http://skew.org/xml/stylesheets/url-encode/url-encode.xsl -->
<xsl:variable name="ascii"> !"#$%&amp;'()*+,-./0123456789:;&lt;=&gt;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~</xsl:variable>
<xsl:variable name="latin1">&#160;&#161;&#162;&#163;&#164;&#165;&#166;&#167;&#168;&#169;&#170;&#171;&#172;&#173;&#174;&#175;&#176;&#177;&#178;&#179;&#180;&#181;&#182;&#183;&#184;&#185;&#186;&#187;&#188;&#189;&#190;&#191;&#192;&#193;&#194;&#195;&#196;&#197;&#198;&#199;&#200;&#201;&#202;&#203;&#204;&#205;&#206;&#207;&#208;&#209;&#210;&#211;&#212;&#213;&#214;&#215;&#216;&#217;&#218;&#219;&#220;&#221;&#222;&#223;&#224;&#225;&#226;&#227;&#228;&#229;&#230;&#231;&#232;&#233;&#234;&#235;&#236;&#237;&#238;&#239;&#240;&#241;&#242;&#243;&#244;&#245;&#246;&#247;&#248;&#249;&#250;&#251;&#252;&#253;&#254;&#255;</xsl:variable>

<!-- Characters that usually don't need to be escaped -->
<xsl:variable name="safe">!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~</xsl:variable>

<xsl:variable name="hex" >0123456789ABCDEF</xsl:variable>
<xsl:template name="url-encode">
	<xsl:param name="str"/>   
	<xsl:if test="$str">
		<xsl:variable name="first-char" select="substring($str,1,1)"/>
		<xsl:choose>
			<xsl:when test="contains($safe,$first-char)">
				<xsl:value-of select="$first-char"/>
			</xsl:when>
			<xsl:otherwise>
				<xsl:variable name="codepoint">
					<xsl:choose>
						<xsl:when test="contains($ascii,$first-char)">
						<xsl:value-of select="string-length(substring-before($ascii,$first-char)) + 32"/>
						</xsl:when>
						<xsl:when test="contains($latin1,$first-char)">
						<xsl:value-of select="string-length(substring-before($latin1,$first-char)) + 160"/>
						</xsl:when>
						<xsl:otherwise>
						<xsl:message terminate="no">Warning: string contains a character that is out of range! Substituting "?".</xsl:message>
						<xsl:text>63</xsl:text>
						</xsl:otherwise>
					</xsl:choose>
				</xsl:variable>
				<xsl:variable name="hex-digit1" select="substring($hex,floor($codepoint div 16) + 1,1)"/>
				<xsl:variable name="hex-digit2" select="substring($hex,$codepoint mod 16 + 1,1)"/>
				<xsl:value-of select="concat('%',$hex-digit1,$hex-digit2)"/>
			</xsl:otherwise>
		</xsl:choose>
		<xsl:if test="string-length($str) &gt; 1">
			<xsl:call-template name="url-encode">
			<xsl:with-param name="str" select="substring($str,2)"/>
			</xsl:call-template>
		</xsl:if>
	</xsl:if>
</xsl:template>

xmlns

document() 函数引入 GSE 的 xml 文件进行解析时发现无法取 MINiML 的一切后代元素,本以为是 document() 函数不接受 gzip 处理过的下载流数据,然而经过手动使用 JS 的 XSLT 库进行解析后发现问题仍然没有解决,后来发现其中的 xmlns 属性删掉以后就能正常解析。CSDN上这个帖子很好说明了这一问题。用户 cds27 的解答一针见血。

那是因为你没有加命名空间,加上命名空间就可以用了。 如下:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
    xmlns:n="http://www.example.com"
>

然后所有的节点引用都要加上命名空间,例如:

<xsl:template match="n:root/music">

节点/变量拼接要用 node-set() 函数(兼容 XSLT 1.0)

为了 Country 和 City 的统计,我需要在批量查询过程中收集一些节点,在查询结束后检索积累下的节点,获得计数数据。于是有

<xsl:variable name="neoNode">
	<GDS>
		<AC><xsl:value-of select="$accession" /></AC>
		<Country><xsl:value-of select="$contact/geo:Country" /></Country>
		<City><xsl:value-of select="$contact/geo:City" /></City>
		<Design><xsl:value-of select="$gsm/geo:Series/geo:Overall-Design" /></Design>
	</GDS>
</xsl:variable>

<xsl:variable name="union">
	<xsl:copy-of select="$stat" />
	<xsl:copy-of select="$neoNode" />
</xsl:variable>

其中$stat是之前收集的节点,需要在循环中不断拼接,但是最后通过$stat/GDS却检索不到任何节点,我觉得可能$stat被当成纯文字而不是一个对象了。参照 Stack Overflow 上 Dimitre Novatchev 的解答 才知道真正的原因:

There is a way (non-standard) in XSLT 1.0 to create temporary trees dynamically and evaluate XPath expressions on them, however this requires using the xxx:node-set() function.
Whenever nodes are dynamically created inside the body of an xsl:variable or an xsl:param, the type of that xsl:variable / xsl:param is RTF (Result Tree Fragment) and the W3 XSLT 1.0 Spec. limits severyly the kind of XPath expressions that can be evaluated against an RTF.

使用了 ext (http://exslt.org/common) 提供的 node-set 函数才把这个问题解决了。拼接时候不需要使用,在检索时使用一下就好。

Saxon-JS(传说中兼容 XSLT 3.0 的 XSLT JS 实现)

虽然 W3C 很推荐 XSLT 3.0,但是 Mozilla Firefox MDN 也说了,XSLT 在浏览器中普遍仅支持 1.0 版本(可运行<xsl:value-of select="system-property('xsl:version')" />查看浏览器支持的 XSLT 版本),如果想使用 2.0 及以上特性只能另请高明,比如 Saxon-JS。这是十分沮丧的事情。高版本的 XSLT 可能就能支持我需要的 URL 转义函数。
不过话说回来,Saxon 方法好,可以在控制台用 xsl 文件转换 xml 文件,它也有自己的 JS 实现了,而且2020年6月12日更新的,号称支持 3.0。但是我怎么也没办法跑起来,总是遇到 code 为 SXJS0006 的 XError,再无任何具体信息。在其论坛也似乎无人提及,似乎只有我遇到了这个错误,我也不想折腾了。

Frameless

Frameless 也支持 XSLT 特性,提供了更加简洁的 XSLT 调用方法,只需要一行 script 标签引入,有如浏览器原生支持:

<script src="stylesheet.xsl" type="text/xsl" data-input="document.xml"></script>

还有一个类似 Vue 的数据同步功能:

<p>
  <input data-ref="$message">
  <input data-ref="$message">
  <span data-text="{string-length($message)} characters">?</span>
</p>

虽然很吸引人,但是目前为止,经过测试,只有数据同步功能还可以工作,它的主打 XSLT 转换已经失效,不管如何努力,script 标签最后只生成了空注释<!-- -->,即使使用另一种方法xslt.importStylesheetURI也只能读取到 xsl 而不能执行 then 内的程序——then根本没有执行!不得不感叹 xml 数据格式的式微。
XSLT Fiddle 上的 Frameless 似乎还能正常工作,等待后续研究其机制。

xslt.js

xslt.js 的 XSLT 转换功能还能正常工作,但仅限于非 jQuery 方式。可以通过这个方式来弥补上述 Frameless script 标签方式的失效。

window.onload = function() {
    $("script[src$='.xsl']").each(function(index, element) {
    	var input = element.getAttribute('data-input')
    	var temp = element.getAttribute('src')
    	var containerID = `xslt-transform-of-${input.replace('.','-')}-from-${temp.replace('.','-')}`
    	var containerTag = 'div'
    	$(element).wrap(`<${containerTag} id="${containerID}"></${containerTag}>`)
    	new Transformation().setXml(input)
    		.setXslt(temp).transform(containerID)
    })
}

缺点

  • CORS 跨域问题
  • 浏览器迭代次数限制
  • GSE 的城市名信息不统一,有全大写的,影响城市数据的统计(后续修正)

计划

计划通过 Gitee Pages 发布在线版,届时应该能克服本地访问存在的CORS跨域问题,同时为大家提供表单输入检索的界面,不再需要编辑 xml 文本文件,也可以整合演示自己所发现的 Frameless 和 xslt.js 功能残余,取决于需求人数和个人精力吧。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!