autoSql 和 autoXml:来自基因组项目的代码生成器

作者:Jim Kent

在各种事务的计划中,将数据从一个源移动到另一个源并非难事。例如,如果您的源数据是制表符分隔的文件,并且您需要在 SQL 关系数据库中使用它,您可以编写一个小 SQL 定义,然后生成一个 C 程序来从源文件读取数据并将其写入数据库。但是,当您处理一个大型项目时,或者在我们的例子中,是真正的大型项目,并且您发现自己正在处理数十个来源,这些来源为您提供千兆字节的数据时,编写所有这些代码很快就会变得乏味。

为了解决这个问题,这里有两个工具可以完成这项工作。它们共同为 SQL 生成数据库定义,编写带有数据定义和函数原型的 C 头文件,编写 C 代码以在 C 结构之间获取数据,并为 XML 解析器生成 C 代码。

人类基因组浏览器

人类基因组是编码在我们 DNA 中的说明手册。它由三十亿对化学字母组成,通常以首字母 G、C、A 和 T 闻名。基因组数据是 24 条长的这些字母链——这并非轻松的阅读。人类基因组浏览器是加州大学圣克鲁兹分校的一个网站,它为世界各地的科学家提供了这座数据山的视觉表示。浏览器将序列数据本身与基因组特定区域功能的更高级别注释相结合。用户可以定位并放大他们感兴趣的基因,链接到对基因组该部分进行的研究,并将基因组数据与其他物种的基因组数据进行比较。浏览器将特定类型的注释堆叠为基因组坐标位置下方的轨道

基因组浏览器具有 HTML/CGI 前端,允许用户查看基因组轨道并(在动态生成的图像映射的帮助下)单击基因组轨道。表单字段为用户提供了一种设置缩放级别和控制轨道数据密度的方法。CGI 源代码是 C,基因组数据存储在 SQL 数据库中。

数据量很大。浏览器源代码必须处理基因预测以及人类基因组与其他物种基因组之间相似性的数据格式。使问题复杂化的是,我们与至少十几个外部来源合作,每个来源都有自己的数据格式。即使我们不想在内部使用他们的数据格式,我们仍然需要编写一个解析器来读取它们并将其转换为我们自己的格式。我们使用代码生成器可能有一半是为了更容易地从其他组导入文件。

进入 autoSql

autoSql 生成 SQL 和 C 代码,用于将数据保存和加载到数据库。通过使用 autoSql,我们不需要编写繁琐的数据定义,这涉及到代码的读取和写入。例如,浏览器大约有 30 个公共轨道和 30 个实验轨道。每个轨道都与关系数据库中的一个表相关联。所有将轨道表加载到内存中的模块都由 autoSql 生成。

XML 连接

在项目后期,我们开始使用 XML 与日本的一个研究小组合作。XML 也可用于通过 DAS(分布式注释系统,一种通过互联网传输基因组数据的协议)与其他公共站点交换数据。

autoXml 给定 XML DTD 文件,生成 XML 解析器的 C 代码。由于 XML I/O 比 SQL I/O 更代码密集,因此 autoXml 已经被证明是有用的。

物有所值的编码

autoSql 和 autoXml 共同证明是无价的省时工具。autoSql 一直是浏览器项目的关键主力。在 1,200 行代码中,它生成了浏览器程序的一半,即数万行代码。

虽然我们不像使用 SQL 那样多地使用 XML,但我们已经通过 autoXml 收回了成本。在一个从日本 Riken 小鼠基因组注释项目导入数据的项目中,autoXml 生成了大约 1,500 行代码。(它本身只有 1,200 行。)

您可以从 www.soe.ucsc.edu/~kent/exe 下载 autoSql 和 autoXml 的二进制文件。本文的源代码、Linux 可执行文件和示例位于 www.soe.ucsc.edu/~kent/src/autoCode.tgz

autoSql 概述

autoSql 是一个程序,它基于对象规范自动生成 SQL 表创建脚本和 C 代码,用于将数据保存和加载到数据库。(有关此过程的概述,请参见图 1。)

