solr.in.action-ch06

一.基本文本分析
1.Analyzer
在<fieldType>元素内,你应至少定义一个<analyzer>元素决定文本是如何被分析的.实际上,通常定义两个分离的<analyzer>元素,一个用于索引,一个用于用户搜索时输入文本的分析.text_general字段类型就使用了这种方法.你经常需要另外的分析用于处理查询,超过了用于索引一个文档的需要.例如,为了避免增加索引的大小并且更易管理同义词,添加同义词通常只是在查询文本分析期间完成.
2.Tokenizer(断词器)
在solr当中,每个<analyzer>将文本分析处理分析两个阶段:tokenization和token filtering.根据百度百科,Token应理解成符文这个专业术语.我理解tokenization(符文化或断词)就是将文本解析成一个一个的单词或者原子的短语项的过程.从技术来讲,还有第三个阶段,在tokenization之前启用预处理,这样你可以应用字符过滤器.
在tokenization阶段,使用某种解析方式将文本分成符文流.最基本的tokenizer就是文本仅根据空格分割的WhitespaceTokenizer,更通用的就是StandardTokenizer,它根据空格和标点符号,执行智能解析分割词语,正确地处理URL,电子邮件地址和首字母缩略词.定义一个tokenizer,你需要为你的tokenizer指定java实现类工厂.要使用通用的StandardTokenizer,就指定solr.StandardTokenizerFactor.
在solr中,你必须指定工厂类,而不是底层的Tokenizer实现类,因为大多数tokenizer不提供默认无参构造函数.通过使用工厂方法,Solr能够使你在XML以标准的方式定义一个tokenizer.在背后,每个工厂类知道如何将XML配置属性转换成一个指定的Tokenizer实现的实例.所有的tokenizer产生一个符文流.它能被零个或多个过滤器来进行一些符文的转换的排序.
3.Token filter(符文过滤器)
在一个符文上,一个符文过滤器进行三个动作之一:
a.转换——改变符文成另一种形式,例如小写转换的所有字母或词干化
b.符文注入——添加符文到流中,使用同文词过滤器完成.
c.符文移除——移除符文,通过停用词过滤器完成.
在每一个符文上,过滤器可以链接在一起应用一系列的转换.
4.StandardTokenize提供以下功能:
a.使用空格和标准的标点符号拆分,如句号,逗号,分号等(请注意省略号...和表情:从流中删除).
b.保留互联网域名和电子邮件地址作为一个单一的标记。
c.拆分带有连字符号的词语为两个符文;例如,i-Pad变成i和Pad.
d.支持可配置的最大令牌长度属性.默认值是255.
e.除掉来自于标签和提示开头的#和@符号
5.使用StopFilterFactory移除停用词
在分析期间,Solr的StopFilterFactory从符文流移除停用词,因为他们帮助用户找到相关文档,增加了一点点的价值.在索引期间移除停用词帮助减少索引的大小,并且能改善搜索性能.因为它减少了solr要处理的文档数和包括停用词的查询相关计算中评分的词语数.定义StopFilterFactory例子
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt" />
6.LowerCaseFilterFactory--转换成小写
虽然有时一个大写名词能改善结果的精度,但是在大多数情况下,你都想要应用小写转换过滤器.如果所有小写转换,你有一个同义词列表,那么你应在应用同义词过滤器之前应用小写转换过滤器.定义LowerCaseFilterFactory例子,
<filter class="solr.LowerCaseFilterFactory"/>
7.使用solr的分析表单测试你的分析.
服务器已运行,导航到管理员控制台http://localhost:8983/solr/,单击collection1,再点击Analysis.一旦输入字段类型和文本来分析,单击Analyse Values按钮来查看结果.在这个表单,Analyse Fieldname / FieldType选text_general.Field Value (Index)输入
#Yummm :) Drinking a latte at Caffe Grecco in SF's historic North Beach... Learning text analysis with #SolrInAction by @ManningBooks on my i-Pad  然后点击Analyse Values.
注意到文本是先使用StandardTokenizer被解析的,缩写为ST,然后每个符文通过StopFilter(SF)和LowercaseFilter(LCF).下一步,在Field Value (Query)输入drinking a latte,再点Analyse Values按钮看结果,solr会高亮任何在文档匹配查询的词语.
如果再次输入San Francisco drink cafe ipad.点Analyse Values按钮,会看到没有匹配的词语


