分享
 
 
 

PHP5的XML新特性

王朝php·作者佚名  2006-01-30
窄屏简体版  字體: |||超大  

PHP5的XML新特性

作者 Christian Stocker 翻译 ice_berg16(寻梦的稻草人)

面向的读者

这篇文章的面向对象是所有对PHP5的XML新功能感兴趣的各个水平的PHP开发者。我们假定读者掌握XML的基本知识。然而,如果你已经在你的PHP当中使用了XML,那么这篇文章也会让你受益非浅。

介绍

在当今的互联网世界,XML已经不再是一个时髦词了,它已经被广泛的接受和规范的使用了。因此相对于PHP4,PHP5对于XML的支持更受到了重视。在PHP4中你面对的几乎都是非标准,API中断,内存泄漏以及其它不完全的功能。尽管有些不足已经在PHP4.3中得到改进,开发者们还是决定抛弃原有的代码,在PHP5重写全部代码。

这篇文章将对PHP5中关于XML的所有令人激动的新特性逐一介绍。

PHP4 的 XML

早期的PHP版本就已经开始支持XML了,而这只是一个基于SAX的接口,它可以轻松的解析任何XML文档。随着PHP4中加入了DOMXML扩展模块,XML被更好的支持了。后来XSLT做为补充被加了进来。在整个PHP4的阶段,其它一些功能如HTML,XSLT和DTD验证也被加到了DOMXML扩展中,不幸的是,由于XSLT和DOMXML扩展始终处于实验阶段,API部分也被不止一次的修改,它们还是不能以默认方式安装。此外,DOMXML扩展没有遵循W3C制定的DOM标准,而有自己的命名方法。虽然在PHP4.3中这部分得到了改善并且许多内存泄漏和其它一些功能也得以修复,但它始终没有发展到一个稳定的阶段,一些深入的问题已经几乎不可能修复。只有SAX扩展被已默认方式安装,其它的一些扩展从未得到广泛的使用。

基于所有这些原因,PHP的XML开发者决定在PHP5重写全部代码,并遵循使用标准。

PHP5的XML

在PHP5中所有支持XML的部分几乎全部重新编写.现在的所有XML扩展都是基于GNOME项目的LIBXML2库。这将允许在不同的扩展模块之间互相操作,核心开发者只需要在一个底层的库上进行开发。例如,复杂的内存管理只实现一次就可以让所有XML相关扩展得到改善。

除了继承PHP4中闻名的SAX解析器之外,PHP5还支持遵循W3C标准的DOM和基于LIBXSLT引擎的XSLT。同时还加入了PHP独有的SimpleXML扩展和符合标准的SOAP扩展。随着XML越来越被重视,PHP开发者决定在默认安装方式中加入更多对XML的支持。这就意味着你现在可以使用SAX,DOM和SimpleXML,而这些扩展将会在更多的服务器上安装。然后对于XSLT和SOAP的支持,还需要在PHP编译时被显式的配置。

数据流的支持

现在所有的XML扩展都支持PHP数据流,即使你不从PHP中直接访问。例如,在PHP5中你可以从一个文件或从一条指令访问数据流。基本上你能够在任何可以访问普通文件的地方访问PHP数据流。

PHP4.3中简要的介绍了数据流,在PHP5中已经得到了进一步的提高,包含文件存取,网络存取和其它操作,如共享一套功能函数。你甚至可以使用PHP代码来实现你自己的数据流,这样数据存取将变得非常简单。关于这部分的更多细节请参考PHP文档。

SAX

SAX的全称是Simple API for XML,它是用于解析XML文档的接口,是基于回调形式的。从PHP3开始就已经支持了SAX,到现在也没有太大的变化。在PHP5中,API接口并没有改变,所以你的代码仍然可以运行。唯一不同的是它不再基于EXPAT库,而是基于LIBXML2库。

这个变化带来了一些对命名空间支持上的问题,这个问题在LIBXML2.2.6版本中已经得到解决。但是LIBXML2以前的版本中并没有解决,因此如果你使用了xml_parse_create_ns();强烈建议在你的系统上安装LIBXML2.2.6。

DOM

DOM (文档对象模型)是由W3C制定的一套访问XML文档树的标准。在PHP4可以使用DOMXML来对此进行操作,DOMXML的最主要问题是它不符合标准的命名方法。而且在很长一段时间内还存在内存泄漏问题(PHP4.3已经修复了这个问题)。

新的DOM扩展是基于W3C标准完成的,包含方法和属性名称。如果你在其它语言中熟悉DOM,例如在JavaScript中,那么在PHP中编写类似的功能将变得非常容易。你不必每次都查看文档,因为方法和参数都是相同的。