autoSql and autoXml: Code Generators from the Genome Project

图 1. 通过 autoSql 运行定义文件

规范语言有点古怪,但已被证明对许多工作有效。我们最初开发 autoSql 用于关系数据库;事实证明,它生成的代码也可以从许多平面格式加载,只要它们是文本格式。

一个简单的例子

想象一下一个简单的地址簿,它只存储姓名、街道地址、邮政编码和州。为此的 autoSql 规范将是

table addressBook
"A simple address book"
   (
   string name;
     "Name - first, last, both, we don't care"
   lstring address;  "Street address"
   string city;  "City"
   uint zipCode; "A zip code
     is always positive, so can be unsigned"
   char[2] state;
     "Just store the abbreviation for the state"
   )

如果这看起来有点像 C 结构和 SQL 表定义的混合体,那是因为 Jim 在创建 autoSql 语言时在 C 和 SQL 之间切换。

当您通过 autoSql 运行地址簿模板时,该程序会生成 SQL 表定义

   #A simple address book
   CREATE TABLE addressBook (
   name varchar(255) not null,    # Name -
       first, last, both, we don't care
   address longblob not null,     # Street address
   city varchar(255) not null,    # City
   zipCode int unsigned not null, # A zip code is
       always positive, so can be unsigned
   state char(2) not null,        # Just store
       the abbreviation for the state
                  #Indices
   PRIMARY KEY(name)
    );

以及以下 C 结构定义

struct addressBook
/* A simple address book */
    {
    struct addressBook *next;
      /* Next in singly linked list. */
    char *name;
      /* Name - first, last, both, we don't care */
    char *address;    /* Street address */
    char *city;       /* City */
    unsigned zipCode; /* A zip code is always
      positive, so can be unsigned */
    char state[3];    /* Just store
      the abbreviation for the state */
    };
通常在 C 中,您一次访问 SQL 数据库的单行。该行以字符串数组的形式返回。将数字的 ASCII 表示形式转换为二进制数字取决于 C 程序。这不是难事,但是当您输入 20 或 30 行看起来像这样的代码后
   point->x = atoi(row[1]);
   point->y = atoi(row[2]);
您会感谢 autoSql 为您生成的以下两个例程
    void addressBookStaticLoad(char **row,
          struct addressBook *ret);
    /* Load a row from addressBook table into ret. */
    /* The contents of ret will be replaced at the */
    /* next call to this function. */
    struct addressBook *addressBookLoad(char **row);
    /* Load a addressBook from row fetched with */
    /* select * from addressBook from database.*/
    /* Dispose of this with addressBookFree(). */
第一个例程通常在您只想一次处理一个项目时使用。它不分配任何动态内存,因此速度非常快。第二个例程将结构保存到动态内存。由于 C 结构始终包含一个“next”字段,因此您可以轻松地使用此例程来构建地址簿条目的列表。

使用动态内存的唯一问题是您必须记住释放它。虽然 autoSql 无法记住为您释放事物,但它可以生成例程来释放单个动态分配的结构或动态分配的结构列表。这就是接下来的两个例程所做的

    void addressBookFree(struct addressBook **pEl);
    /* Free a single dynamically allocated
     * addressBook such as created with
     * addressBookLoad(). */
    void addressBookFreeList(
       struct addressBook **pList);
    /* Free a list of dynamically
     * allocated addressBook's */

无需编写代码即可加载结构是很不错的,但有时您也需要写入结构。autoSql 假定您要么希望以制表符分隔的格式保存结构,要么以逗号分隔的格式保存结构。它生成一个可以执行以下操作的例程

    void addressBookOutput(struct addressBook *el,
       FILE *f, char sep, char lastSep);
    /* Print out addressBook. Separate fields with
     * sep. Follow last field with lastSep. */
以及使逗号或制表符操作更方便的宏
    #define addressBookTabOut(el,f)
      addressBookOutput(el,f,'\t','\\n');
    /* Print out addressBook as a line in a
     * tab-separated file. */
    #define addressBookCommaOut(el,f)
      addressBookOutput(el,f,',',',');
    /* Print out addressBook as a comma
     * separated list including final comma. */
autoSql 生成一个例程来读取逗号分隔的列表。虽然您不太可能直接调用此例程,但比简单字符串或整数更复杂的字段将以逗号分隔的列表形式保存在数据库中。此例程允许 autoSql 拥有包含其他对象的对象。
对象类型

autoSql 有三种类型的对象

  • 简单:不包含可变大小数组的对象。

  • 对象:可以包含可变大小数组的对象。下一个指针自动作为与对象对应的 C 结构中的第一个字段插入。

  • 表:类似于对象,但程序生成 SQL 以及 C 定义。

简单对象与其他对象在程序处理数组声明的方式上有所不同。在字段声明中

simple point[3] triangle;  "A three sided figure"

这三个点在内存中存储为 C 数组。如果将其声明为

object point[3] triangle;  "A three sided figure"
这三个点将在内存中存储为单链表。
字段类型

支持以下基本字段类型

  • int:32 位有符号整数

  • uint:32 位无符号整数

  • short:16 位有符号整数

  • ushort:16 位无符号整数

  • byte:8 位有符号整数

  • ubyte:8 位无符号整数

  • float:单精度 IEEE 浮点数

  • char:8 位字符(只能在数组中使用)

  • string:最大长度为 255 字节的可变长度字符串

  • lstring:最大长度为 20 亿字节的可变长度字符串

此外,简单、对象和表类型可以用作字段。

定长和可变数组声明

数组可以声明为定长或可变大小。通过将字段名称放在数组声明中的方括号内来声明可变大小的数组。此字段必须在数组之前定义。

更复杂的例子

假设您刚刚构建了一个惊人的 3-D 建模程序。唯一的问题是现在您需要将结构保存在数据库中。清单 1 是一种您可以使用 autoSql 构建数据库的方法。将其另存为 threeD.as 并运行

autoSql threeD.as threeD

最终将生成 393 行无错误(我认为!)C 代码和 14 行 SQL,只需投入 33 行规范。(有关完整的 autoSql 语法,请参阅清单 2。)

清单 1. 使用 autoSql 构建数据库

清单 2. autoSql 语法

autoXml 概述

autoXml 给定 XML DTD 文件,生成 XML 解析器的 C 代码。它将为 DTD 中的每个“element”生成一个结构,并使用结构的每个属性的字段填充该结构。默认情况下,它将生成一个解析器,该解析器忽略 DTD 中没有的元素和属性,但在其他方面是一个验证解析器。如果您使用 -picky 标志,它将是完全验证的。

autoXml 解析器会将整个文件加载到内存中。如果这是一个问题,您将不得不求助于较低级别的 xap 解析器,它很像常用的 expat 解析器,但速度更快一些。

简短的 XML 和 DTD 教程

如果您发现自己被到目前为止的所有首字母缩略词搞糊涂了,您可能不熟悉 XML(可扩展标记语言)。它具有基于标记的格式,XML 文档的一个简单示例可能是

   <POLYGON id="square">
      <DESCRIPTION>
         This is soooo square man
      </DESCRIPTION>
      <POINT x="0" y="0" ->
      <POINT x="0" y="1" ->
      <POINT x="1" y="1" ->
      <POINT x="1" y="0" ->
   </POLYGON>

XML 中的所有内容都位于 <TAG></TAG> 对之间。标记可能具有关联的文本、属性和子标记。在上面的示例中,POLYGON 具有子标记 DESCRIPTION 和 POINT,属性 id 且没有文本。DESCRIPTION 具有文本“This is soooo square man”,并且没有子标记或属性。POINT 具有属性 x 和 y。POINT 还说明了一个小的 XML 快捷方式:仅包含属性的标记可以写成 <TAG att=“something” -> 作为 <TAG att=“something”></TAG> 的快捷方式。

XML 非常像 HTML,但存在显着差异。所有属性都必须用引号括起来在 XML 中,而引号在 HTML 中是可选的。标记必须在 XML 中严格嵌套,而 HTML 允许标记打开但不关闭。HTML 中的标记是预定义的。在 XML 中,标记的定义取决于您。

