使用星号PHP脚本匹配电话前缀的最快方法

时间:2009-09-28 21:32:32

标签: php performance caching asterisk

并提前感谢您的帮助。

背景 - 我正在编写一个PHP脚本,需要找出调用者试图达到的目的地。电话号码前缀是标识目的地的字符串。对于每个调用,程序必须找到与字符串匹配的最长前缀。例如,数字30561234567将匹配305但不会匹配3057或304.如果存在3056,则它将是首选匹配。

在研究了几个数据结构之后,每个节点存储一个数字并包含指向其他10个可能选择的指针的树似乎是理想的。这可以实现为一个数组数组,其中脚本可以检查3,在那里找到一个数组,然后在该新数组上检查0,找到另一个数组,依此类推,直到找到一个值而不是数组。该值将是目标ID(脚本的输出)。

要求 - 性能绝对至关重要。检查这些前缀所花费的时间会延迟调用,并且每个服务器都必须处理大量调用,因此数据结构必须存储在内存中。目前大约有6000个前缀。

问题 - 每次服务器接收调用时都会运行该脚本,因此数据必须保存在某种缓存服务器中。在检查了memcached和APC(高级PHP缓存)之后,我决定使用APC,因为它[更快] [3](它是一个本地内存存储)

我遇到的问题是数组数组最多可以变为10个数组,并且将是一个非常复杂的数据结构,如果我将缓存作为对象添加,将需要很长时间来反序列化

但是,如果我将每个单独的数组分别添加到缓存中(有一些逻辑命名结构可以很容易地找到它,就像数组3的3,那么30代表数组30,305代表那个补丁之后的数组等等)。 。)我将不得不从缓存中多次获取不同的数组(每次调用最多10个),这使得我经常点击缓存。

我是以正确的方式来做这件事的吗?也许有另一种解决方案?或者我提议的方法之一优于另一方法?

感谢您输入,

Alex

6 个答案:

答案 0 :(得分:2)

我看到的方式,使用简单的数组结构应该可以正常工作......

示例代码:(请注意,对于性能,前缀是数组中的键,而不是值)

// $prefixes = array(3=>1, 30=>1, 304=>1,305=>1,3056=>1,306=>1,31=>1, 40=>1);

function matchNumber($number)
{
  $prefixes = getPrefixesFromCache();

  $number = "$number";
  // try to find the longest prefix matching $number
  while ($number != '') {
    if (isset($keys[$number]))
      break;
    // not found yet, subtract last digit
    $number = substr($number, 0, -1);
  }
  return $number;
}

另一种方法是直接查询缓存中的数字 - 在这种情况下,可以进一步优化:

  1. 拆分数字串2。
  2. 在缓存中查询该字符串。
  3. 如果不存在,请转到1
  4. 存在时,将该值存储为结果,然后添加 另一个数字。
  5. Snippet :(请注意,query_cache_for()应替换为缓存机制使用的任何函数)

    function matchNumber($number)
    {
      $temp = "$number";
      $found = false;
      while (1) {
        $temp = substr($temp, 0, ceil(strlen($temp)/2) );
        $found = query_cache_for($temp);
        if ($found)
          break;
        if (strlen($temp) == 1)
          return FALSE; // should not happen!
      }
      while ($found) {
        $result = $temp;
        // add another digit
        $temp .= substr($number, strlen($temp), 1);
        $found = query_cache_for($temp);
      }
      return $result;
    }
    

    这种方法具有明显的优势,即每个前缀都是缓存中的单个元素 - 密钥可以是'asterix_prefix_< number>'例如,该值不重要(1个工作)。

答案 1 :(得分:2)

以下是N-ary树结构的一些示例代码;

class PrefixCache {
 const EOS = 'eos';
 protected $data;

 function __construct() {
  $this->data = array();
  $this->data[self::EOS] = false;
 }

 function addPrefix($str) {
  $str = (string) $str;
  $len = strlen($str);

  for ($i=0, $t =& $this->data; $i<$len; ++$i) {
   $ch = $str[$i];

   if (!isset($t[$ch])) {
    $t[$ch] = array();
    $t[$ch][self::EOS] = false;
   }

   $t =& $t[$ch];
  }

  $t[self::EOS] = true;
 }

 function matchPrefix($str) {
  $str = (string) $str;
  $len = strlen($str);

  $so_far = '';
  $best = '';

  for ($i=0, $t =& $this->data; $i<$len; ++$i) {
   $ch = $str[$i];

   if (!isset($t[$ch]))
    return $best;
   else {
    $so_far .= $ch;
    if ($t[$ch][self::EOS])
     $best = $so_far;

    $t =& $t[$ch];     
   }
  }

  return false; // string not long enough - potential longer matches remain
 }

 function dump() {
  print_r($this->data);
 }
}

然后可以将其称为

