使用 Go 语言的流模式来解析 DrugBank 的 XML(或者任何 XML 大文件)[通俗易懂]

使用 Go 语言的流模式来解析 DrugBank 的 XML(或者任何 XML 大文件)[通俗易懂]如果直接解析一个大文件,那文件里的内容都会加载到内存,会使解析文件内容变得很慢,使用 Go 语言中的流模式可以有效处理这类问题。

当我想解析 DrugBank 的整个数据集时碰到了一个问题,这个数据集包含了一个 (670MB) XML 文件(如果想要描述 DrugBank 的公开论文,可以看:[1][2][3] 以及 [4])。

事实上,我想要的是 Structure External Links 链接下的 CSV 文件。使用这种方式解析似乎还有一些其它的用处,因为 DrugBank 版本的 XML 格式似乎比单独的 CSV 文件包含更多的信息。所以不管怎么样,这迫使我想出如何在 Go 中使用流模式来解析大型 XML 文件的方法,像 XMLStarlet 这些旧的工具会在处理 DrugBank 文件阻塞好几分钟(也许是试图把文件的内容全部读入内存?),这让人在迭代开发周期中失去了任何想法。而且,Go 对流式解析 XML 的支持非常棒。

尽管 Go 对流式解析 XML 的支持很不错,但在文档里面并没有详细讲述如何使用流的方式来实现它,还好 David Singleton这篇博客启发了我。基本上,你可以从他的文章来开始学习,但是我也想写一篇自己的博客来记录过程中想到的一些具体细节和特征。

想法:把 DrugBank 的 XML 解析为 TSV

简而言之,我们想要解析 DrugBank 的 XML 文件,这文件里包含了数据集中每种药物的大量分层信息,并只想提取其中的部分字段,然后把这些字段信息输出到由制表符分隔的格式优美的(.tsv)文件中。

以下是在这个例子中针对每种药物我们想要提取的字段(基于上面提到的现实问题):

  • InchiKey(一个表示化学结构的散列 ID)
  • Approved/Withdrawn status
  • ChEMBL ID(化合物包含字段)
  • PubChem Compound ID (CID)
  • PubChem Substance ID (SID)

DrugBank 的 XML 格式

DugBank 的 XML 格式在其最外层是最简单的:它基本上只包含了很多在 <drugbank></drugbank> 闭标签里的 <drug></drug> 元素。而 <drug> 标签内的相对来说比较复杂。但因为 Go 使用标签来将 XML 解析为结构体,我们可以跳过大部分信息,而只关注感兴趣的部分。

一个仅包含我们感兴趣字段的 DrugBank XML 概要示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<drugbank xmlns="http://www.drugbank.ca" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.drugbank.ca http://www.drugbank.ca/docs/drugbank.xsd" version="5.0" exported-on="2017-12-20">
<drug type="small molecule" created="2005-06-13" updated="2017-12-19">
  <name>Bivalirudin</name>
  <groups>
    <group>approved</group>
    <group>investigational</group>
  </groups>
  <external-identifiers>
    <external-identifier>
      <resource>PubChem Compound</resource>
      <identifier>16129704</identifier>
    </external-identifier>
    <external-identifier>
      <resource>PubChem Substance</resource>
      <identifier>46507415</identifier>
    </external-identifier>
    <external-identifier>
      <resource>ChEMBL</resource>
      <identifier>CHEMBL2103749</identifier>
    </external-identifier>
  </external-identifiers>
  <calculated-properties>
    <property>
      <kind>InChIKey</kind>
      <value>OIRCOABEOLEUMC-GEJPAHFPSA-N</value>
      <source>ChemAxon</source>
    </property>
  </calculated-properties>
</drug>
</drugbank>

实际上,每个 <drug> 元素内容的行数都比这多,那就确实需要合适的 XML 解析工具。

把 XML 映射到 Go 的结构体

Go 中 XML 的解析使用和 JSON 等其它格式相同的策略:定义一个或多个解析特定 XML 元素和属性的结构体。XML(或 JSON )和结构字段之间的映射是使用所谓的“标记”完成的,这些标记在结构体所定义字段之后的单引号中添加。因此,定义合理的结构体以到 XML 元素的字段映射是该完成工作的核心,并且这将直接影响你实现代码的简单程度。

下面你可以看到我定义的结构体(听起来是对的,对吗?)从中可以解析出我感兴趣的数据:

type Drugbank struct {
	XMLName xml.Name `xml:"drugbank"`
	Drugs   []Drug   `xml:"drug"`
}

type Drug struct {
	XMLName              xml.Name             `xml:"drug"`
	Name                 string               `xml:"name"`
	Groups               []string             `xml:"groups>group"`
	CalculatedProperties []Property           `xml:"calculated-properties>property"`
	ExternalIdentifiers  []ExternalIdentifier `xml:"external-identifiers>external-identifier"`
}

type Property struct {
	XMLName xml.Name `xml:"property"`
	Kind    string   `xml:"kind"`
	Value   string   `xml:"value"`
	Source  string   `xml:"source"`
}

type ExternalIdentifier struct {
	XMLName    xml.Name `xml:"external-identifier"`
	Resource   string   `xml:"resource"`
	Identifier string   `xml:"identifier"`
}

我们可以注意到以下几点:

  • 如前所述,后面单引号内的内容代表 XML 中要映射到特定字段的结构。
  • 请注意,对于嵌套层次结构,我们需要多个结构类型,例如 “Property” 和 “ExternalIdentifier” …然后把它们链接到主 “Drug” 结构体中。
  • 我们还需要一个架构体来表示最高级别元素 <drugbank>
  • 每个结构体都需要有一个 xml.Name 类型的字段(为了简单起见,命名为 XMLName ),该字段在XML中定义了它的名字,这样我们就有地方可以添加我们的 XML 映射标签了。
  • 注意,当我们有一些字段的切片(“列表”)时,比如 “Drug” 结构体中的 “CalculatedProperties” 字段,我们需要指定一个二级路径(xml:"calculated-properties**>**property")并将其放入 XML 结构体中,以便能获取到位于分组 ”calculate-properties“ 元素内的单个 ”property“ XML 元素。

设置好结构体之后,我们就可以写 Go 代码了,这份代码会按照 David 的博客以流的方式循环遍历读取一个 XML 文件,同时也会创建一个 TSV 写入器,这样我们就可以用流的方式将提取到的输出写入到一个 drugbank_extracted.tsv 新文件(为简洁起见,导入和主函数都省略了)。

xmlFile, err := os.Open("drugbank.xml")
if err != nil {
	panic("Could not open file: drugbank.xml")
}

tsvFile, err := os.Create("drugbank_extracted.tsv")
if err != nil {
	panic("Could not create file: drugbank_extracted.tsv")
}

tsvWrt := csv.NewWriter(tsvFile)
tsvWrt.Comma = '\t'
tsvHeader := []string{"inchikey", "status", "chembl_id", "pubchem_sid", "pubchem_cid"}
tsvWrt.Write(tsvHeader)

// Implement a streaming XML parser according to guide in
// http://blog.davidsingleton.org/parsing-huge-xml-files-with-go
xmlDec := xml.NewDecoder(xmlFile)
for {
	t, tokenErr := xmlDec.Token()
	if tokenErr != nil {
		if tokenErr == io.EOF {
			break
		} else {
			panic("Failed to read token:" + tokenErr.Error())
		}
	}
	switch startElem := t.(type) {
	case xml.StartElement:
		if startElem.Name.Local == "drug" {
			var status string
			var inchiKey string
			var chemblID string
			var pubchemSID string
			var pubchemCID string

			drug := &Drug{}
			decErr := xmlDec.DecodeElement(drug, &startElem)
			if err != nil {
				panic("Could not decode element" + decErr.Error())
			}
			for _, g := range drug.Groups {
				if g == "approved" {
					status = "A"
				}
				// Withdrawn till "shadow" (what's the correct term?) approved status
				if g == "withdrawn" {
					status = "W"
				}
			}
			for _, p := range drug.CalculatedProperties {
				if p.Kind == "InChIKey" {
					inchiKey = p.Value
				}
			}

			for _, eid := range drug.ExternalIdentifiers {
				if eid.Resource == "ChEMBL" {
					chemblID = eid.Identifier
				} else if eid.Resource == "PubChem Substance" {
					pubchemSID = eid.Identifier
				} else if eid.Resource == "PubChem Compound" {
					pubchemCID = eid.Identifier
				}
			}

			tsvWrt.Write([]string{inchiKey, status, chemblID, pubchemSID, pubchemCID})
		}
	case xml.EndElement:
		continue
	}
}
tsvWrt.Flush()
xmlFile.Close()
tsvFile.Close()

使用 SciPipe 来使之变成一个可重复的工作流

现在,我们可以使用 SciPipe(我正在开发的基于 Go 的工作流库)将它放到一个小工作流中,在这里我们会自动下载 DrugBank 数据,解压缩它之后再运行 XML 到 TSV 的 代码。查看这个 gist 来了解完整的工作流代码。

