首页 文章 精选 留言 我的

精选列表

搜索[搭建],共10000篇文章
优秀的个人博客,低调大师

如何在双十一给自己送个“陪聊女友”——基于飞桨&Plato搭建多轮对话模型

近年来,机器人对话应该是NLP领域最火热的领域之一了。几乎每一个手机操作系统都内置了对话机器人,智能音箱的最大卖点之一也是智能对话。那么,这种对话是如何实现的呢?我们可不可以自己定制,训练自己的对话机器人呢?回答当然是肯定的。今天,我们就尝试用百度开源模型Plato训练一个对话模型。 下载安装命令 ## CPU版本安装命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安装命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu 百度Plato-2对话模型 Plato是百度推出的一个多轮对话模型。该模型的最大改进在于通过引入离散隐变量实现了对话场景中多回答中的择优,即,对于同一个问题,实现不同场景下的不同回答的选择。最新推出的Plato-2在中英文效果上,已全面超越 Google Meena、Facebook Blender、微软小冰等先进模型。 图一:Plato-2模型框架 模型的整体框架如图一所示。该模型采用了经典的Transformer结构,通过注意力机制提高了模型针对不同长度对话的生成效果。隐变量z的引入,使预训练模型依据z可以生成多种回答,最终从多种回答中择优。在训练中,该模型采用两阶段训练的方法。第一阶段,基于表现良好的预训练模型,训练出一对一回答的模型;第二阶段,引入评估和隐变量z,训练出一对多回答的模型。模型的具体原理可以参考原论文,论文地址: https://arxiv.org/abs/2006.16779 对话模型训练工具 飞桨Knover工具包 百度飞桨推出的Konver工具包是一个非常强大的对话模型训练工具,我们可以通过Konver工具包快速地训练属于自己的Plato模型。Knover工具包目前支持了Plato-2模型的训练。具体参考链接:https://github.com/PaddlePaddle/Knover。在这里,我们解释一下Knover工具包中一些常见的问题。 1. 工具包分析 我们可以用train.py来进行微调训练,用infer.py 导出想要的结果。这里介绍一下这个模型的一些代码细节。 package文件夹中存放了其自带的试验数据的词集,语句切分模型(spm.model, 即sentencepiece model,这个模型用在语句的预处理上,必须给定),以及模型的细节参数(词集大小,隐含层个数,激活函数等等,具体查看package/dialog_en/24L.json。 models文件夹存放了模型的各个子模块,plato模块也在其中。data文件夹存放了实验用的小文件。tasks文件夹中包含了模型两种应用任务的实现,包括“下一句语句预测”和“会话生成”。需要注意的是,这个应用任务的选择是必须给出的,对应参数 --tasks, 分别写作NextSentencePrediction和DialogGeneration。不过实测只有DialogGeneration可以生成预测语句,NextSentencePrediction并不能生成预测语句(为打分模型,负责对回答的效果打分,以便Plato模型选择最佳的回答)。 需要指出的是,在infer.py中需要指出需要保存的内容,对应参数 --output_name,输出结果有3项:data_id, score,和response(response在NextSentencePrediction中并不生成),同时,需要在子目录创建output文件夹,否则会报错(因为源码中并没有自动创建这个文件夹,如果微调中保存了checkpoints,则会自动创建output文件夹)。 2. 常用参数 这里需要指出,--do_generation这个参数,推测是手动确定执行任务是否是生成句子的任务的,由于不需要提前指定,所以默认一直是False,但是这会在infer任务中报错,而且对于NSPModel来说,并不存在这个参数,但是在infer时仍然会报错。解决的方法是在执行Plato时加上--do_generation参数并赋值为True。 --init_pretraining_params 预训练模型所在文件夹,如果需要加载(当然需要)预训练模型需要给出这个参数 --init_checkpoint 保存节点的所在文件夹,如果给出了init_checkpoint则从该文件夹初始化训练参数(等于覆盖了init_pretraining_params的参数) train.py --train_file 训练文件地址 --valid_file 评估文件地址 --model 用到的模型名称:Plato:plato;NSPModel:next_sentence_prediction model --config_path 模型细节参数配置文件,如24L.json --task 模型应用任务 NextSentencePrediction;DialogGeneration;UnifiedTransformer --vocab_path 词集路径 --spm_model_file sentencepiece model文件的路径 --num_epochs 训练周期数 --log_steps 输出训练详情的间隔步数 --validation_steps 评价间隔步数 --save_steps 保存间隔步数 infer.py --infer_file 需要推断的文件 --output_name 需要保存的对象,response;data_id;score --model 用到的模型名称:Plato:plato;NSPModel:next_sentence_prediction model --config_path 模型细节参数配置文件,如24L.json --task 模型应用任务 NextSentencePrediction;DialogGeneration;UnifiedTransformer --vocab_path 词集路径 --spm_model_file sentencepiece model文件的路径 训练Plato模型 1. 数据准备 Plato-2模型的输入采用了token,role,turn,position相融合的表示方式。在训练和测试过程中,我们需要搞清楚文本数据需要经过怎样的转化才能作为输入,以及输出数据需要怎样的处理才能转换成文本。目前我们可以获取各种开放的对话训练集,如百度千言数据集和清华开放数据集。目前飞桨正在进行的千言多技能会话比赛中,报名即可下载多个对话数据集,我们即用这些数据集进行以下的训练。 这里也号召有兴趣的朋友参加千言多技能会话比赛,奖励多多哟!地址在此: https://aistudio.baidu.com/aistudio/competition/detail/55 1.1 中文分词 中文必须面对的一个问题就是如何实现分词。在公开的开放域对话数据集中,大多数已经做了分词,然而真实场景中语句是不可能时时刻刻都被分词了的。在Knover的源码中,对输入的处理是通过了sentencepiece工具(BERT也使用的这个)。sentencepiece提供了一种方便快捷的分词操作,我们可以直接将整个数据集放进去,设定分词的单元数量,然后等待训练出一个好用的分词模型(会输出一个预训练模型,之后对每个语句都会用这个模型进行编码和解码,即分词,转换成数字编码,输出再转换回句子)。 Knover中训练必须输入的参数spm_model,就是对应sentencepiece的预训练模型。我们当然可以自己训练一个sentencepiece的预训练模型出来,但是考虑到分词模型对效果的影响,推荐大家使用千言多技能对话中给出的baseline模型(luge-dialogue)中附带的spm.model文件,这个文件分词的效果已经非常出色了。当然,别忘了搭配词表vocab.txt使用。 仔细分析luge的spm.model我们可以发现,这个预训练模型其实是根据已经分词的句子训练的,虽说如此,因为分词单元足够多,也覆盖了所有常见的单个中文词。我们可以直接把语句送入得到编码,也可以先用jieba分词预先分一次,然后再编码。用sentencepiece模型的例子如下: importsentencepieceassp importjieba text="我今天做了一顿丰盛的晚餐!" SPM=sp.SentencePieceProcessor() SPM.load('spm.model') #直接分词 ids=SPM.encode_as_ids(text) print(ids) print(SPM.decode_ids(ids)) #先用jieba分词,再用sentencepiece编码 text=''.join(list(jieba.cut(text))) ids=SPM.encode_as_ids(text) print(ids) print(SPM.decode_ids(ids)) 1.2 文本的输入 Plato对文本输入的支持还是挺多样化的,它支持直接输入原始文本,也支持输入经过tokenize的分词序列,或者是已经编码(encoded)的数字序列。但是无论Plato支持的格式如何,在进行训练和预测之前,都会转换成能够被识别的标准格式。在Knover中,这个格式是通过定义的Record完成的。Record的定义如下: fromcollectionsimportnamedtuple Record=namedtuple("Record",fields,defaults=(None,)*len(fields)) 在解释fields的值之前,我们先来思考一下Plato需要哪些输入。在完成一段对话时,我们通常会综合对话的历史和自己所知的历史知识来进行判断,来决定自己将要回答什么。而在对话生成中,这些信息也是需要考虑的。因此,Plato需要的输入有两个,首先,是当前对方的问话,其次是已经进行过的历史对话信息,最后是背景知识。 由于各种条件的限制,背景知识可能并没有办法获取,所以至少需要的是已进行的历史对话信息,和此时对方的问话。进一步,我们需要考虑更多的信息:如果纪录了历史对话,我们如何判断每段对话的起始位置,如何判断从什么时候开始生成需要的回答,在训练集中,我们还要知道哪一部分是训练中给出的回答用于调整模型的参数。我们通过图二可以直观地看出这些输入信息的复杂性: 图二:Plato的输入结构 上图给出了Plato模型需要的输入,当然这些是以Embedding的形式给出的,而Embedding是在模型中转化的,它在转化之前是以数字编码存在的。Embedding现在已经是语言处理技术的标配了,它把每一个标记映射到空间中,增加其表征能力。我们暂时忽略最前边的latent,它是表示不同回答方式的隐变量,用于Plato在众多可能回答中选择正确的回答,我们这里不关心这个是怎么实现的,所以不展开讨论。在latent之后,有contex和response两个内容,其中context包含了众多信息:历史对话,背景知识,以及对话与对话之间分隔的符号[EOU], [BOU]等等,如果有背景知识的话,也会列到context中response则是训练中需要的部分,在测试中这一部分是空的。 TokenEmbeddings表示各语言单元的Embedding(词向量);RoleEmbeddings是各个语言单元在其中扮演的角色,这个主要是用来区分内容是context(EA)还是response(EB)(亦或是背景知识,背景知识可以作为response的角色,也可以单独成为一类,即EC);TurnEmbeddings表示每一部分在当前回合中的相对回合数;PositionEmbeddings则是每个语言单元的位置,一般是range(0, len(context))。 知道了这些,我们回到Record上来看这个输入应该怎么得到。由定义可知,Record是带名称的元组,这样我们立马可以知道,这个元组是通过名称来调用其中的内容的。fields的内容是什么呢?从官方的源码中可以总结出:fields = ["token_ids", "type_ids", "pos_ids", "tgt_start_idx", "data_id"]。也就是说,输入需要给出5个部分,token_ids就是处理过的语言单位的编码;type_ids就是个语言单位扮演的角色,是context还是response;pos_ids是各个语言单位的位置;tgt_start_idx是回复生成的开始位置,也即context的最后一个词的位置;data_id就是这个训练样本的标记。 如下给出一个例子,可以清楚的知道一个输入是如何形成的: fromcollectionsimportnamedtuple fields=["token_ids","type_ids","pos_ids","tgt_start_idx","data_id"] Record=namedtuple("Record",fields,defaults=(None,)*len(fields)) #新的会话 question ='我刚刚去动物园了。' #历史对话 history ='你好?[SEP]你好,谈谈你自己吧?[SEP]我今天过得很开心呢![SEP]是嘛,你今天干了什么?' #背景知识 background ='天气:晴朗,地点:北京,人物:男孩,动物园,熊猫' #回答 answer ='北京动物园吧,那里的熊猫很受欢迎呢!' question=SPM.encode_as_ids(question) history=[SPM.encode_as_ids(text)fortextinhistory.split("[SEP]")] background=SPM.encode_as_ids(background) answer=SPM.encode_as_ids(answer) token_ids=[] type_ids=[] data_id=0#如果样本多的话,会按排序进行标记 token_ids+=[0]+background+[2]#0表示语句开头,2表示句子结尾 type_ids+=[0]+len(background)*[0]+[0]#0表示context类,1表示response类 forlineinhistory: token_ids+=line+[2] type_ids+=len(line)*[0]+[0] token_ids+=question+[2] type_ids+=len(question)*[0]+[0] token_ids+=[0]+answer+[2] type_ids+=[1]+len(answer)*[1]+[1]#注意符号的变化 fields_value={} fields_value["token_ids"]=token_ids fields_value["type_ids"]=type_ids fields_value["pos_ids"]=list(range(len(type_ids))) fields_value["tgt_start_idx"]=fields_value["pos_ids"][-1] fields_value["data_id"]=data_id record=Record(**fields_value) print(record) 1.3 文本的转换 得到record以后,Plato还会将其中的各个标签的元组分别转换成矩阵,这里涉及到填充的过程,由于与其他文本填充的操作基本相同,这里不再赘述。 2. 训练模型 对于Plato-2模型来说,其训练包括两个过程:首先,训练出一个一对一的对话模型(UnifiedTransformer),然后基于这个模型,训练Plato模型。Plato模型的模型结构和UnifiedTransformer很接近(参数中多了一个latent_type_size)。 2.1 配置准备 由于在训练模型的时候,需要输入--config_path,这个参数用来读取模型的配置(Transformer层数量等等),这里我们需要定义两个模型的配置文件(**.json)。如下参数生成两个配置文件,配置即为我数据集中附带的模型的配置,如果有兴趣和算力,可以自己改配置训练,最有效的参数是num_hidden_layers和hidden_size,增加这些值会增加模型的规模。 importjson ##定义UnifiedTransformer的参数 key={'pre_encoder_cmd':'d','preprocess_cmd':'n','postprocess_cmd':'da','post_cls_cmd':'n','cls_bias':True, 'attention_probs_dropout_prob':0.1,'hidden_act':'gelu','hidden_dropout_prob':0.1,'hidden_size':768, 'initializer_range':0.02,'max_position_embeddings':512,'num_attention_heads':12, 'num_hidden_layers':12,'type_vocab_size':2,'role_type_size':32,'vocab_size':30004} f=open("12L.json","w") json_data=json.dump(key,f) f.close() ##定义Plato的参数 key={'pre_encoder_cmd':'d','preprocess_cmd':'n','postprocess_cmd':'da','post_cls_cmd':'n','cls_bias':True, 'attention_probs_dropout_prob':0.1,'hidden_act':'gelu','hidden_dropout_prob':0.1,'hidden_size':768, 'initializer_range':0.02,'max_position_embeddings':512,'latent_type_size':20,'num_attention_heads':12, 'num_hidden_layers':12,'type_vocab_size':2,'role_type_size':32,'vocab_size':30004} f=open("12L_P.json","w") json_data=json.dump(key,f) f.close() 2.2 训练一对一模型(UnifiedTransformer) 用命令行的方式来调用.py文件进行训练,对于模型的训练,我们可以用Knover工具包中的train.py文件进行。代码如下: pythonKnover/train.py\ --modelUnifiedTransformer--taskDialogGeneration--vocab_pathKnover/config/vocab.txt--spm_model_fileKnover/config/spm.model\ --train_filepro_data/train.txt--valid_filepro_data/valid.txt--data_formatnumerical--file_formatfile--config_pathKnover/config/12L.json\ --init_checkpointKnover/latest_model/ut_model\ --in_tokensTrue--batch_size16000-lr1e-5--warmup_steps1000--weight_decay0.01--num_epochs20\ --max_src_len384--max_tgt_len128--max_seq_len512\ --log_step100--validation_steps20000--save_steps5000\ --save_pathKnover/output\ --is_distributedFalse\ --is_cnTrue\ --start_step 2.3 训练一对多模型(Plato) 用3.2中得到的模型,继续训练Plato模型。由于是接着3.2模型进行的调整,--lr最好不要设置的过大(3.2模型--lr的十分之一即可)。注意更改配置文件,否则会报错。 pythonKnover/train.py\ --modelPlato--taskDialogGeneration--vocab_pathKnover/config/vocab.txt--spm_model_fileKnover/config/spm.model\ --train_filepro_data/train.txt--valid_filepro_data/valid.txt--data_formatnumerical--file_formatfile--config_pathKnover/config/12L_P.json\ --init_checkpointKnover/latest_model/pt_model\ --in_tokensTrue--batch_size1000-lr1e-5--warmup_steps1000--weight_decay0.01--num_epochs20\ --max_src_len384--max_tgt_len128--max_seq_len512\ --log_step100--validation_steps20000--save_steps100\ --save_pathKnover/output\ --start_step\ --is_cnTrue 3. 保存模型 当Plato训练完后,我们可以保存相应的模型,以较少模型参数的容量占用。我们需要把NSPModel和Plato模型分开储存,代码如下: ##保存NSPModel pythonKnover/save_inference_model.py\ --modelNSPModel\ --taskNextSentencePrediction --vocab_pathKnover/config/vocab.txt--spm_model_fileKnover/config/spm.model\ --init_checkpointKnover/latest_model/pt_model\ --inference_model_pathNSP\ --config_pathKnover/config/12L.json ##保存Plato pythonKnover/save_inference_model.py\ --modelPlato\ --do_generationtrue\ --taskDialogGeneration\ --vocab_pathKnover/config/vocab.txt--spm_model_fileKnover/config/spm.model\ --init_checkpointKnover/latest_model/pt_model\ --inference_model_pathPlato\ --config_pathKnover/config/12L_P.json 训练模型的应用 在模型训练完之后,便用训练后的模型做一些应用了。比如生成下一句话,或者进行多轮对话。 1. 对话生成 我们可以直接用Knover提供的infer.py生成下一句话。我们可以通过生成的NSPModel和Plato进行上述操作,代码如下: pythonKnover/infer.py\ --modelPlato--taskDialogGeneration--vocab_pathKnover/config/vocab.txt--spm_model_fileKnover/config/spm.model\ --infer_filepro_data/test.txt--data_formatnumerical--file_formatfile--config_pathKnover/config/12L_P.json\ --init_pretraining_paramsPlato--nsp_inference_model_pathNSP--ranking_scorensp_score\ --batch_size1\ --max_src_len384--max_tgt_len128--max_seq_len512\ --output_nameresponse\ --do_generationTrue--num_samples20--topk5--is_cnTrue\ --do_generationtrue--save_pathKnover/output--log_step1 2. 用Paddlehub实现多轮对话 PaddleHub是飞桨预训练模型应用工具,开发者可以便捷地使用高质量的预训练模型结合Fine-tune API快速完成模型迁移到部署的全流程工作。PaddleHub提供的预训练模型涵盖了图像分类、目标检测、词法分析、语义模型、情感分析、视频分类、图像生成、图像分割、文本审核、关键点检测等主流模型。更多详情可查看官网,链接: https://www.paddlepaddle.org.cn/hub 2.1 英文多轮对话 在Paddlehub中已经集成了两个plato对话模型,分别是plato2_en_base和plato2_en_large。其中,第一个是小模型,占用1.2G;第二个是大模型,占用12G。小模型可以在4G显存以上的电脑上直接运行,但是如果需要运行大模型,则至少需要一块16G的V100了。官方给出的这两个模型其实对话效果已经很棒了,特别是大模型,效果相当惊艳。然而,遗憾的是并不支持中文对话。如果实在想用英文对话,可以用几个简单的代码实现该模型的调用: importpaddlehubashub module=hub.module("plato2_en_base") #直接产生对话: text="whatyouwanttosay" response=module.generate([text]) #多轮对话 withmodule.interactive_mode(max_turn=3): your_words=input() response=module.generate(your_words) print("robotanswer:%s"%response) 其中,module在非对话模式(interactive_mode)下,需要输入一个储存多个问句的list;在对话模式下,则每次输入一个问句。 2.2 实现中文多轮对话 当然,只有英文对话是不够的,我们不是已经有了中文模型了吗?通过对官方hub模块的修改,我们可以将自己的模型嵌入进去,实现中文对话! 当你调用了一次官方的plato2_en_base模型后(为什么我写plato2_en_base呢?因为plato2_en_large也跑不动啊!除非你有16G显存的显卡),对应的模型已经保存到了paddlehub的离线模型库中,你可以在C:\Users\你的计算机名\paddlehub\modules\路径下找到自己下载的plato2_en_base文件夹。 首先,我们来分析一下官方模块的组成部分,如下,当我们把plato2_en_base下载下来后,可以看到其主要由以下几个部分组成: 图三:plato2_en_base组成 我们需要改的只有红线标出的部分。首先我们要准备的东西有:save_inference_model.py导出的NSPModel和PlatoModel,分别对应图中的NSP和Plato;查询字典vocab.txt以及sentencepiece预训练模型spm.model。这些文件分别放到图中所示的对应位置。 以上工作完成后,如果你确保了你替换的文件都是按照官方模块中的名字进行的命名,那么你可以直接进行调用,即用官方的名称调用。当然,稍微有强迫症的人可能并不想用官方的名字,我们可以对module进行如下更改来更改模块的名字: name即你想更改的名字,在调用模型时可以用这个名字来进行搜索。当然,如果你的模型时另外存到了其他地方,你可以用hub.module(directory=dirs)而非hub.module(name)来调用,dirs直接指定到你自己的模块所在的文件夹。更进一步地,你也可以对模块的文件夹更改名字,但是需要注意的是,如果你更改了文件夹的名字,记得将所有py文件中的import选项与文件名相关的import进行更改,即类似from plato2_en_base import ...字段的plato2_en_base更改为你想要定制的文件夹名称。 经过这些简单的操作,我们就可以通过以下几行简单的代码进行自己模型的对话了! importpaddlehubashub importos os.environ["CUDA_VISIBLE_DEVICES"]='0' module=hub.Module(directory='plato2_cn_small') withmodule.interactive_mode(max_turn=3): whileTrue: human_utterance=input() iflen(human_utterance)>0: print("You:"+human_utterance) robot_utterance=module.generate(human_utterance) print("Robot:%s"%robot_utterance[0]) else: break 最后,下边是一个对话展示: ​ 相关模型我已经公开到了AI Studio上,项目链接: https://aistudio.baidu.com/aistudio/projectdetail/1197592 下载安装命令 ## CPU版本安装命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安装命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu >>访问 PaddlePaddle 官网,了解更多相关内容。

