Lucene
2018-06-27 09:44:31来源:博客园 阅读 ()
概述及应用场景
1、Lucene:是一个世界上最流行的开源的全文检索框架
官网网址:http://lucene.apache.org
版本:7.3.3
Jdk要求:1.8
▲1、Lucene的作用?
1、比如购物商城:假设通过传统的SQL语句进行书籍查询的时候 ,输入关键字‘Lucene实战’,进行查询的时候,查询字段中的数据必须有 ‘Lucene实战’这些关键字必须连在一块。 这是通过sql语句搞不定的
2、性能问题:在对大数据进行检索时,lucene的检索速度明显快于传统的sql检索
3、实现高亮效果
4、 做系统的站内检索,即对一个系统内的资源进行搜索。
如BBS、BLOG中的文章搜索,网上商店中的商品搜索、OA系统中公文检索、邮件检索……
▲2、Lucene的使用
1、解压下载的Lucene压缩包,得到如下文件(Lucene/api/lucene-7.3.0)
analysis:分词器
core:Lucene核心jar包
demo:Lucene示例
docs:Lucene使用文档
highlighter:实现高亮效果
2、Lucene与mysql、oracle等数据库的比较
-1、mysql、oracle等数据库需要建立自己的数据库以及表
-2、Lucene需要建立自己的索引库
索引的创建_更新_删除
▲一、代码实现
/** *添加索引(单字分词器) */ private static void addIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//创建分词器 单字分词器 Analyzer analyzer=new StandardAnalyzer(); //创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//往索引库中添加记录 Lucene中一个document实例代表数据表中的一条记录 Document document=new Document(); /* *StringFiled:不会对关键字进行分词 *TextFiled:会对关键字进行分词 * * Store.YES:会将数据进行存储并分词 * Store.NO:不会将数据进行存储,不会分词,索引还是会创建。有索引,没有内容。 */ document.add(new StringField("articalId", "0001", Store.YES)); document.add(new TextField("title", "生活不止眼前的苟且", Store.YES)); document.add(new TextField("content", "妈妈坐在门前,哼着花儿与少年,虽已隔多年。", Store.YES));
indexWriter.addDocument(document);
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("添加成功"); } catch (Exception e) { e.printStackTrace(); } } |
/** * 更新索引(单字分词器) */ private static void updateIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//创建分词器 单字分词器 Analyzer analyzer=new StandardAnalyzer(); //创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//往索引库中添加记录 Lucene中一个document实例代表数据表中的一条记录 Document document=new Document(); /* *StringFiled:不会对关键字进行分词 *TextFiled:会对关键字进行分词 * * Store.YES:会将数据进行存储并分词 * Store.NO:不会将数据进行存储,不会分词,索引还是会创建。有索引,没有内容。 */ document.add(new StringField("articalId", "0001", Store.YES)); document.add(new TextField("title", "幽幽而来", Store.YES)); document.add(new TextField("content", "这世界真好呀", Store.YES));
//更新的原理:先删除之前的索引,再创建新的索引====删除与添加两个动作的合集 indexWriter.updateDocument(new Term("articalId","0001"), document);
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("更新成功"); } catch (Exception e) { e.printStackTrace(); } }
|
/** * 删除索引(单字分词器) */ private static void deleteIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//创建分词器 单字分词器 Analyzer analyzer=new StandardAnalyzer(); //创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//删除索引库中指定的索引 indexWriter.deleteDocuments(new Term("articalId","0001"));
//删除索引库中全部的索引 //indexWriter.deleteAll();
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("删除成功"); } catch (Exception e) { e.printStackTrace(); } } |
▲二. 用到的API:
a. IndexWriter : 对索引库进行CUD操作.
b. Directory : 存储的目录.
-- FSDirectory : 文件磁盘目录。
-- RAMDirectory: 内存中。
c. IndexWriterConfig: 指定创建索引需要的配置信息.
d. Analyzer : 分词器
e. OpenMode : 指定打开索引库的模式。
-- OpenMode.CREATE : 创建(每次都会创建).
-- OpenModel.APPEND : 追加模式
-- OpenMode.CREATE_OR_APPEND: 创建或追加模式。
f. Document: 文档
g. Store : 是否存储
-- YES: 存储
-- NO: 不存储
全文检索
代码实现
/** * 全文检索(单字分词器) */ private static void searchIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//DirectoryReader的open方法指定需要读取的索引库信息,并返回相应的实例 IndexReader indexReader=DirectoryReader.open(directory); //创建IndexSearcher实例,通过IndexSearcher实例进行全文检索 IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//TermQuery:指定了查询的关键字以及查询哪一个字段;不会对关键字进行分词 Query query=new TermQuery(new Term("title","生")); /*通过indexSearch进行检索,并指定两个参数 *第一个参数:封装查询的相关信息,比如查询的关键字、分词器,是否需要分词,或者需要分词的话,采取什么分词器 *第二个参数:最多只要多少条记录 * *查询数据,最终都封装在TopDocs的实例中 */ TopDocs topDocs=indexSearcher.search(query, 100); //通过topDocs获取匹配全部记录 ScoreDoc[] scoreDocs=topDocs.scoreDocs;
System.out.println("获取到的记录数"+scoreDocs.length);
for (int i = 0; i < scoreDocs.length; i++) { //获取记录的id int id=scoreDocs[i].doc; System.out.println("id:"+id);
//获取文章得分 float score=scoreDocs[i].score; System.out.println("score :"+score);
Document document=indexSearcher.doc(id); String articalId= document.get("articalId"); String title= document.get("title"); String content= document.get("content"); System.out.println("articalId:"+articalId+" title:"+title+" content:"+content); } } catch (Exception e) { e.printStackTrace(); } } |
分词器测试
▲一、代码实现
package org.crdit.com;
import java.util.Arrays;
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.CharArraySet; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cjk.CJKAnalyzer; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.TermToBytesRefAttribute; import org.apache.lucene.util.BytesRef;
/** * @author crd * 分词效果演示 */ public class AnalyzerTest { public static void main(String[] args) throws Exception { String str = "广西桂林好玩的地方";
//单字分词 //Analyzer analyzer = new StandardAnalyzer();
// 二分法分词 //Analyzer analyzer = new CJKAnalyzer();
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar //Analyzer analyzer = new SmartChineseAnalyzer();
// 词库分词 智能分词器 Analyzer analyzer = new SmartChineseAnalyzer(new CharArraySet(Arrays.asList("的", "了","啊"), true));//使用自定义的停用词集合
// 对指定的字符串进行分词 TokenStream ts = analyzer.tokenStream(null, str);
ts.reset(); // 先要对tokenStream执行reset
// 采用循环,不断地获取下一个token while(ts.incrementToken()) { // 获取其中token信息 TermToBytesRefAttribute attr = ts.getAttribute(TermToBytesRefAttribute.class); final BytesRef bytes = attr.getBytesRef(); System.out.println(bytes.utf8ToString()); } ts.close(); } }
|
▲二、分词器比较说明
英文分词器:
a. 按空格来分词。welcome to fkjava
b. 去掉停用词(stop word).
is、an、a、the、of、的、地、吗、嘛、了、得、标识符号(, ; :)
c. 大写字母转成小写字母。
中文分词器:
a、去掉停用词(stop word).
is、an、a、the、of、的、地、吗、嘛、了、得、标识符号(, ; :)
中华人民共和国,采用不同分词器效果如下
单字分词器:(StandardAnalyzer):中|华|人|民|共|和|国
二分法分词(CJKAnalyzer):中华|华人|人民|民共|共和|和国
智能分词器(SmartChineseAnalyzer):中华|华人|人民|共和国
以上项目代码在code/01下
智能分词器
/** * 添加索引 */ private static void addIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb")); //词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer();
//创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//往索引库中添加记录 Lucene中一个document实例代表数据表中的一条记录 Document document=new Document(); /* *StringFiled:不会对关键字进行分词 *TextFiled:会对关键字进行分词 * * Store.YES:会将数据进行存储并分词 * Store.NO:不会将数据进行存储,不会分词,索引还是会创建。有索引,没有内容。 */ document.add(new StringField("articalId", "0001", Store.YES)); document.add(new TextField("title", "桂林好玩的地方", Store.YES)); document.add(new TextField("content", "可以竹筏游漓江,观美景", Store.YES));
indexWriter.addDocument(document);
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("添加成功"); } catch (Exception e) { e.printStackTrace(); } } |
/** * 更新索引 */ private static void updateIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer();
//创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//往索引库中添加记录 Lucene中一个document实例代表数据表中的一条记录 Document document=new Document(); /* *StringFiled:不会对关键字进行分词 *TextFiled:会对关键字进行分词 * * Store.YES:会将数据进行存储并分词 * Store.NO:不会将数据进行存储,不会分词,索引还是会创建。有索引,没有内容。 */ document.add(new StringField("articalId", "0001", Store.YES)); document.add(new TextField("title", "幽幽而来", Store.YES)); document.add(new TextField("content", "这世界真好呀", Store.YES));
//更新的原理:先删除之前的索引,再创建新的索引====删除与添加两个动作的合集 indexWriter.updateDocument(new Term("articalId","0001"), document);
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("更新成功"); } catch (Exception e) { e.printStackTrace(); } } |
/** * 删除索引 */ private static void deleteIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer();
//创建IndexWriterConfig实例 指定添加索引的配置信息 IndexWriterConfig config=new IndexWriterConfig(analyzer); //如果索引不存在,则创建。存在,则追加。 config.setOpenMode(OpenMode.CREATE_OR_APPEND);
//创建IndexWriter IndexWriter indexWriter=new IndexWriter(directory, config);
//删除索引库中指定的索引 indexWriter.deleteDocuments(new Term("articalId","0001"));
//删除索引库中全部的索引 //indexWriter.deleteAll();
//提交事务 indexWriter.commit(); //关闭事务 indexWriter.close();
System.out.println("删除成功"); } catch (Exception e) { e.printStackTrace(); } } |
/** * 全文检索 */ private static void searchIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//DirectoryReader的open方法指定需要读取的索引库信息,并返回相应的实例 IndexReader indexReader=DirectoryReader.open(directory); //创建IndexSearcher实例,通过IndexSearcher实例进行全文检索 IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer(); //创建QueryParser实例 通过QueryParser可以指定需要查询的字段以及分词器信息 QueryParser queryParser=new QueryParser("title", analyzer); //通过queryParser实例得到query实例,并指定查询的关键字 Query query =queryParser.parse("桂林有哪些好玩的地方"); /*通过indexSearch进行检索,并指定两个参数 *第一个参数:封装查询的相关信息,比如查询的关键字、分词器,是否需要分词,或者需要分词的话,采取什么分词器 *第二个参数:最多只要多少条记录 * *查询数据,最终都封装在TopDocs的实例中 */ TopDocs topDocs=indexSearcher.search(query, 100); //通过topDocs获取匹配全部记录 ScoreDoc[] scoreDocs=topDocs.scoreDocs;
System.out.println("获取到的记录数"+scoreDocs.length);
for (int i = 0; i < scoreDocs.length; i++) { //获取记录的id int id=scoreDocs[i].doc; System.out.println("id:"+id);
//获取文章得分 float score=scoreDocs[i].score; System.out.println("score :"+score);
Document document=indexSearcher.doc(id); String articalId= document.get("articalId"); String title= document.get("title"); String content= document.get("content"); System.out.println("articalId:"+articalId+" title:"+title+" content:"+content); } } catch (Exception e) { e.printStackTrace(); } } |
通过MultiFieldQueryParser进行多条件查询
/** * 全文检索 */ private static void searchIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//DirectoryReader的open方法指定需要读取的索引库信息,并返回相应的实例 IndexReader indexReader=DirectoryReader.open(directory); //创建IndexSearcher实例,通过IndexSearcher实例进行全文检索 IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer(); //创建QueryParser实例 通过MultiFieldQueryParser可以指定需要查询的多个字段以及分词器信息 QueryParser queryParser=new MultiFieldQueryParser(new String[]{"title","content"}, analyzer); //通过queryParser实例得到query实例,并指定查询的关键字 Query query =queryParser.parse("桂林");
/*通过indexSearch进行检索,并指定两个参数 *第一个参数:封装查询的相关信息,比如查询的关键字、分词器,是否需要分词,或者需要分词的话,采取什么分词器 *第二个参数:最多只要多少条记录 * *查询数据,最终都封装在TopDocs的实例中 */ TopDocs topDocs=indexSearcher.search(query, 100); //通过topDocs获取匹配全部记录 ScoreDoc[] scoreDocs=topDocs.scoreDocs;
System.out.println("获取到的记录数"+scoreDocs.length);
for (int i = 0; i < scoreDocs.length; i++) { //获取记录的id int id=scoreDocs[i].doc; System.out.println("id:"+id);
//获取文章得分 float score=scoreDocs[i].score; System.out.println("score :"+score);
Document document=indexSearcher.doc(id); String articalId= document.get("articalId"); String title= document.get("title"); String content= document.get("content"); System.out.println("articalId:"+articalId+" title:"+title+" content:"+content); } } catch (Exception e) { e.printStackTrace(); } }
|
条件过滤
// 广州 默认Field里包含"广州"关键词 Query query1 = queryParser.parse("广州"); // 景区 OR 广州 默认Field里包含"景区"或 "广州" Query query2 = queryParser.parse("景区 广州"); //+白云山 +广州 (白云山 AND 广州) AND必须为大写 默认Field里包含"广州"和"白云山" Query query3 = queryParser.parse("广州 AND 白云山"); // title:广州 title字段中包含广州 Query query4 = queryParser.parse("title:广州"); //title:广州 -content:广州|景区 (title:广州 AND NOT content:广州|景区 ) //(title里包含广州,但content里不能包含"广州 或者 景区") Query query5 = queryParser.parse("title:广州 -content:广州|景区"); |
高亮显示
导包:
lucene-highlighter-7.3.0.jar
lucene-memory-7.3.0.jar
/** * 全文检索 */ private static void searchIndex() { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//DirectoryReader的open方法指定需要读取的索引库信息,并返回相应的实例 IndexReader indexReader=DirectoryReader.open(directory); //创建IndexSearcher实例,通过IndexSearcher实例进行全文检索 IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer(); //创建QueryParser实例 通过MultiFieldQueryParser可以指定需要查询的多个字段以及分词器信息 QueryParser queryParser=new MultiFieldQueryParser(new String[]{"title","content"}, analyzer); //通过queryParser实例得到query实例,并指定查询的关键字 Query query =queryParser.parse("桂林");
//通过格式器指定咋样对关键字进行处理 Formatter formatter=new SimpleHTMLFormatter("<font color='red'>", "</font>"); //通过Scorer包装query Scorer fragmentScorer=new QueryScorer(query); //创建高亮器 Highlighter highlighter=new Highlighter(formatter, fragmentScorer);
//创建格式化片段 Fragmenter fragmenter=new SimpleFragmenter(10);
//将格式化片段实例与高亮器关联,最终通过高亮器对文本信息进行处理 highlighter.setTextFragmenter(fragmenter);
/*通过indexSearch进行检索,并指定两个参数 *第一个参数:封装查询的相关信息,比如查询的关键字、分词器,是否需要分词,或者需要分词的话,采取什么分词器 *第二个参数:最多只要多少条记录 * *查询数据,最终都封装在TopDocs的实例中 */ TopDocs topDocs=indexSearcher.search(query, 100); //通过topDocs获取匹配全部记录 ScoreDoc[] scoreDocs=topDocs.scoreDocs;
System.out.println("获取到的记录数"+scoreDocs.length);
for (int i = 0; i < scoreDocs.length; i++) { //获取记录的id int id=scoreDocs[i].doc; System.out.println("id:"+id);
//获取文章得分 float score=scoreDocs[i].score; System.out.println("score :"+score);
Document document=indexSearcher.doc(id);
String articalId= document.get("articalId"); //获取标题信息,高亮前title String title= document.get("title"); //获取内容信息,高亮前的content String content= document.get("content"); System.out.println("articalId:"+articalId+" 高亮前title:"+title+" 高亮前content:"+content);
/*通过高亮器对title和content进行高亮处理 *三个参数的含义: * 第一个:分词器 * 第二个:哪一个字段进行高亮 * 第三个:需要进行高亮的文本 * 备注:如果需要高亮的文本信息不包括查询的关键字则getBestFragment会返回null */ String postTitle=highlighter.getBestFragment(analyzer, "title", title); String postContent=highlighter.getBestFragment(analyzer, "content", content);
System.out.println("articalId:"+articalId+" 高亮后title:"+postTitle+" 高亮后content:"+postContent);
} } catch (Exception e) { e.printStackTrace(); } }
|
分页
/** * 全文检索 * @param pageIndex 当前页码 * @param pageSize 每页显示的记录数 */ private static void searchIndex(int pageIndex,int pageSize) { try { //指定索引所在目录 Directory directory=FSDirectory.open(Paths.get("E:\\Lucene\\Lucene_db\\artical_tb"));
//DirectoryReader的open方法指定需要读取的索引库信息,并返回相应的实例 IndexReader indexReader=DirectoryReader.open(directory); //创建IndexSearcher实例,通过IndexSearcher实例进行全文检索 IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//词库分词 需要导jar包 lucene-analyzers-smartcn.jar Analyzer analyzer = new SmartChineseAnalyzer(); //创建QueryParser实例 通过MultiFieldQueryParser可以指定需要查询的多个字段以及分词器信息 QueryParser queryParser=new MultiFieldQueryParser(new String[]{"title","content"}, analyzer); //通过queryParser实例得到query实例,并指定查询的关键字 Query query =queryParser.parse("桂林");
/*通过indexSearch进行检索,并指定两个参数 *第一个参数:封装查询的相关信息,比如查询的关键字、分词器,是否需要分词,或者需要分词的话,采取什么分词器 *第二个参数:最多只要多少条记录 * *查询数据,最终都封装在TopDocs的实例中 */ TopDocs topDocs=indexSearcher.search(query, 100); //通过topDocs获取匹配全部记录 ScoreDoc[] scoreDocs=topDocs.scoreDocs;
System.out.println("获取到的记录数"+scoreDocs.length);
//定义查询的起始记录号以及结束的记录号 int startSize=(pageIndex-1)*pageSize; int endSize=pageIndex*pageSize>scoreDocs.length?scoreDocs.length:pageIndex*pageSize;
for (int i = startSize; i < endSize; i++) { //获取记录的id int id=scoreDocs[i].doc; System.out.println("id:"+id);
//获取文章得分 float score=scoreDocs[i].score; System.out.println("score :"+score);
Document document=indexSearcher.doc(id); String articalId= document.get("articalId"); String title= document.get("title"); String content= document.get("content"); System.out.println("articalId:"+articalId+" title:"+title+" content:"+content); } } catch (Exception e) { e.printStackTrace(); } } |
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 如何优雅地停止 Spring Boot 应用? 2020-06-08
- 详解SpringBoot(2.3)应用制作Docker镜像(官方方案) 2020-06-08
- 体验SpringBoot(2.3)应用制作Docker镜像(官方方案) 2020-06-07
- 如何在Spring Boot应用启动之后立刻执行一段逻辑?本文详解 2020-06-05
- 如何在Spring Boot应用启动之后立刻执行一段逻辑 2020-06-02
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash