提交按钮用来将输入的信息提交到服务器。代码格式如下:
<input type="submit" name="..." value="...">
其中type="submit"定义提交按钮;name属性定义提交按钮的名称;value属性定义按钮的显示文字。通过提交按钮,可以将表单的信息提交给表单里action所指向的文件。
(1)编写代码如下图所示,在<body>标签中加入以下代码。
(2)在浏览器中打开文件,预览效果图如下所示,输入内容按【提交】按钮,即可将表单中的数据发送到指定的文件。
复位按钮用来重置表单中输入的信息。代码格式如下:
<input type="reset" name="..." value="..." >
其中type="reset"定义复位按钮;name属性定义复位按钮的名称;value属性定义按钮的显示文字。
(1)编写代码如下图所示,在<body>标签中加入以下代码。
(2)在浏览器中打开文件,预览效果图如下所示,输入内容后单机【重置】按钮,即可将表单中的数据清空。
者 | 无名之辈FTER
责编 | 夕颜
出品 | 程序人生(ID:coder_life)
本文翻译自Rasa官方文档,并融合了自己的理解和项目实战,同时对文档中涉及到的技术点进行了一定程度的扩展,目的是为了更好的理解Rasa工作机制。与本文配套的项目GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,欢迎star和issues,我们共同讨论、学习!
对话管理
1.1 多轮对话
多轮对话是相对于单轮对话而言的,单轮对话侧重于一问一答,即直接根据用户的问题给出精准的答案。问答更接近一个信息检索的过程,虽然也可能涉及简单的上下文处理,但通常是通过指代消解和 query 补全来完成的,而多轮对话侧重于需要维护一个用户目标状态的表示和一个决策过程来完成任务,具体来说就是用户带着明确的目的而来,希望得到满足特定限制条件的信息或服务,例如:订餐,订票,寻找音乐、电影或某种商品等。因为用户的需求可以比较复杂,可能需要分多轮进行陈述,用户也可能在对话过程中不断修改或完善自己的需求。此外,当用户的陈述的需求不够具体或明确的时候,机器也可以通过询问、澄清或确认来帮助用户找到满意的结果。
因此,任务驱动的多轮对话不是一个简单的自然语言理解加信息检索的过程,而是一个决策过程,需要机器在对话过程中不断根据当前的状态决策下一步应该采取的最优动作(如:提供结果,询问特定限制条件,澄清或确认需求,等等)从而最有效的辅助用户完成信息或服务获取的任务。
注:任务驱动的多轮对话系统通常为封闭域(domain)(闲聊系统为开放域),而特定限制条件对应于槽(Slot),也就是说,用户在满足特定限制条件时就是一次槽值填充的过程,如果用户能够在一次会话中,满足全部的限制条件,那么就不必进行多轮对话,即可直接使用户得到满意的信息或服务。
1.2 对话管理
对话管理,即Dialog Management(DM),它控制着人机对话的过程,是人机对话系统的重要组成部分。DM会根据NLU模块输出的语义表示执行对话状态的更新和追踪,并根据一定策略选择相应的候选动作。简单来说,就是DM会根据对话历史信息,决定此刻对用户的反应,比如在任务驱动的多轮对话系统中,用户带着明确的目的如订餐、订票等,用户需求比较复杂,有很多限制条件,可能需要分多轮进行陈述,一方面,用户在对话过程中可以不断修改或完善自己的需求,另一方面,当用户的陈述的需求不够具体或明确的时候,机器也可以通过询问、澄清或确认来帮助用户找到满意的结果。如下图所示,DM 的输入就是用户输入的语义表达(或者说是用户行为,是 NLU 的输出)和当前对话状态,输出就是下一步的系统行为和更新的对话状态。这是一个循环往复不断流转直至完成任务的过程。
从本质上来说,**任务驱动的对话管理实际就是一个决策过程,系统在对话过程中不断根据当前状态决定下一步应该采取的最优动作(如:提供结果,询问特定限制条件,澄清或确认需求等),从而最有效的辅助用户完成信息或服务获取的任务。**对话管理的任务大致有:
对话状态维护(dialog state tracking, DST)
对话状态是指记录了哪些槽位已经被填充、下一步该做什么、填充什么槽位,还是进行何种操作。用数学形式表达为,t+1 时刻的对话状态S(t+1),依赖于之前时刻 t 的状态St,和之前时刻 t 的系统行为At,以及当前时刻 t+1 对应的用户行为O(t+1)。可以写成S(t+1)←St+At+O(t+1)。
生成系统决策(dialog policy)
根据 DST 中的对话状态(DS),产生系统行为(dialog act),决定下一步做什么 dialog act 可以表示观测到的用户输入(用户输入 -> DA,就是 NLU 的过程),以及系统的反馈行为(DA -> 系统反馈,就是 NLG 的过程)。
作为接口与后端/任务模型进行交互
Rasa Core
Rasa Core是Rasa框架提供的对话管理模块,它类似于聊天机器人的大脑,主要的任务是维护更新对话状态和动作选择,然后对用户的输入作出响应。所谓对话状态是一种机器能够处理的对聊天数据的表征,对话状态中包含所有可能会影响下一步决策的信息,如自然语言理解模块的输出、用户的特征等;所谓动作选择,是指基于当前的对话状态,选择接下来合适的动作,例如向用户追问需补充的信息、执行用户要求的动作等。举一个具体的例子,用户说“帮我妈妈预定一束花”,此时对话状态包括自然语言理解模块的输出、用户的位置、历史行为等特征。在这个状态下,系统接下来的动作可能是:
向用户询问可接受的价格,如“请问预期价位是多少?”;
向用户确认可接受的价格,如“像上次一样买价值200的花可以吗?”
直接为用户预订
2.1 Stories
Rasa的故事是一种训练数据的形式,用来训练Rasa的对话管理模型。故事是用户和人工智能助手之间的对话的表示,转换为特定的格式,其中用户输入表示为相应的意图(和必要的实体),而助手的响应表示为相应的操作名称。Rasa核心对话系统的一个训练示例称为一个故事。这是一个故事数据格式的指南。两段对话样本示例:
<!-- ##表示story的描述,没有实际作用 -->
## greet + location/price + cuisine + num people
* greet
- utter_greet
* inform{"location": "rome", "price": "cheap"}
- action_on_it
- action_ask_cuisine
* inform{"cuisine": "spanish"}
- action_ask_numpeople
* inform{"people": "six"}
- action_ack_dosearch
<!-- Form Action-->
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
Story格式大致包含三个部分:
1. 用户输入(User Messages)
使用*开头的语句表示用户的输入消息,我们无需使用包含某个具体内容的输入,而是使用NLU管道输出的intent和entities来表示可能的输入。需要注意的是,如果用户的输入可能包含entities,建议将其包括在内,将有助于policies预测下一步action。这部分大致包含三种形式,示例如下:
(1)* greet 表示用户输入没有entity情况;
(2)* inform{"people": "six"} 表示用户输入包含entity情况,响应这类intent为普通action;
(3)* request_weather 表示用户输入Message对应的intent为form action情况;
2. 动作(Actions)
使用-开头的语句表示要执行动作(Action),可分为utterance actions和custom actions,其中,前者在domain.yaml中定义以utter_为前缀,比如名为greet的意图,它的回复应为utter_greet;后者为自定义动作,具体逻辑由我们自己实现,虽然在定义action名称的时候没有限制,但是还是建议以action_为前缀,比如名为inform的意图fetch_profile的意图,它的response可为action_fetch_profile。
3. 事件(Events)
Events也是使用-开头,主要包含槽值设置(SlotSet)和激活/注销表单(Form),它是是Story的一部分,并且必须显示的写出来。Slot Events和Form Events的作用如下:
(1)Slot Events
Slot Events的作用当我们在自定义Action中设置了某个槽值,那么我们就需要在Story中Action执行之后显著的将这个SlotSet事件标注出来,格式为- slot{"slot_name": "value"}。比如,我们在action_fetch_profile中设置了Slot名为account_type的值,代码如下:
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
url = "http://myprofileurl.com"
data = requests.get(url).json
return [SlotSet("account_type", data["account_type"])]
那么,就需要在Story中执行action_fetch_profile之后,添加- slot{"account_type" : "premium"}。虽然,这么做看起来有点多余,但是Rasa规定这么做必须的,目的是提高训练时准确度。
## fetch_profile
* fetch_profile
- action_fetch_profile
- slot{"account_type" : "premium"}
- utter_welcome_premium
当然,如果您的自定义Action中将槽值重置为None,则对应的事件为-slot{"slot_name": }。
(2)Form Events
在Story中主要存在三种形式的表单事件(Form Events),它们可表述为:
Form Action事件
Form Action即表单动作事件,是自定义Action的一种,用于一个表单操作。示例如下:
- restaurant_form
Form activation事件
form activation即激活表单事件,当form action事件执行后,会立马执行该事件。示例如下:
- form{"name": "restaurant_form"}
Form deactivation事件
form deactivation即注销表单事件,作用与form activation相反。示例如下:
- form{"name": }
总之,我们在构建Story时,可以说是多种多样的,因为设计的故事情节是多种多样的,这就意味着上述三种内容的组合也是非常灵活的。另外,在设计Story时Rasa还提供了Checkpoints 和OR statements两种功能,来提升构建Story的灵活度,但是需要注意的是,东西虽好,但是不要太贪了,过多的使用不仅增加了复杂度,同时也会拖慢训练的速度。其中,Checkpoints用于模块化和简化训练数据,示例如下:
## first story
* greet
- action_ask_user_question
> check_asked_question
## user affirms question
> check_asked_question
* affirm
- action_handle_affirmation
> check_handled_affirmation
## user denies question
> check_asked_question
* deny
- action_handle_denial
> check_handled_denial
## user leaves
> check_handled_denial
> check_handled_affirmation
* goodbye
- utter_goodbye
在上面的例子中,可以使用> check_asked_question表示first story,这样在其他story中,如果有相同的first story部分,可以直接用> check_asked_question代替。而OR Statements主要用于实现某一个action可同时响应多个意图的情况,比如下面的例子:
## story
* affirm OR thankyou
- action_handle_affirmation
2.2 Domain
Domain,译为**“领域”**,它描述了对话机器人应知道的所有信息,类似于“人的大脑”,存储了意图intents、实体entities、插槽slots以及动作actions等信息,其中,intents、entities在NLU训练样本中定义,slots对应于entities类型,只是表现形式不同。domain.yml文件组成结构如下:
具体介绍如下:
1. intents
intents:
- affirm
- deny
- greet
- request_weather
- request_number
- inform
- inform_business
- stop
- chitchat
intents,即意图,是指我们输入一段文本,希望Bot能够明白这段文本是什么意思。在Rasa框架中,意图的定义是在NLU样本中实现的,并且在每个意图下面我们需要枚举尽可多的样本用于训练,以达到Bot能够准确识别出我们输入的一句话到底想要干什么。
2. session_config
session_config:
carry_over_slots_to_new_session: true
session_expiration_time: 60
session_config,即会话配置,这部分的作用为配置一次会话(conversation session)是否有超时限制。上例演示的是,每次会话的超时时间为60s,如果用户开始一段会话后,在60s内没有输入任何信息,那么这次会话将被结束,然后Bot又会开启一次新的会话,并将上一次会话的Slot值拷贝过来。当然,我们希望舍弃上一次会话Slot的值,可以将carry_over_slots_to_new_session设置为false。另外,当session_expiration_time被设置为0时,Bot永远不会结束当前会话并一直等待用户输入(注:执行action_session_start仍然可以开始一次新的会话,在设置为0的情况下)。
3. slots
slots:
date_time:
type: unfeaturized
auto_fill: false
address:
type: unfeaturized
auto_fill: false
Slots,即插槽,它就像对话机器人的内存,它通过键值对的形式可用来收集存储用户输入的信息(实体)或者查询数据库的数据等。关于Slots的设计与使用,详情请见本文2.6小节。
4. entities
entities:
- date_time
- address
entities,即实体,类似于输入文本中的关键字,需要在NLU样本中进行标注,然后Bot进行实体识别,并将其填充到Slot槽中,便于后续进行相关的业务操作。
5. actions
actions:
- utter_answer_affirm # utter_开头的均为utter actions
- utter_answer_deny
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_answer_whoareyou
- utter_answer_whattodo
- utter_ask_date_time
- utter_ask_address
- utter_ask_number
- utter_ask_business
- utter_ask_type
- action_default_fallback # default actions
当Rasa NLU识别到用户输入Message的意图后,Rasa Core对话管理模块就会对其作出回应,而完成这个回应的模块就是action。Rasa Core支持三种action,即default actions、utter actions以及 custom actions。关于如何实现Actions和处理业务逻辑,我们在一篇文章中详谈,这里仅作简单了解。
6. forms
forms:
- weather_form
forms,即表单,该部分列举了在NLU样本中定义了哪些Form Actions。关于Form Actions的相关知识,请移步至本文的2.7小节。
7. responses
responses:
utter_answer_greet:
- text: "您好!请问我可以帮到您吗?"
- text: "您好!很高兴为您服务。请说出您要查询的功能?"
utter_ask_date_time:
- text: "请问您要查询哪一天的天气?"
utter_ask_address:
- text: "请问您要查下哪里的天气?"
utter_default:
- text: "没听懂,请换种说法吧~"
responses部分就是描述UtterActions具体的回复内容,并且每个UtterAction下可以定义多条信息,当用户发起一个意图,比如 “你好!”,就触发utter_answer_greet操作,Rasa Core会从该action的模板中自动选择其中的一条信息作为结果反馈给用户。
2.3 Responses
Responses的作用就是自动响应用户输入的信息,因此我们需要管理这些响应(Responses)。Rasa框架提供了三种方式来管理Responses,它们是:
在domain.yaml文件中存储Responses;
在训练数据中存储Responses;
自定义一个NLG服务来生成Responses。
由于第一种我们在本文2.2(7)小节有过介绍,而创建NLG服务是这样的:
nlg:
url: http://localhost:5055/nlg # url of the nlg endpoint
# you can also specify additional parameters, if you need them:
# headers:
# my-custom-header: value
# token: "my_authentication_token" # will be passed as a get parameter
# basic_auth:
# username: user
# password: pass
# example of redis external tracker store config
tracker_store:
type: redis
url: localhost
port: 6379
db: 0
password: password
record_exp: 30000
# example of mongoDB external tracker store config
#tracker_store:
#type: mongod
#url: mongodb://localhost:27017
#db: rasa
#user: username
#password: password
2.4 Actions
当Rasa NLU识别到用户输入Message的意图后,Rasa Core对话管理模块就会对其作出回应,而完成这个回应的模块就是action。Rasa Core支持三种action,即default actions、utter actions以及 custom actions。关于如何实现Actions和处理业务逻辑,我们在一篇文章中详谈,这里仅作简单了解。
1. default actions
DefaultAction是Rasa Core默认的一组actions,我们无需定义它们,直接可以story和domain中使用。包括以下三种action:
action_listen:监听action,Rasa Core在会话过程中通常会自动调用该action;
action_restart:重置状态,比初始化Slots(插槽)的值等;
action_default_fallback:当Rasa Core得到的置信度低于设置的阈值时,默认执行该action;
2. utter actions
UtterAction是以utter_为开头,仅仅用于向用户发送一条消息作为反馈的一类actions。定义一个UtterAction很简单,只需要在domain.yml文件中的actions:字段定义以utter_为开头的action即可,而具体回复内容将被定义在templates:部分,这个我们下面有专门讲解。定义utter actions示例如下:
actions:
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_introduce_self
- utter_introduce_selfcando
- utter_introduce_selffrom
3. custom actions
CustomAction,即自定义action,允许开发者执行任何操作并反馈给用户,比如简单的返回一串字符串,或者控制家电、检查银行账户余额等等。它与DefaultAction不同,自定义action需要我们在domain.yml文件中的actions部分先进行定义,然后在指定的webserver中实现它,其中,这个webserver的url地址在endpoint.yml文件中指定,并且这个webserver可以通过任何语言实现,当然这里首先推荐python来做,毕竟Rasa Core为我们封装好了一个rasa-core-sdk专门用来处理自定义action。关于action web的搭建和action的具体实现,我们在后面详细讲解,这里我们看下在在Rasa Core项目中需要做什么。假如我们在天气资讯的人机对话系统需提供查询天气和空气质量两个业务,那么我们就需要在domain.yml文件中定义查询天气和空气质量的action,即:
actions:
...
- action_search_weather
另外,FormAction也是自定义actions,但是需要在domainl.yaml文件的forms字段声明。
forms:
- weather_form
2.5 Policies
Policies是Rasa Core中的策略模块,对应类rasa_core.policies.Policy,它的作用就是使用合适的策略(Policy)来预测一次对话后要执行的行为(Actions)。预测的原理是衡量命中的哪些Policies哪个置信度高,由置信度高的Policy选择合适的Action执行。假如出现不同的Policy拥有相同的置信度,那么就由它们的优先级决定,即选择优先级高的Policy。Rasa对提供的Policies进行了优先级排序,具体如下表:
它们的描述与作用如下:
Memoization Policy
MemoizationPolicy只记住(memorizes)训练数据中的对话。如果训练数据中存在这样的对话,那么它将以置信度为1.0预测下一个动作,否则将预测为None,此时置信度为0.0。下面演示了如何在策略配置文件config.yml文件中,配置MemoizationPlicy策略,其中,max_history(超参数)决定了模型查看多少个对话历史以决定下一个执行的action。
policies:
- name: "MemoizationPolicy"
max_history: 5
注:max_history值越大训练得到的模型就越大并且训练时间会变长,关于该值到底该设置多少,我们可以举这么个例子,比如有这么一个Intent:out_of_scope来描述用户输入的消息off-topic(离题),当用户连续三次触发out_of_scope意图,这时候我们就需要主动告知用户需要向其提供帮助,如果要Rasa Core能够学习这种模型,max_history应该至少为3。story.md中表现如下:
* out_of_scope
- utter_default
* out_of_scope
- utter_default
* out_of_scope
- utter_help_message
Keras Policy
KerasPolicy策略是Keras框架中实现的神经网络来预测选择执行下一个action,它默认的框架使用LSTM(Long Short-Term Memory,长短期记忆网络)算法,但是我们也可以重写KerasPolicy.model_architecture函数来实现自己的框架(architecture)。KerasPolicy的模型很简单,只是单一的LSTM+Dense+softmax,这就需要我们不断地完善自己的story来把各种情况下的story进行补充。下面演示了如何在策略配置文件config.yml文件中,配置KerasPolicy策略,其中,epochs表示训练的次数,max_history同上。
policies:
- name: KerasPolicy
epochs: 100
max_history: 5
Embedding Policy
基于机器学习的对话管理能够学习复杂的行为以完成任务,但是将其功能扩展到新领域并不简单,尤其是不同策略处理不合作用户行为的能力,以及在学习新任务(如预订酒店)时,如何将完成一项任务(如餐厅预订)重新应用于该任务时的情况。EmbeddingPolicy,即循环嵌入式对话策略(Recurrent Embedding Dialogue Policy,REDP),它通过将actions和对话状态嵌入到相同的向量空间(vector space)能够获得较好的效果,REDP包含一个基于改进的Neural Turing Machine的记忆组件和注意机制,在该任务上显著优于基线LSTM分类器。
EmbeddingPolicy效果上优于KerasPolicy,但是它有个问题是耗时,因为它没有使用GPU、没有充分利用CPU资源。KerasPolicy和EmbeddingPolicy比较示意图如下:
配置EmbeddingPolicy参数:
policies:
- name: EmbeddingPolicy
epochs: 100
featurizer:
- name: FullDialogueTrackerFeaturizer
state_featurizer:
- name: LabelTokenizerSingleStateFeaturizer
注:新版的Rasa将EmbeddingPolicy重命名为TEDPolicy,但是我在config.yml配置文件中将其替换后,提示无法找到TEDPolicy异常,具体原因不明,暂还未涉及源码分析。
Form Policy
FormPolicy是MemoizationPolicy的扩展,用于处理(form)表单的填充事项。当一个FormAction被调用时,FormPolicy将持续预测表单动作,直到表单中的所有槽都被填满,然后再执行对应的FormAction。如果在Bot系统中使用了FormActions,就需要在config.yml配置文件中进行配置。
policies:
- name: FormPolicy
Mapping Policy
MappingPolicy可用于直接将意图映射到要执行的action,从而实现被映射的action总会被执行,其中,这种映射是通过triggers属性实现的。举个栗子(domain.yml文件中):
intents:
- greet: {triggers: utter_goodbye}
其中,greet是意图;utter_goodbye是action。一个意图最多只能映射到一个action,我们的机器人一旦收到映射意图的消息,它将执行对应的action。然后,继续监听下一条message。需要注意的是,对于上述映射,我们还需要要在story.md文件中添加如下样本,否则,任何机器学习策略都可能被预测的action_greet在dialouge历史中突然出现而混淆。
Fallback Policy
如果意图识别的置信度低于nlu_threshold,或者没有任何对话策略预测的action置信度高于core_threshold,FallbackPolicy将执行fallback action。通俗来说,就是我们的对话机器人意图识别和action预测的置信度没有满足对应的阈值,该策略将使机器人执行指定的默认action。configs.yml配置如下:
policies:
- name: "FallbackPolicy"
# 意图理解置信度阈值
nlu_threshold: 0.3
# action预测置信度阈值
core_threshold: 0.3
# fallback action
fallback_action_name: 'action_default_fallback'
其中,action_default_fallback是Rasa Core中的一个默认操作,它将向用户发送utter_default模板消息,因此我们需要确保在domain.yml文件中指定此模板。当然,我们也可以在fallback_action_name字段自定义默认回复的action,比如my_fallback_cation,就可以这么改:
policies:
- name: "FallbackPolicy"
nlu_threshold: 0.4
core_threshold: 0.3
fallback_action_name: "my_fallback_action"
2.6 Slots
Slots,槽值,相当于机器人的内存(memory),它们以键值对的形式存在,用于存储用户输入时消息时比较重要的信息,而这些信息将为Action的执行提供关键数据。Slots的定义位于domain.yaml文件中,它们通常与Entities相对应,即Entities有哪些,Slots就有哪些,并且Slots存储的值就是NLU模型提取的Entities的值。
2.6.1 Slots Type
1. Text类型
示例:
# domain.yaml
slots:
cuisine:
type: text
2. Boolean类型
示例:
slots:
is_authenticated:
type: bool
3. categorical类型
示例:
slots:
risk_level:
type: categorical
values:
- low
- medium
- high
4. Float类型
示例:
slots:
temperature:
type: float
min_value: -100.0
max_value: 100.0
5. List类型
示例:
slots:
shopping_items:
type: list
6. Unfeaturized 类型
示例:
slots:
internal_user_id:
type: unfeaturized
2.6.2 Slots Set
Slots值填充有多种方式,它们的操作方式如下:
1. Slots Initial
# domain.yaml
slots:
name:
type: text
initial_value: "human"
在domain.yaml文件中声明slots时,可以通过initial_value字段为当前slot提供一个初始值,也就是说,当会话开始时,被设定初始值的slot已经被填充好。当然,这个操作不是必须的。
2. Slots Set from NLU
# stories.md
# story_01
* greet{"name": "Ali"}
- slot{"name": "Ali"}
- utter_greet
假如在stories.md文件添加一个包含-slot{}的story,这就意味着当NLU模型提取到一个名为name的实体且这个实体有在domain.yaml中定义,那么NLU模型提取到的实体值会被自动填充到name槽中。实际上,对于Rasa来说,就算你不添加-slot{}字段,这个实体值也会被提取并自动填充到name槽中。当然,如果你希望禁止这种自动填充行为,改为添加-slot{}字段填充,可以在domain.yaml定义slot时,设置auto_fill的值为False,即:
# domain.yaml
slots:
name:
type: text
auto_fill: False
3. Slots Set By Clicking Buttons
# domain.yaml
utter_ask_color:
- text: "what color would you like?"
buttons:
- title: "blue"
payload: '/choose{"color": "blue"}' # 格式 '/intent{"entity":"value",...}'
- title: "red"
payload: '/choose{"color": "red"}'
在点击Button时填充Slots的值,是指当我们的Bot(Rasa Core)在回复用户时,可以在回复的消息中附加Button信息,这种Button类似于快捷键,用户获取到之后,可以直接将其发送给Rasa Core,它会直接进行解析以识别intent和提取entity,并将entity的值填充到slot中。比如你让用户通过点击一个按钮来选择一种颜色,那么可以在domain.yaml中utter_ask_color的回复中添加buttons:/choose{"color": "blue"}和/choose{"color": "red"}。注:通常每个button由title和payload字段组成。
4. Slots Set by Actions
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
url = "http://myprofileurl.com"
data = requests.get(url).json
return [SlotSet("account_type", data["account_type"])]
该示例演示了如何在Custom Action中通过返回事件来填充Slots的值,即调用SlotSet事件函数并将该事件return。需要注意的是,为了达到这个目的,我们在编写Story时必须包含该Slot,即使用-slot{}实现,只有这样Rasa Core就会从提供的信息中进行学习,并决定执行正确的action。Story.md示例如下:
# story_01
* greet
- action_fetch_profile
- slot{"account_type" : "premium"}
- utter_welcome_premium
# story_02
* greet
- action_fetch_profile
- slot{"account_type" : "basic"}
- utter_welcome_basic
其中,account_type在domain.yaml中定义如下:
slots:
account_type:
type: categorical
values:
- premium
- basic
2.6.3 Slots Get
目前主要有两种获取Slots值方式:
1. Get Slot in responses
responses:
utter_greet:
- text: "Hey, {name}. How are you?"
在domain.yaml的responses部分,可以通过{slotname}的形式获取槽值。
2. Get Slot in Custom Action
from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests
class FetchProfileAction(Action):
def name(self):
return "fetch_profile"
def run(self, dispatcher, tracker, domain):
# 获取slot account_type的值
account_type = tracker.get_slot('account_type')
return
Tracker,可理解为跟踪器,作用是以会话会话的形式维护助手和用户之间的对话状态。通过Tracker,能够轻松获取整个对话信息,其中就包括Slot的值。
2.7 Form
在Rasa Core中,当我们执行一个action需要同时填充多个slot时,可以使用FormAction来实现,因为FormAction会遍历监管的所有slot,当发现相关的slot未被填充时,就会向用户主动发起询问,直到所有slot被填充完毕,才会执行接下来的业务逻辑。使用步骤如下:
(1)构造story
在story中,不仅需要考虑用户按照我们的设计准确的提供有效信息,而且还要考虑用户在中间过程改变要执行的意图情况或称输入无效信息,因为对于FormAction来说,如果无法获得预期的信息就会报错,这里我们分别称这两种情况为happy path、unhappy path。示例如下:
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"} 激活form
- form{"name": } 使form无效
## unhappy path
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
注:* request_restaurant为意图;- restaurant_form为form action;- form{"name": "restaurant_form"}为激活form;- form{"name": }为注销form;- action_deactivate_form为默认的action,它的作用是用户可能在表单操作过程中改变主意,决定不继续最初的请求,我们使用这个default action来禁止(取消)表单,同时重置要请求的所有slots。
构建stroy最好使用官方提供的Interactive Learning,防止漏掉信息,详细见本文2.8小节。
(2)添加form字段到Domain
在doamin文件下新增forms:部分,并将所有用到的form名称添加到该字段下:
intents:
- request_weather
forms:
- weather_form
(3)配置FormPolicy
在工程的配置文件configs.yml中,新增FormPolicy策略:
policies:
- name: EmbeddingPolicy
epochs: 100
max_history: 5
- name: FallbackPolicy
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: FormPolicy
(4)Form Action实现
class WeatherForm(FormAction):
def name(self) -> Text:
"""Unique identifier of the form"""
return "weather_form"
@staticmethod
def required_slots(tracker: Tracker) -> List[Text]:
"""A list of required slots that the form has to fill"""
return ["date_time", "address"]
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
"""Define what the form has to do
after all required slots are filled"""
address = tracker.get_slot('address')
date_time = tracker.get_slot('date_time')
return
当form action第一被调用时,form就会被激活并进入FormPolicy策略模式。每次执行form action,required_slots会被调用,当发现某个还未被填充时,会主动去调用形式为uter_ask_{slotname}的模板(注:定义在domain.yml的templates字段中);当所有slot被填充完毕,submit方法就会被调用,此时本次form操作完毕被取消激活。
2.8 Interactive Learning
虽然我们可以容易的人工构建story样本数据,但是往往会出现一些考虑不全,甚至出错等问题,基于此,Rasa Core框架为我们提供了一种交互式学习(Interactive Learning)来获得所需的样本数据。在互动学习模式中,当你与机器人交谈时,你会向它提供反馈,这是一种强大的方法来探索您的机器人可以做什么,也是修复它所犯错误的最简单的方法。基于机器学习的对话的一个优点是,当你的机器人还不知道如何做某事时,你可以直接教它。
(1)开启Action Server
python -m rasa run actions --port 5055 --actions actions --debug
(2)开启Interactive Learning
python -m rasa interactive -m models/20200313-101055.tar.gz --endpoints configs/endpoints.yml --config configs/config.yml
# 或者(没有已训练模型情况)
# rasa会先训练好模型,再开启交互式学习会话
python -m rasa interactive --data /data --domain configs/domain.yml --endpoints configs/endpoints.yml --config configs/config.yml
分别执行(1)、(2)命令后,我们可以预设一个交互场景根据终端的提示操作即可。如果一个交互场景所有流程执行完毕,按Ctrl+C结束并选择Start Fresh进入下一个场景即可。当然Rasa还提供了可视化界面,以帮助你了解每个Story样本构建的过程,网址:http://localhost:5005/visualization.html。
执行流程大致如下:
Bot loaded. Visualisation at http://localhost:5006/visualization.html .
Type a message and press enter (press 'Ctr-c' to exit).
? Your input -> 查询身份证439912199008071234
? Is the intent 'request_idcard' correct for '查询身份证[439912199008071234](id_number)' and are all entities labeled correctly? Yes
------
Chat History
# Bot You
───────────────────────────────────────────────────────────────────
1 action_listen
───────────────────────────────────────────────────────────────────
2 查询身份证[439912199008071234](id_number)
intent: request_idcard 1.00
Current slots:
address: None, business: None, date-time: None, id_number: None, requested_slot: None
------
? The bot wants to run 'number_form', correct? Yes
Chat History
# Bot You
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 action_listen
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2 查询身份证[439912199008071234](id_number)
intent: request_idcard 1.00
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 number_form 1.00
您要查询的身份证号码439912199008071234所属人为张三,湖南长沙人,现在就职于地球村物业。
form{"name": "number_form"}
slot{"id_number": "439912199008071234"}
form{"name": }
slot{"requested_slot": }
Current slots:
address: None, business: None, date-time: None, id_number: 439912199008071234, requested_slot: None
------
? The bot wants to run 'action_listen', correct? Yes
生成的一个Story示例如下:
## interactive_story_10
# unhappy path:chitchat stop but continue path
* greet
- utter_answer_greet
* request_number{"type": "身份证号码"}
- number_form
- form{"name": "number_form"}
- slot{"type": "身份证号码"}
- slot{"number": }
- slot{"business": }
- slot{"requested_slot": "number"}
* chitchat
- utter_chitchat
- number_form
- slot{"requested_slot": "number"}
* stop
- utter_ask_continue
* affirm
- number_form
- slot{"requested_slot": "number"}
* form: request_number{"number": "440123199087233467"}
- form: number_form
- slot{"number": "440123199087233467"}
- slot{"type": "身份证号码"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_noworries
改进ChitChatAssistant项目
3.1 config.yml
# zh_jieba_mitie_embeddings_config.yml
language: "zh"
pipeline:
- name: "MitieNLP"
model: "data/total_word_feature_extractor_zh.dat"
- name: "JiebaTokenizer"
dictionary_path: "data/dict"
- name: "MitieEntityExtractor"
- name: "EntitySynonymMapper"
- name: "RegexFeaturizer"
- name: "MitieFeaturizer"
- name: "EmbeddingIntentClassifier"
policies:
- name: FallbackPolicy
nlu_threshold: 0.5
ambiguity_threshold: 0.1
core_threshold: 0.5
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: FormPolicy
- name: MappingPolicy
- name: EmbeddingPolicy
epochs: 500
考虑到目前项目的样本较少,这里使用MITIE+EmbeddingPolicy组合,虽然训练时慢了点,但是能够保证实体提取的准确性,同时又能够提高意图识别的命中率。
3.2 weather_stories.md
## happy path
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
## happy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
- form{"name": }
* thanks
- utter_noworries
## unhappy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## very unhappy path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* chitchat
- utter_chitchat
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop but continue path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* affirm
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop and really stop path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
## chitchat stop but continue path
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* affirm
- weather_form
- form{"name": }
* thanks
- utter_noworries
## stop but continue and chitchat path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* stop
- utter_ask_continue
* affirm
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## chitchat stop but continue and chitchat path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* affirm
- weather_form
* chitchat
- utter_chitchat
- weather_form
- form{"name": }
* thanks
- utter_noworries
## chitchat, stop and really stop path
* greet
- utter_answer_greet
* request_weather
- weather_form
- form{"name": "weather_form"}
* chitchat
- utter_chitchat
- weather_form
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": }
## interactive_story_1
## 天气 + 时间 + 地点 + 地点
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
- form: weather_form
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
- form: weather_form
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "广州"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_1
## 天气 + 时间 + 地点 + 时间
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
- form: weather_form
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
- form: weather_form
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "上海"} OR request_weather{"address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "广州"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_2
## 天气/时间/地点 + 地点
* request_weather{"date_time": "明天", "address": "上海"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "广州"} OR request_weather{"address": "广州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_3
## 天气/时间/地点 + 时间
* request_weather{"address": "深圳", "date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"date_time": "大后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_2
## 天气/时间/地点 + 地点 + 时间
* request_weather{"date_time": "明天", "address": "上海"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "北京"} OR request_weather{"address": "北京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "上海"}
- slot{"address": "北京"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "北京"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_3
## 天气/时间/地点 + 地点 + 地点
* request_weather{"date_time": "后天", "address": "北京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "北京"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "深圳"} OR request_weather{"address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "北京"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "南京"} OR request_weather{"address": "南京"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"address": "南京"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_4
## 天气/时间/地点 + 时间 + 地点
* request_weather{"date_time": "明天", "address": "长沙"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "长沙"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "长沙"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "长沙"}
- slot{"date_time": "大后天"}
- form{"name": }
- slot{"requested_slot": }
* affirm
- utter_answer_affirm
## interactive_story_5
## 天气/时间/地点 + 时间 + 时间
* request_weather{"date_time": "后天", "address": "深圳"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "深圳"}
- slot{"date_time": "明天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"address": "广州"} OR request_weather{"address": "广州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "深圳"}
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_4
## 天气/时间 + 地点 + 时间
* request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
- form: weather_form
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "明天"}
- slot{"address": "广州"}
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_5
## 天气/地点 + 时间 + 时间
* request_weather{"address": "广州"}
- weather_form
- form{"name": "weather_form"}
- slot{"address": "广州"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "后天"}
- form: weather_form
- slot{"date_time": "后天"}
- form{"name": }
- slot{"requested_slot": }
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "后天"}
- slot{"address": "广州"}
- slot{"date_time": "明天"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
## interactive_story_1
## 天气/时间/地点 + chit + chit(restart)+询问天气
* request_weather{"date_time": "今天", "address": "广州"}
- weather_form
- form{"name": "weather_form"}
- slot{"date_time": "今天"}
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* chitchat
- utter_chitchat
* chitchat
- utter_chitchat
- action_restart
* request_weather
- weather_form
- form{"name": "weather_form"}
- slot{"requested_slot": "date_time"}
* form: inform{"date_time": "今天"}
- form: weather_form
- slot{"date_time": "今天"}
- slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
- form: weather_form
- slot{"address": "广州"}
- form{"name": }
- slot{"requested_slot": }
* thanks
- utter_answer_thanks
在构建Story样本时,主要是使用Interactive Learning工具实现,以确保枚举尽可能多的unhappy story,同时又能够防止在构建样本时出现信息遗漏的情况。此外,本版本中除了查询天气这个案例,还新增了其他案例,并列举了如何使用同义词、自定义字典以及正则表达式的使用方法,详细见最新版项目。
GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,欢迎star和issues,我们共同讨论、学习!
原文链接:
https://blog.csdn.net/AndrExpert/article/details/105434136
☞没有监控和日志咋整?老程序员来支招
☞朱广权李佳琦直播掉线,1.2亿人在线等
☞RPC的超时设置,一不小心就是线上事故!
☞拿下Gartner容器产品第一,阿里云打赢云原生关键一战!
☞深聊Solidity的测试场景、方法和实践,太详细了,必须收藏!
☞万字干货:一步步教你如何在容器上构建持续部署!
☞据说,这是当代极客们的【技术风向标】...
今日福利:评论区留言入选,可获得价值299元的「2020 AI开发者万人大会」在线直播门票一张。 快来动动手指,写下你想说的话吧。
文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。文档对象模型 (DOM) 是对HTML文件的另一种展示,通俗地说,一个HTML 文件,我们可以用编辑器以代码的形式展示它,也可以用浏览器以页面的形式展示它,同一份文件通过不同的展示方式,就有了不一样的表现形式。而DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来,我们可以使用脚本或者程序语言通过DOM 来改变或者控制web页面。
我们可以通过JavaScript 来调用document和window元素的API来操作文档或者获取文档的信息。
Node 是一个接口,有许多接口都从Node 继承方法和属性:Document, Element, CharacterData (which Text, Comment, and CDATASection inherit), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReference。Node 有一个nodeType的属性表示Node 的类型,是一个整数,不同的值代表不同的节点类型。具体如下表所示:
节点类型常量
已弃用的节点类型常量
假设我们要判断一个Node 是不是一个元素,通过查表可知元素的nodeType属性值为1,代码可以这么写:
复制代码if(X.nodeType === 1){
console.log('X 是一个元素');
}
在Node 类型中,比较常用的就是element,text,comment,document,document_fragment这几种类型。
Element提供了对元素标签名,子节点和特性的访问,我们常用HTML元素比如div,span,a等标签就是element中的一种。Element有下面几条特性:(1)nodeType为1(2)nodeName为元素标签名,tagName也是返回标签名(3)nodeValue为null(4)parentNode可能是Document或Element(5)子节点可能是Element,Text,Comment,Processing_Instruction,CDATASection或EntityReference
Text表示文本节点,它包含的是纯文本内容,不能包含html代码,但可以包含转义后的html代码。Text有下面的特性:(1)nodeType为3(2)nodeName为#text(3)nodeValue为文本内容(4)parentNode是一个Element(5)没有子节点
Comment表示HTML文档中的注释,它有下面的几种特征:(1)nodeType为8(2)nodeName为#comment(3)nodeValue为注释的内容(4)parentNode可能是Document或Element(5)没有子节点
Document表示文档,在浏览器中,document对象是HTMLDocument的一个实例,表示整个页面,它同时也是window对象的一个属性。Document有下面的特性:(1)nodeType为9(2)nodeName为#document(3)nodeValue为null(4)parentNode为null(5)子节点可能是一个DocumentType或Element
DocumentFragment是所有节点中唯一一个没有对应标记的类型,它表示一种轻量级的文档,可能当作一个临时的仓库用来保存可能会添加到文档中的节点。DocumentFragment有下面的特性:(1)nodeType为11(2)nodeName为#document-fragment(3)nodeValue为null(4)parentNode为null
用如其名,这类API是用来创建节点的
createElement通过传入指定的一个标签名来创建一个元素,如果传入的标签名是一个未知的,则会创建一个自定义的标签,注意:IE8以下浏览器不支持自定义标签。
语法
复制代码 let element = document.createElement(tagName);
使用createElement要注意:通过createElement创建的元素并不属于HTML文档,它只是创建出来,并未添加到HTML文档中,要调用appendChild或insertBefore等方法将其添加到HTML文档树中。
例子:
复制代码 let elem = document.createElement("div");
elem.id = 'test';
elem.style = 'color: red';
elem.innerHTML = '我是新创建的节点';
document.body.appendChild(elem);
运行结果为:
createTextNode用来创建一个文本节点
语法
复制代码 var text = document.createTextNode(data);
createTextNode接收一个参数,这个参数就是文本节点中的文本,和createElement一样,创建后的文本节点也只是独立的一个节点,同样需要appendChild将其添加到HTML文档树中
例子:
复制代码 var node = document.createTextNode("我是文本节点");
document.body.appendChild(node);
运行结果为:
cloneNode返回调用该方法的节点的一个副本
语法
复制代码 var dupNode = node.cloneNode(deep);
node 将要被克隆的节点dupNode 克隆生成的副本节点deep(可选)是否采用深度克隆,如果为true,则该节点的所有后代节点也都会被克隆,如果为false,则只克隆该节点本身.
这里有几点要注意:(1)和createElement一样,cloneNode创建的节点只是游离有HTML文档外的节点,要调用appendChild方法才能添加到文档树中(2)如果复制的元素有id,则其副本同样会包含该id,由于id具有唯一性,所以在复制节点后必须要修改其id(3)调用接收的deep参数最好传入,如果不传入该参数,不同浏览器对其默认值的处理可能不同
注意如果被复制的节点绑定了事件,则副本也会跟着绑定该事件吗?这里要分情况讨论:(1)如果是通过addEventListener或者比如onclick进行绑定事件,则副本节点不会绑定该事件(2)如果是内联方式绑定比如:<div onclick="showParent()"></div>,这样的话,副本节点同样会触发事件。
例子:
复制代码<body>
<div id="parent">
我是父元素的文本
<br/>
<span>
我是子元素
</span>
</div>
<button id="btnCopy">复制</button>
</body>
<script>
var parent = document.getElementById("parent");
document.getElementById("btnCopy").onclick = function(){
var parent2 = parent.cloneNode(true);
parent2.id = "parent2";
document.body.appendChild(parent2);
}
</script>
运行结果为:
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。因此,使用文档片段document fragments 通常会起到优化性能的作用。
语法
复制代码 let fragment = document.createDocumentFragment();
例子:
复制代码<body>
<ul id="ul"></ul>
</body>
<script>
(function()
{
var start = Date.now();
var str = '', li;
var ul = document.getElementById('ul');
var fragment = document.createDocumentFragment();
for(var i=0; i<1000; i++)
{
li = document.createElement('li');
li.textContent = '第'+(i+1)+'个子节点';
fragment.appendChild(li);
}
ul.appendChild(fragment);
})();
</script>
运行结果为:
节点创建型API主要包括createElement,createTextNode,cloneNode和createDocumentFragment四个方法,需要注意下面几点:(1)它们创建的节点只是一个孤立的节点,要通过appendChild添加到文档中(2)cloneNode要注意如果被复制的节点是否包含子节点以及事件绑定等问题(3)使用createDocumentFragment来解决添加大量节点时的性能问题
前面我们提到节点创建型API,它们只是创建节点,并没有真正修改到页面内容,而是要调用·appendChild·来将其添加到文档树中。我在这里将这类会修改到页面内容归为一类。修改页面内容的api主要包括:appendChild,insertBefore,removeChild,replaceChild。
appendChild我们在前面已经用到多次,就是将指定的节点添加到调用该方法的节点的子元素的末尾。
语法
复制代码 parent.appendChild(child);
child节点将会作为parent节点的最后一个子节点。appendChild这个方法很简单,但是还有有一点需要注意:如果被添加的节点是一个页面中存在的节点,则执行后这个节点将会添加到指定位置,其原本所在的位置将移除该节点,也就是说不会同时存在两个该节点在页面上,相当于把这个节点移动到另一个地方。如果child绑定了事件,被移动时,它依然绑定着该事件。
例子:
复制代码<body>
<div id="child">
要被添加的节点
</div>
<br/>
<br/>
<br/>
<div id="parent">
要移动的位置
</div>
<input id="btnMove" type="button" value="移动节点" />
</body>
<script>
document.getElementById("btnMove").onclick = function(){
var child = document.getElementById("child");
document.getElementById("parent").appendChild(child);
}
</script>
运行结果:
insertBefore用来添加一个节点到一个参照节点之前
语法
复制代码 parentNode.insertBefore(newNode,refNode);
parentNode表示新节点被添加后的父节点newNode表示要添加的节点refNode表示参照节点,新节点会添加到这个节点之前
例子:
复制代码<body>
<div id="parent">
父节点
<div id="child">
子元素
</div>
</div>
<input type="button" id="insertNode" value="插入节点" />
</body>
<script>
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.getElementById("insertNode").onclick = function(){
var newNode = document.createElement("div");
newNode.textContent = "新节点"
parent.insertBefore(newNode,child);
}
</script>
运行结果:
关于第二个参数参照节点还有几个注意的地方:(1)refNode是必传的,如果不传该参数会报错(2)如果refNode是undefined或null,则insertBefore会将节点添加到子元素的末尾
删除指定的子节点并返回
语法
复制代码 var deletedChild = parent.removeChild(node);
deletedChild指向被删除节点的引用,它等于node,被删除的节点仍然存在于内存中,可以对其进行下一步操作。注意:如果被删除的节点不是其子节点,则程序将会报错。我们可以通过下面的方式来确保可以删除:
复制代码if(node.parentNode){
node.parentNode.removeChild(node);
}
运行结果:
通过节点自己获取节点的父节点,然后将自身删除
replaceChild用于使用一个节点替换另一个节点
语法
复制代码 parent.replaceChild(newChild,oldChild);
newChild是替换的节点,可以是新的节点,也可以是页面上的节点,如果是页面上的节点,则其将被转移到新的位置oldChild是被替换的节点
例子:
复制代码<body>
<div id="parent">
父节点
<div id="child">
子元素
</div>
</div>
<input type="button" id="insertNode" value="替换节点" />
</body>
<script>
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.getElementById("insertNode").onclick = function(){
var newNode = document.createElement("div");
newNode.textContent = "新节点"
parent.replaceChild(newNode,child)
}
运行结果:
页面修改型API主要是这四个接口,要注意几个特点:(1)不管是新增还是替换节点,如果新增或替换的节点是原本存在页面上的,则其原来位置的节点将被移除,也就是说同一个节点不能存在于页面的多个位置(2)节点本身绑定的事件会不会消失,会一直保留着。
这个接口很简单,根据元素id返回元素,返回值是Element类型,如果不存在该元素,则返回null
语法
复制代码 var element = document.getElementById(id);
使用这个接口有几点要注意:(1)元素的Id是大小写敏感的,一定要写对元素的id(2)HTML文档中可能存在多个id相同的元素,则返回第一个元素(3)只从文档中进行搜索元素,如果创建了一个元素并指定id,但并没有添加到文档中,则这个元素是不会被查找到的
例子:
复制代码<body>
<p id="para1">Some text here</p>
<button onclick="changeColor('blue');">blue</button>
<button onclick="changeColor('red');">red</button>
</body>
<script>
function changeColor(newColor) {
var elem = document.getElementById("para1");
elem.style.color = newColor;
}
</script>
运行结果:
返回一个包括所有给定标签名称的元素的HTML集合HTMLCollection。 整个文件结构都会被搜索,包括根节点。返回的 HTML集合是动态的, 意味着它可以自动更新自己来保持和 DOM 树的同步而不用再次调用document.getElementsByTagName()
语法
复制代码 var elements = document.getElementsByTagName(name);
(1)如果要对HTMLCollection集合进行循环操作,最好将其长度缓存起来,因为每次循环都会去计算长度,暂时缓存起来可以提高效率(2)如果没有存在指定的标签,该接口返回的不是null,而是一个空的HTMLCollection(3)name是一个代表元素的名称的字符串。特殊字符 "*" 代表了所有元素。
例子:
复制代码<body>
<div>div1</div>
<div>div2</div>
<input type="button" value="显示数量" id="btnShowCount"/>
<input type="button" value="新增div" id="btnAddDiv"/>
</body>
<script>
var divList = document.getElementsByTagName("div");
document.getElementById("btnAddDiv").onclick = function(){
var div = document.createElement("div");
div.textContent ="div" + (divList.length+1);
document.body.appendChild(div);
}
document.getElementById("btnShowCount").onclick = function(){
alert(divList.length);
}
</script>
这段代码中有两个按钮,一个按钮是显示HTMLCollection元素的个数,另一个按钮可以新增一个div标签到文档中。前面提到HTMLCollcetion元素是即时的表示该集合是随时变化的,也就是是文档中有几个div,它会随时进行变化,当我们新增一个div后,再访问HTMLCollection时,就会包含这个新增的div。
运行结果:
getElementsByName主要是通过指定的name属性来获取元素,它返回一个即时的NodeList对象
语法
复制代码 var elements = document.getElementsByName(name)
使用这个接口主要要注意几点:(1)返回对象是一个即时的NodeList,它是随时变化的(2)在HTML元素中,并不是所有元素都有name属性,比如div是没有name属性的,但是如果强制设置div的name属性,它也是可以被查找到的(3)在IE中,如果id设置成某个值,然后传入getElementsByName的参数值和id值一样,则这个元素是会被找到的,所以最好不好设置同样的值给id和name
例子:
复制代码<script type="text/javascript">
function getElements()
{
var x=document.getElementsByName("myInput");
alert(x.length);
}
</script>
<body>
<input name="myInput" type="text" size="20" /><br />
<input name="myInput" type="text" size="20" /><br />
<input name="myInput" type="text" size="20" /><br />
<br />
<input type="button" onclick="getElements()" value="How many elements named 'myInput'?" />
</body>
运行结果:
这个API是根据元素的class返回一个即时的HTMLCollection
语法
复制代码 var elements = document.getElementsByClassName(names); // or:
var elements = rootElement.getElementsByClassName(names);
这个接口有下面几点要注意:(1)返回结果是一个即时的HTMLCollection,会随时根据文档结构变化(2)IE9以下浏览器不支持(3)如果要获取2个以上classname,可传入多个classname,每个用空格相隔,例如
复制代码 var elements = document.getElementsByClassName("test1 test2");
例子:
复制代码 var elements = document.getElementsByClassName('test');
复制代码 var elements = document.getElementsByClassName('red test');
复制代码 var elements = document.getElementById('main').getElementsByClassName('test');
复制代码 var testElements = document.getElementsByClassName('test');
var testDivs = Array.prototype.filter.call(testElements, function(testElement){
return testElement.nodeName === 'DIV';;
});
这两个API很相似,通过css选择器来查找元素,注意选择器要符合CSS选择器的规则
document.querySelector返回第一个匹配的元素,如果没有匹配的元素,则返回null
语法
复制代码 var element = document.querySelector(selectors);
注意,由于返回的是第一个匹配的元素,这个api使用的深度优先搜索来获取元素。
例子:
复制代码<body>
<div>
<div>
<span class="test">第三级的span</span>
</div>
</div>
<div class="test">
同级的第二个div
</div>
<input type="button" id="btnGet" value="获取test元素" />
</body>
<script>
document.getElementById("btnGet").addEventListener("click",function(){
var element = document.querySelector(".test");
alert(element.textContent);
})
</script>
两个class都包含“test”的元素,一个在文档树的前面,但是它在第三级,另一个在文档树的后面,但它在第一级,通过querySelector获取元素时,它通过深度优先搜索,拿到文档树前面的第三级的元素。运行结果:
语法
复制代码 var elementList = document.querySelectorAll(selectors);
例子:
复制代码 var matches = document.querySelectorAll("div.note, div.alert");
返回一个文档中所有的class为"note"或者"alert"的div元素
复制代码<body>
<div class="test">
class为test
</div>
<div id="test">
id为test
</div>
<input id="btnShow" type="button" value="显示内容" />
</body>
<script>
document.getElementById("btnShow").addEventListener("click",function(){
var elements = document.querySelectorAll("#test,.test");
for(var i = 0,length = elements.length;i<length;i++){
alert(elements[i].textContent);
}
})
</script>
这段代码通过querySelectorAll,使用id选择器和class选择器选择了两个元素,并依次输出其内容。要注意两点:(1)querySelectorAll也是通过深度优先搜索,搜索的元素顺序和选择器的顺序无关(2)返回的是一个非即时的NodeList,也就是说结果不会随着文档树的变化而变化兼容性问题:querySelector和querySelectorAll在ie8以下的浏览器不支持。
运行结果:
在html文档中的每个节点之间的关系都可以看成是家谱关系,包含父子关系,兄弟关系等等
每个节点都有一个parentNode属性,它表示元素的父节点。Element的父节点可能是Element,Document或DocumentFragment
返回元素的父元素节点,与parentNode的区别在于,其父节点必须是一个Element,如果不是,则返回null
返回一个即时的NodeList,表示元素的子节点列表,子节点可能会包含文本节点,注释节点等
一个即时的HTMLCollection,子节点都是Element,IE9以下浏览器不支持children属性为只读属性,对象类型为HTMLCollection,你可以使用elementNodeReference.children[1].nodeName来获取某个子元素的标签名称
只读属性返回树中节点的第一个子节点,如果节点是无子节点,则返回 null
返回当前节点的最后一个子节点。如果父节点为一个元素节点,则子节点通常为一个元素节点,或一个文本节点,或一个注释节点。如果没有子节点,则返回null
返回一个布尔值,表明当前节点是否包含有子节点.
返回当前节点的前一个兄弟节点,没有则返回nullGecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如Node.firstChild和Node.previousSibling之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点
previousElementSibling返回当前元素在其父元素的子元素节点中的前一个元素节点,如果该元素已经是第一个元素节点,则返回null,该属性是只读的。注意IE9以下浏览器不支持
Node.nextSibling是一个只读属性,返回其父节点的childNodes列表中紧跟在其后面的节点,如果指定的节点为最后一个节点,则返回nullGecko内核的浏览器会在源代码中标签内部有空白符的地方插入一个文本结点到文档中.因此,使用诸如Node.firstChild和Node.previousSibling之类的方法可能会引用到一个空白符文本节点, 而不是使用者所预期得到的节点
nextElementSibling返回当前元素在其父元素的子元素节点中的后一个元素节点,如果该元素已经是最后一个元素节点,则返回null,该属性是只读的。注意IE9以下浏览器不支持
设置指定元素上的一个属性值。如果属性已经存在,则更新该值; 否则将添加一个新的属性用指定的名称和值
语法
复制代码 element.setAttribute(name, value);
其中name是特性名,value是特性值。如果元素不包含该特性,则会创建该特性并赋值。
例子:
复制代码<body>
<div id="div1">ABC</div>
</body>
<script>
let div1 = document.getElementById("div1");
div1.setAttribute("align", "center");
</script>
运行结果:
如果元素本身包含指定的特性名为属性,则可以世界访问属性进行赋值,比如下面两条代码是等价的:
复制代码 element.setAttribute("id","test");
element.id = "test";
getAttribute()返回元素上一个指定的属性值。如果指定的属性不存在,则返回null或""(空字符串)
语法
复制代码 let attribute = element.getAttribute(attributeName);
attribute是一个包含attributeName属性值的字符串。attributeName是你想要获取的属性值的属性名称
例子:
复制代码<body>
<div id="div1">ABC</div>
</body>
<script>
let div1 = document.getElementById("div1");
let align = div1.getAttribute("align");
alert(align);
</script>
运行结果:
removeAttribute()从指定的元素中删除一个属性
语法
复制代码 element.removeAttribute(attrName)
attrName是一个字符串,将要从元素中删除的属性名
例子:
复制代码<body>
<div id="div1" style="color:red" width="200px">ABC
</div>
</body>
<script>
let div = document.getElementById("div1")
div.removeAttribute("style");
</script>
在运行之前div有个style="color:red"的属性,在运行之后这个属性就被删除了
运行结果:
Window.getComputedStyle()方法给出应用活动样式表后的元素的所有CSS属性的值,并解析这些值可能包含的任何基本计算假设某个元素并未设置高度而是通过其内容将其高度撑开,这时候要获取它的高度就要用到getComputedStyle
语法
复制代码 var style = window.getComputedStyle(element[, pseudoElt]);
element是要获取的元素,pseudoElt指定一个伪元素进行匹配。返回的style是一个CSSStyleDeclaration对象。通过style可以访问到元素计算后的样式
getBoundingClientRect用来返回元素的大小以及相对于浏览器可视窗口的位置
语法
复制代码 var clientRect = element.getBoundingClientRect();
clientRect是一个DOMRect对象,包含left,top,right,bottom,它是相对于可视窗口的距离,滚动位置发生改变时,它们的值是会发生变化的。除了IE9以下浏览器,还包含元素的height和width等数据
例子:
复制代码 elem.style.color = 'red';
elem.style.setProperty('font-size', '16px');
elem.style.removeProperty('color');
例子:
复制代码 var style = document.createElement('style');
style.innerHTML = 'body{color:red} #top:hover{background-color: red;color: white;}';
document.head.appendChild(style););
JavaScript中的API太多了,将这些API记住并熟练使用对JavaScript的学习是有很大的帮助
作者:yyzclyang
链接:https://juejin.cn/post/6844903604445249543
*请认真填写需求信息,我们会在24小时内与您取得联系。