PHP处理海量数据实战

来源:互联网 发布:八爪鱼数据采集器官网 编辑:程序博客网 时间:2024/06/05 07:53

看了July的一些关于Java处理海量数据的问题研究,他的想法独到深刻,很值得我们学习http://blog.csdn.net/v_july_v/article/details/6685962

wally_yu选择了其中的一道题验证,具体题目如下:

海量日志数据,提取出某日访问百度次数最多的那个IP。

July给出的解决方法如下:

方案1:首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有2^32个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。 

wally_yu使用python模拟了July的想法,写的很详细具体,由于他在分割大文件的时候出现了一点点的BUG,所以最后他并没有采用分割大文件的方法。http://blog.csdn.net/quicktest/article/details/7453189#comments

我借鉴了他的做法和博客的评论,使用PHP重新验证了July的思想。

废话不多说,下手。

生成数据

我构造了10万条IP,每一条ip以‘10.192’开头,并把这些数据保存到一个文本中,命名为file.txt.

function generageMassiveIPAddr($fileLaction, $numberOfLines){    // 打开此文件    $handle = fopen($fileLaction, 'a+');    $ip = '';    while ($numberOfLines) {        $ip .= '10.192.' . rand(0, 255) . '.' . rand(0, 255) . "\r\n";        $numberOfLines--;    }    fwrite($handle, $ip);    fclose($handle);    unset($handle);}// 生成了148KB的文件generageMassiveIPAddr('./file.txt', 100000);

注意:生成数据的时候,每天记录必须换行,方便以后逐行读取。

我只生成了10万条数据,如果需要更多,可以自行修改。

处理思路

july提供的方法是“分而治之”,简单来说,就是使用一个HASH算法,把一个大文件拆分成若干个小文件,分割时需要考虑到内存的大小,因为我的生成的数据不大,所以也没有详细的计算,我就直接把这个大文件拆分了10个小文件。
wally_yu使用的HASH算法是,根据文件的大小来分割,这样会出现一个问题:
每个ip可能在不同文件中都有记录,也许这个倒霉的ip是第一个文件的第二名,在第二个文件也是第二名,你用最大值进行比较,就会把这个倒霉的ip忽略掉,但其实这个ip才是真正的最大值。
这就是wally_yu采用文件大小分割出现的bug.

我的做法如下:

1)分割大文件,我的HASH算法是:逐行读取大文件,把获取的IP转化成整形,取模10(这个数字是我随意定义的),根据取模10结果把ip存储到不同的文件中,这样就会保证ip不会出现在两个不同的文件中。
2)对小文件遍历,获取文件中出现频率次数最高的IP, 这样遍历10个小文件后,就可以获取到10这样IP,保存到一个数组里面,形式为:ip的具体值 => ip的出现次数。
3)对生成的数组排序,既可以获取到出现次数最大的IP
// 拆分文件, 根据ip hash拆分文件function fileSplit($fileLaction, $num){    $handle = fopen($fileLaction, 'r');    if ($handle) {        while(! feof($handle)) {            // 去掉最后的换行符            $buffer = trim(fgets($handle));            $ip = ip2long($buffer);            // 分成num个文件            $i = $ip % $num;            $childHandle = fopen('child_' . $i . '.txt', 'a');            $buffer = $buffer . "\r\n";            fwrite($childHandle, $buffer);            fclose($childHandle);        }    }    fclose($handle);    unset($handle);}

注意:这里的实现方式其实不大好,因为我每读取到一个IP,就写到文件,如果数据过大过多,这样硬盘的压力会比较大,有一个简单的做法,就是创建一个数组,下标为IP取模的值,保存将要写入文件的IP,当数组的长度大于一个值时,把数组的内容根据下标写入到不同的文件中。

 对于PHP来说,操作文件后,注意销毁操作文件的资源。

遍历文件

以上步骤进行过后,会生成10个文件,需要遍历这些文件。 生成这些文件在同一个文件夹内,我们可以遍历这个文件夹,既可遍历所有的文件,假设 文件夹名为./web
代码如下:
function myGlob($targetFolder) {    $filePath = $targetFolder . '/*.txt';    $files    = glob($filePath);    return $files;}

获取小文件中ip出现次数最多一个

思路为:
1)初始化一个$ipValue 记录小文件中ip次数出现最多的那个IP的值  $ipMax 表示IP出现的次数。
2)构建名为“ip”的Dictionary,“key”为IP地址,“value”为该IP地址出现的次数,用来记录每一个小文件的所有IP地址
3)遍历文件的时候,同时,把$ipValue,$ipMax 更新为IP出现的次数最多的那个IP
4)  构建名为“result”的Dictionary,“key”为IP地址,“value”为该IP地址出现的次数,用来记录11个文件每一个的最多出现的IP
5)  遍历result,即可以获取到IP
// 打开一个文件夹,遍历文件里面的各种子文件的内容function findIp($targetFolder){    // 遍历文件夹    $files = myGlob($targetFolder);    // 用来统计每个子文件中出现次数最多的IP    $result = array();    foreach ($files as $file) {        // 用来统计每个文件的中各个IP出现的个数        $ip = array();        // 标识最大出现次数的ip的值        $ipValue = '';        // 标识这个iP出现的次数        $ipMax  = 0;        $handle = fopen($file, 'r');        if ($handle) {            while(! feof($handle)) {                // 去掉换行,这里需要特别注意                $buffer = trim(fgets($handle));                if (isset($ip[$buffer])) {                    $ip[$buffer] = $ip[$buffer] + 1;                } else {                    $ip[$buffer] = 1;                }                if ($ip[$buffer] > $ipMax) {                    $ipMax = $ip[$buffer];                    $ipValue = $buffer;                }            }        }        fclose($handle);        $result[] =  array($ipValue => $ipMax);    }}

在php中,对数组排序很简单:
asort($result);array_pop($result);
根据数组的Value值排序,升序,然后弹出最后一个元素。

总结:
1)这里这是实现了july的想法,没有在大数据下模拟,如果读者有兴趣可以自己模拟一下。
2)以前在写代码时,特别是使用数组的时候,完全不考虑这个数组的大小,在代码运行时,数组的内容全部载入内存,这样会损耗性能。
3)关于hash算法,我们常用的是模/n 还有一个比较常用的就是md5, 取首字母,这样的结果会有16个 (0-F)