需求背景
在我司负责的其中一个微服务为公司的各个事业线提供了整个短信接口。
受限于日益抓紧的电信运营商的政策,短信发送越来越困难。
各个短信服务商都提出了类似的报备短信模板的要求,否则短信发送速率会受影响。
一般而言,各个系统发送新短信的时候会向短信服务商做增量报备。
但是一旦更换服务商,就需要整理所有短信模板了。
初版
人工整理过往短信模板,需要多事业线人员参与,耗费人力成本很大,而且容易漏。
维护短信模板文档,可以作为以后的方案,但是首次使用仍然需要人工整理过往短信模板,避不开工作量。
故考虑通过收集近期1~3个月短信自动分析短信模板。
经研究,php提供了similar_text()
计算文本相似度,单条短信时间复杂度O(n^3)
于是我们可以通过设置合理的阈值,遍历短信记录和生成的示例,生成短信示例。
整个脚本的时间复杂度是O(m.n)
m:短信记录数 n:短信模板数目
改进
平均模板数目有200个,因种种原因我们更换过6次短信服务商。
使用上述方案,每次集中报备都需要手动扣出非模板的动态内容,效率不高。
故需要有模板分析工具。
其中由于短信模板中的动态内容内容常为名字等普通格式的内容,无法使用正则等传统方式分析,也没找到相关的类库,工具或者方法。
固考虑通过将相似度高的同类字符串进行比对,不断提取公共字串来逼近短信模板。
对于两个长度均为n的字符串,按常规双指针遍历方法统计最长的公共字符串,经线下环境测试,在短信长度大于30字时,已无法在正常时间内完成计算两个短信的计算,即无法用于任何长营销短信的短信分析。
因为实际上两条短信计算公共字符串所需要的时间复杂度为O(2^n)
n短信平均长度
,在n大于20的时候已经无法作为小常数系数忽略。
即整个脚本的时间复杂度暴增为O(m.n.2^l)
m:短信记录数 n:短信模板数目 l:平均短信长度
思路
下续为方便说明,定义
短信A为字符串A:其元素为a1,a2.....an..aN
短信B为字符串B:其元素为b1,b2.....bm..bM
a1~an
和b1~bm
对应的公共字符串叫c(m,n)
O(2^n+m)
其中大量时间用于计算重叠子问题a1,a2.....an..aN
,b1,b2.....bm..bM
的各自子集的公共字串,故只要可以分离出重复低次项,即可使用dp的思路降低时间复杂度。
任取短信A的一子串a1~an
,与B的子串b1~bm
显而易见:
-
若an=bm 则
a1~an
,b1~bm
的的最长子串 c(n,m)为c(n-1,m-1)加上最后的an -
若an!=bm 则 an/bm与最长字串无关,且c(n,m)等价于,c(n-1,m),c(m-1,n)中长度较长者
即实际上在已知an与bm的值的情况下,只需要事先再知道c(n-1,m-1),c(n-1,m),c(n,m-1)三个中间值的信息,即可直接计算出c(n,m),即c(n,m)可在最多n+m次降次后,即可直接,无需完整遍历两个字符串,重复的计算成本被剔除。
为此我们保存一个N*M长度的矩阵用于保存c(n,m)所有中间值的长度。
此外,为了还原子字符串的生长方向需要记录每个c(n,m)是由c(n-1,m-1),c(n-1,m),c(n,m-1)哪一个计算而来
矩阵
从上述矩阵即可知道,两个公共字符串即为*好陈*牛
,这种算法的时间从O(2^(M+N))
优化到O(M*N)
M:字符串A长度,N字符串B长度
宏观上,整个脚本的时间复杂度回归到O(m.n)
m:短信记录数 n:短信模板数目。
脚本在线上灰度执行和全量执行都没有问题目标达成。
附源码实现(PHP)
<?php
/**
* 根据两个字符串,推断公共的模板
* @author Jiankang maijiankang@foxmail.com
* Class FindStringTemplate
*/
class FindStringTemplate{
const MARK_LEFT=1;
const MARK_UP=2;
const MARK_LEFT_UP=3;
/**
* 最长公共子字符串衍生方向矩阵
* @var array
*/
private $matrixDirection = [[]];
/**
* 最长公共子字符串长度矩阵
* @var array
*/
private $matrixLen = [[]];
private $str;
private $str2;
private $len1;
private $len2;
private $par='*';
private $strTmpl='';
private $link = false;
/**
*
* FindStringTemplate constructor.
* @param string $str1 字符串1
* @param string $str2 字符串2
* @param string $par 动态部分使用什么代替
*/
public function __construct($str1, $str2,$par='*'){
//目前的执行速度够快了,如果后续短信进一步增加,可以使用SplFixedArray优化常量系数
$str1 = isset($str1)?$str1:'';;
$str2 = isset($str2)?$str2:'';;
$this->str=preg_split('//u', $str1, -1, PREG_SPLIT_NO_EMPTY);
$this->str2=preg_split('//u', $str2, -1, PREG_SPLIT_NO_EMPTY);
$this->len1 = count($this->str);
$this->len2 = count($this->str2);
$this->par=$par;
}
public function getTempl(){
$this->buildMatrix();
if($this->matrixDirection[0][0]!=self::MARK_LEFT_UP){
$this->strTmpl =$this->par;
$this->link=true;
}else{
$this->strTmpl ='';
$this->link=false;
}
$this->buildTempl($this->matrixDirection , $this->len1-1, $this->len2-1);
//$this->strTmpl=preg_replace("|\\{$this->par}+|",$intPar,$this->strTmpl);
return $this->strTmpl;
}
/**
* 方向矩阵构建
* @author Jiankang maijiankang@foxmail.com
*/
private function buildMatrix(){
$str1=$this->str ;
$str2=$this->str2 ;
for($i = 0; $i < $this->len1; $i++){
for($j = 0; $j < $this->len2; $j++) {
$this->matrixLen[$i][$j] = 0;
}
}
for($i = 0; $i < $this->len1; $i++) {
for($j = 0; $j < $this->len2; $j++) {
if($i == 0 || $j == 0) {
if($str1[$i] == $str2[$j])
{
$this->matrixLen[$i][$j] = 1;
$this->matrixDirection[$i][$j] = self::MARK_LEFT_UP;
}else if($i > 0) {
$this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j];
$this->matrixDirection[$i][$j] = self::MARK_UP;
}else if($j > 0) {
$this->matrixLen[$i][$j] = $this->matrixLen[$i][$j-1];
$this->matrixDirection[$i][$j] = self::MARK_LEFT;
}else{
$this->matrixDirection[$i][$j] = self::MARK_LEFT;
}
}else if($str1[$i] == $str2[$j]) {
$this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j-1] + 1;
$this->matrixDirection[$i][$j] = self::MARK_LEFT_UP;
}else if($this->matrixLen[$i-1][$j] > $this->matrixLen[$i][$j-1])
{
$this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j];
$this->matrixDirection[$i][$j] = self::MARK_UP;
}else {
$this->matrixLen[$i][$j] = $this->matrixLen[$i][$j-1];
$this->matrixDirection[$i][$j] = self::MARK_LEFT;
}
}
}
}
/**
* 根据方向矩阵推导短信模板
* @param $matrixDirection
* @param $row
* @param $col
* @author Jiankang maijiankang@foxmail.com
*/
private function buildTempl($matrixDirection, $row, $col){
$str=$this->str;
if($this->len1 == 0 || $this->len2 == 0 || !($row < $this->len1 && $col < $this->len2)){
return;
}
if($matrixDirection[$row][$col] == self::MARK_LEFT_UP) {
if($row > 0 && $col > 0){
self::buildTempl($matrixDirection, $row-1, $col-1);
}
$this->strTmpl.=$str[$row];
$this->link=false;
}else if($matrixDirection[$row][$col] == self::MARK_LEFT) {
if($col > 0){
self::buildTempl($matrixDirection, $row, $col-1);
}
if(!$this->link){
$this->link=true;
$this->strTmpl.=$this->par;
}
}else if($matrixDirection[$row][$col] == self::MARK_UP) {
if($row > 0){
self::buildTempl($matrixDirection, $row-1, $col);
}
if(!$this->link){
$this->link=true;
$this->strTmpl.=$this->par;
}
}
}
}
网友评论