PHP导出MySQL数据到Excel文件

May 12th, 2010

经常会碰到需要从数据库中导出数据到Excel文件,用一些开源的类库,比如PHPExcel,确实比较容易实现,但对大量数据的支持很不好,很容易到达PHP内存使用上限。这里的方法是利用fputcsv写CSV文件的方法,直接向浏览器输出Excel文件。

<?php
// 输出Excel文件头,可把user.csv换成你要的文件名
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename="user.csv"');
header('Cache-Control: max-age=0');
 
// 从数据库中获取数据,为了节省内存,不要把数据一次性读到内存,从句柄中一行一行读即可
$sql = 'select * from tbl where ……';
$stmt = $db->query($sql);
 
// 打开PHP文件句柄,php://output 表示直接输出到浏览器
$fp = fopen('php://output', 'a');
 
// 输出Excel列名信息
$head = array('姓名', '性别', '年龄', 'Email', '电话', '……');
foreach ($head as $i => $v) {
    // CSV的Excel支持GBK编码,一定要转换,否则乱码
    $head[$i] = iconv('utf-8', 'gbk', $v);
}
 
// 将数据通过fputcsv写到文件句柄
fputcsv($fp, $head);
 
// 计数器
$cnt = 0;
// 每隔$limit行,刷新一下输出buffer,不要太大,也不要太小
$limit = 100000;
 
// 逐行取出数据,不浪费内存
while ($row = $stmt->fetch(Zend_Db::FETCH_NUM)) {
 
    $cnt ++;
    if ($limit == $cnt) { //刷新一下输出buffer,防止由于数据过多造成问题
        ob_flush();
        flush();
        $cnt = 0;
    }
 
    foreach ($row as $i => $v) {
        $row[$i] = iconv('utf-8', 'gbk', $v);
    }
    fputcsv($fp, $row);
}

简单易用,非常节省内存,不依赖第三方类库。

Tags: ,

基于MongoDb的S3实现

May 7th, 2010

原理是利用MongoDb的GridFS,伸展性方面交由MongoDb的auto sharding去实现,这里用PHP给MongoDb绑了个S3出来,支持选择文件存储节点,支持文件分目录存储,这样的好处是对于一些受时间影响比较明显的文件,可以按照年月的形式存储,减轻历史包袱。

首先,配置MongoDb GridFS节点信息:

<?php
$s3Config = array(
    'foo' => array(
        'server' => '127.0.0.1',
        'database' => 'test',
        'user' => 'test',
        'password' => 'foobar',
        'domain' => 'http://s3.foobar.com'
    ),
 
    'bar' => array(
        'server' => '127.0.0.1',
        'database' => 'test',
        'user' => 'test',
        'password' => 'foobar',
        'domain' => 'http://s3.foobar.com'
    ),
);

MongoDb的S3绑定:

<?php
/**
 * 统一文件存储
 *
 */
class Api_S3
{
    protected $_node;
 
    protected $_dir;
 
    protected $_config;
 
    /**
     * 构造函数
     *
     * @param string $node
     * @param string $dir
     * @param array $config
     */
    public function __construct($node, $dir = null, $config = null)
    {
        $this->_config = $config;
 
        $this->path($node, $dir, false);
    }
 
    /**
     * 设置文件路径
     *
     * @param string $node
     * @param string $dir
     * @return Api_S3
     */
    public function path($node, $dir, $connect = true)
    {
        $this->_node = $node;
        $this->_dir = empty($dir) ? 'fs' : $dir;
 
        if (empty($this->_config[$this->_node])) {
            throw new Cola_Exception('Api_S3: invalidate node');
        }
 
        if ($connect) {
            $this->_gridFS = $this->_gridFS();
        }
 
        return $this;
    }
 
    /**
     * GridFS
     *
     * @return MongDbGridFS
     */
    protected function _gridFS()
    {
        $mongo = new Cola_Com_Mongo($this->_config[$this->_node]);
 
        return $mongo->gridFS($this->_dir);
    }
 
    /**
     * 获得文件句柄
     *
     * @param string $name
     * @return MongoGridFSFile
     */
    public function file($name)
    {
        if (empty($this->_gridFS)) {
            $this->_gridFS = $this->_gridFS();
        }
 
        return $this->_gridFS->findOne(array('filename' => $name));
    }
 
    /**
     * 获得文件内容
     *
     * @param string $name
     */
    public function read($name)
    {
        $file = $this->file($name);
 
        return $file->getBytes();
    }
 
