https://brandnewuser.iteye.com/blog/2305140

lucene中的TokenStream,TokenFilter之间关系
TokenStream是一个能够在被调用后产生语汇单元序列的类,其中有两个类型:Tokenizer和TokenFilter,两者的不同在于TokenFilter中包含了一个TokenStream作为input,该input仍然可以为一种TokenFilter进行递归封装,是一种组合模式;而Tokenzier接受一个Reader对象读取字符并创建语汇单元,TokenFilter负责处理输入的语汇单元,通过新增、删除或者修改属性的方式来产生新的语汇单元。


 
对照我们之前分析的同义词TokenizerFactory相关配置,其数据流的过程如下:
Java代码  
  1. java.io.Reader -> com.chenlb.mmseg4j.solr.MMSegTokenizer -> SynonymFilter -> StopFilter -> WordDelimiterFilter -> LowerCaseFilter -> RemoveDuplicatesTokenFilter
对于某些TokenFilter来说,在分析过程中对事件的处理顺序非常重要。当指定过滤操作顺序时,还应该考虑这样的安排对于应用程序性能可能造成的影响。
在solr中,schema.xml(最新版本已经修改为managed-schema)的作用是告诉solr该如何对输入的文档进行索引。
http://www.liaozhida.net/solr/solr%E7%B3%BB%E5%88%97%E4%B8%83%E8%AF%A6%E8%A7%A3schema-xml%E7%89%B9%E6%80%A7.html
对于每个不同的field,需要设置其对应的数据类型,数据类型决定了solr如何去解释每个字段,以及怎样才能搜索到这个字段。在字段分析器中(field analyzers),指导solr怎样对输入的数据进行处理然后再构建出索引,类似于文本处理器或者文本消化器。
当一个document被索引或者检索操作的时候,分析器Analyzer会审阅字段field的文本内容,然后生成一个token流,analyzer可以由多个tokenizer和filter组成;tokenizer可以将field字段的内容切割成单个词或token,进行分词处理;filters可以接收tokenizer分词输出的token流,进行转化过滤处理,例如对词元进行转换(简繁体转换),舍弃无用词元(虚词谓词)。tokenizer和filter一起组成一个管道或者链条,对输入的文档和输入的查询文本进行处理,一系列的tokenizer和filter被称为分词器analyzer,得到的结果被存储成为索引字典用来匹配查询输入条件。
此外,我们还可以将索引分析器和查询分析器分开,例如下面的字段配置的意思:对于索引,先经过一个基本的分析器,然后转换为小写字母,接着过滤掉不在keepword.txt中的词,最后将剩下的词元转换为同义词;对于查询,先经过一个基本的分词器,然后转换为小写字母就可以了。
Java代码  
  1. <fieldType name="nametext" class="solr.TextField">
  2. <analyzer type="index">
  3. <tokenizer class="solr.StandardTokenizerFactory"/>
  4. <filter class="solr.LowerCaseFilterFactory"/>
  5. <filter class="solr.KeepWordFilterFactory" words="keepwords.txt"/>
  6. <filter class="solr.SynonymFilterFactory" synonyms="syns.txt"/>
  7. </analyzer>
  8. <analyzer type="query">
  9. <tokenizer class="solr.StandardTokenizerFactory"/>
  10. <filter class="solr.LowerCaseFilterFactory"/>
  11. </analyzer>
  12. </fieldType>

