目錄
問題由來
原始– grep
設計
代碼
進化– 正規
這裡介紹兩個使用中遇到的小坑:
上偽程式碼:
我終於開始意識到要拿訊息去關鍵字比較。如果我用關鍵字為鍵建立一個 hash 表,用資訊裡的字去 hash 表裡查找,如果查到就認為匹配命中,這樣不是能達到 O(優化巨量關鍵字的匹配) 的效率了麼?
代码
结果
终级 – Trie树
trie树
设计
构造trie树
匹配
他径 – 多进程
結果
總結
首頁 後端開發 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(&#39;/(?<!^)(?!$)/u&#39;, $msg) 是通过正则匹配到两个字符之间的&#39;&#39;来将两个字符拆散,而两个括号里的 (?<!^)(?!$) 是分别用来限定捕获组不是第一个,也不是最后一个(不使用这两个捕获组限定符也是可以的,直接使用//作为模式会导致拆分结果在前后各多出一个空字符串项)。 捕获组的概念和用法可见我之前的博客 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

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

解釋一下explorer.exe進程是什麼 解釋一下explorer.exe進程是什麼 Feb 18, 2024 pm 12:11 PM

explorer.exe是什麼進程在我們使用Windows作業系統的時候,常常會聽到一個名詞"explorer.exe".那麼,你是否好奇這個進程到底是什麼?在本文中,我們將詳細解釋explorer.exe是什麼進程以及其功能和作用。首先,explorer.exe是Windows作業系統的關鍵流程,它負責管理和控制Windows資源管理器(Window

小米 14 Ultra怎麼調整光圈? 小米 14 Ultra怎麼調整光圈? Mar 19, 2024 am 09:01 AM

光圈大小的調整對於拍照效果有著至關重要的影響,小米14Ultra在相機光圈調整方面提供了前所未有的靈活性。為了讓大家都能順利調節光圈,實現光圈大小的自由調節,小編在這裡為大家帶來了小米14Ultra怎麼設定光圈的詳細教學。小米14Ultra怎麼調整光圈?啟動相機,切換至“專業模式”,選擇主鏡頭-W鏡頭。點選光圈,開啟光圈轉盤,A為自動,按需選擇f/1.9或f/4.0。

r5 5600x最高能帶動什麼顯示卡 最新用5600X搭配RX6800XT效能 r5 5600x最高能帶動什麼顯示卡 最新用5600X搭配RX6800XT效能 Feb 25, 2024 am 10:34 AM

10月29日,AMD終於發表了備受用戶期待的重磅產品,即基於全新RDNA2架構的RX6000系列遊戲顯示卡。這款顯示卡與先前推出的基於全新ZEN3架構的銳龍5000系列處理器相輔相成,形成了一個全新的雙A組合。這次的發布不僅使得競爭對手「雙英」黯然失色,也對整個DIY硬體圈產生了重大影響。接下來,圍繞筆者手中這套AMD銳龍5600X和RX6800XT的組合作為測試例子,來見證下現如今的AMD究竟有多麼Yse?首先說說CPU處理器部分,上一代採用ZEN2架構的AMD銳龍3000系列處理器其實已經令用

發生0x0000004e錯誤代表了什麼問題 發生0x0000004e錯誤代表了什麼問題 Feb 18, 2024 pm 01:54 PM

0x0000004e是什麼故障在電腦系統中,故障是常見的問題。當電腦遇到故障時,系統通常會因為無法正常運作而出現停機、當機或出現錯誤提示。而在Windows系統中,有一個特定的故障碼0x0000004e,這是一個藍屏錯誤代碼,表示系統遇到了一個嚴重的錯誤。 0x0000004e藍色畫面錯誤是由於系統核心或驅動程式問題導致的。這種錯誤通常會導致電腦系統

Cheat Engine怎麼設定中文?ce修改器設定中文的方法 Cheat Engine怎麼設定中文?ce修改器設定中文的方法 Mar 18, 2024 pm 01:20 PM

Ce修改器(CheatEngine)是一款專用於對遊戲內存進行修改和編輯的遊戲修改工具,那麼在CheatEngine中怎麼設置中文呢?接下來小編為大夥講述ce修改器設置中文的方法內容,希望可以幫助到有需要的朋友。在我們下載的新軟體中,若發現它不是中文介面,可能會讓人感到困惑。儘管這款軟體不是由中國開發的,但我們仍有方法將其轉換為中文版本。只要簡單地套用中文補丁,就能解決這個問題。在下載並安裝了CheatEngine(ce修改器)軟體後,開啟安裝位置,找到名為languages的資料夾,如下圖所示

記憶體頻率和時序哪個對效能影響較大 記憶體頻率和時序哪個對效能影響較大 Feb 19, 2024 am 08:58 AM

記憶體是電腦中非常重要的組件之一,它對電腦的效能和穩定性有著重要影響。在選擇記憶體時,人們往往會專注於兩個重要的參數,即時序和頻率。那麼,對於記憶體效能來說,時序和頻率哪個更重要呢?首先,我們來了解時序和頻率的概念。時序指的是記憶體晶片在接收和處理資料時所需的時間間隔。它通常以CL值(CASLatency)來表示,CL值越小,記憶體的處理速度越快。而頻率則是內

榮耀 90 GT怎麼更新榮耀MagicOS 8.0? 榮耀 90 GT怎麼更新榮耀MagicOS 8.0? Mar 18, 2024 pm 06:46 PM

榮耀90GT是一款性價比很高的智慧型手機,擁有出色的效能和出色的使用者體驗。然而,有時候我們可能會遇到一些問題,例如榮耀90GT怎麼更新榮耀MagicOS8.0呢?這個步驟因為不同的手機不同的機型可能會有些差別,那麼,讓我們一起來探討一下,如何正確地升級系統。榮耀90GT怎麼更新榮耀MagicOS8.0?2月28日訊息,榮耀今天為旗下90GT/100/100Pro三款手機推送MagicOS8.0公測更新,包版本號為8.0.0.106(C00E106R3P1)1.確保您的榮耀90GT的電池電量充足,

DaVinci Resolve Studio 已支援AMD顯示卡的AV1硬體編碼 DaVinci Resolve Studio 已支援AMD顯示卡的AV1硬體編碼 Mar 06, 2024 pm 10:04 PM

最近新消息,lackMagic目前推出了達文西DaVinciResolveStudio影片編輯軟體的18.5PublicBeta2公測版更新,為AMDRadeon顯示卡帶來了AV1編碼支援。更新到最新版本後,AMD顯示卡用戶將能夠在DaVinciResolveStudio中利用硬體加速來進行AV1編碼。儘管官方並未具體指明支援的架構或型號,但預計所有的AMD顯示卡用戶都可以嘗試這項功能。 2018年,AOMedia發布了全新的視訊編碼標準AV1(AOMediaVideoCodec1.0)。 AV1是由多家

See all articles