    /**
     * 写入文件
     *
     * @param string $name
     * @param string $data
     * @param array $extra
     * @param boolean $overWrite
     * @return boolean
     */
    public function write($name, $data, $extra = array(), $overWrite = false)
    {
        $extra = (array)$extra + array('filename' => basename($name));
 
        if ($filetype = $this->_type($name)) {
            $extra['filetype'] = $filetype;
        }
 
        if ($this->file($extra['filename'])) {
            if ($overWrite) {
                $this->delete($extra['filename']);
            } else {
                throw new Cola_Exception('Api_S3: file exists');
            }
        }
 
        return $this->_gridFS->storeBytes($data, $extra);
    }
 
    /**
     * 复制系统文件
     *
     * @param string $file
     * @param array $extra
     * @param boolean $overWrite
     * @return boolean
     */
    public function copy($file, $extra = array(), $overWrite = false)
    {
        $extra = (array)$extra + array('filename' => basename($file));
 
        if ($filetype = $this->_type($file)) {
            $extra['filetype'] = $filetype;
        }
 
        if ($this->file($extra['filename'])) {
            if ($overWrite) {
                $this->delete($extra['filename']);
            } else {
                throw new Cola_Exception('Api_S3: file exists');
            }
        }
 
        return $this->_gridFS->storeFile($file, $extra);
    }
 
    /**
     * 删除文件
     *
     * @param string $name
     * @return boolean
     */
    public function delete($name)
    {
        if (empty($this->_gridFS)) {
            $this->_gridFS = $this->_gridFS();
        }
 
        return  $this->_gridFS->remove(array('filename' => $name));
    }
 
    /**
     * 获得文件地址
     *
     * @param string $name
     * @param string $default
     * @return string
     */
    public function getUrl($name, $default = false)
    {
        $data = array(
            'domain' => rtrim($this->_config[$this->_node]['domain'], '/'),
            'path'   => $this->_node . (('fs' == $this->_dir) ? '' : $this->_dir),
            'name'   => $name
        );
        return  implode('/', $data);
    }
 
    /**
     * 设置文件属性
     *
     * @param string $name
     * @param array $attr
     * @return boolean
     */
    public function setAttr($name, $attr)
    {
        if (!$file = $this->file($name)) {
            throw new Cola_Exception('Api_S3: file not exists');
        }
 
        $file->file = $attr + $file->file;
 
        return $this->_gridFS->save($file->file);
    }
 
    /**
     * 获得文件属性
     *
     * @param string $name
     * @return array
     */
    public function getAttr($name)
    {
        $file = $this->file($name);
        return $file->file;
    }
 
    /**
     * 获得文件类型
     *
     * @param string $file
     * @return string
     */
    protected function _type($file)
    {
        return mime_content_type($file);
    }
}

文件存入,支持自选节点,自定义目录,自定义文件名,可以自动添加文件类型:

<?php
$s3 = new Api_S3($node, $dir, $s3Config);
$s3->copy($file, array('filename' => $name, 'filetype' => $type));

文件读取,以”http://s3.foobar.com/foo/201005/foobar.jpg”为例,foo映射到节点名,201005映射到目录名,foobar.jpg映射到文件名:

<?php
$s3 = new Api_S3($node, $dir, $s3Config);
$file = $s3->file($name);
 
Cola_Response::lastModified($file->file['uploadDate']->sec);
Cola_Response::etag($file->file['md5']);
 
if (isset($file->file['filetype'])) {
    header("Content-Type: {$file->file['filetype']}");
}
 
echo $file->getBytes();

注意到我们利用了文件的修改时间设置http头的last modified,以及用文件的md5信息设置etag值,这样的好处是可以大大减少带宽使用,当然,你也可以设置expire时间来减少重复请求。

关于性能问题,可以在PHP读取的上一层,加一个Squid之类的反向代理服务,基本上就不会有问题。

Tags: , ,

ColaPHP 0.7beta发布

April 3rd, 2010

ColaPHP的2010.3月度发布计划版本,稍微晚了几天,代号Recode,和0.6beta相比,框架结构有比较大的变化,主要修改如下:

  • 精简框架核心,除FrontController、Router、MVC外,其他功能组件化
  • Cola_Db、Cola_Cache、Cola_Log、Cola_Yaml已组件化成Cola_Com_Db、Cola_Com_Cache、Cola_Com_Log、Cola_Com_Yaml
  • Controller中可直接使用$this->com->db($config)之类的接口来调用组件
  • 少量代码重构以及bug fix

下载ColaPHP 0.7beta,阅读ColaPHP文档,访问ColaPHP项目

