搜尋
首頁後端開發php教程優化巨量關鍵字的匹配

優化巨量關鍵字的匹配

Aug 21, 2017 am 10:19 AM
關鍵字匹配


問題由來

前些天工作中遇到一個問題:

有60萬個短訊息記錄日誌,每條約50 字,5萬關鍵字,長度2-8 字,絕大部分為中文。要求將這 60萬 筆記錄中包含的關鍵字全部提取出來並統計各關鍵字的命中次數。

本文完整介紹了我的實作方式,看我如何將需要運行十小時的任務優化到十分鐘以內。雖然實作語言是 PHP,但本文介紹的更多的思想,應該可以給大家一些幫助。

原始– grep

設計

一開始接到任務的時候,我的小心思立刻轉了起來,日誌+ 關鍵字+ 統計,我沒有想到自己寫程式碼實現,而是先想到了linux 下常用的日誌統計指令grep

grep指令的用法不再多提,使用grep 'keyword' | wc -l 可以很方便地進行統計關鍵字命中的資訊條數,而php的exec()# 函數允許我們直接呼叫linux 的shell 指令,雖然這樣執行危險指令時會有安全隱患。

代碼

上偽代碼:

foreach ($word_list as $keyword) {
    $count = intval(exec("grep '{$keyword}' file.log | wc -l"));
    record($keyword, $count);
}

在一台老機器上跑的,話說老機器效率真的差,跑了6小時。估計最新機器2-3小時吧,後面的優化都使用的新機器,而且需求又有變動,正文才剛開始。

原始,原始在想法和方法

進化– 正規

設計

交了差之後,隔天產品又提出了新的想法,說以後想把某資料來源連結進來,訊息以資料流的形式傳遞,而不再是文件了。而且還要求了訊息統計的即時性,一下把我想把資料寫到檔案再統計的想法也推翻了,為了方案的可擴展性,現在的統計對像不再是一個整體,而是要考慮拿n個單條的訊息來配對了。

這時,略懵的我只好祭了最傳統的工具- 正則。正規的實作也不難,各個語言也都封裝好了正規匹配函數,重點是模式(pattern)的建構。

當然這裡的模式建構也不難,##/keyword優化巨量關鍵字的匹配|keword2|.../,用##|將關鍵字連接起來即可。 正規小坑

這裡介紹兩個使用中遇到的小坑:

    正規模式長度太長導致匹配失敗:PHP 的正規有回溯限制,以防止消耗掉所有的進程可用堆疊, 最終導致php 崩潰。太長的模式會導致 PHP 偵測到回溯過多,中斷匹配,經測試預設設定時最大模式長度為 32000 位元組 左右。 php.ini 內
  • pcre.backtrack_limit 參數為最大回溯次數限制,預設值為優化巨量關鍵字的匹配000000,修改或 #php.ini# 或在腳本開始時使用 ini_set('pcre.backtrack_limit', n); 將其設為一個較大的數字可以提高單次符合最大模式長度。當然也可以將關鍵字分批統計(我用了這個=_=)。

  • 模式中含有特殊字元導致大量warning:匹配過程中發現PHP 報出大量warning:
  • unknown modifier 亂碼<strong></strong>#,仔細檢查發現關鍵字中有/字符,可以使用preg_quote()函數過濾一遍關鍵字即可。

  • 程式碼

上偽程式碼:

$end = 0;
$step = 優化巨量關鍵字的匹配500;
$pattern = array();
// 先将pattern 拆成多个小块
while ($end < count($word_list)) {
    $tmp_arr = array_slice($word_list, $end, $step);
    $end += $step;
    $item = implode(&#39;|&#39;, $tmp_arr);
    $pattern[] = preg_quote($item);
}
 
$content = file_get_contents($log_file);
$lines = explode("\n", $content);
foreach ($lines as $line) {
    // 使用各小块pattern分别匹配
    for ($i = 0; $i < count($pattern); $i++) {
        preg_match_all("/{$pattern[$i]}/", $line, $match);
    }
    $match = array_unique(array_filter($match));
    dealResult($match);
}

為了完成任務,硬著頭皮進程跑了一夜。當第二天我發現跑了將近十個小時的時候內心是崩潰的。 。 。太慢了,完全達不到使用要求,這時,我已經開始考慮改換方法了。

當產品又改換了關鍵字策略,替換了一些關鍵字,要求重新運行一遍,並表示還會繼續優化關鍵字時,我完全否定了現有方案。絕對不能用關鍵字去匹配訊息,這樣一條一條用全部關鍵字去匹配,效率實在是不可忍受。

進化,需求和實現的進化

覺醒– 拆詞

設計

我終於開始意識到要拿訊息去關鍵字比較。如果我用關鍵字為鍵建立一個 hash 表,用資訊裡的字去 hash 表裡查找,如果查到就認為匹配命中,這樣不是能達到 O(優化巨量關鍵字的匹配) 的效率了麼?

可是一則短訊息,我如何把它拆分為剛好好的字去配對呢,分詞?分詞也是需要時間的,而且我的關鍵字都是些無語意的詞,建構詞庫、使用分詞工具又是很大的問題,最後我想到

拆詞

为什么叫拆词呢,我考虑以蛮力将一句话拆分为<strong>所有可能</strong>的词。如(我是好人)就可以拆成(我是、是好、好人、我是好、是好人、我是好人)等词,我的关键词长度为 2-8,所以可拆词个数会随着句子长度迅速增加。不过,可以用标点符号、空格、语气词(如的、是等)作为分隔将句子拆成小短语再进行拆词,会大大减少拆出的词量。

其实分词并没有完整实现就被后一个方法替代了,只是一个极具实现可能的构想,写这篇文章时用伪代码实现了一下,供大家参考,即使不用在匹配关键词,用在其他地方也是有可能的。

代码

$str_list = getStrList($msg);
foreach ($str_list as $str) {
    $keywords = getKeywords($str);
    foreach ($keywords as $keyword) {
        // 直接通过PHP数组的哈希实现来进行快速查找
        if (isset($word_list[$keyword])) {
            record($keyword);
        }
    }
}
/**
* 从消息中拆出短句子
*/
function getStrList($msg) {
    $str_list = array();
    $seperators = array(&#39;,&#39;, &#39;。&#39;, &#39;的&#39;, ...);
 
    $words = preg_split(&#39;/(?<!^)(?!$)/u&#39;, $msg);
    $str = array();
    foreach ($words as $word) {
        if (in_array($word, $seperators)) {
            $str_list[] = $str;
            $str = array();
        } else {
            $str[] = $word;
        }
    }
 
    return array_filter($str_list);
}
 
/**
* 从短句中取出各个词
*/
function getKeywords($str) {
    if (count($str) < 2) {
        return array();
    }
 
    $keywords = array();
    for ($i = 0; $i < count($str); $i++) {
        for ($j = 2; $j < 9; $j++) {
            $keywords[] = array_slice($str, $i, $j); // todo 限制一下不要超过数组最大长度
        }
    }
 
    return $keywords;
}

结果

我们知道一个 utf-8 的中文字符要占用三个字节,为了拆分出包含中英文的每一个字符,使用简单的 split() 函数是做不到的。

这里使用了 preg_split('/(? 是通过正则匹配到两个字符之间的''来将两个字符拆散,而两个括号里的 (? 是分别用来限定捕获组不是第一个,也不是最后一个(不使用这两个捕获组限定符也是可以的,直接使用//作为模式会导致拆分结果在前后各多出一个空字符串项)。  捕获组的概念和用法可见我之前的博客 PHP正则中的捕获组与非捕获组

由于没有真正实现,也不知道效率如何。估算每个短句长度约为 優化巨量關鍵字的匹配0 字左右时,每条短消息约50字左右,会拆出 200 个词。虽然它会拆出很多无意义的词,但我相信效率绝不会低,由于其 hash 的高效率,甚至我觉得会可能比终极方法效率要高。

最终没有使用此方案是因为它对句子要求较高,拆词时的分隔符也不好确定,最重要的是它不够优雅。。。这个方法我不太想去实现,统计标识和语气词等活显得略为笨重,而且感觉拆出很多无意义的词感觉效率浪费得厉害。

觉醒,意识和思路的觉醒

终级 – Trie树

trie树

于是我又来找谷哥帮忙了,搜索大量数据匹配,有人提出了 使用 trie 树的方式,没想到刚学习的 trie 树的就派上了用场。我上上篇文章刚介绍了 trie 树,在空间索引 – 四叉树 里字典树这一小节,大家可以查看一下。

当然也为懒人复制了一遍我当时的解释(看过的可以跳过这一小节了)。

字典树,又称前缀树或 trie 树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。

我们可以类比字典的特性:我们在字典里通过拼音查找晃(huang)这个字的时候,我们会发现它的附近都是读音为huang的,可能是声调有区别,再往前翻,我们会看到读音前缀为huan的字,再往前,是读音前缀为hua的字… 取它们的读音前缀分别为 h qu hua huan huang。我们在查找时,根据 abc...xyz 的顺序找到h前缀的部分,再根据 ha he hu找到 hu 前缀的部分…最后找到 huang,我们会发现,越往后其读音前缀越长,查找也越精确,这种类似于字典的树结构就是字典树,也是前缀树。

设计

那么 trie 树怎么实现关键字的匹配呢? 这里以一幅图来讲解 trie 树匹配的过程。

優化巨量關鍵字的匹配

其中要点:

构造trie树

  1. 将关键词用上面介绍的preg_split()函数拆分为单个字符。如科学家就拆分为科、学、家三个字符。

  2. 在最后一个字符后添加一个特殊字符`,此字符作为一个关键词的结尾(图中的粉红三角),以此字符来标识查到了一个关键词(不然,我们不知道匹配到科、学两个字符时算不算匹配成功)。

  3. 检查根部是否有第一个字符()节点,如果有了此节点,到步骤4。 如果还没有,在根部添加值为的节点。

  4. 依次检查并添加学、家 两个节点。

  5. 在结尾添加 ` 节点,并继续下一个关键词的插入。

匹配

然后我们以 <strong>这位科学家很了不起</strong>!为例来发起匹配。

  • 首先我们将句子拆分为单个字符 这、位、...

  • 从根查询第一个字符,并没有以这个字符开头的关键词,将字符“指针”向后移,直到找到根下有的字符节点;

  • 接着在节点下寻找值为 节点,找到时,结果子树的深度已经到了2,关键词的最短长度是2,此时需要在结点下查找是否有`,找到意味着匹配成功,返回关键词,并将字符“指针”后移,如果找不到则继续在此结点下寻找下一个字符。

  • 如此遍历,直到最后,返回所有匹配结果。

代码

完整代码我已经放到了GitHub上:Trie-GitHub-zhenbianshu,这里放上核心。

首先是数据结构树结点的设计,当然它也是重中之重:

$node = array(
    &#39;depth&#39; => $depth, // 深度,用以判断已命中的字数
    &#39;next&#39; => array(
        $val => $node, // 这里借用php数组的哈希底层实现,加速子结点的查找
        ...
    ),
);

然后是树构建时子结点的插入:

// 这里要往节点内插入子节点,所以将它以引用方式传入
private function insert(&$node, $words) {
         if (empty($words)) {
            return;
        }
        $word = array_shift($words);
        // 如果子结点已存在,向子结点内继续插入
        if (isset($node[&#39;next&#39;][$word])) {
            $this->insert($node[&#39;next&#39;][$word], $words);
        } else {
            // 子结点不存在时,构造子结点插入结果
            $tmp_node = array(
                &#39;depth&#39; => $node[&#39;depth&#39;] + 優化巨量關鍵字的匹配,
                &#39;next&#39; => array(),
            );
            $node[&#39;next&#39;][$word] = $tmp_node;
            $this->insert($node[&#39;next&#39;][$word], $words);
        }
    }

最后是查询时的操作:

// 这里也可以使用一个全局变量来存储已匹配到的字符,以替换$matched
private function query($node, $words, &$matched) {
        $word = array_shift($words);
        if (isset($node[&#39;next&#39;][$word])) {
            // 如果存在对应子结点,将它放到结果集里
            array_push($matched, $word);
            // 深度到达最短关键词时,即可判断是否到词尾了
            if ($node[&#39;next&#39;] > 優化巨量關鍵字的匹配 && isset($node[&#39;next&#39;][$word][&#39;next&#39;][&#39;`&#39;])) {
                return true;
            }
            return $this->query($node[&#39;next&#39;][$word], $words, $matched);
        } else {
            $matched = array();
            return false;
        }
    }

结果

结果当然是喜人的,如此匹配,处理一千条数据只需要3秒左右。找了 Java 的同事试了下,Java 处理一千条数据只需要優化巨量關鍵字的匹配秒。

这里来分析一下为什么这种方法这么快:

  • 正则匹配:要用所有的关键词去信息里匹配匹配次数是 key_len * msg_len,当然正则会进行优化,但基础这样,再优化效率可想而知。

  • 而 trie 树效率最差的时候是 msg_len * 9(最长关键词长度 + 優化巨量關鍵字的匹配个特殊字符)次 hash 查找,即最长关键词类似 AAA,信息内容为 AAA...时,而这种情况的概率可想而知。

至此方法的优化到此结束,从每秒钟匹配 優化巨量關鍵字的匹配0 个,到 300 个,30 倍的性能提升还是巨大的。

终级,却不一定是终极

他径 – 多进程

设计

匹配方法的优化结束了,开头说的优化到十分钟以内的目标还没有实现,这时候就要考虑一些其他方法了。

我们一提到高效,必然想到的是 并发,那么接下来的优化就要从并发说起。PHP 是单线程的(虽然也有不好用的多线程扩展),这没啥好的解决办法,并发方向只好从多进程进行了。

那么一个日志文件,用多个进程怎么读呢?这里当然也提供几个方案:

  • 在進程內新增日誌行數計數器,各個進程支援傳入參數n,進程只處理第行數 % n = n 的日誌,這種hack 的反向分散式我已經用得很熟練了,哈哈。這種方法需要進程傳參數,還需要每個進程都分配讀取整個日誌的的內存,而且不夠優雅。

  • 使用linux 的split -l n file.log output_pre 指令,將檔案分割成每份為n 行的文件,然後用多個行程去讀取多個檔案。此方法的缺點就是不靈活,想換一下進程數時需要重新切分檔。

  • 使用 Redis 的 list 佇列暫存日誌,開啟多個行程消費佇列。此方法需要另外向 Redis 內寫入數據,多了一個步驟,但它擴展靈活,而且程式碼簡單優雅。

最終使用了第三種方式來進行。

結果

這種方式雖然也會有瓶頸,最後應該會落在 Redis 的網路 IO 上。我也沒有閒心開 n 個進程去挑戰公司 Redis 的效能,運行 優化巨量關鍵字的匹配0 個進程三四分鐘就完成了統計。即使再加上 Redis 寫入的耗時,優化巨量關鍵字的匹配0分鐘以​​內也妥妥的。

一開始產品對匹配速度已經有了小時的定位了,當我優化巨量關鍵字的匹配0 分鐘就拿出了新的日誌匹配結果,看到產品驚訝的表情,心裡也是略爽的,哈哈~

他徑,也能幫你走得更遠

總結

解決問題的方法有很多種,我認為在解決各種在問題之前,要先了解很多知識,即使只知道它的作用。就像一個工具架,你要先把工具盡量擺得多,才能在遇到問題時選取一個最適合的。接著當然要把這些工具用是純熟了,這樣才能用它們去解決一些怪異問題。

工欲善其事,必先利其器,要解決效能問題,掌握系統層級的方法還略顯不夠,有時候換一種資料結構或演算法,效果可能會更好。感覺自己在這方面還略顯薄弱,慢慢加強吧,各位也共勉。

#

以上是優化巨量關鍵字的匹配的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
繼續使用PHP:耐力的原因繼續使用PHP:耐力的原因Apr 19, 2025 am 12:23 AM

PHP仍然流行的原因是其易用性、靈活性和強大的生態系統。 1)易用性和簡單語法使其成為初學者的首選。 2)與web開發緊密結合,處理HTTP請求和數據庫交互出色。 3)龐大的生態系統提供了豐富的工具和庫。 4)活躍的社區和開源性質使其適應新需求和技術趨勢。

PHP和Python:探索他們的相似性和差異PHP和Python:探索他們的相似性和差異Apr 19, 2025 am 12:21 AM

PHP和Python都是高層次的編程語言,廣泛應用於Web開發、數據處理和自動化任務。 1.PHP常用於構建動態網站和內容管理系統,而Python常用於構建Web框架和數據科學。 2.PHP使用echo輸出內容,Python使用print。 3.兩者都支持面向對象編程,但語法和關鍵字不同。 4.PHP支持弱類型轉換,Python則更嚴格。 5.PHP性能優化包括使用OPcache和異步編程,Python則使用cProfile和異步編程。

PHP和Python:解釋了不同的範例PHP和Python:解釋了不同的範例Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

PHP和Python:深入了解他們的歷史PHP和Python:深入了解他們的歷史Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

在PHP和Python之間進行選擇:指南在PHP和Python之間進行選擇:指南Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

PHP和框架:現代化語言PHP和框架:現代化語言Apr 18, 2025 am 12:14 AM

PHP在現代化進程中仍然重要,因為它支持大量網站和應用,並通過框架適應開發需求。 1.PHP7提升了性能並引入了新功能。 2.現代框架如Laravel、Symfony和CodeIgniter簡化開發,提高代碼質量。 3.性能優化和最佳實踐進一步提升應用效率。

PHP的影響:網絡開發及以後PHP的影響:網絡開發及以後Apr 18, 2025 am 12:10 AM

PHPhassignificantlyimpactedwebdevelopmentandextendsbeyondit.1)ItpowersmajorplatformslikeWordPressandexcelsindatabaseinteractions.2)PHP'sadaptabilityallowsittoscaleforlargeapplicationsusingframeworkslikeLaravel.3)Beyondweb,PHPisusedincommand-linescrip

PHP類型提示如何起作用,包括標量類型,返回類型,聯合類型和無效類型?PHP類型提示如何起作用,包括標量類型,返回類型,聯合類型和無效類型?Apr 17, 2025 am 12:25 AM

PHP類型提示提升代碼質量和可讀性。 1)標量類型提示:自PHP7.0起,允許在函數參數中指定基本數據類型,如int、float等。 2)返回類型提示:確保函數返回值類型的一致性。 3)聯合類型提示:自PHP8.0起,允許在函數參數或返回值中指定多個類型。 4)可空類型提示:允許包含null值,處理可能返回空值的函數。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱工具

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用