标记可以通过两种方式在 XML 中定义:通过 DTD 文件或通过 XML 模式。每种方法都有优点和缺点。DTD 文件相对简单,并且被各种解析器和 XML 浏览器识别。另一方面,DTD 文件无法表达某个属性必须是数字的。XML 模式更复杂。它们本身是用一种 XML 编写的,这在某些方面很好。它们尚未得到广泛支持。目前 autoXml 仅适用于具有一些适度扩展的 DTD 文件。

这是一个描述上述 POLYGON 格式的 DTD 文件

<!ELEMENT POLYGON (DESCRIPTION? POINT+)>
<!ATTLIST POLYGON id CDATA #REQUIRED>
<!ELEMENT DESCRIPTION (#PCDATA)>
<!ELEMENT POINT>
<!ATTLIST POINT x CDATA #REQUIRED>
<!ATTLIST POINT y CDATA #REQUIRED>
<!ATTLIST POINT z CDATA "0">

DTD 有两种主要类型的定义:ELEMENT 和 ATTLIST(或属性)。元素定义包括元素的名称和子元素的可选括号列表。子元素必须在 DTD 中的其他位置定义,但 #PCDATA 子元素除外,该子元素用于指示元素可以在其标记之间包含文本。每个子元素后面可以跟以下字符之一

  • ?: 子元素是可选的。

  • +: 子元素至少出现一次。

  • *: 子元素出现 0 次或多次。

如果没有后续字符,则子元素恰好出现一次。

ATTLIST 定义一个属性并将其与一个元素关联。最好将 ATTLIST 与其 ELEMENT 放在一起。以下是 ATTLIST 中的字段

  • element:与此关联的元素的名称。

  • name:此属性的名称。

  • type:通常为 CDATA。可以是引用或日期,但 autoXml 不支持这些。

  • default:如果属性不存在,则此字段包含要使用的默认值。此字段中的关键字 #REQUIRED 表示属性必须存在。关键字 #IMPLIED 表示可以缺少此属性(在这种情况下,在 autoXml 读取后,它将具有 NULL 或零值)。

autoXml 扩展和限制

autoXml 扩展了 ATTLIST 的类型字段,以包括 INT 或 FLOAT,用于数值而不是字符串值。类似地,您可以使用 #INT 或 #FLOAT 代替 #PCDATA,以将数值类型放在文本字段中。如果您包含这些扩展,请在您的 DTD 文件上使用 .dtdx 而不是 .dtd 后缀。

目前,autoXml 仅在 DTD 注释从单独一行开始时才能处理它们。autoXml 期望所有 ELEMENT 和 ATTLIST 声明都适合在一行中。它不处理超出将引用 ID 保存为字符串的引用数据类型。

清单 3. autoXml 代码生成

有关 autoXml 生成的源代码的完整示例,请参阅清单 3。除了清单 3 中显示的 .h 文件外,autoXml 还生成相应的 .c 文件。每个 XML 文件都必须有一个根对象。在本例中,根对象是 POLYGON(我们的 DTD 原样不允许我们每个文件有多个多边形)。您可以使用 polyPolygonLoad() 函数读取符合此 DTD 的 XML 文件,并使用 polyPolygonSave 将其保存回去。

autoSql 和 autoXml 在各种数据上都能很好地工作,正如您所见,从地址簿到基因轨道。我们希望您会在自己的项目中发现这些工具很有用。

资源

autoSql and autoXml: Code Generators from the Genome Project
Jim Kent 博士及其在人类基因组项目方面的工作已在《纽约时报》、《旧金山纪事报》、《软件开发》杂志和其他出版物中进行了报道。他目前正在研究跨物种基因组比较和 Parasol,这是他的千节点集群的作业控制器。

autoSql and autoXml: Code Generators from the Genome Project
Heidi Brumbaugh (heidi_b@pacbell.net) 自八十年代后期以来一直是计算机出版行业的作家和编辑。访问她的项目链接并阅读她的一些小说,请访问 www.heidi.to
加载 Disqus 评论