由于使用了新的W3C标准,基于DOMXML的代码将不能运行。在PHP中的API有很大的不同。但是如果你的代码中使用了类似W3C标准的方法命名方式,移植并不是很困难。你只需要将载入函数和保存函数修改,删除函数名中的下划线(DOM标准使用首字母大写)。其它各处的调节当然也是必须的,但是主要逻辑部分可以保持不变。

读取DOM

我不会在这篇文章中解释DOM扩展的所有特性,那也是没有必要的。或许你应该将HTTP://www.w3.org/DOM的文档加入书签。它与PHP5的DOM部分基本上相同。

在这篇文章的大多数例子中我们将使用同一个XML文件,zend.com上有非常简单的RSS版本。将下面的文本粘贴到一个文本文件中并保存为articles.xml。

http://www.zend.com/zend/week/week172.php http://www.zend.com/zend/tut/tut-hatwar3.php

要将这个例子载入到一个DOM对象,首先要创建一个DOMDocument对象,然后载入XML文件。

$dom = new DomDocument();

$dom->load("articles.xml");

正像上面所提及的,你可以使用PHP的数据流来载入一个XML文档,你应该这样写:

$dom->load("file:///articles.xml");

(或者其它类型的数据流)

如果你想将XML文档输出到浏览器或做为标准标出,使用:

print $dom->saveXML();

如果你想把它保存成文件,请使用:

print $dom->save("newfile.xml");

(注意这样做会将文件大小发送到stdout)

当然这个例子没有太多的功能,让我们来做些更有用的。我们来取得所有的title元素。有很多方法可以办到,最简单的就是使用getElementsByTagName($tagname):

$titles = $dom->getElementsByTagName("title");

foreach($titles as $node) {

print $node->textContent . "\n";

}

textContent属性并不是W3C标准,它可以让我们很方便的快速读取一个元素的所有文本节点,使用W3C的标准读取是下面这样:

$node->firstChild->data;

(这时候你要确保firstChild结点是你需要的文本结点,否则你还得遍历所有子结点来查找)。

另外一个要注意的问题是getElementsByTagName()返回一个DomNodeList,对象,而不是像PHP4中get_elements_by_tagname()那样返回一个数组,但是正像你在这个例子中看到的那样,你可以使用foreach语句轻松的遍历它。你也可以直接使用$titles->item(0)来访问结点。该方法将返回第一个title元素。

另一个取得所有title元素的办法是从根结点遍历,你可以看到,这个方法更复杂,但是如果你需要的不只是title元素的时候,这个方法也就更灵活。

foreach ($dom->documentElement->childNodes as $articles) {

//如果节点是一个元素(nodeType == 1)并且名字是item就继续循环

if ($articles->nodeType == 1 && $articles->nodeName == "item") {

foreach ($articles->childNodes as $item) {

//如果节点是一个元素,并且名字是title就打印它.

if ($item->nodeType == 1 && $item->nodeName == "title") {

print $item->textContent . "\n";

}

}

}

}

XPath XPaht 就像是XML的SQL,使用XPath你可以在一个XML文档中查询符合一些模式语法的特定结点。想使用XPath获得所有title结点,只需要这么做:

$xp = new domxpath($dom);

$titles = $xp->query("/articles/item/title");

foreach ($titles as $node) {

print $node->textContent . "\n";

}

?>

这样和使用getElementsByTagName()方法差不多,但是Xpath要强大的多,例如,如果我们有一个title元素是article的子元素(而不是item的子元素),getElementsByTagName()就会将它返回。而使用/articles/item/title语法,我们只会得到在指定深度和位置的title元素。这只是一个简单的例子,再深入一点可能是这样:

/articles/item[position() = 1]/title 返回第一个item元素的所有

/articles/item/title[@id = '23'] 返回所有含有id属性并且值为23的title

/articles//title 返回所有articles元素下面的title(译者注://代表任意深度)

你也可以查询含有特殊兄弟元素的点,含有特殊文本内容的元素,或者使用命名空间等等。如果你必须大量的查询XML文档,适当的学习使用XPath会节省你很多时间,它使用简单,执行速度快,比标准的DOM需要更少的代码。

向DOM中写入数据 文档对象模型并不是只能读取和查询,你也可以操作和写入。(DOM标准有点冗长,因为编写者想尽量支持能够想像到的每一个环境,但是它工作的非常好)。看看下面这个例子,它在我们的article.xml文件中添加了一个新元素。