要在 gist 中运行这个 Go 文件,简单来说你需要做以下几步:

  • 创建一个文件 drugbank_userinfo.txt ,文件中包含使用以下形式记录的 DrugBank 网站用户名和密码:USERNAME:PASSWORD
  • 安装 Go 语言
  • 使用 go get github.com/scipipe/scipipe/... 命令安装 scipipe
  • 确保你已经安装了 curl,在 Ubuntu 上:可以使用 sudo apt-get install curl 命令安装

然后,你应该就可以运行它了,使用下面这个命令:

go run drugbank_xml_to_tsv_with_scipipe.go

完整的 SciPipe 工作流代码示例

我还在下面列出了完整的 SciPipe 工作流代码,(我)会一直维护到 Github 关闭的那一天;):

package main

import (
	"encoding/csv"
	"encoding/xml"
	"io"
	"os"

	sp "github.com/scipipe/scipipe"
)

// --------------------------------------------------------------------------------
// Workflow definition
// --------------------------------------------------------------------------------

func main() {
	wf := sp.NewWorkflow("exvsdb", 2)

	// DrugBank XML
	download := wf.NewProc("download", "curl -Lfv -o {o:zip} -u $(cat drugbank_userinfo.txt) https://www.drugbank.ca/releases/5-0-11/downloads/all-full-database")
	download.SetPathStatic("zip", "dat/drugbank.zip")

	unzip := wf.NewProc("unzip", `unzip -d dat/ {i:zip}; mv "dat/full database.xml" {o:xml}`)
	unzip.SetPathStatic("xml", "dat/drugbank.xml")
	unzip.In("zip").Connect(download.Out("zip"))

	xmlToTSV := wf.NewProc("xml2tsv", "# Custom Go code with input: {i:xml} and output: {o:tsv}")
	xmlToTSV.SetPathExtend("xml", "tsv", ".extr.tsv")
	xmlToTSV.In("xml").Connect(unzip.Out("xml"))
	xmlToTSV.CustomExecute = NewXMLToTSVFunc() // Getting the custom Go function in a factory method for readability

	wf.Run()
}

// --------------------------------------------------------------------------------
// DrugBank struct definitions
// --------------------------------------------------------------------------------

type Drugbank struct {
	XMLName xml.Name `xml:"drugbank"`
	Drugs   []Drug   `xml:"drug"`
}

type Drug struct {
	XMLName              xml.Name             `xml:"drug"`
	Name                 string               `xml:"name"`
	Groups               []string             `xml:"groups>group"`
	CalculatedProperties []Property           `xml:"calculated-properties>property"`
	ExternalIdentifiers  []ExternalIdentifier `xml:"external-identifiers>external-identifier"`
}

type Property struct {
	XMLName xml.Name `xml:"property"`
	Kind    string   `xml:"kind"`
	Value   string   `xml:"value"`
	Source  string   `xml:"source"`
}

type ExternalIdentifier struct {
	XMLName    xml.Name `xml:"external-identifier"`
	Resource   string   `xml:"resource"`
	Identifier string   `xml:"identifier"`
}

// --------------------------------------------------------------------------------
// Components
// --------------------------------------------------------------------------------

// NewXMLToTSVFunc returns a CustomExecute function to be used by the XML to TSV
// component in the workflow above
func NewXMLToTSVFunc() func(t *sp.Task) {
	return func(t *sp.Task) {
		fh, err := os.Open(t.InPath("xml"))
		if err != nil {
			sp.Fail("Could not open file", t.InPath("xml"))
		}

		tsvWrt := csv.NewWriter(t.OutIP("tsv").OpenWriteTemp())
		tsvWrt.Comma = '\t'
		tsvHeader := []string{"inchikey", "status", "chembl_id", "pubchem_sid", "pubchem_cid"}
		tsvWrt.Write(tsvHeader)

		// Implement a streaming XML parser according to guide in
		// http://blog.davidsingleton.org/parsing-huge-xml-files-with-go
		xmlDec := xml.NewDecoder(fh)
		for {
			t, tokenErr := xmlDec.Token()
			if tokenErr != nil {
				if tokenErr == io.EOF {
					break
				} else {
					sp.Fail("Failed to read token:", tokenErr)
				}
			}
			switch startElem := t.(type) {
			case xml.StartElement:
				if startElem.Name.Local == "drug" {
					var status string
					var inchiKey string
					var chemblID string
					var pubchemSID string
					var pubchemCID string

					drug := &Drug{}
					decErr := xmlDec.DecodeElement(drug, &startElem)
					if err != nil {
						sp.Fail("Could not decode element", decErr)
					}
					for _, g := range drug.Groups {
						if g == "approved" {
							status = "A"
						}
						// Withdrawn till "shadow" (what's the correct term?) approved status
						if g == "withdrawn" {
							status = "W"
						}
					}
					for _, p := range drug.CalculatedProperties {
						if p.Kind == "InChIKey" {
							inchiKey = p.Value
						}
					}

					for _, eid := range drug.ExternalIdentifiers {
						if eid.Resource == "ChEMBL" {
							chemblID = eid.Identifier
						} else if eid.Resource == "PubChem Substance" {
							pubchemSID = eid.Identifier
						} else if eid.Resource == "PubChem Compound" {
							pubchemCID = eid.Identifier
						}
					}

					tsvWrt.Write([]string{inchiKey, status, chemblID, pubchemSID, pubchemCID})
				}
			case xml.EndElement:
				continue
			}
		}
		tsvWrt.Flush()
		fh.Close()
	}
}