当然运行这个例子之前,不要忘记先修改schema.xml.细节要对比solr-in-action\example-docs\ch6\schema.xml,主要做如下修改
a.删除169行<field name="text".....字段定义.
b.增加以下字段定义
   <!-- Solr In Action: Chapters 5 and 6 -->
   <field name="screen_name" type="string" indexed="true" stored="true"/>
   <field name="type" type="string" indexed="true" stored="true"/>
   <field name="timestamp" type="tdate" indexed="true" stored="true"/>
   <field name="lang" type="string" indexed="true" stored="true"/>
   <field name="favourites_count" type="int" indexed="false" stored="true"/>
   <field name="text" type="text_general" indexed="true" stored="true" multiValued="true"/>
   <field name="link" type="string" indexed="true" stored="true" multiValued="true"/>
   <field name="catch_all" type="text_general" indexed="true" stored="false" multiValued="true"/>
c.修改字段类型定义
即将<fieldType name="text_general"下面两个analyzer的StopFilterFactory的words值由stopwords.txt改为lang/stopwords_en.txt


二.为微博文本自定义一个字段类型text_microblog
    <fieldType name="text_microblog" class="solr.TextField" positionIncrementGap="100">
      <analyzer type="index">
        <charFilter class="solr.PatternReplaceCharFilterFactory"
                    pattern="([a-zA-Z])\1+"
                    replacement="$1$1"/>                    
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>        
        <filter class="solr.WordDelimiterFilterFactory" 
                generateWordParts="1" 
                splitOnCaseChange="0"
                splitOnNumerics="0"
                stemEnglishPossessive="1"
                preserveOriginal="0"
                catenateWords="1" 
                generateNumberParts="1" 
                catenateNumbers="0" 
                catenateAll="0" 
                types="wdfftypes.txt"/>
        <filter class="solr.StopFilterFactory"
                ignoreCase="true"
                words="lang/stopwords_en.txt"
                />                
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.ASCIIFoldingFilterFactory"/>
        <filter class="solr.KStemFilterFactory"/>
      </analyzer>
      <analyzer type="query">      
        <charFilter class="solr.PatternReplaceCharFilterFactory"
                    pattern="([a-zA-Z])\1+"
                    replacement="$1$1"/>                    
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>                           
        <filter class="solr.WordDelimiterFilterFactory" 
                splitOnCaseChange="0"
                splitOnNumerics="0"
                stemEnglishPossessive="1"
                preserveOriginal="0"
                generateWordParts="1" 
                catenateWords="1" 
                generateNumberParts="0" 
                catenateNumbers="0" 
                catenateAll="0" 
                types="wdfftypes.txt"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.ASCIIFoldingFilterFactory"/>
        <filter class="solr.StopFilterFactory"
                ignoreCase="true"
                words="lang/stopwords_en.txt"
                />                
        <filter class="solr.SynonymFilterFactory" 
                synonyms="synonyms.txt" 
                ignoreCase="true" 
                expand="true"/>
        <filter class="solr.KStemFilterFactory"/>
      </analyzer>      
    </fieldType>


PatternReplaceCharFilterFactory:在断词前使用与正则表达式替换字符
WhitespaceTokenizerFactory:仅使用空格分割文本
WordDelimiterFilterFactory: 根据标点智能分割符文,大小写更改,处理像在标签和提示上像#和@这样的特殊字符
ASCIIFoldingFilterFactory:可能的话,转换变音词成等效的ASCII
KStemFilterFactory: 在英文文本上词干化,
SynonymFilterFactory:为通用词语注入同义词到查询
下面一个一个来介绍上面的Factory.