$item = $dom->createElement("item");

$title = $dom->createElement("title");

$titletext = $dom->createTextNode("XML in PHP5");

$title->appendChild($titletext);

$item->appendChild($title);

$dom->documentElement->appendChild($item);

print $dom->saveXML();

首先,我们创建了所有需要的结点,一个item元素,一个title元素和一个包含item标题的文本结点,然后我们将所有的结点链接起来,把文本结点加到title元素上,把title元素加到item元素上,最后我们把item元素插入到articles根元素上。现在,我们的XML文档中有一个新的文章列表了。

扩展类(class) 好了,上面的例子都可以在PHP4下面用DOMXML扩展来做(只是API有一些不同),能够自己扩展DOM类是PHP5的一个新特性,这使得书写更多可读性强的代码变得可能。下面是用DOMDocument类重新写的整个例子:

class Articles extends DomDocument {

function __construct() {

//必须调用!

parent::__construct();

}

function addArticle($title) {

$item = $this->createElement("item");

$titlespace = $this->createElement("title");

$titletext = $this->createTextNode($title);

$titlespace->appendChild($titletext);

$item->appendChild($titlespace);

$this->documentElement->appendChild($item);

}

}

$dom = new Articles();

$dom->load("articles.xml");

$dom->addArticle("XML in PHP5");

print $dom->save("newfile.xml");

HTML PHP5中一个经常不被注意到的特性是libxml2库对HTML的支持,你不仅可以使用DOM扩展载入结构良好(well-formed)的XML文档,还可以载入非结构良好的(not-well-formed)HTML文档,把它当做标准的DOMDocument对象,使用所有能用的方法和特性,比如XPath和SimpleXML。

当你需要访问一个你无法控制站点的内容时,HTML的性能就显示十分有用了。在 XPath, XSLT 或 SimpleXML的帮助下,你省掉了许多代码,像使用正则表达式比较字符串或者SAX解析器。当HTML文档结构不是很好的时候,这个办法尤其有用(这是个频繁的问题!)。

下面的代码获得并解析php.net的首页,将返第一个title元素的内容。

$dom = new DomDocument();

$dom->loadHTMLFile("http://www.php.net/");

$title = $dom->getElementsByTagName("title");

print $title->item(0)->textContent;

请注意当指定元素没有找到时,你的输出可能会包含错误。如果你的网站还在使用PHP输出HTML4代码,有一个好消息要告诉你,DOM扩展不仅能载入HTML文档,而且还能将他们保存为HTML4格式的文件。在你添加完DOM文档后,使用$dom->saveHTML()来保存。要注意的是,为了使输出的HTML代码符合W3C标准,最好不用使用 整齐的扩展?(tidy extension)。Libxml2 库支持的HTML并不会考虑到每个可能发生的事情,也不能很好的处理非通用格式的输入。

验证 XML文档的验证越来越重要了。例如,如果你从一些国外资源中获得了一个XML文档,在你处理之前你需要检验它是否符合某个确定的格式。幸运的是你不需要在PHP中写自己的验证程序,因为你可以使用三个应用最广泛的标准之一(DTD,XML Schema 或RelaxNG)来完成它。.

DTD是一个产生于SGML时代的标准,缺少一些XML的新特性(如命名空间),而且由于它不是用XML写的,它也很难被解析和转换。 XML Schemai是由W3C制定的一个标准,它应用广泛,几乎包含了所有验证XML文档所需要的内容。 RelaxNG 是复杂的XML Schema标准的对头,是由自由者组织创建的,由于它比XML Schema更容易实现,越来越多的程序开始支持RelaxNG了 如果你没有遗留下来的计划文档或者非常复杂的XML文档,那么使用RelaxNG吧。它书写和阅读都比较简单,越来越多的工具也支持它。甚至还有一个工具叫Trang,它可以从XML范本中自动创建一个RelaxNG文档。而且只有RelaxNG(和老化的DTDS)被libxml2完全支持,尽管libxml2也即将完全支持ML Schema。

验证XML文档的语法相当简单:

$dom->validate('articles.dtd'); $dom->relaxNGValidate('articles.rng'); $dom->schemaValidate('articles.xsd'); 目前,所有这些都只会简单的返回true或false,错误会被做为PHP警告输出。显然想返回给用户友好的信息这并不是一个好主意,在PHP5.0以后的版本里会有所改善。到底该怎么实现目前还在讨论之中,但是错误报告肯定会处理的更好。