在Lucene实战一书中,详解了如何从头编写一个同义词Analyzer,通过改写termAttribute以及positionIncrementAttribute的方式来达到实现同义词的方式,不过由于书上的示例比较陈旧,而charTermAttribute不能达到修改同义词元的目的(只能进行append),因此替换最终的目的没有达到。
Java代码  
  1. public class SynonymFilter extends TokenFilter {
  2. private static final String TOKEN_TYPE_SYNONYM = "SYNONYM";
  3. private Stack<String> synonymStack;
  4. private SynonymEngine synonymEngine;
  5. private AttributeSource.State current;
  6. private final CharTermAttribute bytesTermAttribute;
  7. private final PositionIncrementAttribute positionIncrementAttribute;
  8. /**
  9. * Construct a token stream filtering the given input.
  10. *
  11. * @param input
  12. */
  13. protected SynonymFilter(TokenStream input, SynonymEngine synonymEngine) {
  14. super(input);
  15. this.synonymEngine = synonymEngine;
  16. synonymStack = new Stack<>();
  17. this.bytesTermAttribute = addAttribute(CharTermAttribute.class);
  18. this.positionIncrementAttribute = addAttribute(PositionIncrementAttribute.class);
  19. }
  20. @Override
  21. public boolean incrementToken() throws IOException {
  22. if (!synonymStack.isEmpty()) {
  23. String syn = synonymStack.pop();
  24. restoreState(current);
  25. //            bytesTermAttribute.setBytesRef(new BytesRef(syn.getBytes()));
  26. //            bytesTermAttribute.resizeBuffer(0);
  27. bytesTermAttribute.append(syn);
  28. positionIncrementAttribute.setPositionIncrement(0);
  29. return true;
  30. }
  31. if (!input.incrementToken()) {
  32. return false;
  33. }
  34. if (addAliasesToStack()) {
  35. current = captureState();
  36. }
  37. return true;
  38. }
  39. private boolean addAliasesToStack() throws IOException {
  40. String[] synonyms = synonymEngine.getSynonyms(bytesTermAttribute.toString());
  41. if (synonyms == null) {
  42. return false;
  43. }
  44. for (String synonym : synonyms) {
  45. synonymStack.push(synonym);
  46. }
  47. return true;
  48. }
  49. }
Analyzer,用于将tokenizer和filter串联起来:
Java代码  
  1. public class SynonymAnalyzer extends Analyzer {
  2. @Override
  3. protected TokenStreamComponents createComponents(String fieldName) {
  4. StandardTokenizer source = new StandardTokenizer();
  5. return new TokenStreamComponents(source, new SynonymFilter(new StopFilter(new LowerCaseFilter(source),
  6. new CharArraySet(StopAnalyzer.ENGLISH_STOP_WORDS_SET, true)), new TestSynonymEngine()));
  7. }
  8. }
我们定义一个简易的同义词匹配引擎:
Java代码  
  1. public interface SynonymEngine {
  2. String[] getSynonyms(String s) throws IOException;
  3. }
  4. public class TestSynonymEngine implements SynonymEngine {
  5. public static final Map<String, String[]> map = new HashMap<>();
  6. static {
  7. map.put("quick", new String[]{"fast", "speedy"});
  8. }
  9. @Override
  10. public String[] getSynonyms(String s) throws IOException {
  11. return map.get(s);
  12. }
  13. }
