浅谈使用xml作为配置文件初始化自己的项目
2018-02-07 17:18
579 查看
当一个项目的代码不断增加,其中很多的内容诸如全局变量、提示语言等等都有必要放在一个独立的文件,方便变更。这个独立的文件有很多种,可以是init文件、conf文件、xml文件,为了通用性,我选择了xml文件作为自己的配置文件。对于《字符级的CNN文本分类器》一文中,我的xml文件是这样的:
<?xml version="1.0" encoding="UTF-8" ?>
<config>
<directories>
<directory name="train_text_home">TrainText</directory>
<directory name="test_text_home">TestText</directory>
<directory name="checkpoint_home">CheckPoints</directory>
<directory name="summary_home">Summaries</directory>
<directory name="log_home">Logs</directory>
</directories>
<files>
<file name="model_file">ENCharCNNTextClassification_%s</file>
<file name="summary_file">ENCharCNNTextClassification_%s</file>
<file name="log_file">Logs_of_%s.log</file>
</files>
<messages>
<module name="pre_train.py">
<message name="encode_success">One-Hot encoding done! Totally %d words have been skipped.</message>
<message name="start_train_file">Start to train from file: %s.</message>
<message name="done_train_file">Finish to train from file: %s.</message>
<message name="open_dir">Open directory: %s.</message>
</module>
<module name="char_cnn.py">
<message name="checkpoint_restore">Checkpoint: %s has been restored.</message>
<message name="checkpoint_restore_fail">No checkpoints being restored.</message>
<message name="display_steps">Total steps: %d, batch cost: %.4f, batch accuracy: %.2f%%, time to use: %d seconds.</message>
<message name="display_test">Accuracy: %.2f%%.</message>
</module>
<module name="main.py">
<message name="done_train">The train has been done!</message>
<message name="done_validation">The validation has been done!</message>
</module>
</messages>
<options>
<option name="train_name" type="str">ag_news</option>
<option name="max_to_keep" type="int">10</option>
<option name="epochs" type="int">55</option>
<option name="display_steps" type="int">2000</option>
<option name="save_steps" type="int">2000</option>
</options>
<hyperparameters>
<hyperparameter name="length0" type="int">1014</hyperparameter>
<hyperparameter name="n_class" type="int">4</hyperparameter>
<hyperparameter name="batch_s
11fb7
ize" type="int">128</hyperparameter>
<hyperparameter name="learning_rate" type="float">0.01</hyperparameter>
<hyperparameter name="decay_steps" type="int">1000</hyperparameter>
<hyperparameter name="decay_rate" type="float">0.8</hyperparameter>
<hyperparameter name="keep_prob" type="float">0.5</hyperparameter>
<hyperparameter name="grad_clip" type="int">5</hyperparameter>
</hyperparameters>
</config>xml是一种非常开放的语言,所有的标签、属性命名都没有相关的规定,什么时候使用子标签,什么时候使用属性全凭作者的习惯,这里我给出一种判断的标准:对于数据的存储,尽量使用子标签,例如上述中hyperparameter(超参)属于我要存的数据,所以我用标签来存。但name和type属于这个数据的一些属性,我使用属性去存储。但其实这样的分类法有时候并不会很清晰。例如电影,一部电影有标题、简介、演员等内容,这些属于这部电影的属性,但从另外一个角度来说,这些又是属于我们要存起来的数据。所以有另外一种标准,我们可以把需要直接使用的内容看成数据,例如这个超参直接赋给程序,电影的标题、简介和演员直接输出到前端页面,而name和type属于对这些数据的描述,我们不会直接使用它们,所以作为属性。在上述文件中,大家还可以看到我在部分内容里面使用了格式字符串“%s%d%f”等,关于这点的意义之一,我在另一篇文章中提到,就是为了可以存储整条文本,方便语言包的制作。具体怎样指定相关的变量,后面会介绍。
有了xml文件,就需要有代码去读,同样为了全局性考虑,我专门写了一个类来读取配置文件,做我程序的初始化工作:
import os
import logging
import xml.etree.ElementTree
class ProjectInitializer:
config = None
work_dir = os.getcwd()
@classmethod
def init_my_project(cls, config_file='config.xml'):
assert os.path.isfile(config_file)
assert (os.path.splitext(config_file)[-1]) == '.xml'
cls.config = xml.etree.ElementTree.parse(config_file).getroot()
cls._check_dirs()
cls._init_logging()
@classmethod
def get_dir_path(cls, home_name):
path = cls.config.find("./directories/directory[@name=\'%s\']" % home_name).text
if os.path.isabs(path):
return path
else:
return os.path.join(cls.work_dir, path)
@classmethod
def get_file_path(cls, home_name, file_name, *args):
real_file_name = cls.config.find("./files/file[@name=\'%s\']" % file_name).text
if len(args) > 0:
real_file_name = real_file_name % args
return os.path.join(cls.get_dir_path(home_name), real_file_name)
@classmethod
def message_about(cls, module_file_path, event_name, *args):
module_name = os.path.basename(module_file_path)
mess_module = cls.config.find("./messages/module[@name=\'%s\']" % module_name)
assert mess_module is not None
message = mess_module.find("./message[@name=\'%s\']" % event_name).text
if len(args) > 0:
message = message % args
return message
@classmethod
def option(cls, opt_name):
tag = cls.config.find("./options/option[@name=\'%s\']" % opt_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def hyper_para(cls, para_name):
tag = cls.config.find("./hyperparameters/hyperparameter[@name=\'%s\']" % para_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def _check_dirs(cls):
for directory in cls.config.findall("./directories/directory"):
path = cls.get_dir_path(directory.get('name'))
if not os.path.isdir(path):
os.mkdir(path)
@classmethod
def _init_logging(cls):
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s - %(message)s', '%Y %b %d %H:%M:%S')
handlers = [
logging.FileHandler(cls.get_file_path('log_home', 'log_file', cls.option('train_name'))),
logging.StreamHandler()
]
logging.getLogger().setLevel(logging.INFO)
for handler in handlers:
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)且看我逐行解析:
class ProjectInitializer:
config = None
work_dir = os.getcwd()类变量config是存储xml解析树的,在import类的时候,因为配置文件的路径不确定,所以先初始化为None,而work_dir则是要获取当前的工作目录作为全局变量,这样的好处是,无论后面我们是否改变了工作目录,我们配置文件指定的路径都可以跟这个路径组合成绝对路径,固定存储的位置。
xml获取相关标签的公式如下:
Predicates (expressions within square brackets) must be preceded by a tag name, an asterisk, or another predicate.
@classmethod
def init_my_project(cls, config_file='config.xml'):
assert os.path.isfile(config_file)
assert (os.path.splitext(config_file)[-1]) == '.xml'
cls.config = xml.etree.ElementTree.parse(config_file).getroot()
cls._check_dirs()
cls._init_logging()第一个静态方法是用来初始化项目的,会在main函数的第一句被调用。从代码我们可以清楚看到,主要工作就是读取指定的xml文件,检查需要的目录是否存在,以及初始化logging的相关信息。
@classmethod
def get_dir_path(cls, home_name):
path = cls.config.find("./directories/directory[@name=\'%s\']" % home_name).text
if os.path.isabs(path):
return path
else:
return os.path.join(cls.work_dir, path)
@classmethod
def get_file_path(cls, home_name, file_name, *args):
real_file_name = cls.config.find("./files/file[@name=\'%s\']" % file_name).text
if len(args) > 0:
real_file_name = real_file_name % args
return os.path.join(cls.get_dir_path(home_name), real_file_name)这两个方法是用来获取相关的目录和文件路径的,为了便于在win和Linux之间迁移,我配置文件一般不会存路径分割符,而像代码中的使用os.path中的join方法来连接,这样就保证了可迁移性。同时在第二个方法中我们可以看到,函数使用了Python的一个可变参数的特性*args,这个特性允许使用着动态输入若干个参数,例如参数文件中的log文件我们会允许输入一个字符串来区分不同的日志,这样调用者获取log文件路径的时候,只需要把这个字符串按顺序放在函数里面,代码便会替换相应的内容,生成真正的文件名,再组合目录的路径成为绝对路径返回。由于这个参数的个数是可变的,也满足了字符串中替代符个数不定的需要。
@classmethod
def message_about(cls, module_file_path, event_name, *args):
module_name = os.path.basename(module_file_path)
mess_module = cls.config.find("./messages/module[@name=\'%s\']" % module_name)
assert mess_module is not None
message = mess_module.find("./message[@name=\'%s\']" % event_name).text
if len(args) > 0:
message = message % args
return message这个方法是用来获取提示信息的,由于提示信息非常多,而且一般每个模块有固定的提示信息,很少是各个模块共用的,所以在提示信息保存的xml里面,我增加了一个父标签<module>,标签的内容是这个文件的文件名。然后在调用函数的时候我们有一个取巧的方法:
(MyInit.message_about(__file__, 'done_train')__file__这个变量存储了当前模块的文件名,这样在不同的模块里面,我们便可以统一使用这样的代码来进行调用,减少了错误的发生:
MyInit.message_about(__file__, 'start_train_file', file_path)上面是带参数的调用。
@classmethod
def option(cls, opt_name):
tag = cls.config.find("./options/option[@name=\'%s\']" % opt_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def hyper_para(cls, para_name):
tag = cls.config.find("./hyperparameters/hyperparameter[@name=\'%s\']" % para_name)
value = eval(tag.get('type'))(tag.text)
return value这两个都是获取全局变量的方法,其中option和hyperparameter并没有太严格的区分,我凭我个人感觉认为后者对神经网络的定义相关性更大。方法中值得一提的是,tag.text存的都是str变量,这样我们在使用的时候可能会面临变量类型的报错,所以我特意增加了一个type属性,然后使用Python的特殊方法eval执行,例如是int的变量,eval('int)('4')的效果就等同于int('4'),这样返回的全局变量变可以通过xml文件动态设置属性了。
@classmethod
def _check_dirs(cls):
for directory in cls.config.findall("./directories/directory"):
path = cls.get_dir_path(directory.get('name'))
if not os.path.isdir(path):
os.mkdir(path)该方法是检查相关的目录是否存在,没有就创建,代码比较简单。
@classmethod
def _init_logging(cls):
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s - %(message)s', '%Y %b %d %H:%M:%S')
handlers = [
logging.FileHandler(cls.get_file_path('log_home', 'log_file', cls.option('train_name'))),
logging.StreamHandler()
]
logging.getLogger().setLevel(logging.INFO)
for handler in handlers:
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)最后一个方法是logging的初始化,由于本文主要讲的是xml读取,这部分代码只是简单介绍一下。formatter是用来定义格式的,handlers里面存了需要输出的地方,本代码里面是logfile和屏幕,把它设置到getLogger()获取的root logger里面,就可以对所有的log执行了。
上述类就是用来初始化我的代码的,我做人工智能的编程基本上都是照搬这两个文件,然后修改一下xml配置文件的内容,非常方便。
<?xml version="1.0" encoding="UTF-8" ?>
<config>
<directories>
<directory name="train_text_home">TrainText</directory>
<directory name="test_text_home">TestText</directory>
<directory name="checkpoint_home">CheckPoints</directory>
<directory name="summary_home">Summaries</directory>
<directory name="log_home">Logs</directory>
</directories>
<files>
<file name="model_file">ENCharCNNTextClassification_%s</file>
<file name="summary_file">ENCharCNNTextClassification_%s</file>
<file name="log_file">Logs_of_%s.log</file>
</files>
<messages>
<module name="pre_train.py">
<message name="encode_success">One-Hot encoding done! Totally %d words have been skipped.</message>
<message name="start_train_file">Start to train from file: %s.</message>
<message name="done_train_file">Finish to train from file: %s.</message>
<message name="open_dir">Open directory: %s.</message>
</module>
<module name="char_cnn.py">
<message name="checkpoint_restore">Checkpoint: %s has been restored.</message>
<message name="checkpoint_restore_fail">No checkpoints being restored.</message>
<message name="display_steps">Total steps: %d, batch cost: %.4f, batch accuracy: %.2f%%, time to use: %d seconds.</message>
<message name="display_test">Accuracy: %.2f%%.</message>
</module>
<module name="main.py">
<message name="done_train">The train has been done!</message>
<message name="done_validation">The validation has been done!</message>
</module>
</messages>
<options>
<option name="train_name" type="str">ag_news</option>
<option name="max_to_keep" type="int">10</option>
<option name="epochs" type="int">55</option>
<option name="display_steps" type="int">2000</option>
<option name="save_steps" type="int">2000</option>
</options>
<hyperparameters>
<hyperparameter name="length0" type="int">1014</hyperparameter>
<hyperparameter name="n_class" type="int">4</hyperparameter>
<hyperparameter name="batch_s
11fb7
ize" type="int">128</hyperparameter>
<hyperparameter name="learning_rate" type="float">0.01</hyperparameter>
<hyperparameter name="decay_steps" type="int">1000</hyperparameter>
<hyperparameter name="decay_rate" type="float">0.8</hyperparameter>
<hyperparameter name="keep_prob" type="float">0.5</hyperparameter>
<hyperparameter name="grad_clip" type="int">5</hyperparameter>
</hyperparameters>
</config>xml是一种非常开放的语言,所有的标签、属性命名都没有相关的规定,什么时候使用子标签,什么时候使用属性全凭作者的习惯,这里我给出一种判断的标准:对于数据的存储,尽量使用子标签,例如上述中hyperparameter(超参)属于我要存的数据,所以我用标签来存。但name和type属于这个数据的一些属性,我使用属性去存储。但其实这样的分类法有时候并不会很清晰。例如电影,一部电影有标题、简介、演员等内容,这些属于这部电影的属性,但从另外一个角度来说,这些又是属于我们要存起来的数据。所以有另外一种标准,我们可以把需要直接使用的内容看成数据,例如这个超参直接赋给程序,电影的标题、简介和演员直接输出到前端页面,而name和type属于对这些数据的描述,我们不会直接使用它们,所以作为属性。在上述文件中,大家还可以看到我在部分内容里面使用了格式字符串“%s%d%f”等,关于这点的意义之一,我在另一篇文章中提到,就是为了可以存储整条文本,方便语言包的制作。具体怎样指定相关的变量,后面会介绍。
有了xml文件,就需要有代码去读,同样为了全局性考虑,我专门写了一个类来读取配置文件,做我程序的初始化工作:
import os
import logging
import xml.etree.ElementTree
class ProjectInitializer:
config = None
work_dir = os.getcwd()
@classmethod
def init_my_project(cls, config_file='config.xml'):
assert os.path.isfile(config_file)
assert (os.path.splitext(config_file)[-1]) == '.xml'
cls.config = xml.etree.ElementTree.parse(config_file).getroot()
cls._check_dirs()
cls._init_logging()
@classmethod
def get_dir_path(cls, home_name):
path = cls.config.find("./directories/directory[@name=\'%s\']" % home_name).text
if os.path.isabs(path):
return path
else:
return os.path.join(cls.work_dir, path)
@classmethod
def get_file_path(cls, home_name, file_name, *args):
real_file_name = cls.config.find("./files/file[@name=\'%s\']" % file_name).text
if len(args) > 0:
real_file_name = real_file_name % args
return os.path.join(cls.get_dir_path(home_name), real_file_name)
@classmethod
def message_about(cls, module_file_path, event_name, *args):
module_name = os.path.basename(module_file_path)
mess_module = cls.config.find("./messages/module[@name=\'%s\']" % module_name)
assert mess_module is not None
message = mess_module.find("./message[@name=\'%s\']" % event_name).text
if len(args) > 0:
message = message % args
return message
@classmethod
def option(cls, opt_name):
tag = cls.config.find("./options/option[@name=\'%s\']" % opt_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def hyper_para(cls, para_name):
tag = cls.config.find("./hyperparameters/hyperparameter[@name=\'%s\']" % para_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def _check_dirs(cls):
for directory in cls.config.findall("./directories/directory"):
path = cls.get_dir_path(directory.get('name'))
if not os.path.isdir(path):
os.mkdir(path)
@classmethod
def _init_logging(cls):
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s - %(message)s', '%Y %b %d %H:%M:%S')
handlers = [
logging.FileHandler(cls.get_file_path('log_home', 'log_file', cls.option('train_name'))),
logging.StreamHandler()
]
logging.getLogger().setLevel(logging.INFO)
for handler in handlers:
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)且看我逐行解析:
class ProjectInitializer:
config = None
work_dir = os.getcwd()类变量config是存储xml解析树的,在import类的时候,因为配置文件的路径不确定,所以先初始化为None,而work_dir则是要获取当前的工作目录作为全局变量,这样的好处是,无论后面我们是否改变了工作目录,我们配置文件指定的路径都可以跟这个路径组合成绝对路径,固定存储的位置。
xml获取相关标签的公式如下:
20.5.2.2. Supported XPath syntax
Syntax | Meaning |
---|---|
tag | Selects all child elements with the given tag. For example, spamselects all child elements named spam, and spam/eggselects all grandchildren named eggin all children named spam. |
* | Selects all child elements. For example, */eggselects all grandchildren named egg. |
. | Selects the current node. This is mostly useful at the beginning of the path, to indicate that it’s a relative path. |
// | Selects all subelements, on all levels beneath the current element. For example, .//eggselects all eggelements in the entire tree. |
.. | Selects the parent element. Returns Noneif the path attempts to reach the ancestors of the start element (the element findwas called on). |
[@attrib] | Selects all elements that have the given attribute. |
[@attrib='value'] | Selects all elements for which the given attribute has the given value. The value cannot contain quotes. |
[tag] | Selects all elements that have a child named tag. Only immediate children are supported. |
[tag='text'] | Selects all elements that have a child named tagwhose complete text content, including descendants, equals the given text. |
[position] | Selects all elements that are located at the given position. The position can be either an integer (1 is the first position), the expression last()(for the last position), or a position relative to the last position (e.g. last()-1). |
positionpredicates must be preceded by a tag name.详细内容请参考xml.etree.ElementTree文档
@classmethod
def init_my_project(cls, config_file='config.xml'):
assert os.path.isfile(config_file)
assert (os.path.splitext(config_file)[-1]) == '.xml'
cls.config = xml.etree.ElementTree.parse(config_file).getroot()
cls._check_dirs()
cls._init_logging()第一个静态方法是用来初始化项目的,会在main函数的第一句被调用。从代码我们可以清楚看到,主要工作就是读取指定的xml文件,检查需要的目录是否存在,以及初始化logging的相关信息。
@classmethod
def get_dir_path(cls, home_name):
path = cls.config.find("./directories/directory[@name=\'%s\']" % home_name).text
if os.path.isabs(path):
return path
else:
return os.path.join(cls.work_dir, path)
@classmethod
def get_file_path(cls, home_name, file_name, *args):
real_file_name = cls.config.find("./files/file[@name=\'%s\']" % file_name).text
if len(args) > 0:
real_file_name = real_file_name % args
return os.path.join(cls.get_dir_path(home_name), real_file_name)这两个方法是用来获取相关的目录和文件路径的,为了便于在win和Linux之间迁移,我配置文件一般不会存路径分割符,而像代码中的使用os.path中的join方法来连接,这样就保证了可迁移性。同时在第二个方法中我们可以看到,函数使用了Python的一个可变参数的特性*args,这个特性允许使用着动态输入若干个参数,例如参数文件中的log文件我们会允许输入一个字符串来区分不同的日志,这样调用者获取log文件路径的时候,只需要把这个字符串按顺序放在函数里面,代码便会替换相应的内容,生成真正的文件名,再组合目录的路径成为绝对路径返回。由于这个参数的个数是可变的,也满足了字符串中替代符个数不定的需要。
@classmethod
def message_about(cls, module_file_path, event_name, *args):
module_name = os.path.basename(module_file_path)
mess_module = cls.config.find("./messages/module[@name=\'%s\']" % module_name)
assert mess_module is not None
message = mess_module.find("./message[@name=\'%s\']" % event_name).text
if len(args) > 0:
message = message % args
return message这个方法是用来获取提示信息的,由于提示信息非常多,而且一般每个模块有固定的提示信息,很少是各个模块共用的,所以在提示信息保存的xml里面,我增加了一个父标签<module>,标签的内容是这个文件的文件名。然后在调用函数的时候我们有一个取巧的方法:
(MyInit.message_about(__file__, 'done_train')__file__这个变量存储了当前模块的文件名,这样在不同的模块里面,我们便可以统一使用这样的代码来进行调用,减少了错误的发生:
MyInit.message_about(__file__, 'start_train_file', file_path)上面是带参数的调用。
@classmethod
def option(cls, opt_name):
tag = cls.config.find("./options/option[@name=\'%s\']" % opt_name)
value = eval(tag.get('type'))(tag.text)
return value
@classmethod
def hyper_para(cls, para_name):
tag = cls.config.find("./hyperparameters/hyperparameter[@name=\'%s\']" % para_name)
value = eval(tag.get('type'))(tag.text)
return value这两个都是获取全局变量的方法,其中option和hyperparameter并没有太严格的区分,我凭我个人感觉认为后者对神经网络的定义相关性更大。方法中值得一提的是,tag.text存的都是str变量,这样我们在使用的时候可能会面临变量类型的报错,所以我特意增加了一个type属性,然后使用Python的特殊方法eval执行,例如是int的变量,eval('int)('4')的效果就等同于int('4'),这样返回的全局变量变可以通过xml文件动态设置属性了。
@classmethod
def _check_dirs(cls):
for directory in cls.config.findall("./directories/directory"):
path = cls.get_dir_path(directory.get('name'))
if not os.path.isdir(path):
os.mkdir(path)该方法是检查相关的目录是否存在,没有就创建,代码比较简单。
@classmethod
def _init_logging(cls):
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s - %(message)s', '%Y %b %d %H:%M:%S')
handlers = [
logging.FileHandler(cls.get_file_path('log_home', 'log_file', cls.option('train_name'))),
logging.StreamHandler()
]
logging.getLogger().setLevel(logging.INFO)
for handler in handlers:
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)最后一个方法是logging的初始化,由于本文主要讲的是xml读取,这部分代码只是简单介绍一下。formatter是用来定义格式的,handlers里面存了需要输出的地方,本代码里面是logfile和屏幕,把它设置到getLogger()获取的root logger里面,就可以对所有的log执行了。
上述类就是用来初始化我的代码的,我做人工智能的编程基本上都是照搬这两个文件,然后修改一下xml配置文件的内容,非常方便。
相关文章推荐
- 使用XML作为项目的配置文件使用,并解析之,获得数据作为链接数据库的参数
- 使用XML作为项目的配置文件使用,并解析之,获得数据作为链接数据库的参数
- 使用XML作为项目的配置文件使用,并解析之,获得数据作为链接数据库的参数
- Spring 3.0 学习-DI 依赖注入_创建Spring 配置-使用一个或多个XML 文件作为配置文件,使用自动注入(byName),在代码中使用注解代替自动注入,使用自动扫描代替xml中bea
- 使用Lua作为C语言项目的配置文件实例
- 服务器使用Tomcat配置server.xml文件通过域名直接跳转到项目
- duilib中加入自己定义控件之后怎么可以在xml文件里配置使用
- 使用eclipse在tomcat下部署项目不覆盖配置文件server.xml
- 服务器使用Tomcat配置server.xml文件通过域名直接跳转到项目
- Ant的项目配置文件build.xml(使用jboss-4.2.3GA-jdk6.zip)
- 项目中使用Spring时配置web.xml的listener侦听接口不能初始化的问题
- 使用xml作为数据库的配置文件的路径读取问题
- 大型Java项目中使用maven进行管理,pom.xml文件中build的配置
- 让Maven项目使用Nexus作为远程仓库的settings.xml配置
- 使用XML作为配置文件的方式完成模拟TOMCAT(XML,socket访问浏览器,DTD)
- 一个基于servlet 3.0的不使用web.xml配置文件的建议web项目demo
- Spring MVC 使用 applicationContext.xml 读取项目外 配置文件 配置连接池
- BCG 中使用XML作为配置文件
- 在java程序项目中如何使用xml配置文件存储信息简述