SimpleXML SimpleXML 是PHP的XML家族中最后一个被加入的成员,加入SimpleXML扩展的目的是为了提供一个使用标准对象属性和迭代器访问XML文档的更简单的方法。该扩展没有太多的方法,虽然如此它还是相当强大的。从我们的文档的取得所有title节点比原来需要更少的代码。

$sxe = simplexml_load_file("articles.xml");

foreach($sxe->item as $item) {

print $item->title ."\n";

}

这是在干什么?首先将articles.xml载入到一个SimpleXML对象。然后取得所有$sxe中的item元素,最后$item->title返回title元素的内容,就是这样。你也可以使用关联数组查询属性,使用: $item->title['id']。

看到了吧,这后面真是太神奇了,有许多不同的办法可以得到我们想要的结果,例如, $item->title[0]返回和例子中相同的结果,另一方面,foreach($sxe->item->title as $item)只返回第一个title,并不是所有在文档中的title元素。(就像我在XPath中预期的那样)。

SimpleXML 实际上是使用了Zend引擎2新特性的第一个扩展。因此也成了这些新特性的测试点,你要知道在开发阶段bugs和不可预料的错误可不是少数。

除了上面例子中所使用的遍历所有节点的方法,在SimpleXML中也有一个XPath接口,它为访问单个结点提供了更简单的办法。

foreach($sxe->xpath('/articles/item/title') as $item) {

print $item . "\n";

}

不可否认,这段代码也不比前面例子中的短,但是提供了更复杂或更深的嵌套XML文档,你会发现和SimpleXML一起使用XPath会节省你很多的输入。

向 SimpleXML 文档写入数据 你不仅可以解析和读取SimpleXML,而且还可以改变SimpleXML文档。至少我们加入一些扩展:

$sxe->item->title = "XML in PHP5 "; //title元素的新内容。

$sxe->item->title['id'] = 34; // title元素的新属性。

$xmlString = $sxe->asXML(); // 将SimpleXML对象做为序列化的XML字符串返回

print $xmlString;

互用协作性 由于SimpleXML也是基于libxml2库的,你可以在几乎不影响速度的情况下轻松的将SimpleXML对象转化成DomDocument对象。(文档不用进行内部复制),由于这个机制,你拥有了二个对象的最好部分,使用一个适合你手头工作的工具吧,它是这样使用的:

$sxe = simplexml_import_dom($dom); $dom = dom_import_simplexml($sxe); XSLT XSLT是用来将XML文档转换为其它XML文档的语言,XSLT本身是用XML编写的,属于功能性语言家族,在程序处理上和面对对象语言(像PHP)有所不同。PHP4中有二种XSLT处理器:Sablotron(在广泛使用的XSLT扩展中)和Libxslt(在domxml扩展中),这两种API不互相兼容,并且使用方法也不相同。PHP5只支持libxslt处理器,之所以选择它是因为它是基于Libxml2的,因此也更符合PHP5的XML概念。

理论上将Sablotron绑定到PHP5上也是可能的,但是不幸的是没人来做。因此,如果你正在使用Sablotron,你不得不在PHP5中切换到libxslt处理器。Libxslt 是带有JavaScript异常处理支持的Sablotron,甚至可以使用PHP强大的数据流来重新实现Sablotron独有的计划处理(scheme handlers)。此外,libxslt 是 最快的XSLT处理器之一,所以你还免费得到了速度的提升。(执行速度是Sablotron的二倍)。

和本文讨论的其它扩展一样,你可以在XSL扩展,DOM扩展和vice versa之间交换XML文档,实际上,你必须得这么做,因为EXT/XSL扩展并没有载入和保存XML文档的接口,只能使用DOM扩展。一开始学习XSLT转换,你不需要掌握太多的内容,这里也不存在W3C标准,因为这个API中从Mozilla“借”过来的。

首先你需要一个XSLT样式表,将下列文本粘贴到一个新文件并且保存灰articls.xsl

然后用PHP脚本调用它::

/* 将XML和XSL文档载入到DOMDocument对象*/

$xsl = new DomDocument();

$xsl->load("articles.xsl");

$inputdom = new DomDocument();

$inputdom->load("articles.xml");

/* 创建XSLT处理器,并导入样式表*/

$proc = new XsltProcessor();

$xsl = $proc->importStylesheet($xsl);

$proc->setParameter(null, "titles", "Titles");

/* 转换并输出XML文档 */

$newdom = $proc->transformToDoc($inputdom);

print $newdom->saveXML();

?>

