Customize Dictionary or Filter dictionary by Synonym & Thesaurus, customize zhparser’s xdb
背景
以前写过一些关于PostgreSQL 中文分词的用法文章,参考
《PostgreSQL chinese full text search 中文全文检索》
本文主要补充一些内容:
1. PostgreSQL文本检索的原理
2. 如何调试parser
3. 如何实现词组替换
4. 如何添加或修改解析过程用的中文词组
正文
1. PostgreSQL文本检索的大致原理:
首先parser负责对文本进行拆分和归类(生成lexeme);对于英语系的拆分比较简单,因为单词间有空格隔开,PostgreSQL默认的parser仅仅支持英文体系的解析。对中文来说需要自定义parser,例如zhparser是一个例子。
然后PG将解析好的lexeme根据字典进行匹配,用户可用配置多个字典,例如匿名字典,一般字典。
匿名字典可用于替换,例如将某些词义相同的词替换为某一个词。
一般字典用于匹配,匹配到的lexeme是有效词,没有匹配到的lexeme丢弃。
PG默认支持的parser如下:
digoal=> select * from pg_ts_parser ;
prsname | prsnamespace | prsstart | prstoken | prsend | prsheadline | prslextype
---------+--------------+------------+----------------+----------+---------------+--------------
default | 11 | prsd_start | prsd_nexttoken | prsd_end | prsd_headline | prsd_lextype
(1 row)
解析后的lexeme分为几类:
digoal=> select * from pg_catalog.ts_token_type('default');
tokid | alias | description
-------+-----------------+------------------------------------------
1 | asciiword | Word, all ASCII
2 | word | Word, all letters
3 | numword | Word, letters and digits
4 | email | Email address
5 | url | URL
6 | host | Host
7 | sfloat | Scientific notation
8 | version | Version number
9 | hword_numpart | Hyphenated word part, letters and digits
10 | hword_part | Hyphenated word part, all letters
11 | hword_asciipart | Hyphenated word part, all ASCII
12 | blank | Space symbols
13 | tag | XML tag
14 | protocol | Protocol head
15 | numhword | Hyphenated word, letters and digits
16 | asciihword | Hyphenated word, all ASCII
17 | hword | Hyphenated word, all letters
18 | url_path | URL path
19 | file | File or path name
20 | float | Decimal notation
21 | int | Signed integer
22 | uint | Unsigned integer
23 | entity | XML entity
(23 rows)
我们可以为每一种类型创建不同的字典来对付。
PG怎么管理lexeme类型和字典的映射关系呢?用ts config。
语法:
创建TS CONFIG
Command: CREATE TEXT SEARCH CONFIGURATION
Description: define a new text search configuration
Syntax:
CREATE TEXT SEARCH CONFIGURATION name (
PARSER = parser_name |
COPY = source_config
)
配置lexeme类型和字典的关系。
ALTER TEXT SEARCH CONFIGURATION name
ADD MAPPING FOR token_type [, ... ] WITH dictionary_name [, ... ]
ALTER TEXT SEARCH CONFIGURATION name
ALTER MAPPING FOR token_type [, ... ] WITH dictionary_name [, ... ]
2. 如何调试parser,如何修改全文检索的配置
ts_debug([ config regconfig, ] document text,
OUT alias text,
OUT description text,
OUT token text,
OUT dictionaries regdictionary[],
OUT dictionary regdictionary,
OUT lexemes text[])
returns setof record
ts_debug函数用于调试parser, 例如:
我准备使用english这个配置,这个配置中包含的lexeme类型和字典的对应关系如下:
digoal=> \dF+ english
Text search configuration "pg_catalog.english"
Parser: "pg_catalog.default"
Token | Dictionaries
-----------------+--------------
asciihword | english_stem
asciiword | english_stem
email | simple
file | simple
float | simple
host | simple
hword | english_stem
hword_asciipart | english_stem
hword_numpart | simple
hword_part | english_stem
int | simple
numhword | simple
numword | simple
sfloat | simple
uint | simple
url | simple
url_path | simple
version | simple
word | english_stem
调试一下:
digoal=> select * from ts_debug('english','hello digoal, http://blog.163.com/digoal@126/');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+--------------------------+----------------+--------------+----------------------------
asciiword | Word, all ASCII | hello | {english_stem} | english_stem | {hello}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | digoal | {english_stem} | english_stem | {digoal}
blank | Space symbols | , | {} | |
protocol | Protocol head | http:// | {} | |
url | URL | blog.163.com/digoal@126/ | {simple} | simple | {blog.163.com/digoal@126/}
host | Host | blog.163.com | {simple} | simple | {blog.163.com}
url_path | URL path | /digoal@126/ | {simple} | simple | {/digoal@126/}
(8 rows)
parser将这个文本解析为8个lexeme,dictionary 字段表示匹配到的字典是哪个,可以看到每个lexeme类型用什么字典来处理的呢?
例如:
asciiword类型 (hello,digoal)用到了english_stem字典,和TS CONFIG配置一致。
protocol类型在english这个TS CONFIG中没有指定对应的字典,所以没有匹配。
url_path类型使用simple字典进行匹配。
最后我们看看得到的tsvector是什么?
digoal=> select * from to_tsvector('english','hello digoal, http://blog.163.com/digoal@126/');
to_tsvector
-------------------------------------------------------------------------------------
'/digoal@126/':5
'blog.163.com':4
'blog.163.com/digoal@126/':3
'digoal':2
'hello':1
(1 row)
显然,和调试结果一样,有5个lexeme有字典匹配,没有匹配到的就丢弃了。
现在我可以给english这个配置加一下protocol的字典,这样就可以输出对应的词组了。
digoal=> \c digoal postgres
You are now connected to database "digoal" as user "postgres".
digoal=# alter text search configuration english add mapping for protocol with simple;
ALTER TEXT SEARCH CONFIGURATION
digoal=# select * from to_tsvector('english','hello digoal, http://blog.163.com/digoal@126/');
to_tsvector
-------------------------------------------------------------------------------------------------
'/digoal@126/':6
'blog.163.com':5
'blog.163.com/digoal@126/':4
'digoal':2
'hello':1
'http://':3
(1 row)
digoal=# select * from ts_debug('english','hello digoal, http://blog.163.com/digoal@126/');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+--------------------------+----------------+--------------+----------------------------
asciiword | Word, all ASCII | hello | {english_stem} | english_stem | {hello}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | digoal | {english_stem} | english_stem | {digoal}
blank | Space symbols | , | {} | |
protocol | Protocol head | http:// | {simple} | simple | {http://}
url | URL | blog.163.com/digoal@126/ | {simple} | simple | {blog.163.com/digoal@126/}
host | Host | blog.163.com | {simple} | simple | {blog.163.com}
url_path | URL path | /digoal@126/ | {simple} | simple | {/digoal@126/}
(8 rows)
3. 如何实现词组替换
我们前面已经讲了,一个lexeme类型可以有多个字典来对付。
按照字典的先后顺序进行匹配,对于一般字典,匹配到了就直接返回匹配成功,如果没有匹配到,才会将这个lexeme交给下一个字典进行匹配。
但是还有两种字典,它们匹配到后不返回,而是替换为另一个词,然后将替换后的词交给下面的字典进行匹配。
这样有利于对多义词进行筛选合并,例如有10个词含义是一样的,那么我们可以替换为1个词来表示,减少最终分词的数量。
例如将刘德华,黎明,郭富城,张学友替换为四大天王。将德哥,周正中替换为digoal。
这两种字典是synonym dictionary, Thesaurus dictionary。它们的配置文件在PG_HOME/share/tsearch_data目录下面,后缀名分为syn和ths,例如:
-rw-r--r-- 1 root root 73 May 1 18:01 synonym_sample.syn
-rw-r--r-- 1 root root 473 May 1 18:01 thesaurus_sample.ths
下面我们来试用一下。
例如我还是用english这个配置,先调试一下看看,这几个中文词解析为word类型,试用了english_stem字典。
digoal=> select * from ts_debug('english','郭富城 刘德华 张学友 黎明');
alias | description | token | dictionaries | dictionary | lexemes
-------+-------------------+--------+----------------+--------------+----------
word | Word, all letters | 郭富城 | {english_stem} | english_stem | {郭富城}
blank | Space symbols | | {} | |
word | Word, all letters | 刘德华 | {english_stem} | english_stem | {刘德华}
blank | Space symbols | | {} | |
word | Word, all letters | 张学友 | {english_stem} | english_stem | {张学友}
blank | Space symbols | | {} | |
word | Word, all letters | 黎明 | {english_stem} | english_stem | {黎明}
(7 rows)
现在我创建一个syn文件,内容如下:
postgres@db-192-168-173-33-> cd /opt/pgsql/share/tsearch_data/
postgres@db-192-168-173-33-> vi digoal.syn
刘德华 四大天王
郭富城 四大天王
张学友 四大天王
黎明 四大天王
德哥 digoal
周正中 digoal
然后要创建一个synonym字典,模板用synonym,匿名配置文件用digoal.syn:
digoal=# CREATE TEXT SEARCH DICTIONARY my_synonym (
TEMPLATE = synonym,
SYNONYMS = digoal
);
我们先不修改配置,看看分词结果:
digoal=# select * from to_tsvector('english','刘德华 张学友 黎明 郭富城 德哥 周正中');
to_tsvector
---------------------------------------------------------------
'刘德华':1 '周正中':6 '张学友':2 '德哥':5 '郭富城':4 '黎明':3
(1 row)
修改english的word类型的字典关系,将匿名字典放最前面。
digoal=# ALTER TEXT SEARCH CONFIGURATION english
ALTER MAPPING FOR word
WITH my_synonym, english_stem;
ALTER TEXT SEARCH CONFIGURATION
现在分词结果变了,都替换成了我想要的结果:
digoal=# select * from to_tsvector('english','刘德华 张学友 黎明 郭富城 德哥 周正中');
to_tsvector
---------------------------------
'digoal':5,6 '四大天王':1,2,3,4
(1 row)
注意匿名字典一定要放前面,否则english_stem先匹配了,就不会有替换效果了。
Thesaurus是synonym字典的扩展,支持的是多词和缩写的对应关系,并不是多对一的关系。
例如
$ vi digoal.ths
刘德华 郭富城 黎明 张学友 : 四大天王
德哥 周正中 : digoal
as soon as possible : ASAP
这仅仅代表”刘德华 郭富城 黎明 张学友”按照顺序出现时,匹配为四大天王。
这样不好理解,但是换个词可能更好理解,例如as soon as possible替换为首字母缩写ASAP。
digoal=# CREATE TEXT SEARCH DICTIONARY thesaurus_simple (
TEMPLATE = thesaurus,
DictFile = digoal,
Dictionary = pg_catalog.english_stem);
CREATE TEXT SEARCH DICTIONARY
digoal=# ALTER TEXT SEARCH CONFIGURATION english
ALTER MAPPING FOR asciiword, word
WITH thesaurus_simple,english_stem;
ALTER TEXT SEARCH CONFIGURATION
测试:
digoal=# select * from ts_debug('english','as soon as possible');
ERROR: thesaurus sample word "as" is a stop word (rule 3)
HINT: Use "?" to represent a stop word within a sample phrase.
CONTEXT: SQL function "ts_debug" statement 1
digoal=# \q
postgres@db-192-168-173-33-> exit
logout
[root@db-192-168-173-33 tsearch_data]# vi digoal.ths
刘德华 郭富城 黎明 张学友 : 四大天王
德哥 周正中 : digoal
? soon ? possible : ASAP
digoal=> select * from to_tsvector('english','as soon as possible');
to_tsvector
-------------
'asap':1
(1 row)
digoal=> select * from ts_debug('english','as soon as possible');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+----------+---------------------------------+--------------+-----------
asciiword | Word, all ASCII | as | {thesaurus_simple,english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | soon | {thesaurus_simple,english_stem} | english_stem | {soon}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | as | {thesaurus_simple,english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | possible | {thesaurus_simple,english_stem} | english_stem | {possibl}
(7 rows)
一定要完全匹配才能替换
digoal=# select * from to_tsvector('english','德哥 周正中');
to_tsvector
-------------
'digoal':1
(1 row)
digoal=# select * from to_tsvector('english','郭富城 刘德华 张学友 黎明');
to_tsvector
-------------------------------------------
'刘德华':2 '张学友':3 '郭富城':1 '黎明':4
(1 row)
digoal=# select * from to_tsvector('english','刘德华 郭富城 黎明 张学友');
to_tsvector
--------------
'四大天王':1
(1 row)
?替换的是stop word, 所以会这样, the,as都是stop word
digoal=> select * from to_tsvector('english','as soon the possible');
to_tsvector
-------------
'asap':1
(1 row)
但是1是uint
digoal=> select * from to_tsvector('english','as soon 1 possible');
to_tsvector
----------------------------
'1':3 'possibl':4 'soon':2
(1 row)
digoal=> select * from ts_debug('english','as soon 1 possible');
...
uint | Unsigned integer | 1 | {simple} | simple | {1}
4. 如何添加或修改解析过程用的中文词组
例如我们要扩充中文词组,这个和中文的parser有关,例如用到zhparser的话,它这里用的是xdb,所以加词组要修改的实际就是xdb。
中文分词的安装过程参考
《PostgreSQL chinese full text search 中文全文检索》
假设环境已经安装好了,加词组的例子:
$PG_HOME/share/tsearch_data目录中有中文词典如下:
-rw-r--r-- 1 root root 14M May 3 14:32 dict.utf8.xdb
创建中文parser:
digoal=# create extension zhparser;
CREATE EXTENSION
digoal=# select * from pg_ts_parser ;
prsname | prsnamespace | prsstart | prstoken | prsend | prsheadline | prslextype
----------+--------------+-------------+-----------------+-----------+---------------+---------------
default | 11 | prsd_start | prsd_nexttoken | prsd_end | prsd_headline | prsd_lextype
zhparser | 25956 | zhprs_start | zhprs_getlexeme | zhprs_end | prsd_headline | zhprs_lextype
(2 rows)
digoal=> CREATE TEXT SEARCH CONFIGURATION zhcfg (PARSER = zhparser);
CREATE TEXT SEARCH CONFIGURATION
目前没有配置lexeme类型和字典的对应关系。
digoal=> \dF+ zhcfg
Text search configuration "digoal.zhcfg"
Parser: "public.zhparser"
Token | Dictionaries
-------+--------------
调试中文parser, 可以看到匹配的类型为e
digoal=> select * from ts_debug('zhcfg','as soon the possible');
alias | description | token | dictionaries | dictionary | lexemes
-------+-------------+----------+--------------+------------+---------
e | exclamation | as | {} | |
e | exclamation | soon | {} | |
e | exclamation | the | {} | |
e | exclamation | possible | {} | |
(4 rows)
但是因为没有配置类型和字典的对应关系, 所以看不到输出.
digoal=> select * from to_tsvector('zhcfg','as soon the possible');
to_tsvector
-------------
(1 row)
zhparser支持哪些类型呢? 如下:
digoal=> select * from pg_catalog.ts_token_type('zhparser');
tokid | alias | description
-------+-------+-------------------------------
97 | a | adjective
98 | b | differentiation (qu bie)
99 | c | conjunction
100 | d | adverb
101 | e | exclamation
102 | f | position (fang wei)
103 | g | root (ci gen)
104 | h | head
105 | i | idiom
106 | j | abbreviation (jian lue)
107 | k | head
108 | l | tmp (lin shi)
109 | m | numeral
110 | n | noun
111 | o | onomatopoeia
112 | p | prepositional
113 | q | quantity
114 | r | pronoun
115 | s | space
116 | t | time
117 | u | auxiliary
118 | v | verb
119 | w | punctuation (qi ta biao dian)
120 | x | unknown
121 | y | modal (yu qi)
122 | z | status (zhuang tai)
(26 rows)
创建对应关系, 所有类型使用英语和simple字典, 使用英语词典可以帮助我们去除一些无意义的词如as the this a等.
digoal=> ALTER TEXT SEARCH CONFIGURATION zhcfg ADD MAPPING FOR a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z WITH english_stem, simple;
ALTER TEXT SEARCH CONFIGURATION
现在可以正常的匹配字典了.
digoal=> select * from ts_debug('zhcfg','as soon the possible 周正中 德哥 刘德华');
alias | description | token | dictionaries | dictionary | lexemes
-------+-------------+----------+-----------------------+--------------+-----------
e | exclamation | as | {english_stem,simple} | english_stem | {}
e | exclamation | soon | {english_stem,simple} | english_stem | {soon}
e | exclamation | the | {english_stem,simple} | english_stem | {}
e | exclamation | possible | {english_stem,simple} | english_stem | {possibl}
q | quantity | 周 | {english_stem,simple} | english_stem | {周}
v | verb | 正中 | {english_stem,simple} | english_stem | {正中}
n | noun | 德哥 | {english_stem,simple} | english_stem | {德哥}
n | noun | 刘德华 | {english_stem,simple} | english_stem | {刘德华}
(8 rows)
digoal=> select * from to_tsvector('zhcfg','as soon the possible 周正中 德哥 刘德华');
to_tsvector
----------------------------------------------------------
'possibl':4 'soon':2 '刘德华':8 '周':5 '德哥':7 '正中':6
(1 row)
很明显,我的名字不在中文parser词典中,我现在要加进去。
修改它:dict.utf8.xdb
# wget http://www.xunsearch.com/scws/down/phptool_for_scws_xdb.zip
# unzip phptool_for_scws_xdb.zip
inflating: make_xdb_file.php
inflating: readme.txt
inflating: xdb.class.php
inflating: dump_xdb_file.php
# chmod 755 *.php
步骤,将XDB导出文本,编辑文本,合成XDB。
1. 导出
# yum install -y php php-mbstring
# php ./dump_xdb_file.php /opt/pgsql/share/tsearch_data/dict.utf8.xdb ./xdb
目前有28万多词组
# wc -l xdb
284647 xdb
[root@db-192-168-173-33 soft_bak]# less xdb
# WORD TF IDF ATTR
当机立断 14.01 8.10 i
禎 1.01 0.00 @
银朱 11.85 12.31 n
集科 13.63 8.03 n
负电 12.69 10.49 n
那霸 12.53 10.85 nr
无名肿毒 12.33 12.16 l
.....
导出后内容有4个字段,包括单词,tf(),idf,属性:
添加删除修改自定义词库只要编辑该文件即可,以下为相关规范:
文件为纯文本文件,编码必须是 UTF-8,可用任何编辑器修改
每行一条记录表示一个词,每行包含 1~4 个字段,字段之间用空格或制表符(\t)分隔
字段含义依次表示 “词语”,“词频(TF)”,“逆词频率(IDF)”,“词性(ATTR)”
后面三个字段如果省略依次使用 scws 的默认值
特殊词性 ! 可用于表示删除该词
自定义词典优先于内置词典加载和使用,以 # 开头的行为注释
TF和IDF,它们合起来称作TF-IDF(term frequency– inverse document frequency),是一种用于资讯检索与资讯探勘的常用加权技术,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。
TFIDF的主要思想是:如果某个词或短语在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用分类。
说起来很不好理解,其实也不需要理解,SCWS也提供了新词生词的TF/IDF计算器,可以自动获得词语的权重值。
ATTR是词性,也就是标示词语是名字、动词、形容词等等词性的。
详细的词性标示方法请看SCWS的说明:(词典词性标注详解)
我要添加的词语是”芽菜“,是名词,用n标示。
在 http://www.xunsearch.com/scws/demo/get_tfidf.php 网址可以生成词的IF,IDF
例如芽菜得到的IF、IDF值分别是13.82和7.48,那么在xdb结尾我追加了如下一行:
芽菜 13.82 7.48 n
其中属性和zhparser支持的类型对应。例如,见alias:
digoal=> select * from ts_debug('zhcfg','禎一口吸尽西江水当机立断无名肿毒');
alias | description | token | dictionaries | dictionary | lexemes
-------+---------------+----------------+-----------------------+--------------+------------------
x | unknown | 禎 | {english_stem,simple} | english_stem | {禎}
l | tmp (lin shi) | 一口吸尽西江水 | {english_stem,simple} | english_stem | {一口吸尽西江水}
i | idiom | 当机立断 | {english_stem,simple} | english_stem | {当机立断}
l | tmp (lin shi) | 无名肿毒 | {english_stem,simple} | english_stem | {无名肿毒}
(4 rows)
xdb中的@属性对应zhparser里的x,详见代码
2. 编辑
我要把周正中添加到xdb里面,这是个人名,就用n作为ATTR,TF,IDF查一下
http://www.xunsearch.com/scws/demo/get_tfidf.php
结果:
WORD=周正中 TF=12.07 IDF=12.38
加到xdb最后一行如下:
周正中 12.07 12.38 n
3. 合成xdb文件
注意事项
3.1. 词典导出:dump_xdb_file.php 在命令行模式下运行
php dump_xdb_file.php <要导出的.xdb文件> [存入的文本文件]
第二参数省略则直接输出到标准输出。
3.2. 词典生成:make_xdb_file.php 同样是在命令行模式下运行(需要安装 mbstring 扩展)
默认是处理 gbk 编码的文本,如果你的文本是 utf8,则需要修改该程序的第一行,把
define(‘IS_UTF8_TXT’, false); 改为 true
php make_xdb_file.php <要生成的.xdb> [导入的文本文件]
如果使用php写的合成工具有问题,建议直接用scws提供的合成工具导入。
[root@db-192-168-173-33 soft_bak]# rm -f /opt/pgsql/share/tsearch_data/dict.utf8.xdb
[root@db-192-168-173-33 soft_bak]# php /opt/soft_bak/make_xdb_file.php /opt/pgsql/share/tsearch_data/dict.utf8.xdb ./xdb
使用scsw提供的合成工具导入,需要安装scws,不过这个在部署zhparser时已经安装了,下面的步骤可以省略。
wget http://www.xunsearch.com/download/xunsearch-full-latest.tar.bz2
tar -jxvf xunsearch-full-latest.tar.bz2
cd xunsearch-full-1.4.9
cd packages
tar -jxvf scws-1.2.3-dev.tar.bz2
cd scws-1.2.3-dev
安装之, 使用:
# cd /opt/scws-1.2.2/bin/
# ll
total 52
-rwxr-xr-x 1 root root 29995 May 3 14:29 scws
-rwxr-xr-x 1 root root 18833 May 3 14:29 scws-gen-dict
# ./scws-gen-dict -h
scws-gen-dict (scws-mkdict/1.2.2)
Convert the plain text dictionary to xdb format.
Copyright (C)2007 by hightman.
Usage: scws-gen-dict [options] [input file] [output file]
-i Specified the plain text dictionary(default: dict.txt).
-o Specified the output file path(default: dict.xdb)
-c Specified the input charset(default: gbk)
-p Specified the PRIME num for xdb
-v Show the version.
-h Show this page.
Report bugs to <hightman2@yahoo.com.cn>
# rm -f /opt/pgsql/share/tsearch_data/dict.utf8.xdb
# ./scws-gen-dict -i /opt/soft_bak/xdb -o /opt/pgsql/share/tsearch_data/dict.utf8.xdb -c UTF-8
Reading the input file: /opt/soft_bak/xdb ...OK, total nodes=378213
Optimizing... OK
Dump the tree data to: /opt/pgsql/share/tsearch_data/dict.utf8.xdb ... OK, all been done!
# ll /opt/pgsql/share/tsearch_data/dict.utf8.xdb
-rw------- 1 root root 14315542 May 3 18:34 /opt/pgsql/share/tsearch_data/dict.utf8.xdb
现在可以查询一下新增的词组是否正常分词了:
digoal=> select * from ts_debug('zhcfg','禎一口吸尽西江水当机立断无名肿毒周正中德哥你好');
...
n | noun | 周正中 | {english_stem,simple} | english_stem | {周正中}
n | noun | 德哥 | {english_stem,simple} | english_stem | {德哥}
...
参考
1. 《PostgreSQL chinese full text search 中文全文检索》
2. http://www.postgresql.org/docs/devel/static/textsearch-dictionaries.html
3. http://www.xunsearch.com/scws/download.php