日度归档:2009年1月4日

php分词实现mysql全文检索

感谢thinkphp中英分词算法的作者!

最近项目需要做全文检索方面,所以找了thinkphp中英分词算法进行拓展,首先把词库sqlite形式转成了mysql

其次把分词后的中文转变成区位码形式,从而支持mysql的全文检索!

演示:http://www.ye55.cn/fc/fc.php

下载:http://www.ye55.cn/fc/group.zip   (内含词库sql文件,包含近30W词的词库,再次感谢yhustc)

主要的文件:WordSegment.class.php

 

PHP代码
  1. <?php   
  2. /** 
  3.  +—————————————————————————— 
  4.  * 中英文分词类库 
  5.  * 使用正向扫描最大字长匹配算法进行分词,使用未匹配词的队列识别字典中没有的词 
  6.  * 提供SQLITE字典查询作为参考,可以自己扩展字典查找的findinDict方法 
  7.  +—————————————————————————— 
  8.  */  
  9. class WordSegment  
  10. {//类定义开始  
  11.     // 存放结果的数组  
  12.     var $result = array();  
  13.   
  14.     /** 
  15.      +———————————————————- 
  16.      * 字典的连接句柄和字典查询次数 
  17.      +———————————————————- 
  18.      * @var integer 
  19.      * @access protected 
  20.      +———————————————————- 
  21.      */  
  22.     protected $db;  
  23.     var $querytimes = 0;  
  24.       
  25.     protected $enChar = array("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","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","1","2","3","4","5","6","7","8","9","0","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","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","0","1","2","3","4","5","6","7","8","9");  
  26.   
  27.     /** 
  28.      +———————————————————- 
  29.      * 高频词列表,这次字很难单独组词,会干扰分词程序 
  30.      +———————————————————- 
  31.      * @var array 
  32.      * @access protected 
  33.      +———————————————————- 
  34.      */  
  35.     protected $highfreq = array(‘我’,‘是’,‘为’,‘了’,‘的’,‘你’,‘他’,‘她’,‘它’,‘们’,‘这’,‘那’,‘在’,‘和’,‘一’,‘不’,‘有’,‘对’,‘中’,‘这’,‘要’,‘上’,‘也’,‘人’,‘等’,‘说’);  
  36.   
  37.     /** 
  38.      +———————————————————- 
  39.      * 标点符号列表 
  40.      +———————————————————- 
  41.      * @var array 
  42.      * @access protected 
  43.      +———————————————————- 
  44.      */  
  45.     protected   
  46.     $sign = array(‘\r’,‘\n’,‘\t’,‘`’,‘~’,‘!’,‘@’,‘#’,‘$’,‘%’,‘^’,‘&’,‘*’,‘(‘,‘)’,‘-‘,‘_’,‘+’,‘=’,‘|’,‘\\’,’\,‘"’,‘;’,‘:’,‘/’,‘?’,‘.’,‘>’,‘,’,‘<‘,‘[‘,‘{‘,‘]’,‘}’,‘·’,‘~’,‘!’,‘@’,‘#’,‘¥’,‘%’,‘……’,‘&’,‘×’,‘(’,‘)’,‘-’,‘——’,‘=’,‘+’,‘\’,‘|’,‘【’,‘{’,‘】’,‘}’,‘‘’,‘“’,‘”’,‘;’,‘:’,‘、’,‘?’,‘。’,‘》’,‘,’,‘《’,‘ ‘,‘ ’);  
  47.   
  48.     /** 
  49.      +———————————————————- 
  50.      * 挂载字典 
  51.      +———————————————————- 
  52.      * @static 
  53.      * @access public  
  54.      +———————————————————- 
  55.      * @param string $src 字典源 
  56.      +———————————————————- 
  57.      * @return void 
  58.      +———————————————————- 
  59.      */  
  60.      function openDict($pdo) {  
  61.         $this->db= new PDO($pdo[‘dbType’].‘:host=’.$pdo[‘dbHost’].‘;dbname=’.$pdo[‘dbName’], $pdo[‘dbUser’],$pdo[‘dbPass’]);  
  62.         $this->db->exec(‘SET NAMES utf8’);  
  63.      }  
  64.   
  65.     /** 
  66.      +———————————————————- 
  67.      * 卸载字典 
  68.      +———————————————————- 
  69.      * @static 
  70.      * @access public  
  71.      +———————————————————- 
  72.      * @param string $src 字典源 
  73.      +———————————————————- 
  74.      * @return void 
  75.      +———————————————————- 
  76.      */  
  77.      function closeDict() {  
  78.         $this->db=null;  
  79.      }  
  80.   
  81.     /** 
  82.      +———————————————————- 
  83.      * 在字典中查找字串.提供sqlite版本作为参考,用户可以自己扩展 
  84.      +———————————————————- 
  85.      * @static 
  86.      * @access public  
  87.      +———————————————————- 
  88.      * @param string $string 待查找的字符串 
  89.      +———————————————————- 
  90.      * @return bool 
  91.      +———————————————————- 
  92.      */  
  93.      function findinDict($string) {  
  94.         $this->querytimes++;  
  95.         $sql = "SELECT `word` FROM `dict` where `word`=‘".$string."’ limit 1"; 
  96.         $rs = $this->db->query($sql); 
  97.         if ($row=$rs->fetch(PDO::FETCH_ASSOC)) 
  98.             return true; 
  99.         else 
  100.             return false; 
  101.      } 
  102.  
  103.     /** 
  104.      +———————————————————- 
  105.      * 用中英文标点对句子进行粗分,划分成短句 
  106.      +———————————————————- 
  107.      * @static 
  108.      * @access public  
  109.      +———————————————————- 
  110.      * @param string $sentence 完整的句子 
  111.      * @param string $minSen 通过标点断句最短的词组长度 
  112.      * @param string $saveInter 是否保留标点符号 
  113.      * @param string $encoding 文字编码,默认为utf-8 
  114.      +———————————————————- 
  115.      * @return array 
  116.      +———————————————————- 
  117.      */ 
  118.      function cnSplit($sentence, $minSen, $saveInter, $encoding) { 
  119.         $len = mb_strlen($sentence,$encoding); 
  120.         $substring = array(); 
  121.         $cnTmpStr = "";  
  122.         $enTmpStr = ""; 
  123.  
  124.         for($i=0;$i<$len;$i++) 
  125.         { 
  126.             $char = mb_substr($sentence,$i,1,$encoding); 
  127.             if(in_array($char,$this->sign)) 
  128.             { 
  129.                 if($cnTmpStr != "") 
  130.                 { // 一连串的中文放入待分词的词组 
  131.                     if(mb_strlen(trim($cnTmpStr),$encoding)<=$minSen) // 遇到标点了,根据设置的标点断句最短的词组长度判断是否直接分词 
  132.                         $substring[] = array(trim($cnTmpStr),’1′); 
  133.                     else 
  134.                         $substring[] = array(trim($cnTmpStr),’0′); 
  135.                     $cnTmpStr = "";  
  136.                 }  
  137.   
  138.                 if($enTmpStr != "") 
  139.                 { // 一连串的英语字母或数字可以直接返回分词结果 
  140.                     $substring[] = array(trim($enTmpStr),’1′); 
  141.                     $enTmpStr = ""; 
  142.                 } 
  143.  
  144.                 if($saveInter) // 如果要保留标点可以直接返回分词结果 
  145.                     $substring[] = array($char,’1′); 
  146.             } 
  147.             else if(in_array($char,$this->enChar)) 
  148.             { 
  149.                 if($cnTmpStr != "") 
  150.                 { // 遇到英文或数字了,可以给中文句子断句了 
  151.                     if(mb_strlen(trim($cnTmpStr),$encoding)<=$minSen) // 遇到标点了,根据设置的标点断句最短的词组长度判断是否直接分词 
  152.                         $substring[] = array(trim($cnTmpStr),’1′); 
  153.                     else 
  154.                         $substring[] = array(trim($cnTmpStr),’0′); 
  155.                     $cnTmpStr = ""; 
  156.                 } 
  157.  
  158.                 $enTmpStr .= $char; 
  159.             } 
  160.             else 
  161.             { 
  162.                 if($enTmpStr != "") 
  163.                 { // 遇到中文了,可以给英文句子或数字断句了 
  164.                     $substring[] = array(trim($enTmpStr),’1′); 
  165.                     $enTmpStr = ""; 
  166.                 } 
  167.  
  168.                 $cnTmpStr .= $char; 
  169.             } 
  170.         } 
  171.          
  172.         // 追加没有添加到子句中的中英文句子 
  173.         if($cnTmpStr != "") {  
  174.             if($enTmpStr == "" && mb_strlen(trim($cnTmpStr),$encoding)<=$minSen) // 要判断一下后面没有英文词组,这样句子是在没有标点符号的情况下结束了 
  175.                 $substring[] = array(trim($cnTmpStr),’1′); 
  176.             else 
  177.                 $substring[] = array(trim($cnTmpStr),’0′); 
  178.         } 
  179.         if($enTmpStr != "") $substring[] = array(trim($enTmpStr),’1′); 
  180.  
  181.         return $substring; 
  182.      } 
  183.  
  184.      /** 
  185.      +———————————————————- 
  186.      * 分词函数 
  187.      +———————————————————- 
  188.      * @static 
  189.      * @access public  
  190.      +———————————————————- 
  191.      * @param string $sentence 待分词的句子 
  192.      * @param string $maxlen 每次取子串最长字数,默认为8个字.越大分词越慢,但是越准确 
  193.      * @param string $minSen 通过标点断句最短的词组长度 
  194.      * @param string $saveSingle 是否保留不能组词的单个的字 
  195.      * @param string $saveInter 是否保留标点符号 
  196.      * @param string $encoding 文字编码,默认为utf-8 
  197.      * @param string $dict 字典的连接字符串 
  198.      +———————————————————- 
  199.      * @return array 
  200.      +———————————————————- 
  201.      */ 
  202.      function segment($sentence,$pdo=array(‘dbType’=>’mysql’,’dbHost’=>’localhost’,’dbName’=>’furyee’,’dbUser’=>’root’,’dbPass’=>’root’),$maxlen = 8, $minSen = 3, $saveSingle = false, $saveInter = false, $encoding=’utf-8′) { 
  203.         $this->openDict($pdo); // 挂载字典 
  204.          
  205.         $this->result = array(); 
  206.         $this->querytimes = 0; 
  207.  
  208.         $subSens = $this->cnSplit($sentence, $minSen, $saveInter, $encoding); //使用标点将长句分成短句 
  209.  
  210.         foreach($subSens as $item) 
  211.         { 
  212.             if($item[1] == ‘1’) 
  213.             { 
  214.                 $this->result[] = trim($item[0]); 
  215.                 continue; 
  216.             } 
  217.             else 
  218.                 $subSen = $item[0]; 
  219.  
  220.             $bFind = false; 
  221.             $i = $j = $N = 0; // i,j是扫描的指针.N是本次扫描的子串字数上界 
  222.             $M = $maxlen; // 每次取子串最长字数,默认为8个字.M越大分词越慢,但是越准确 
  223.             $tmpStr = ”; //用来记录没有匹配的字,多个连续的未匹配的字认为组合成一个词. 
  224.             $sub_str = ”; //每次取的子串 
  225.  
  226.             $senLen = mb_strlen($subSen,$encoding); //字符串长度 
  227.              
  228.             while($i < $senLen) { 
  229.                 $N = ($i+$M) < $senLen ? $M : $senLen-$i; 
  230.                 //N是本次扫描的子串字数上界 
  231.                 $bFind = false; 
  232.                 for($j = $N; $j > 0; $j–) { 
  233.                     //取子串到字典中匹配 
  234.                     $sub_str = mb_substr($subSen,$i,$j,$encoding); //从$i指的地方开始,取$j的长度 
  235.  
  236.                     if($this->findinDict($sub_str)) { 
  237.                         // 字典中有该词 
  238.                         if(mb_strlen($tmpStr,$encoding) < 2 && !$saveSingle) //临时字符串中只有一个字或没有词 
  239.                             $tmpStr = ""; //清空它  
  240.                         else if($tmpStr != "") 
  241.                         { 
  242.                             $this->result[] = $tmpStr; //多个连续的没有匹配的字认为他组成一个生词 
  243.                             $tmpStr = ""; 
  244.                         } 
  245.  
  246.                         $this->result[] = $sub_str; 
  247.  
  248.                         $bFind = true; 
  249.                         $i+=$j; //指针后移 
  250.                         break; 
  251.                     } 
  252.                 } 
  253.  
  254.                 if(!$bFind) { 
  255.                     if(in_array($sub_str,$this->highfreq)) //当前单个字无法匹配,而且它是高频词 
  256.                     { 
  257.                         if(mb_strlen($tmpStr,$encoding) ==1 && !$saveSingle) 
  258.                         //临时字符串中只有一个字,遇到高频词可以进行断句,所以要判断一下临时队列 
  259.                             $tmpStr = ""; //清空它  
  260.                         else if($tmpStr != "") 
  261.                         { 
  262.                             $this->result[] = $tmpStr; //多个连续的没有匹配的字认为他组成一个生词 
  263.                             $tmpStr = ""; 
  264.                         } 
  265.  
  266.                         if($saveSingle) // 如果要保留单个的高频字,将它保留下来,否则剔除 
  267.                             $this->result[] = $sub_str; 
  268.                     } 
  269.                     else 
  270.                         $tmpStr .= $sub_str; //不是标点,是一个没有匹配的单个的字 
  271.                     $i++; 
  272.                 } 
  273.             } 
  274.             if($tmpStr !="" ) $this->result[] = $tmpStr; // 扫描结束,临时队列还有词,那应该是最后面无法进行分词的一些字 
  275.         } 
  276.         $this->closeDict(); //卸载字典 
  277.         return $this->result; 
  278.      } 
  279.  
  280.     function zhcode($sentence,$pdo=array(‘dbType’=>’mysql’,’dbHost’=>’localhost’,’dbName’=>’furyee’,’dbUser’=>’root’,’dbPass’=>’root’),$maxlen = 8, $minSen = 3, $saveSingle = false, $saveInter = false, $encoding=’utf-8′) 
  281.     { 
  282.         $val=”; 
  283.         $arr=$this->segment($sentence,$pdo,$maxlen,$minSen,$saveSingle,$saveInter,$encoding); 
  284.          
  285.         $str=implode(‘ ‘,$arr); 
  286.         $strlen=mb_strlen($str,$encoding); 
  287.          
  288.         for($i=0;$i<$strlen;$i++){ 
  289.             $tmpstr=mb_substr($str,$i,1,$encoding);          
  290.             if(strlen($tmpstr)==1){ 
  291.                 $val.=$tmpstr; 
  292.             }else{ 
  293.                 //echo $tmpstr.'<br>’; 
  294.                 $tmpstr=iconv(‘UTF-8′,’GB2312’,$tmpstr); 
  295.                 $str_qwm = sprintf("%02d%02d",ord($tmpstr[0])-160,ord($tmpstr[1])-160);  
  296.                 //echo iconv(‘GB2312′,’UTF-8′,$tmpstr).$str_qwm.'<br>’;  
  297.                 $val.=$str_qwm;   
  298.             }  
  299.         }  
  300.           
  301.         return array($val,$arr);  
  302.     }  
  303. }  
  304. ?>  

 

 

演示文件: fc.php

 

PHP代码
  1. <?php  
  2. $starttime = ExecTime();  
  3. include(‘WordSegment.class.php’);  
  4. $str=‘冻番茄www.phpd.cn<p>现在市面上的樱花啊,爆米花的笔都可以画出美丽可爱的立体图案,可以装饰手机什么的,但是价钱贵,还不方便,比方说爆米花的,就要通过加热才会有效果.</p><p>最好有一种写出来就是爆米花的效果的笔.那就最好了,最好还可以通过不停的温度有不同的颜色(就像会变色的手表)和效果……</p><p><img src="/upload/image/2008-09-01/2008090104363891/55a19b38ee7f6671e64b7ee46feb2b0a.jpg" alt="" /></p>’;  
  5.   
  6. $ws = new WordSegment; // 实例化一个分词类的对象  
  7. $result = $ws->zhcode($str);  
  8. print_r($result);  
  9. $totaltime =ExecTime()-$starttime ;  
  10. echo "<BR><BR><BR>分词时间: $totaltime 秒<br><br>www.phpd.cn 冻番茄";  
  11. function ExecTime(){  
  12.     $time = explode(" ", microtime());  
  13.     $usec = (double)$time[0];  
  14.     $sec = (double)$time[1];  
  15.     return $sec + $usec;  
  16. }  
  17. ?>