优秀的个人博客,低调大师

Proxy-Go 控制面板 v2.1 发布,一秒搭建专业代理服务!

刚刚用了它,隔壁代理小李都馋哭了。 Proxy-Go 控制面板 ProxyAdmin是强大的代理服务工具snail007/goproxy的控制面板,运行了它,一秒让你的服务器变为强大的代理服务器,友好的交互界面,小白也能轻松上手,让你用起来得心应手,心情舒畅。 更新内容: 1.更新sdk内核至最新版v9.5。 2.针对有拨号的机器进行了优化。 功能预览 展现客户端参数 实时日志 参数文件管理 调试模式 下载地址: Gitee Github 详细信息:https://github.com/snail007/proxy_admin_free

优秀的个人博客,低调大师

直播揭秘飞冰 | 淘系中后台负责人教你快速搭建企业微前端架构

主题:微前端 + icejs 助力企业级中后台开发时间:3月5日 19:00-20:00嘉宾:大果 阿里巴巴淘系技术前端技术专家 ★上期直播地址:http://mudu.tv/watch/4662084★PPT获取:关注「淘系技术」微信公众号,后台回复“前端”即可获取全部PPT资料! 分享简介 icejs 是飞冰团队在 2020 年初发布的一个 React 研发框架,目前已广泛应用于淘系中后台业务中。icejs 致力于建设一套上手简单、生态完善的研发框架,从基础的路由到状态管理再到 SSR、微前端等我们是如何思考与设计的?icejs 与社区已有的框架 nextjs、nuxtjs 等又有哪些区别?本次直播带你一探究竟。 另一方面,今年在国内前端社区突然火起来的微前端到底是解决什么问题的?在淘系业务场景下微前端方案 icestark 又是怎