由于0.7beta做框架架构上变化有点大,不建议立即采用,生产环境中建议使用稳定的0.6beta,下一个版本0.8beta开发代号:Mini,主要对架构、代码做进一步重构优化。

一种比较省内存的稀疏矩阵Python存储方案

March 13th, 2010

推荐系统中经常需要处理类似user_id, item_id, rating这样的数据,其实就是数学里面的稀疏矩阵,scipy中提供了sparse模块来解决这个问题,但scipy.sparse有很多问题不太合用:1、不能很好的同时支持data[i, ...]、data[..., j]、data[i, j]快速切片;2、由于数据保存在内存中,不能很好的支持海量数据处理。

要支持data[i, ...]、data[..., j]的快速切片,需要i或者j的数据集中存储;同时,为了保存海量的数据,也需要把数据的一部分放在硬盘上,用内存做buffer。这里的解决方案比较简单,用一个类Dict的东西来存储数据,对于某个i(比如9527),它的数据保存在dict['i9527']里面,同样的,对于某个j(比如3306),它的全部数据保存在dict['j3306']里面,需要取出data[9527, ...]的时候,只要取出dict['i9527']即可,dict['i9527']原本是一个dict对象,储存某个j对应的值,为了节省内存空间,我们把这个dict以二进制字符串形式存储,直接上代码:

'''
Sparse Matrix
'''
import struct
import numpy as np
import bsddb
from cStringIO import StringIO
 
class DictMatrix():
    def __init__(self, container = {}, dft = 0.0):
        self._data  = container
        self._dft   = dft
        self._nums  = 0
 
    def __setitem__(self, index, value):
        try:
            i, j = index
        except:
            raise IndexError('invalid index')
 
        ik = ('i%d' % i)
        # 为了节省内存,我们把j, value打包成字二进制字符串
        ib = struct.pack('if', j, value)
        jk = ('j%d' % j)
        jb = struct.pack('if', i, value)
 
        try:
            self._data[ik] += ib
        except:
            self._data[ik] = ib
        try:
            self._data[jk] += jb
        except:
            self._data[jk] = jb
        self._nums += 1
 
    def __getitem__(self, index):
        try:
            i, j = index
        except:
            raise IndexError('invalid index')
 
        if (isinstance(i, int)):
            ik = ('i%d' % i)
            if not self._data.has_key(ik): return self._dft
            ret = dict(np.fromstring(self._data[ik], dtype = 'i4,f4'))
            if (isinstance(j, int)): return ret.get(j, self._dft)
 
        if (isinstance(j, int)):
            jk = ('j%d' % j)
            if not self._data.has_key(jk): return self._dft
            ret = dict(np.fromstring(self._data[jk], dtype = 'i4,f4'))
 
        return ret
 
    def __len__(self):
        return self._nums
 
    def __iter__(self):
        pass
 
    '''
    从文件中生成matrix
    考虑到dbm读写的性能不如内存,我们做了一些缓存,每1000W次批量写入一次
    考虑到字符串拼接性能不太好,我们直接用StringIO来做拼接
    '''
    def from_file(self, fp, sep = '\t'):
        cnt = 0
        cache = {}
        for l in fp:
            if 10000000 == cnt:
                self._flush(cache)
                cnt = 0
                cache = {}
            i, j, v = [float(i) for i in l.split(sep)]
 
            ik = ('i%d' % i)
            ib = struct.pack('if', j, v)
            jk = ('j%d' % j)
            jb = struct.pack('if', i, v)
 
            try:
                cache[ik].write(ib)
            except:
                cache[ik] = StringIO()
                cache[ik].write(ib)
 
            try:
                cache[jk].write(jb)
            except:
                cache[jk] = StringIO()
                cache[jk].write(jb)
 
            cnt += 1
            self._nums += 1
 
        self._flush(cache)
        return self._nums
 
    def _flush(self, cache):
        for k,v in cache.items():
            v.seek(0)
            s = v.read()
            try:
                self._data[k] += s
            except:
                self._data[k] = s
 
if __name__ == '__main__':
    db = bsddb.btopen(None, cachesize = 268435456)
    data = DictMatrix(db)
    data.from_file(open('/path/to/log.txt', 'r'), ',')

测试4500W条rating数据(整形,整型,浮点格式),922MB文本文件导入,采用内存dict储存的话,12分钟构建完毕,消耗内存1.2G,采用示例代码中的bdb存储,20分钟构建完毕,占用内存300~400MB左右,比cachesize大不了多少,数据读取测试:

import timeit
timeit.Timer('foo = __main__.data[9527, ...]', 'import __main__').timeit(number = 1000)

消耗1.4788秒,大概读取一条数据1.5ms。

采用类Dict来存储数据的另一个好处是你可以随便用内存Dict或者其他任何形式的DBM,甚至传说中的Tokyo Cabinet….

好的,码完收工。

Tags: ,

ColaPHP 0.6beta发布

February 28th, 2010

ColaPHP月度发布计划版本,代号:Easy,和0.5beta相比变化不大,主要修改如下:

  • 增加Yaml处理,底层调用symfony yaml包处理
  • 增加自定义异常类,后续准备将框架中的异常细分(好处是将来可以对异常做针对性处理)
  • 少量代码重构以及bug fix

下载ColaPHP 0.6beta,阅读ColaPHP文档,访问ColaPHP项目

下一个版本0.7beta开发代号:Recode,主要对代码做进一步重构优化。

Tags:

[视频]MongoDb In Action

February 23rd, 2010

上次OpenParty上分享MongoDb In Action的视频,Slides在这里,貌似偶有点结巴:)

Tags:

基于关联规则的推荐系统

February 21st, 2010

首先,要了解关联规则的几个概念,定义N为总事务数,N(A)N(B)分别为项集A、项集B出现的次数,N(AB)为项集A、项集B同时出现的次数,A、B为不相交项集A∩B=Ø,规则A→B表示由A推到B:

支持度(Support):

Support

支持度是一种重要度量,支持度低的规则很可能是偶然现象,对推荐意义不大,另外支持度是数据剪枝的一个重要依据。

置信度(Confidence):

Confidence

置信度,字面上的解释就是这个规则到底有多可信,对于给定的规则A→B,置信度越高,B出现在包含A的事务中的概率越高。

提升度(Lift):

Lift

Support(A→B)其实就是AB的联合概率P(AB),Support(A) 、 Support(B)分别为A、B的概率估计P(A)P(B),如果A、B相互独立,则P(AB) = P(A) × P(B),所以只有 Lift > 1 才表示A、B正相关,且越大越好。

为什么要引入提升度的概念呢?还是拿歌曲来做例子,比如歌曲A、歌曲C为小众歌曲,歌曲B为口水歌,共有10万个用户,有200个人听过歌曲A,这200个人里面有60个听过口水歌B,有40个人听过歌曲C,同时听过歌曲C的人数是300,听过口水歌B的人为50000,那么Confidence(A→B) = 0.3,Confidence(A→C) = 0.2,从置信度来看貌似A和B更相关,但是10W人里面有5W听过歌曲B,说明有一半的用户都喜欢歌曲B,但听过歌曲A的人里面只有30%的人喜欢歌曲B,很明显歌曲A和歌曲B负相关,计算Lift(A→B) = 0.6,小于1,负相关,Lift(A→C) = 66.7,远大于1,正相关。

当然,还有一些其他的度量因子,可自行参阅其他文档。

可以进入正题了,我们要实验的是一个文学类的网站数据,数据格式如下:

用户ID 图书ID

表示此用户阅读过该图书,我们首先要解决的问题是:喜欢图书A的用户还喜欢其他哪些图书?(图书之间的相关性)

推荐流程

  • 数据清理:对用户和图书分别计数,过滤掉一些超不活跃的用户和超冷门的图书
  • 计算两两图书之间的支持度、置信度、提升度,根据最低支持度、最低置信度、最低提升度剪枝,把低于最小值的规则扔掉
  • 对图书A进行推荐:找出图书A的所有规则,按照置信度降序排序,Top-N即为和图书A最相关的前N本图书

非常简单,关键的就是数据清理以及规则剪枝设置,这需要对业务熟悉一些,提升度的话,如果不确认,大于1即可。

结果示例:

古龙:剑毒梅香(中) 古龙:剑毒梅香(上)|古龙:剑毒梅香(下)|武林第一少年:血欲江湖|笑傲江湖之风清扬别传|草根续写:天龙八部续
古龙:剑毒梅香(下) 古龙:剑毒梅香(中)|古龙:剑毒梅香(上)|武林第一少年:血欲江湖|笑傲江湖之风清扬别传|恐怖宿舍惊魂夜:女生寝室|倚天屠龙记之复兴明教|至尊武神:六脉神剑闹武林|草根续写:天龙八部续
温瑞安:四大名捕猿猴月 四大名捕会京师:逆水寒|温瑞安:四大名捕铁布衫|四大名捕震关东-亡命|四大名捕破神枪: 惨绿|四大名捕破神枪-妖红|四大名捕震关东-追杀|温瑞安:四大名捕谈亭会|温瑞安:四大名捕开谢花|温瑞安:四大名捕碎梦刀|四大名捕走龙蛇|温瑞安:四大名捕猛鬼庙|温瑞安神州奇侠:人世间
异界玄奇:尸池 荒村血鬼洞房:剥皮新娘|真实恐怖:鬼宅小区|丫鬟不好当:王爷,请自重|人鬼恋:我的老婆不是人|灵异事件全曝光:诡异档案|恐怖的盗墓历险:荒村古墓|校园僵尸|古墓惊魂夜:坟岭村笔记|阴阳眼之鬼瞳:荒道门|极度恐怖乱坟头:墓地惊叫|盗墓传说|不解迷:殡仪馆里的化妆师|凶尸宿舍惊魂声:猛鬼校园|惊声尖叫:太平间美丽女尸|生化疯狂撕杀之丧尸异形|僵尸当街:遇上美女天师|生化异族的入侵:吸血传说|凶宅女尸:学院惊魂夜|棺木里的眼球:古井沉尸|校园恐怖女生寝室3:诡铃
变成有钱人并不难: 理财YS 快速发财: 怎样做无本生意|创业指南:三十六计|成功三宝:习惯、心态、人脉|掌控自己命运:读孙子兵法|改变你一生的30个招术|帮你成高手:口才决定成败|左右逢源的做人心机术|穷人与富人的差别|最快的致富秘诀: 赢在观念|做个聪明的老板: 经商要会说话|把话说得滴水不漏全集|职场:1分钟读懂对方心理|恋人浪漫短信|李嘉诚的谋局与处世|股票入门:股票认知大全|男人了解女人,女人了解男人|教你理财:理财高手|心机–做人的一种智慧|必修课:这样做女孩最命好|女人的身体·女人的智慧
……

不再多举例子了,目测感觉大多比较靠谱。

Simpler could be better.

Tags: ,

ColaPHP 0.5beta发布

January 24th, 2010

ColaPHP的第一个beta版本,代号:Practice,已经实践的优化,可以适量应用在实际项目中,相比较0.4alpha,比较大的修改如下:

  • 增加字符串加密助手,支持XOR、mcrypt加密,支持混淆
  • 增加分页类
  • 重构了HTTP请求类,可定制性更强
  • 重构了Validate类,增加批量校验
  • 其他代码重构以及bug fix

下载ColaPHP 0.5beta,阅读ColaPHP文档,访问ColaPHP项目

下一个版本0.6beta开发代号:Easy,将在易用性方面进一步优化。

Tags:

MongoDb In Action

January 23rd, 2010

This slide will tell you how to use MongoDb as MySQL in your application.

Tags: ,

Python处理MP3的歌词和图片

January 21st, 2010

一些MP3播放器(包括iphone、ipod、itouch、blackberry等)可以在播放mp3的时候显示专辑图片、歌词等信息而不需要额外的图片文件和歌词文件,仅仅一个mp3文件就搞定,比较有意思。除了用专门的软件(比如itunes)来制作这样的mp3,我们还可以用程序来批量生成。

查阅mp3头信息ID3V2的技术文档,发现可以往ID3信息里面加入歌词和图片信息(可以在页面上查找Lyrics、Attached picture就能发现相应的内容)。有了官方格式上的支持,我们要做的就是把歌词和图片加入到MP3文件中去。

测试一些开源的软件包,发现一个比较可靠的:eyeD3,由python语言编写,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#coding=utf-8
import eyeD3
import re
 
# mp3文件
mp3_file = '/path/to/foobar.mp3'
# lrc歌词文件
lrc_file = '/path/to/foobar.lrc'
# 专辑图片
pic_file = '/path/to/foobar.jpg'
 
# 实例化eyeD3
tag = eyeD3.Tag()
 
# 绑定到mp3文件
tag.link(mp3_file)
 
# 去掉原文件中可能存在的图片
tag.removeImage()
 
# 去掉原文件中可能存在的歌词
tag.removeLyrics()
 
# 设定编码,非常重要,否则不支持中文
tag.encoding = '\x01'
 
# 添加图片
tag.addImage(3, pic_file, u'')
 
# 添加歌词,注意要utf-8编码,去掉lrc中时间信息
tag.addLyrics(re.sub('(\[.*?\][\n]*)+', '', unicode(open(lrc_file, 'r')).read(), 'utf8')))
 
# 更新到文件
tag.update()

代码非常简单,需要注意的是设定编码,不然歌词就乱码了。有了eyeD3之后,可以写个爬虫,从网上抓下歌词和图片直接灌进MP3文件里面,剩下的就是享受了。

Tags: , , , ,