对最终结果进行测试:
Java代码  
  1. public static void main(String[] args) throws IOException {
  2. SynonymAnalyzer analyzer = new SynonymAnalyzer();
  3. TokenStream tokenStream = analyzer.tokenStream("contents", new StringReader("The quick brown fox"));
  4. tokenStream.reset();
  5. CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
  6. OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
  7. PositionIncrementAttribute positionIncrementAttribute =
  8. tokenStream.addAttribute(PositionIncrementAttribute.class);
  9. TypeAttribute typeAttribute = tokenStream.addAttribute(TypeAttribute.class);
  10. int position = 0;
  11. while (tokenStream.incrementToken()) {
  12. int positionIncrement = positionIncrementAttribute.getPositionIncrement();
  13. if (positionIncrement > 0) {
  14. position += positionIncrement;
  15. System.out.println();
  16. System.out.print(position + " : ");
  17. }
  18. System.out.printf("[%s : %d ->  %d : %s]", charTermAttribute.toString(), offsetAttribute.startOffset(), offsetAttribute.endOffset(),
  19. typeAttribute.type());
  20. }
测试出的结果,可以看出位置1的谓词the已经被剔除,位置2处加入了较多的同义词,由于使用的append,所以同义词记在了一起。
Java代码  
  1. 2 : [quick : 4 ->  9 : <ALPHANUM>][quickspeedy : 4 ->  9 : <ALPHANUM>][quickfast : 4 ->  9 : <ALPHANUM>]
  2. 3 : [brown : 10 ->  15 : <ALPHANUM>]
  3. 4 : [fox : 16 ->  19 : <ALPHANUM>]

Solr同义词设置
Solr中的同义词使用的是 SynonymFilterFactory 来进行加载的,我们需要在定义schema时,对某个字段设置同义词时,可以使用:
Java代码  
  1. <fieldtype name="textComplex" class="solr.TextField" positionIncrementGap="100">
  2. <analyzer type="index">
  3. <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="/Users/mazhiqiang/develop/tools/solr-5.5.0/server/solr/product/conf/dic" />
  4. <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt"/>
  5. <filter class="solr.WordDelimiterFilterFactory"/>
  6. <filter class="solr.LowerCaseFilterFactory"/>
  7. <filter class="solr.NGramFilterFactory" minGramSize="1" maxGramSize="20"/>
  8. <filter class="solr.StandardFilterFactory"/>
  9. </analyzer>
  10. <analyzer type="query">
  11. <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="/Users/mazhiqiang/develop/tools/solr-5.5.0/server/solr/product/conf/dic" />
  12. <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
  13. <filter class="solr.StopFilterFactory" ignoreCase="false" words="stopwords.txt"/>
  14. <filter class="solr.WordDelimiterFilterFactory"/>
  15. <filter class="solr.LowerCaseFilterFactory"/>
  16. <!--  <filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="20"/> -->
  17. <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
  18. </analyzer>
  19. </fieldtype>
需要配置对应的 synonyms 属性,指定 定义同义词的配置文件,设置是否忽略大小写等属性。
而在加载同义词时,对文件进行逐行读取(使用LineNumberReader),对于每一行的数据,先使用 => 作为分隔符,同义词在左右两边(左边作为input,右边作为output)都可以配置成多个,以逗号分隔,最后以笛卡尔积的形式将其放至map中。
Java代码  
  1. String line = null;
  2. while ((line = in.readLine()) != null) {
  3. if (line.length() == 0 || line.charAt(0) == '#') {
  4. continue; // ignore empty lines and comments
  5. }
  6. // TODO: we could process this more efficiently.
  7. String sides[] = split(line, "=>");
  8. if (sides.length > 1) { // explicit mapping
  9. if (sides.length != 2) {
  10. throw new IllegalArgumentException("more than one explicit mapping specified on the same line");
  11. }
  12. String inputStrings[] = split(sides[0], ",");
  13. CharsRef[] inputs = new CharsRef[inputStrings.length];
  14. for (int i = 0; i < inputs.length; i++) {
  15. inputs[i] = analyze(unescape(inputStrings[i]).trim(), new CharsRefBuilder());
  16. }
  17. String outputStrings[] = split(sides[1], ",");
  18. CharsRef[] outputs = new CharsRef[outputStrings.length];
  19. for (int i = 0; i < outputs.length; i++) {
  20. outputs[i] = analyze(unescape(outputStrings[i]).trim(), new CharsRefBuilder());
  21. }
  22. // these mappings are explicit and never preserve original
  23. for (int i = 0; i < inputs.length; i++) {
  24. for (int j = 0; j < outputs.length; j++) {
  25. add(inputs[i], outputs[j], false);
  26. }
  27. }
所有的同义词加载完成后,会生成一个SynonymMap,该map就被用来在全文检索的过程中进行同义词替换。
在我们对某个单词进行查询时,可以查询到我们设置的字段query分析器结构,生成一个TokenizerChain对象,对应的Tokenizer为我们设置的分词器,filters为我们设置的过滤器链条,会根据过滤器链条Chain进行

通过input的方式设置同义词Filter,组成该链条结果。
Java代码  
  1. @Override
  2. protected TokenStreamComponents createComponents(String fieldName) {
  3. Tokenizer tk = tokenizer.create();
  4. TokenStream ts = tk;
  5. for (TokenFilterFactory filter : filters) {
  6. ts = filter.create(ts);
  7. }
  8. return new TokenStreamComponents(tk, ts);
  9. }
而具体到每个FilterFactory,例如SynonymFilterFactory,都通过create方法来创建对应的Filter用于同义词过滤。
Java代码  
  1. @Override
  2. public TokenStream create(TokenStream input) {
  3. // if the fst is null, it means there's actually no synonyms... just return the original stream
  4. // as there is nothing to do here.
  5. return map.fst == null ? input : new SynonymFilter(input, map, ignoreCase);
  6. }
创建一个SynonymFilter来进行最后真正的筛选,将同义词进行替换,整体的类结构图如下:


 

lucene内置的Token
lucene中除了内置的几个Tokenizer,在solr中的field analyzer以及index中也得到了应用,下面就对这几种filter进行测试,我们分析的文本为:Please email clark.ma@gmail.com by 09, re:aa-bb
StandardAnalyzer
1 : [please : 0 ->  6 : <ALPHANUM>]
2 : [email : 7 ->  12 : <ALPHANUM>]
3 : [clark.ma : 13 ->  21 : <ALPHANUM>]
4 : [gmail.com : 22 ->  31 : <ALPHANUM>]
6 : [09 : 35 ->  37 : <NUM>]
7 : [re:aa : 39 ->  44 : <ALPHANUM>]
8 : [bb : 45 ->  47 : <ALPHANUM>]
去除空格,标点符号,@;
ClassicAnalyzer
1 : [please : 0 ->  6 : <ALPHANUM>]
2 : [email : 7 ->  12 : <ALPHANUM>]
3 : [clark.ma@gmail.com : 13 ->  31 : <EMAIL>]
5 : [09 : 35 ->  37 : <ALPHANUM>]
6 : [re : 39 ->  41 : <ALPHANUM>]
7 : [aa : 42 ->  44 : <ALPHANUM>]
8 : [bb : 45 ->  47 : <ALPHANUM>]
能够识别互联网域名和email地址,
LetterTokenizer
1 : [Please : 0 ->  6 : word]
2 : [email : 7 ->  12 : word]
3 : [clark : 13 ->  18 : word]
4 : [ma : 19 ->  21 : word]
5 : [gmail : 22 ->  27 : word]
6 : [com : 28 ->  31 : word]
7 : [by : 32 ->  34 : word]
8 : [re : 39 ->  41 : word]
9 : [aa : 42 ->  44 : word]
10 : [bb : 45 ->  47 : word]
丢弃掉所有的非文本字符
KeywordTokenizer
1 : [Please email clark.ma@gmail.com by 09, re:aa-bb : 0 ->  47 : word]
将整个文本当做一个词元
LowerCaseTokenizer
1 : [please : 0 ->  6 : word]
2 : [email : 7 ->  12 : word]
3 : [clark : 13 ->  18 : word]
4 : [ma : 19 ->  21 : word]
5 : [gmail : 22 ->  27 : word]
6 : [com : 28 ->  31 : word]
7 : [by : 32 ->  34 : word]
8 : [re : 39 ->  41 : word]
9 : [aa : 42 ->  44 : word]
10 : [bb : 45 ->  47 : word]
对其所有非文本字符,过滤空格,标点符号,将所有的大写转换为小写
NGramTokenizer
可以定义最小minGramSize(default=1), 最大切割值maxGramSize(default=2),生成的词元较多。
假设minGramSize=2, maxGramSize=3,输入abcde,输出:ab abc abc bc bcd cd cde
读取字段并在给定范围内生成多个token
PathHierachyTokenizer
c:\my document\filea\fileB,new PathHierarchyTokenizer('\\', '/')
1 : [c: : 0 ->  2 : word][c:/my document : 0 ->  14 : word][c:/my document/filea : 0 ->  20 : word][c:/my document/filea/fileB : 0 ->  26 : word]
使用新的文件目录符去代替文本中的目录符
PatternTokenizer
需要两个参数,pattern正则表达式,group分组。
pattern=”[A-Z][A-Za-z]*” group=”0″
输入: “Hello. My name is Inigo Montoya. You killed my father. Prepare to die.”
输出: “Hello”, “My”, “Inigo”, “Montoya”, “You”, “Prepare”
进行正则表达式分组匹配
UAX29URLEmailTokenizer
1 : [Please : 0 ->  6 : <ALPHANUM>]
2 : [email : 7 ->  12 : <ALPHANUM>]
3 : [clark.ma@gmail.com : 13 ->  31 : <EMAIL>]
4 : [by : 32 ->  34 : <ALPHANUM>]
5 : [09 : 35 ->  37 : <NUM>]
6 : [re:aa : 39 ->  44 : <ALPHANUM>]
7 : [bb : 45 ->  47 : <ALPHANUM>]
去除空格和标点符号,但保留url和email连接
Lucene内置的TokenFilter
过滤器能够组成一个链表,每一个过滤器处理上一个过滤器处理过后的词元,所以过滤器的排序很有意义,第一个过滤器最好能处理大部分常规情况,最后一个过滤器是带有针对特殊性的。
ClassicFilter “I.B.M. cat’s can’t” ==> “I.B.M”, “cat”, “can’t” 经典过滤器,可以过滤无意义的标点,需要搭配ClassicTokenizer使用
ApostropheFilter
1 : [abc : 0 ->  3 : <ALPHANUM>]
2 : [I.B.M : 4 ->  9 : <ALPHANUM>]
3 : [cat : 10 ->  15 : <ALPHANUM>]
4 : [can : 16 ->  21 : <ALPHANUM>]
省略所有的上撇号
LowerCaseFilter
1 : [i.b.m : 0 ->  5 : <ALPHANUM>]
2 : [cat's : 6 ->  11 : <ALPHANUM>]
3 : [can't : 12 ->  17 : <ALPHANUM>]
转换成小写
TypeTokenFilter
<filter class=”solr.TypeTokenFilterFactory” types=”email_type.txt” useWhitelist=”true”/>
如果email_type.txt设置为ALPHANUM,会保留该类型的所有分析结果,否则会被删除掉
给定一个文件并设置成白名单还是黑名单,只有符合条件的type才能被保留
TrimFilter   去掉空格
TruncateTokenFilter
1 : [I.B : 0 ->  5 : <ALPHANUM>]
2 : [cat : 6 ->  11 : <ALPHANUM>]
3 : [can : 12 ->  17 : <ALPHANUM>]
截取文本长度,左边为prefixLength=3
PatternCaptureGroupFilter 可配置属性pattern和preserve_original(是否保留原文) 从输入文本中保留能够匹配正则表达式的
PatternReplaceFilter    
StopFilter   创建一个自定义的停词词库列表,过滤器遇到停词就直接过滤掉
KeepWordFilter 与StopFilter的含义正好相反  
LengthFilter 设置一个最小值min和最大值max 为词元的长度设置在一个固定范围
WordDelimiterFilter

A:-符号 wi-fi 变成wi fi
B:驼峰写法 LoveSong 变成 love song 对应参数
C:字母-数字 xiaomi100 变成 xiaomi 100
D:–符号 like–me 变成 like me
E:尾部的’s符号 mother’s 变成 mother
F:-符号 wi-fi 变成 wifi 于规则A不同的是没有分成两个词元
G:-符号,数字之间 400-884586 变成 400884586
H:-符号 无论字母还是数字,都取消-符号 wi-fi-4 变成wifi4

其他参数
splitOnCaseChange=”1″ 默认1,关闭设为0 规则B
generateWordParts=”1″ 默认1 ,对应规则AB
generateNumberParts=”1″ 默认1 对应规则F
catenateWords=”1″ 默认0 对应规则A
splitOnNumerics=”1″ 默认1,关闭设0 规则C
stemEnglishPossessive 默认1,关闭设0 规则E
catenateNumbers=”1″ 默认0 对应规则G
catenateAll=”1″ 默认0 对应规则 H
preserveOriginal=”1″ 默认0 对词元不做任何修改 除非有其他参数改变了词元

protected=”protwords.txt” 指定这个单词列表的单词不被修改

通过分隔符分割单元

转载于:https://www.cnblogs.com/davidwang456/articles/10470938.html

Lucene 中的Tokenizer, TokenFilter学习相关推荐

  1. 理解Lucene中的Analyzer

    学习一个库,最好去官网.因为很多库API变动十分大,从博客上找的教程都过时了. Lucene原理就是简简单单的"索引",以空间换时间.但是Lucene将这件事做到了极致,后人再有想 ...

  2. Lucene中的同义词

    Lucene中的同义词 Lucene的TokenFilter中,有SynonymFilter和SynonymGraphFilter两种来处理同义词. SynonymFilter不能很好的处理多词同义词 ...

  3. BERT中的Tokenizer说明

    BERT中的Tokenizer说明 预训练BERT的Tokenizer有着强大的embedding的表征能力,基于BERT的Tokenizer的特征矩阵可以进行下游任务,包括文本分类,命名实体识别,关 ...

  4. CVPR2020:点云分析中三维图形卷积网络中可变形核的学习

    CVPR2020:点云分析中三维图形卷积网络中可变形核的学习 Convolution in the Cloud: Learning Deformable Kernels in 3D Graph Con ...

  5. 读后感与机翻《从视频中推断力量和学习人类效用》

    以下是研究朱松纯FPICU概念中U(utility)的相关论文记录: 读后感: 作者干了什么事? (1)算法能够预测当人们与物体交互时,身体各个部位(臀部.背部.头部.颈部.手臂.腿等)所承受的力/压 ...

  6. Blender中的主程序纹理学习课程 Master Procedural Texturing in Blender

    挖掘Blender不可思议的强大节点编辑器的无限潜力. 你会学到: 逐步构建高级和高度可定制的程序纹理. 将许多不同层次的细节结合成一个复杂而现实的结果. 从头开始构建高级程序纹理背后的思维过程. 使 ...

  7. UE5虚幻引擎5中的实时特效学习 Introduction to real time FX in Unreal Engine 5

    MP4 |视频:h264,1280×720 |音频:AAC,44.1 KHz,2 Ch 语言:英语+中英文字幕(根据原英文字幕机译更准确) |时长:40节课(3h 36m) |大小解压后:2.65 G ...

  8. MySQL中的联合索引学习教程

    MySQL中的联合索引学习教程 这篇文章主要介绍了MySQL中的联合索引学习教程,其中谈到了联合索引对排序的优化等知识点,需要的朋友可以参考下 联合索引又叫复合索引.对于复合索引:Mysql从左到右的 ...

  9. python中字典和集合的区别_Python中字典和集合学习小结

    映射类型: 表示一个任意对象的集合,且可以通过另一个几乎是任意键值的集合进行索引 与序列不同,映射是无序的,通过键进行索引 任何不可变对象都可用作字典的键,如字符串.数字.元组等 包含可变对象的列表. ...

最新文章

  1. openStack调试
  2. 从零开始React:一档 React环境搭建,语法规则,基础使用
  3. jquery中的each各种神奇遍历用法
  4. TensorFlow 2.0 - Keras Pipeline、自定义Layer、Loss、Metric
  5. 对谈|人工智能来了,翻译们会失业吗?
  6. 信息学奥赛C++语言:统计数字字符个数
  7. MYSQL DELETE 别名
  8. 求链表是否有环和第一个交点
  9. 静态成员变量以及静态成员函数
  10. mysql课件_MYSQL讲课时的PPT课件.ppt
  11. gohost -- go 开发的命令行hosts配置管理工具
  12. JS编程练习题(javascript)
  13. Java Caledar类(日历类)判断本周周数
  14. 毕业软件测试论文大纲,测试论文大纲模板范本 测试论文提纲怎样写
  15. HTML元素居中的方法
  16. 明哥,给大学生的几点建议
  17. 一次和前端的相互甩锅的问题记录
  18. Hololens2开机无法启动无法开机问题
  19. Android 播放视频
  20. 记录下在线扩容服务器遇到的问题 NOCHANGE: partition 1 is size 419428319. it cannot be grown

热门文章

  1. numpy随机生成01矩阵_NumPy数组基本介绍
  2. linux连接磁盘阵列,CentOS/Linux 连接 iSCSI 磁盘阵列
  3. matlab usewhitebg,我有一个matlab的程序运行出错,各位大神求救,很急啊
  4. python计算汽车的平均油耗_汽车行车电脑中的平均油耗是按哪个行驶里程计算的?...
  5. 13 登陆_13级!凌晨,“黑格比”登陆!对莆田的最新影响……
  6. oracle union详解,Oracle中的union和join
  7. 燃烧温度计算程序_【知识库】燃气燃烧器如何安全操作?
  8. html如何添加关闭按钮,大神你好,请问怎么在以下代码的div中添加一个关闭按钮?...
  9. 有没有测试水泥稳定性的软件,水泥稳定碎石土7天无侧限抗压强度制件(参考模板)...
  10. Android:相对布局综合小演练—智能家居,按键快速美化的小技巧