$pre = new PrefixCache();

$pre->addPrefix('304');
$pre->addPrefix('305');
// $pre->addPrefix('3056');
$pre->addPrefix('3057');

echo $pre->matchPrefix('30561234567');

根据需要执行(返回305;如果取消注释3056,则返回3056)。

请注意,我为每个节点添加了一个terminal-flag;这可以避免错误的部分匹配,即如果你添加前缀3056124,它将正确匹配3056而不是返回305612。

每次避免重新加载的最佳方法是将其转换为服务;但是,在这样做之前,我会测量APC的运行时间。它可能足够快。

亚历克斯:你的答案绝对正确 - 但不适用于这个问题:)

答案 2 :(得分:1)

由于您只使用数字,因此直接使用字符串效率很低。

您可以执行二进制搜索算法。如果您存储所有前缀(以数字方式),填充到15个位置然后按顺序,您可以扫描6000个代码,大约是log2(6000)〜= 13步。

例如,如果您有以下代码:

  • 01,0127,01273,0809,08

您可以将以下内容存储在数组中:

  1. 0100000亿
  2. 0127000亿
  3. 0127300亿
  4. 0800000亿
  5. 0809000亿
  6. 步骤如下:

    1. 将传入号码降至15 地方。
    2. 执行二进制搜索以查找 最接近的最低代码(以及它在上面数组中的索引)
    3. 查找a中代码的长度 单独的数组(使用索引)
    4. 一些示例代码可以看到它的实际效果:

      // Example for prefixes 0100,01,012,0127,0200
      $prefixes = array('0100','0101','0120','0127','0200');
      $prefix_lengths = array(4,2,3,4,4);
      $longest_length_prefix = 4;
      
      echo GetPrefix('01003508163');
      
      function GetPrefix($number_to_check) {
          global $prefixes;
          global $prefix_lengths;
          global $longest_length_prefix;
      
          $stripped_number = substr($number_to_check, 0, $longest_length_prefix);
      
          // Binary search
          $window_floor = 0;
          $window_ceiling = count($prefixes)-1;
          $prefix_index = -1;
      
          do {
              $mid_point = ($window_floor+$window_ceiling)>>1;
      
              if ($window_floor==($window_ceiling-1)) {
                  if ($stripped_number>=$prefixes[$window_ceiling]) {
                      $prefix_index=$window_ceiling;
                      break;
                  } elseif ($stripped_number>=$prefixes[$window_floor]) {
                      $prefix_index=$window_floor;
                      break;
                  } else {
                      break;
                  }
              } else {
                  if ($stripped_number==$prefixes[$mid_point]) {
                      $prefix_index=$mid_point;
                      break;
                  } elseif ($stripped_number<$prefixes[$mid_point]) {
                      $window_ceiling=$mid_point;
                  } else {
                      $window_floor=$mid_point;
                  }
              }
          } while (true);
      
          if ($prefix_index==-1 || substr($number_to_check, 0, $prefix_lengths[$prefix_index])!=substr($prefixes[$prefix_index],0, $prefix_lengths[$prefix_index])) {
              return 'invalid prefix';
          } else {
              return substr($prefixes[$prefix_index], 0, $prefix_lengths[$prefix_index]);
          }
      }
      

答案 3 :(得分:0)

我是通过使用字符串,目标的哈希表来实现的,其中键是表示目标前缀的字符串。关键因素是必须对哈希表进行排序,以便首先检查最长的字符串。一旦找到匹配的前缀,就知道呼叫目的地。

我实际上还有一轮正则表达式,用于更复杂的目的地,并检查目标前缀之前的正则表达式。

我没有测量到比赛所需的时间,但我怀疑最多15分钟。检查desitnation然后用户余额并最终设置呼叫时间限制的整个过程大约需要150ms。就我而言,我使用的是FastAGI和C#Windows服务。只要你花费不到500毫秒,它就会让你的用户无法接受。

答案 4 :(得分:0)

我还运行了一个电话应用程序...我所做的是提供一个内部REST API来调用,这将缓存已知的电话号码并执行所有前缀检查。

此外,我假设您正在寻找国家/地区代码。 NANP只有几个重叠的国家代码。所以我首先看一下NANP,并快速匹配以下数字的数量(7)以确保,否则我会回到国家代码。然后我粗略地了解每个国家通过正则表达式应该有多少电话号码。

我正在使用XML文档并在XPath上进行匹配,然后在可能的情况下缓存XPath结果。

使用REST API的一个很酷的事情是,它可以用来清理数字,然后再将它们存储在数据库中进行计费。

这不是一门精确的科学,但似乎有效。

答案 5 :(得分:0)

找到最长的公共子序列是动态编程的经典应用。解决方案是O(n)。 http://en.wikipedia.org/wiki/Longest_common_subsequence_problem