上面的例子首先使用DOM的方法load()载入XSLT样式表articles.xsl,然后创建了一个新的XsltProcessor对象,该对象导到了后面要使用了XSLT样式表对象,参数可以这样设置setParameter(namespaceURI, name, value),最后XsltProcessor对象使用transformToDoc($inputdom)开始转换并返回一个新的DOMDocument对象。

. 这个API的优点在于你可以使用同一个样式表转换许多XML文档,只需要将它载入一次然后重复使用它,因为transormToDoc()函数可以应用于不同的XML文档。

除了transormToDoc(),还有二个用于转换的方法:transformToXML($dom)返回一个字符串,transformToURI($dom, $uri)将转换之后的文档保存到文件或一个PHP数据流。注意如果你想使用XSLT的一个语法如 或 indent="yes",你不能使用transformToDoc(),因为DOMDocument对象 不能保存该信息,只能当你将转换后的结果直接保存到字符串或文件中时才能这样做。

调用PHP函数 XSLT扩展最后一个新加的特性是能够在XSLT 样式表内部调用任何PHP函数,主张正统的XML支持者一定不会喜欢这个功能(这样的样式表有点复杂,很容易混淆逻辑和设计),在某些地方却是十分有用的。当涉及到函数时XSLT就变得很有限,即使想实现用不同的语言输出一个日期也是非常麻烦的。但是使用这个功能,处理这些就和只使用PHP一样容易。下面是向XSLT添加一个函数的代码:

function dateLang () {

return strftime("%A");

}

$xsl = new DomDocument();

$xsl->load("datetime.xsl");

$inputdom = new DomDocument();

$inputdom->load("today.xml");

$proc = new XsltProcessor();

$proc->registerPhpFunctions();

// 载入文档并使用$xsl来处理

$xsl = $proc->importStylesheet($xsl);

/* 转换并输出XML文档 */

$newdom = $proc->transformToDoc($inputdom);

print $newdom->saveXML();

?>

下面是XSLT样式表datetime.xsl,它会调用这个函数。

下面是要使用样式表转换的XML文档,today.xml(同理,articles.xml也会得到同样结果)。

上面的样式表,PHP脚本和所有的XML文件会用当前系统设置的语言输出星期的名字。你可以给php:function()添加更多的参数,添加的参数会被传递给PHP函数。这里有一个函数php:functionString(),这个函数自动将所有输入的参数转换为字符串,所以你不需要在PHP里进行转换。

注意你需要在转换之前调用$xslt->registerPhpFunctions(),否则PHP函数调用将因为安全原因不会被执行(你始终相信你的XSLT样式表吗?)。目前访问系统还没有实现,也许在将来PHP5的版本中会实现这个功能。

摘要 PHP对XML的支持已经向前迈进了一大步,它符合标准,功能强大,互用协作性强,被作为默认选项安装,已被授权使用。新加入的SimpleXML扩展提供了简单快速访问XML文档的方法,可以节省你很多的代码,尤其是当你有结构化文档或者可以使用强大的XPath时。

感谢libxml2—PHP5 XML扩展所使用的底层库,使用DTD,RelaxNG或XML Schema验证XML文档现在已经被支持了。

XSL支持也得到了翻新,现在使用Libxslt库,比原来的Sablotron库在性能上有很大提高,而且,在XSLT样式表内部调用PHP函数可以让你写出更强大的XSLT代码。

如果你已经在PHP4或其它语言中使用了XML,你会喜欢PHP5的XML特性的,XML在PHP5中有了很大的变化,符合标准,和其它工具,语言是同等的。

链接 PHP 4 相关 Domxml 扩展: http://www.php.net/domxml/ Sablotron 扩展: http://www.php.net/xslt/ Libxslt: http://www.php.net/manual/en/function.domxml-xslt-stylesheet.php PHP 5 相关 SimpleXML: http://www.php.net/simplexml/ Streams: http://www.php.net/manual/en/ref.stream.php 标准 DOM: http://www.w3.org/DOM XSLT: http://www.w3.org/TR/xslt XPath: http://www.w3.org/TR/xpath XML Schema: http://www.w3.org/XML/Schema RelaxNG: http://relaxng.org/ Xinclude: http://www.w3.org/TR/xinclude/ 工具 Libxml2, the underlying library: http://xmlsoft.org/ Trang, a Schema/RelaxNG/etc converter: http://www.thaiopensource.com/relaxng/trang.html 关于作者 Christian Stocker是苏黎士Bitflux GmbH公司的创始人和CEO,他是XSL,DOM和imagick扩展的维护人员,德语书籍PHP de Luxe的合作作者,同时致力于其它开源项目,如Bitflux Editor和Popoon.。可以通用chregu@php.net.和他联系。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有