优秀的个人博客,低调大师

【云栖号案例 | 游戏&娱乐】混合云模式助力斗鱼搭建混搭大数据架构

云栖号案例库:【点击查看更多上云案例】不知道怎么上云?看云栖号案例库,了解不同行业不同发展阶段的上云方案,助力你上云决策! 案例背景 2019杭州云栖大会大数据企业级服务专场,由斗鱼大数据高级专家张龙带来以 “混合云模式下 MaxCompute+Hadoop 混搭大数据架构实践” 为题的演讲。本文讲述了从 Apache Hadoop 阶段到 Cloudera CDH 阶段斗鱼大数据架构的发展历程。提出了上云过程中斗鱼遇到的问题和跳战,包括数据安全、数据同步以及迁移任务。概括了混合云模式给斗鱼带来资源效率更高和资源成本更低的变化。 斗鱼大数据架构发展历程 在2014年中期,斗鱼就开始使用大数据,最开始使用的是简单的HBase和Hadoop。在2015年,开始使用CDH运维大数据集群,主要针对可视化运维。在2017年的下半年,斗鱼开始接触阿里云大数据的一些产品,并且与其他产品做了对比。最终选择了阿里云的MaxCompute。 Apache Hadoop阶段 由于业务场景比较简单,组件较少,并且使用的人也少,但可以灵活的操作,同时集群规模较小,运维要求低,可以自由的利用开源,培养了许多人才。但在发展过程中也遇到了一些阻碍,例如:组件增多,运维成本高,业务增长快,集群扩容操作繁琐,人员增加,数据安全要求高,物理机操作,环境安全难保障。 Cloudera CDH阶段 斗鱼为何选择Cloudera CDH?原因主要有:首先,它能满足业务发展需要,多组件运维成本低,集群扩容操作简单,数据安全及环境安全有保障。其次,CDH在国内被广泛使用。最主要的一点是斗鱼的团队内部有CDH人才。 Cloudera CDH给斗鱼带来了许多便利,包括支持丰富的组件,不用考虑兼容性,可以通过CM统一管理,进行Web化管理,同时支持中文。另外,支持安全管理,以及对Kerberos安全认证。 自建集群遇到了发展瓶颈,涉及到资源效率问题和资源成本问题。资源效率问题包括资源预算审批慢、机器采购周期长以及机房部署效率低。资源成本问题包括机器资源成本高、机房成本高还不稳定以及闲时资源空置较多。 大数据上云的挑战 上云面临的挑战主要是如何保证数据安全,因为数据是企业核心的资源,安全性是非常关键的。其次是如何保持数据同步,是因为云上云下存在着海量数据。最后,因为云下存在大量的历史业务,那该如何将业务安全迁移到云上也是一个问题。 如何保证数据安全? 对于数据丢失的问题,阿里使用原始数据进行备份,这是很关键的。对于核心数据泄露问题,几率是很小的,因为泄露数据之后所要承担的风险远大于打败竞争对手所提供的收益。对于云环境面向外网,如何保证安全访问的问题,可以增加账号访问IP白名单及审计,设置公司内部才可访问。 如何保持数据同步? 由于每天会产生PB级历史数据和TB级数据增量。如何快速准确同步数据问题,可以使用数据同步工具,主要是基于DataX的改造。同时提高网络专线能力,增加多根专线,自动地进行异常切换,与云上平台业务进行隔离。利用数据校验工具,校验数据同步任务以及数据量。 如何安全迁移业务? 业务的安全迁移需要做到三个要求:1.不能引起故障,保证迁移可行性验证。2.迁移成本不能太高,业务侧尽量少改动。3.能上云也要能下云,尽量保证云上云下操作一致性。为了做到不引起故障,要做到三个需要:需要做业务场景测试,保证业务场景全部覆盖到,并且能够识别能够迁移的业务场景。需要数据质量检验,确保相同业务云上云下产出数据的一致性。需要数据效率验证,确保云上任务数据产出时间,同时不影响业务。 如何保证较低的迁移成本? 斗鱼在IDC中运行的任务主要分两部分,第一部分是Java任务,占比很小,特点是基于封装的HiveClient工具进行查询计算。第二部分是XML配置化任务,特点是基于自定义XML文件,支持HiveSQL统计后导入其他存储。针对这些任务的特点,斗鱼也做了相应的改造。针对封装OdpsClient,可以将HiveClient改成OdpsClient,并且改Hive URL为云环境。针对加模板改URL,可以引入MaxCompute参数模型,改Hive URL为云环境。 为了保证能上云也能下云,第一,需要数据能上能下,就是前面提到的数据同步中心。 第二,需要完善的配套工具,云上云下环境尽量透明化使用。第三,多使用通用功能,通过SQL+UDF能覆盖大部分场景。 混合云模式带来的变化 混合云模式带来的变化主要针对资源效率低,难以跟上业务发展,以及资源成本高,企业财务压力大两方面。在资源效率方面,从自建集群到MaxCompute有一些变化,包括提前半年或一年提预算变成按量付费,采购耗时1到3个月变成资源可以无限使用,机房上架1周以上变为无机房概念。相比于IDC自建集群,MaxCompute每年大概节约1000w成本,保障集群零故障。同时也有一些附加的收益,包括阿里云的专业服务,当遇到技术问题时可以请教阿里的专家来帮助解决,以及计算资源可以量化,可以知道钱花在哪些业务了,以及与阿里专家交流,帮助解决业务难题。 在自建机房时,斗鱼也做了一些开发,下图所示为数据开发,包括基于Hue的查询计算和云上的DataStudio数据开发,然后将Hue的API和DataStudio的API集中起来形成斗鱼的大数据开放平台,作用是可以提供给数据部门的人使用,也可以提供给业务部门的分析人员使用。 此外,斗鱼也做了一些实践,称为多活数据中心,如下图所示。斗鱼通过确立自建机房的数据和阿里云数据在这两个数据中心的角色,保证可以在多活数据中心的状态下支撑更多的业务。 混合云带来的变化总结起来,资源成本和资源效率是最大的两个变化,还有可量化的成本、增值服务、额外的专业服务等,不仅可以给我们自己部门人员用,还可以给其他业务部门的人来用,并且他们对使用成本也是直接可见的。 云栖号案例库:【点击查看更多上云案例】不知道怎么上云?看云栖号案例库,了解不同行业不同发展阶段的上云方案,助力你上云决策!

优秀的个人博客,低调大师

Springboot2.0从零开始搭建脚手架-初始化和整合MybatisPlus3.0+

初始化springboot项目 添加web依赖,基于springboot2.1.3稳定版本初始化spring boot项目地址 https://start.spring.io/包名:com.nqmysb.scaffold 导入IDE 下载项目,我这里使用eclipse ,导入eclipse之后如下图 编写控制器 写一个控制器,并启动查看结果,这里直接将controller写在入口类 @RestController @SpringBootApplication public class SpringbootScaffoldApplication { public static void main(String[] args) { SpringApplication.run(SpringbootScaffoldApplication.class, args); } @RequestMapping("/index") public String index(String[] args) { System.out.println("hello world"); return "springboot2.0 hello!"; } } 验证访问 通过访问浏览器查看结果 http://localhost:8080/index ,浏览器显示和控制台打印正常! 热加载配置 在项目pom.xml文件中加入热加载依赖,重新启动,修改代码时项目会自动重启更新项目。 <!-- hot reload --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> 自定义启动banner图案 在src/main/recesources下新建一个banner.txt文件,内容如下:佛系程序员 ${AnsiColor.BRIGHT_YELLOW} =================================================================================== _____ _ _ _ _ _ _ _ | __ \| | | | | | | | | | | | | | | |__) | |__ ___ | |_ ___ | |__| | __ _ ___| | ____ _| |_| |__ ___ _ __ | ___/| '_ \ / _ \| __/ _ \ | __ |/ _` |/ __| |/ / _` | __| '_ \ / _ \| '_ \ | | | | | | (_) | || (_) | | | | | (_| | (__| < (_| | |_| | | | (_) | | | | |_| |_| |_|\___/ \__\___/ |_| |_|\__,_|\___|_|\_\__,_|\__|_| |_|\___/|_| |_| //////////////////////////////////////////////////////////////////// // _ooOoo_ // // o8888888o // // 88" . "88 // // (| ^_^ |) // // O\ = /O // // ____/`---'\____ // // .' \\| |// `. // // / \\||| : |||// \ // // / _||||| -:- |||||- \ // // | | \\\ - /// | | // // | \_| ''\---/'' | | // // \ .-\__ `-` ___/-. / // // ___`. .' /--.--\ `. . ___ // // ."" '< `.___\_<|>_/___.' >'"". // // | | : `- \`.;`\ _ /`;.`/ - ` : | | // // \ \ `-. \_ __\ /__ _/ .-` / / // // ========`-.____`-.___\_____/___.-`____.-'======== // // `=---=' // // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // // 佛祖保佑 永不宕机 永无BUG // //////////////////////////////////////////////////////////////////// :: Spring Boot :: ${spring-boot.version} 启动项目,控制台输出: 关闭banner打印 方式一:在项目主类中添加设置 public static void main(String[] args) { SpringApplication application=new SpringApplication(Application.class); /** * OFF G关闭 * CLOSED 后台控制台输出,默认就是这种 * LOG 日志输出 */ application.setBannerMode(Banner.Mode.OFF); application.run(args); } 方式二:在application.yml配置文件中配置也行 spring: main: banner-mode: off 推荐的ASCII字符图案生成网站 http://www.network-science.de/ascii/http://patorjk.com/software/taag/ 集成Mybatisplus 添加 Mybatisplus ,druid, Oracle数据库驱动依赖 ,这里数据库用Oracle12cMybatisplus 安装文档参考:https://mp.baomidou.com/guide/install.html#release <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.8</version> </dependency> <!-- oracle7 --> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc7</artifactId> <version>12.1.0.2</version> </dependency> <!-- mybatis-plus 引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis- Spring--> <!-- <dependency> mvc引入的包 <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.1.0</version> </dependency> --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> WARNING : 引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis-Spring,以避免因版本差异导致的问题。 配置扫描mapper的注解 @SpringBootApplication @MapperScan("com.nqmysb.scaffold.mapper.*") public class SpringbootScaffoldApplication { public static void main(String[] args) { SpringApplication.run(SpringbootScaffoldApplication.class, args); } } MyBatis-Plus代码生成器整合 添加依赖 <!-- mybatis-plus 代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.1.0</version> </dependency> <!-- mybatis-plus 代码生成器的模板引擎 默认是velocity --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.1</version> </dependency> 修改模板引擎注意!如果您选择了非默认引擎,需要在 AutoGenerator 中 设置模板引擎 AutoGenerator generator = new AutoGenerator(); // set freemarker engine generator.setTemplateEngine(new FreemarkerTemplateEngine()); // set beetl engine generator.setTemplateEngine(new BeetlTemplateEngine()); // set custom engine (reference class is your custom engine class) generator.setTemplateEngine(new CustomTemplateEngine()); 配置数据源这里使用的是oracle数据库 官方实例用的是mysql DataSourceConfig dsc = new DataSourceConfig(); dsc.setDbType(DbType.ORACLE); dsc.setTypeConvert(new OracleTypeConvert()); dsc.setDriverName("oracle.jdbc.driver.OracleDriver"); dsc.setUsername("LC_TEST"); dsc.setPassword("LC_TEST"); dsc.setUrl("jdbc:oracle:thin:@192.168.1.102:1521:orclpdb"); mpg.setDataSource(dsc); 创建数据库表 create table T_USER ( userId VARCHAR2(60) not null, userName VARCHAR2(60), fullName VARCHAR2(60), email VARCHAR2(60), mobile VARCHAR2(60), status VARCHAR2(5) ); 运行generator生成代码 Mapper生成没有方法,因为继承了BaseMapper的方法 mapper记得加上@Mapper注解不然会报错 Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.nqmysb.scaffold.user.mapper.TUserMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} 项目主配置 application.properties server.port=8080 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.url=jdbc:oracle:thin:@//192.168.8.150:1521/orclpdb spring.datasource.username=LC_TEST spring.datasource.password=LC_TEST spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver spring.datasource.max-idle=10 spring.datasource.max-wait=10000 spring.datasource.min-idle=5 spring.datasource.initial-size=5 测试接口 在controller里面写查询方法测试接口 /** * <p> * 前端控制器 * </p> * * @author liaocan * @since 2019-04-07 */ @Controller @RequestMapping("/user/t-user") public class TUserController { @Autowired private TUserServiceImpl TUserService; @RequestMapping("/getUser") @ResponseBody public TUser getUsers() { TUser data = TUserService.getById("007"); System.out.println(data.getMobile()+"----"); return data; } } 启动运行项目,http://localhost:8080/user/t-user/getUser 访问接口至此,springboot2.0整合Mybatis3.0,并实现代码生成器完毕!