(代码许可证:Public Domain


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13390.html

(0)

相关推荐

  • Python安装工具:setup.py

    Python安装工具:setup.pyPython是一种解释型、面向对象、动态数据类型的高级程序设计语言。它具有简洁、易读、易学等特点,在全球范围内得到了广泛的应用。Python在各种领域都有应用,在科学计算、人工智能、数据分析等领域得到了广泛的应用。但是,Python的安装过程却比较繁琐,需要安装各种依赖库、设置环境变量等。这时,Python安装工具——setup.py就派上用场了。

    2024-02-05
    87
  • 使用Anaconda升级Python环境

    使用Anaconda升级Python环境Python是一种强大的解释型语言,拥有着丰富的库和工具,在数据分析、科学计算和机器学习等领域得到广泛应用。然而,由于Python的不断更新和演进,我们需要经常升级Python环境以保证我们的代码可以正常运行。本文将介绍使用Anaconda升级Python环境的方法。

    2024-08-19
    33
  • MySQL架构和存储引擎、系统默认数据库介绍「建议收藏」

    MySQL架构和存储引擎、系统默认数据库介绍「建议收藏」MySQL架构: 采用C/S架构,即客户端/服务器。客户端和服务器区分开,通过客户端发送请求来和服务器交互。 过程: 用户通过开发的应用程序来访问数据库(C/S),应用程序通过连接器(connecte

    2023-06-05
    147
  • Python二维字典操作

    Python二维字典操作字典是Python语言中最常用的一种数据类型,它可以存储键值对的数据,例如一个人的姓名和年龄。而二维字典则是指在字典中再嵌套一个字典,即将一个二维坐标用键值对的方式进行存储。例如,可以用字典存储多个城市的经纬度,其中经纬度又用键值对进行存储。

    2024-05-12
    72
  • 优化python程序的稳定性——numpy set random seed

    优化python程序的稳定性——numpy set random seeda href=”https://beian.miit.gov.cn/”苏ICP备2023018380号-1/a Copyright www.python100.com .Some Rights Reserved.

    2024-03-06
    77
  • 怎样优化oracle数据库,有几种方式_数据库设计的技巧

    怎样优化oracle数据库,有几种方式_数据库设计的技巧数据库之Oracle优化技巧(一) 1.where子句中的连接顺序 在Oracle数据库中,where子句的执行顺序是自下而上进行解析,根据这个原理,表之间的连接必须写在其他where条件之前,那些可

    2023-03-06
    141
  • Python cmp定义及其常见用法

    Python cmp定义及其常见用法Python内建函数cmp()用于比较两个对象的大小。如果两个对象相等,返回0;如果第一个对象小于第二个对象,返回负数;如果第一个对象大于第二个对象,返回正数。cmp()函数可以用于排序、查找、去重等操作。

    2024-02-18
    93
  • 卷积神经网络概述及python实现「建议收藏」

    卷积神经网络概述及python实现「建议收藏」摘要:本文概括地介绍CNN的基本原理 ,并通过阿拉伯字母分类例子具体介绍其实现过程,理论与实践的结合体。 对于卷积神经网络(CNN)而言,相信很多读者并不陌生,该网络近年来在大多数领域都表现优异,尤其是在计算机视觉领域中。但是很多工作人员可能直接调用相关的深度学习工具箱搭建卷积…

    2023-07-21
    123

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注