1.PatternReplaceCharFilterFactory
Solr,在字符被传入断词器解析之前,一个CharFilter就是在一个将进入的字符流的预处理器.之前基本分析介绍过,它发生在tokenization阶段之前.和Token filter很像,多个CharFilter可以链在一起来从文本添加,更改,移除字符.在solr4,有3个内置的CharFilter:
solr.MappingCharFilterFactory--在外部配置文件来定义应用字符的替换
solr.PatternReplaceCharFilterFactory--附带一个可选择的值,使用正则表达式来替换字符
solr.HTMLStripCharFilterFactory---从文本删除HTML标记
使用内置的框架,你也可以实现你自己的CharFilter.在这三个当中,PatternReplaceCharFilterFactory看起来最适后我们当前文本分析的需要,因为推文通常没有嵌入HTML,并且也不需要映射任何字符.因此,我们不会展示如何使用MappingCharFilterFactory或者HTMLStripCharFilterFactory过滤器.所以如果需要的话,请到solr维基获取详细信息(http://wiki.apache.org/solr/),让我们看看如何应用PatternReplaceCharFilterFactory解决我们推文例子的几个问题.
solr.PatternReplaceCharFilterFactory是使用正则表达式来过滤字符的.配置这个工厂,你需要定义两个属性:pattern和replacement.pattern是一个识别我们在文本中想要替换字符的正则表达式.replacement属性指定你想要替换匹配字符的值(即匹配的字符用什么值来替换,通过replacement来指定这个值).如果你不是正则表达式专家,不用担心,大多数情况你可以使用Google或类似搜索引擎找到你需要的表达式.
合并超过两个重复字母,我们需要一个正则表达式标识重复字母的序列,所以([a-zA-Z])\1+就很不错,用正则的话来讲,[a-zA-Z]是字符类,识别小写或大写的单个字母,括号括起的字符类识别匹配的字母作为一个捕获组,\1部分称为一个匹配第一组重复的有限反向引用,+部分说明重复的字母可以出现一次或多次.
上面介绍了表达式匹配,那替换又怎样呢?我们的目标是替换超过两个重复的字母,所以我们需要的是一种方式来解决匹配([a-zA-Z])词语的那部分.我们表达式([a-zA-Z])的部分被称为一个捕获组,它作为$1是可寻址的.这样,我们的replacement值就是$1$1.对于这个例子,yummmm,([a-zA-Z])的计算到第一个"m",整个表达式计算到"mmm",所以"mmm"用"mm"替换.为了在text_microblog字段类型起作用,你加入在这个清单展示的XML.
<charFilter class="solr.PatternReplaceCharFilterFactory"
pattern="([a-zA-Z])\1+"
replacement="$1$1"/>


!!!!说了这么多,就为了解析这三行配置
2.保留标签,提示和含有连字符的词语(分析WhitespaceTokenizerFactory和WORDDELIMITERFILTERFACTORY)
不那么详细记了,挑重要的记一下.StandardTokenizer会分别从标签和提示删除#和@,还有,它分离连字符词语成了两个各个符文.例如,i-Pad变成了i和Pad.因此,正如我们在前面章节看到的一样,查询iPad就不会匹配我们的样例文档.StandardTokenizer将#和@符号当作词语分割符
WHITESPACETOKENIZERFACTORY
WhitespaceTokenizerFactory是一个简单的tokenizer.它仅基于空格来分割.通过它处理推文之后仍然是23个符文.但是表情符号:)和被包含在Beach...的省略号...作为一部分.幸运的是,这些问题使用WordDelimiterFilterFactoryy就很容易解决.
WORDDELIMITERFILTERFACTORY
WordDelimiterFilterFactory提供了一种强大的方案来解决因为基本于空格分割引起的大多数问题.这个过滤器使用一些规则将一个符文分割成子词.先看看配置清单:
<filter class="solr.WordDelimiterFilterFactory" 
generateWordParts="1" 
splitOnCaseChange="0"
splitOnNumerics="0"
stemEnglishPossessive="1"
preserveOriginal="0"
catenateWords="1" 
generateNumberParts="0" 
catenateNumbers="0" 
catenateAll="0" 
types="wdfftypes.txt"/>
默认此过滤器也会除掉#和@,但不像StandardTokenizer,WordDelimiterFilterprovides提供了一种容易的方式,通过使用简单"类型"映射文件,来自定义那些字符看作为字符分割器.在我们的例子当中就是wdfftypes.txt.我们的wdfftypes.txt文件包含了两个映射:
\# => ALPHA
@ => ALPHA
这些设置映射#和@符号到ALPHA类,意味着我们的WordDelimiterFilter实例不会将它们当作词语分割器.在#号开头的反斜杠是为了在读取wdfftypes.txt文件时,让Solr不解释该行作为注释.有了这简单的映射,标签和提示会在我们的文本保留下来.
WordDelimiterFilter一样使用一种粗鲁的方式处理带连字符号的词语.如果我们的推文能在查询中匹配所有的形式,如i Pad,i-Pad,iPad,这是理想的.WordDelimiterFilter需要基于连字符分割i-Pad成两个各个符文,i和Pad,但也必须注入一个新的符文"iPad"到流.当设置generateWordParts=1和catenateWords=1,你可以从WordDelimiterFilter得到的确切行为.一般情况下,WordDelimiterFilter提供了很多用来调节转换的选项.下表给出了每个选项是如何起作用的概述
属性                     如果启用(="1")的行为                                默认
generateWordParts        使用内置解析规则分词和其它选项创建子词部分          启用(1)
splitOnCaseChange        解析期间,当遇到字母大小写变化时,拆分驼峰词语,比如   启用(1)
                         SolrInAction被拆成三个符文,Solr,In和Action