优秀的个人博客,低调大师

linux中利用docker和docker-compose搭建lnmp环境详解 10分钟快速完成

本文主要包括部分注意事项 重要信息提示快速执行安装的纯命令相关介绍配置文件,参考地址1.要求说明: linux, 安装了docker和docker compose 特别注意: 本文中提及的密码与本文的配置文件可能不一致(与新详情参考文中的密码同:123456),简单改成了mima123456, 文中用的123456在进行说明 windows环境中,项目中mysql的连接主机为 172...和localhost都连接失败 (Linux中用宿主机ip连接成功--没探究原因,欢迎评论补全)安装成功后, 我发现项目连接 数据库的时候连接不了, 一直是 Connection timed out(2002) ,但是我使用 cmd , navicate, 都能够连接, 而且 localhost, 172.0.0.1, 和对应的docker ip 都是可以连接的, 就是php项目中无法连接 一直提示 连接超时. 项目运行正常, 我的其他项目连接的其他服务器的数据库也是访问正常的.linux环境未遇到此问题,直接用服务器这个宿主机的ip就链接成功了的windows解决记录:项目连接数据库成功了 使用如下 注意项目中数据库主机用 host=mysql(我在linux中用的宿主机ip是可以直接访问数据库的,windows失败了)$pdo = new PDO(‘mysql:host=mysql;dbname=site1’, ‘root’, ‘123456’);。或者使用$connect = mysqli_connect(‘mysql’,’root’,’123456′,’wordpress’,’3306′);(注意 是 mysqli_connect, 不是 mysql_connect)另外:--强烈建议修改sql密码,配置文件中默认密码公开的注意: 如果本地有phpstudy等环境, 注意关闭环境, 并且注意关闭mysql进程(关闭phpstudy可能还有mysql进程存在), 我的windows遇到了这个问题. 修改项目路径为自己已有项目路径(如果有,比如安装过PHPstudy的www目录),默认下载的配置文件目录下的www注意:配置中localhost在www/site1中,请修改site的域名配置或者localhost的配置指向环境安装过后修改yml配置的数据库密码未能生效, 没找到解决办法, 间接解决方法是: 重新下载配置文件用新的命名,重新执行环境参考链接中的下载的配置文件的版本号3执行失败 改成2就可以了 改成1页失败了的注意:如果本地有phpstudy等环境, 注意关闭环境, 并且注意关闭mysql进程(关闭phpstudy可能还有mysql进程存在), 否则启动会失败2.快速直接安装:直接执行命令即可! 注意 个人强烈建议先根据下面贴出的个人配置中的注释进行调整修改直接执行命令即可!(命令中有#开头的是注释部分 可以一起复制直接执行, 留意过程中是否会报错, 环境电脑不同 可能会遇到问题 ,相应解决. 我的一切顺利) #如自己沒有什麽特殊要求,比如项目路径,mysql密码等,就直接执行命令即可! git clone https://github.com/yeszao/dnmp.git docker-compose up #如果有错,检查,是否需要做上面的注意事项中说的修改版本号 #结果:然后在浏览器中访问localhost,就可以看到页面 #命令结束 如果有,参考这里: git clone https://github.com/yeszao/dnmp.git 然后做上面的注意事项中说的修改版本号,根据下面提供的配置文件 然后执行下面合格命令即可 docker-compose up # 特别注意, 我们的PHP代码需要某个目录的写权限。比如,Wordpress的wp-content/uploads目录,只有写权限我们才能正常上传文件,图片等. #如果有错,检查,是否需要做上面的注意事项中说的修改版本号 #docker exec -it dlnmp_php-fpm_1 /bin/bash #chown -R www-data:www-data /var/www/html #但是我执行上面的失败了, 我的解决: 直接让我们服务器的宿主机中的目录可以写就可以, 因为项目目录在宿主机中. #相应目录执行命令 (注意, WordPress我发现上传目录没有 是我自己建的, 比如wordpress/wp-content/下面建一个uploads目录,然后执行) chmod -R a+w ./wordpress/wp-content/uploads # (对应目录-我的是这个相对路径) #命令结束 结果:如果使用的配置中的默认项目目录在浏览器中访问localhost,就可以看到页面 ,否则需要根据下面配置文件中的提示进行修改配置或者目录新建.3.错误记录:环境安装过后修改yml配置的数据库密码未能生效, 没找到解决办法, 间接解决方法是: 重新下载配置文件用新的命名,重新执行环境尝试过 2种方法2.1.docker-compose build 然后 docker-compose up -dbuild会提示:.....Successfully built 6a55df4e9d16redis uses an image, skippingnginx uses an image, skippingmysql uses an image, skipping2.2.(以下是部分命令记录,修改的mysql密码均失败)docker-compose down 18 docker-compose build 19 docker-compose ps 20 docker-compose up -h 21 docker-compose up --build -d 22 docker-compose up --build -d --force-recreate 23 docker-compose ps 24 docker-compose down 25 docker-compose up -d --force-recreate(根据 docker-compose -h 参考命令执行的)4.详细说明:项目目录,项目路径指定了下载文件夹内的www目录, 如需改成其他目录或自己原有目录,直接修改目录下的文件 docker-compose.yml 内的全部 – ./www 为自己的目录, 比如 – F:/phpStudy2018/www(绝对路径) 或者 – ../phpStudy2018/www(相对路径)mysql密码windows环境中,项目中mysql的连接主机为 172...和localhost都连接失败 (Linux中用宿主机ip连接成功--没探究原因,欢迎评论补全)安装成功后, 我发现项目连接 数据库的时候连接不了, 一直是 Connection timed out(2002) ,但是我使用 cmd , navicate, 都能够连接, 而且 localhost, 172.0.0.1, 和对应的docker ip 都是可以连接的, 就是php项目中无法连接 一直提示 连接超时. 项目运行正常, 我的其他项目连接的其他服务器的数据库也是访问正常的.我修改后添加备注后的基础配置文件内容 # 注意 这里clone的配置文件中是3 我的执行失败 我改成2就可以了 ,1也不行 各自根据情况进行修改 version: "2" services: nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: # 修改此处 ./www/为自己的项目路径, 如果没有可以自定义 ,这里使用的是相对路径 就是git clone下来的目录中的www目录 - ./www/:/var/www/html/:rw - ./conf/conf.d:/etc/nginx/conf.d/:ro # 这里就是站点域名的配置文件 在相对路径 conf/conf.d/ 下面(注意 linux是反斜杠) clone下来的配置目录中的www目录下已经有了site1 和2 的目录,所以访问没问题, 但是如果换成自己的项目目录, 就需要自己新建site1目录(因为site1配置文件中把localhost配置到了site1目录下了)或者自己修改site1里面的配置,把localhost指向到自己的项目目录, - ./conf/nginx.conf:/etc/nginx/nginx.conf:ro - ./log/:/var/log/dnmp/:rw networks: - net-php php: build: ./php/php72/ expose: - "9000" volumes: # 修改此处 ./www/为自己的项目路径, 如果没有可以自定义 ,这里使用的是相对路径 就是git clone下来的目录中的www目录 - ./www/:/var/www/html/:rw - ./conf/php.ini:/usr/local/etc/php/php.ini:ro # 这里就是站点域名的配置文件 在相对路径 conf/conf.d/ 下面(注意 linux是反斜杠) clone下来的配置目录中的www目录下已经有了site1 和2 的目录,所以访问没问题, 但是如果换成自己的项目目录, 就需要自己新建site1目录(因为site1配置文件中把localhost配置到了site1目录下了)或者自己修改site1里面的配置,把localhost指向到自己的项目目录, - ./conf/php-fpm.conf:/usr/local/etc/php-fpm.d/www.conf:rw - ./log/:/var/log/dnmp/:rw networks: - net-php - net-mysql - net-redis mysql: image: mysql:8.0 ports: - "3306:3306" volumes: - ./conf/mysql.cnf:/etc/mysql/conf.d/mysql.cnf:ro - ./mysql/:/var/lib/mysql/:rw networks: - net-mysql environment: # // todo: 修改为自己的密码,此密码github中公开的 MYSQL_ROOT_PASSWORD: "mima123456" redis: image: redis:4.0 networks: - net-redis ports: - "6379:6379" phpmyadmin: image: phpmyadmin/phpmyadmin:latest ports: - "8080:80" networks: - net-mysql environment: - PMA_HOST=mysql - PMA_PORT=3306 phpredisadmin: image: erikdubbelboer/phpredisadmin:latest ports: - "8081:80" networks: - net-redis environment: - REDIS_1_HOST=redis - REDIS_1_PORT=6379 networks: net-php: net-mysql: net-redis: 配置说明详细参考地址http://www.cnblogs.com/xishuai/p/docker-compose.html部分命令参考:YAML 配置命令 配置 说明build 指定 Dockerfile 所在的目录地址,用于构建镜像,并使用此镜像创建容器,比如上面配置的build: .command 容器的执行命令dns 自定义 dns 服务器expose 暴露端口配置,但不映射到宿主机,只被连接的服务访问extends 对docker-compose.yml的扩展,配置在服务中image 使用的镜像名称或镜像 IDlinks 链接到其它服务中的容器(一般桥接网络模式使用)net 设置容器的网络模式(四种:bridge,none,container:[name or id]和host)ports 暴露端口信息,主机和容器的端口映射volumes 数据卷所挂载路径设置Docker Compose 常用命令 命令 说明docker-compose build 构建项目中的镜像,--force-rm:删除构建过程中的临时容器;--no-cache:不使用缓存构建;--pull:获取最新版本的镜像docker-compose up -d 构建镜像、创建服务和启动项目,-d表示后台运行docker-compose run ubuntu ls -d 指定服务上运行一个命令,-d表示后台运行docker-compose logs 查看服务容器输出日志docker-compose ps 列出项目中所有的容器docker-compose pause [service_name] 暂停一个服务容器docker-compose unpause [service_name] 恢复已暂停的一个服务容器docker-compose restart 重启项目中的所有服务容器(也可以指定具体的服务)docker-compose stop 停止运行项目中的所有服务容器(也可以指定具体的服务)docker-compose start 启动已经停止项目中的所有服务容器(也可以指定具体的服务)docker-compose rm 删除项目中的所有服务容器(也可以指定具体的服务),-f:强制删除(包含运行的)docker-compose kill 强制停止项目中的所有服务容器(也可以指定具体的服务)参考来源:http://www.cnblogs.com/xishuai/p/docker-compose.html 文末 阿里云服务器在搞限时活动优惠卷, 需要的自取. 注意, 领取的优惠券30天内有效,尽快使用!领取链接:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=s306eooi&utm_source=s306eooi

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册