背景
写了个 GDS 的查询工具,主要用于检索式检索结果的空间与数量分析,毕竟循证理念嘛,荟萃全球资源,自己是否已经做到了呢?这就需要对自己检索到的数据集的源头进行定位、统计和分析,通过喜闻乐见的 html 可视化报表形式呈现,以确定是否需要对检索式进行改进。
功能
根据检索式检索 GSE 数据集并批量查询元数据,统计 GSE 数据集所来自的国家和城市。高级功能正在逐步开发。
食用方式
- 从我的仓库下载好项目文件。
- 任意文本编辑器打开 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).
- Chrome 浏览器关闭安全模式(
chrome --allow-file-access-from-files --user-data-dir=[自己填个放数据的目录] --disable-web-security
)后以 File 协议打开 XML 文件,批量查询国外网站的数据速度会很慢,请耐心等待浏览器加载完毕。(用的都是 NCBI 的官方数据,安全得很,不用担心)
原理
数据集查询
使用了 Entrez Programming Utilities 的 ESearch 接口,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"> !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~</xsl:variable>
<xsl:variable name="latin1"> ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ</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) > 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 功能残余,取决于需求人数和个人精力吧。
来源:oschina
链接:https://my.oschina.net/baytars/blog/4721734