splitOnNumerics          当遇到数字时,拆分含有字母和数字混合的词语,比如R2D2  启用(1)
                         被拆分成四个符文R,2,D和2
stemEnglishPossessive    从一个词语移除's.比如,SF's变成SF.                   启用(1)
preserveOriginal         包含文本原有的符文,还有由此过滤器所产生的任何其他   禁用(0)
                         符文.比如,SF's和SF都会被包含.
catenateWords            连接子词部分成单一的符文;比如,i-Pad被连字符分成两   禁用(0)
                         个符文,i和Pad,然后连接成iPad产生第三个符文.
generateNumberParts      通过像横杠这样的标点符号将数字数据拆分成多个符文;   启用(1)
                         比如,电话号码867-5309会被分成两个词语,867和5309
catenateNumbers          如果一个数字符文被拆分,然后连接成单一的词语;保留    禁用(0)
                         我们电话号码的引用,867-5309会被分成三个符文,867,
                         5309和8675309
catenateAll              使用generateWordParts="1"和generateNumberParts="1"  禁用(0)
                         连接所有分词部分成单一的符文
如何使用WordDelimiterFactory感觉对你的内容有作用,可能需要点点经验.强列推荐前面小节 使用solr的分析表单测试你的分析
3.使用ASCIIFoldingFilterFactory移除变音符号
在大多数情况下,搜索时,你不确定用户会输入带有变音符的字符.所以如果可能的话,solr提供了ASCIIFoldingFilterFactory来转换字符成他们相当ASCII.在schema.xml,你可以通过增加以下内部包含此过滤器到你的分析器.
<filter class="solr.ASCIIFoldingFilterFactory"/>
最好放这个过滤器在小写转换过滤器之后.那样,你仅对小写字符起作用就可以了.ASCIIFoldingFilter仅对基于拉丁字符起作用,对于其它语言,看一下solr.analysis.ICUFoldingFilterFactory,它从solr3.1开始可用.
4.使用KStemFilterFactory词干化
词干化使用语言提定的规则将词语转换成一种基本的形式.solr提供了很多词干化过滤器,每个都有自己的优缺点.现在,我们使用一个基于Krovetz stemmer的过滤器:solr.KStemFilterFactory.在转换时,这个stemmer比其它流行的stemmer,像PorterStemmer要少侵略性.现在,我们应用KStemFilterFactory从词语dring和learning移除ing.
<filter class="solr.KStemFilterFactory"/>
比较通过 KStemmer和 Porter算法产生的词干
Original term   Stem (KStemmer)    Stem (Porter)
drinking        drink              drink
requirements    requirement        requir
operating       operate            oper
operative       operative          oper
wedding         wedding            wed
learning        learning           learn
从这个就可以看出,Porter更具侵略性.KStemmer用到我们的推文例子,它能将drinking变成drink,但不能将learning变成我们期望的learn.出现这种怪异行为的是因为KStemmer使用了不词干化词语列表,而learning是一个受保护的词.一般情况下,stemmers扩大匹配查询的文档,但是他们不利于结果的精度.我们将会在14章的多语言搜索讨论中,介绍词干化更多细节
5.使用SynonymFilterFactory在搜索时注入同义词
对于重要的词语,SynonymFilterFactory注入同义词到符文流中.例如,当你遇到house时,你想要注入home进入符文流.在大多数情况下,同义词仅在查询分析时被注入.这样能减少索引有的大小,也更容易维护同义词列表的更改.如果在索引其间就注入同义词,你会不得不重新索引所有文档来应用这个变化.如果仅在查询处理时注入同义词,新的同义词能不重建索引被引入.下面是在schema.xml如何定义SynonymFilter
<filter class="solr.SynonymFilterFactory" 
synonyms="synonyms.txt" 
ignoreCase="true" expand="true" /
使用它,你需要考虑将它放入过滤链那里.一般所有其它的转换都用过了,再用SynonymFilterFactory.考虑一下,要是我们就用ASCIIFoldingFilter在同义词过滤之后,那就意味着我们的同义词列表也需要包含多音词.在我们的例子里,有几个符文能从同义词受益的,包括SF, latte,和caffe.为了对这些词于进行映射,添加以下内容到synonyms.txt文件.
sf,san fran,sanfran,san francisco
latte,coffee
caffe,cafe
看到这,虽然solr提供了一种强大的方式来映射,但你可能好奇谁会想要手工配置成千上成的同义词.目前,这是solr的一个问题,当社区扩展到solr,就有很多方案可用.例如一个工具转换英文同义词的WordNet数据库成solr格式(看 https://issues.apache.org/jira/browse/LUCENE-2347)
6.放在一起

启动solr,索引,进行测试.


三.高级文本分析.介绍三方面:
a.schema.xml文本字段的高级选项
b.每种语言的文本分析
c.使用solr的内置框架扩展文本分析
1.高级字段属性.你应该知道这些每一种高级属性,仅适用于文本字段,而不会影响非文本字段,如日期和字符串(?奇怪?字符串不是文本字段)
omitNorms:默认值false.对该字段禁止长度规范化和索引时间提升.这有助于节省存储索引空间.在相关性排序时,规范有助于短文档一点点的提升.因此,如果你的大多数文档是大小差不多,你应考虑省略规范来帮助减少索引的大小.如果在一个字段,你需要索引时间提升的话,你一定不能省略规范(omitNorms="false"),因为solr的编码提升到规范值.规范默认对原始类型,如日期,字符串,数字字段是省略的.
termVectors:默认值false.solr提供一个更多类似这样(More Like This)的功能来查找与一个指定文档相似的那些文档.More Like This功能需要启用词向量,以致solr能计算两个文档间的相似性测量.存储词向量对大索引是昂贵的,所以如果你确实需要它们,就启用.
termPositions:默认值false.常用来改善命中高亮显示功能的性能.在第9章深入介绍.启用词位置会增加你的索引大小.
termOffset:默认值false.常用来改善命中高亮显示功能的性能.在第9章深入介绍.启用词偏移会增加你的索引大小.
2.每种语言的文本分析.虽然本章,我们重点分析英文文本.每种语言都有自己的断词解析规则,停用词列表,词干化规则.一般情况下,对你的索引,你需要对每一种你想要分析的语言,开发一个指定的<fieldType>.那就是说,在本章你学到的很多技术仍适用于其它非英语的分析.
3.使用solr接插件扩展文本分析.对于你的文本分析问题,当solr没有提供一个内置的方案,你会怎么办?正如你在本章所看到的,我们在我们的微博内容,完成一些强大的转换,不用写任何代码,只使用了内置的solr工具.因此,虽然很少遇到文本分析不能通过内置工具解决的需求.但是solr插件框架也可以用来构建指定应用的文件分析组件.
首先,我们需要一个solr不能通过内置工具解决的需求.回顾在第5章我们的多字段讨论,我们索引0个或多个URL到links字段.经过分析,对于这个需求,构建一个自定义的TokenFilter是有意义的,这是solr中最常见和最简单的方式来自定义文本分析.要创建自己义的TokenFilter,你需要开发两个具体的java类,一个继承Lucene's的org.apache.lucene.analysis.TokenFilter类来进行过滤,一个你自定义的过滤器工厂继承org.apache.lucene.analysis.util.TokenFilterFactory类.工厂类需要的,以致solr可实例化使用配置在schema.xml文件提供的配置TokenFilter实例.
自定义TokenFilter类,以下清单显示为了解决短URL的自定义TokenFilter类的骨架
package sia.ch6;


import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import java.io.IOException;
import java.util.regex.Pattern;


public class ResolveUrlTokenFilter extends TokenFilter {


    private final CharTermAttribute termAttribute = addAttribute(CharTermAttribute.class);
    private final Pattern patternToMatchShortenedUrls;


    public ResolveUrlTokenFilter(TokenStream in, Pattern patternToMatchShortenedUrls) {
        super(in);
        this.patternToMatchShortenedUrls = patternToMatchShortenedUrls;
    }
    /*在流中,方法被调用来处理每一个符文*/
    @Override
    public boolean incrementToken() throws IOException {
        if (!input.incrementToken())
            return false;


        char[] term = termAttribute.buffer();
        int len = termAttribute.length();


        String token = new String(term, 0, len);
        if (patternToMatchShortenedUrls.matcher(token).matches()) {
            // token is a shortened URL, resolve it and replace
            termAttribute.setEmpty().append(resolveShortenedUrl(token));
        }


        return true;
    }


    private String resolveShortenedUrl(String toResolve) {
        try {
            // TODO: implement a real way to resolve shortened URLs
            if ("http://bit.ly/3ynriE".equals(toResolve)) {
                return "lucene.apache.org/solr";
            } else if ("http://bit.ly/15tzw".equals(toResolve)) {
                return "manning.com";
            }
        } catch (Exception exc) {
            // rather than failing analysis if you can't resolve the URL,
            // you should log the error and return the un-resolved value
            exc.printStackTrace();
        }
        return toResolve;
    }
}
自定义ResolveUrlTokenFilterFactory类.工厂负责采用在schema.xml指定的属性,并转换它们成需要的参数来创建TokenFilter.以下是我们在schema.xml如何定义我们的Token Filter
<filter class="sia.ch6.ResolveUrlTokenFilterFactory"
shortenedUrlPattern="http:\/\/bit.ly\/[\w\-]+" />
下面清单是工厂的java实现:
package sia.ch6;


import java.util.Map;
import java.util.regex.Pattern;


import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.util.TokenFilterFactory;


public class ResolveUrlTokenFilterFactory extends TokenFilterFactory {
    
    protected Pattern patternToMatchShortenedUrls;
    public ResolveUrlTokenFilterFactory(Map<String,String> args) {
        super(args);
        assureMatchVersion();
        String shortenedUrlPattern = require(args, "shortenedUrlPattern");        
        patternToMatchShortenedUrls = Pattern.compile(shortenedUrlPattern);
    }
    
    @Override
    /*重写创建方法来返回全配置的TokenFilter实例*/
    public TokenFilter create(TokenStream input) {
        return new ResolveUrlTokenFilter(input, patternToMatchShortenedUrls);
    }
}
这里的关键是在文本分析期间,solr使用你的工厂类作为在schema.xml定义的过虑器与一个你自定义TokenFilter实例之间的媒介.最后你要做的一件事就是你需要将包含你的插件类的jar文件进入一个solr在初始化期间能够定位他们的位置.为了简单起见,我们推荐增加一个叫plugin的目录,正如我们在第4章讨论过,并且加入这个位置到solr的config.xml
<lib dir="plugins/" regex=".*\.jar" />






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值