生成汉字卡牌的PHP小应用

上初中时,班里曾经有人发明了一套用化学元素信息做卡牌的游戏,风靡一时。可惜现在已经完全想不起来当时的玩法了。包子上小学后,包爸就想着也效仿这种模式,让包子和小伙伴们有的玩。于是断断续续花了一个学期的时间,在包妈的协助下,和包子一起琢磨制作出大脑锛儿汉字卡牌游戏。临近期末时,规则基本定型,需要做一套汉字卡了,为了省事,自己写了一个PHP web app,到汉典网站上抓取所需的汉字信息,生成可打印的HTML文件。下面就是程序代码,有类似需求的可以省去再造车轮了。当然,不懂代码的话,也可以到我们的大脑锛儿汉字卡牌游戏网站上直接生成。

应用的基本流程是:检查是否有提交的汉字post数据,没有就展示填写汉字的表单网页。如果有数据,就先到使用序列化保存的文本数据文件中寻找是否已有这个汉字的信息,没有就到汉典网站中抓取,并保存到数据文件中。最后组成汉字卡牌数据,使用html模板进行展示。

需要说明的一个是费了一些功夫才搞明白的汉典单个汉字网页url的生成规则(代码第131行),举例来说,“汉”字的网页url是:

http://www.zdic.net/z/1c/js/6C49.htm

红色部分就是“汉”字的unicode64编码,蓝色则是依照这个编码,每1000个汉字为一个区块的区块序号(16进制)。

scrapeContent方法(从156行开始)中,$rules变量中放的是提取汉字网页中各部分信息(拼音、部首、结构……)的正则表达式相关数据,其中拼音因为有可能是多音字,所以需要多次匹配。我测试过几百个汉字,偶尔会有部首、结构、构造等信息抓取不到的情况,也就是说方法返回的数组中,$return['bushou'], $return['bihua'], $return['buwai'], $return['jiegou'], $return['gouzao']这些元素可能没有设置,在写展示方法时要考虑到。

最后的display方法,会根据$type参数('form'或'page')调取不同方法获取html模板展示表单页或汉字卡页,涉及到展示逻辑,共通性不大,就不放在这里了。

<?php  
$path='./data.txt';
$data=getData();
if (isset($_POST['z'])) {  
    $words=processPostData($_POST['z']);
    $content=getContent($words);
    display('page', $content);
} else {
    display('form');
}

function processPostData($raw)  
{
    /*
    * 描述
        处理用户录入的post数据,筛选出中文字符,并去重
    * 更新记录
        * 2017-06-22: 创建
    * 参数
        * $raw: (string)用户录入的汉字字符串
    * 返回
        * $unique: (string)筛选后的中文字符串
    */
    preg_match_all('/[\x{4e00}-\x{9fa5}]/u', $raw, $return);
    $unique=array_unique($return[0]);
    return $unique;
}

function getData()  
{
    /*
    * 描述
        获取已保存的汉字卡牌数据
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        无
    * 返回
        * $data: (array)已保存的汉字卡牌数组
    */
    global $path;
    $data=array();
    if ($file=fopen($path, "r")) {
        while (!feof($file)) {
            $old=unserialize(fgets($file, 9999));
            if ($old) {
                $key=key($old);
                $data[$key]=$old[$key];
            }
        }
    }
    return $data;
}

function getContent($words)  
{
    /*
    * 描述
        根据用户输入的汉字数组读取汉字卡牌数据
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $words: (array)以单个汉字为单元的数组
    * 返回
        * $content: (array)卡牌信息数组
    */
    global $path, $data;
    $return=array();
    foreach ($words as $word) {
        if (isset($data[$word])) {
            $return[]=$data[$word];
        } else {
            $return[]=newKey($word);
        }
    }
    return $return;
}

function newKey($word)  
{
    /*
    * 描述
        创建新汉字卡牌信息
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $word: (string)新汉字字符串
    * 返回
        * $content: (array)新汉字卡牌信息数组
    */
    $url=getUrl($word);
    $content=scrapeContent($url);
    insert($content);
    return $content;
}

function insert($content)  
{
    /*
    * 描述
        新汉字卡牌信息数据写入数据文件
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $content: (array)新汉字卡牌信息数组
    * 返回
        无
    */
    global $path;
    $data=array();
    $data[$content['zi']]=$content;
    $file=fopen($path, "a");
    fwrite($file, serialize($data));
    fwrite($file, "\r\n");
    fclose($file);
}

function getUrl($word)  
{
    /*
    * 描述
        根据新汉字生成zdicn.net汉字网页url
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $word: (string)新汉字字符串
    * 返回
        * $url: (string)网页url
    */
    $code=getUnicode($word);
    $url='http://www.zdic.net/z/'.strval(dechex(floor(hexdec($code)/1000)+1)).'/js/'.$code.'.htm';
    return $url;
}

function getUnicode($word)  
{
    /*
    * 描述
        根据新汉字生成汉字unicode64编码
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $word: (string)新汉字字符串
    * 返回
        * (string)unicode64编码的字符串
    */
    $data=str_split($word);
    $string='';
    foreach ($data as $value) {
        $string.=decbin(ord($value));
    }
    $string=preg_replace('/^.{4}(.{4}).{2}(.{6}).{2}(.{6})$/', '$1$2$3', $string);
    return strtoupper(dechex(bindec($string)));
}

function scrapeContent($url)  
{
    /*
    * 描述
        根据zdicn.net汉字网页url抓取汉字卡牌信息
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $url: (string)网页url
    * 返回
        * $content: (array)抓取到的新汉字卡牌信息数组
    */
    $content=array();
    $fails=0;
    $rules=array(
        array('name'=>'zi', 'match_all'=>0, 'pattern'=>'(?<=ziip">“).'),
        array('name'=>'pinyin', 'match_all'=>1, 'pattern'=>'(?<=dicpy">)\w*(?= <script>)'),
        array('name'=>'bushou', 'match_all'=>0, 'pattern'=>'(?<=z_it2_jbs"><a href="\/z\/jbs\/\?jbs=.{9}" target="_blank">)\w*'),
        array('name'=>'buwai', 'match_all'=>0, 'pattern'=>'(?<=z_it2_jbh"><a href="\/z\/jbs\/\?jbs=.{9}&jbh=)\d*'),
        array('name'=>'bihua', 'match_all'=>0, 'pattern'=>'(?<=z_it2_jzbh"><a href="\/z\/jbs\/zbh\/\?jzbh=)\d*'),
        array('name'=>'jiegou', 'match_all'=>0, 'pattern'=>'(?<= target="_blank">)\w*(?=结构<\/a>)'),
        array('name'=>'gouzao', 'match_all'=>0, 'pattern'=>'(?<=结构<\/a><\/center>)[^\x00-\xff]*(?=<\/td>)')
    );
    $html=str_ireplace(array("\r", "\n", "\t", "\0"), '', getPageHtml($url));
    $content['url']=$url;
    foreach ($rules as $rule) {
        if ($rule['match_all']==1) {
            preg_match_all('/'.$rule['pattern'].'/u', $html, $match);
        } else {
            preg_match('/'.$rule['pattern'].'/u', $html, $match);
        }
        if ($match!=null && count($match[0])>0) {
            $content[$rule['name']]=$match[0];
        } else {
            $fails++;
        }
    }
    return $content;
}

function getPageHtml($url)  
{
    /*
    * 描述
        用curl方式抓取url网页信息
    * 更新记录
        * 2017-06-28: 创建方法
    * 参数
        * $url: (string)网页url
    * 返回
        * $content: (array)抓取到的网页html
    */
    $ch=curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    $content=curl_exec($ch);
    curl_close($ch);
    return $content;
}

function display($type='form', $content='')  
{
    /*
    * 描述
        显示页面
    * 更新记录
        * 2017-06-27: 创建方法
    * 参数
        * $type: (stirng)(default 'form')展示内容类型,有卡牌生成器表单(form),卡牌内容(page)
        * $content: (array)(default null)卡牌内容数组
    * 返回
        无
    */
    if ($type=='form') {
        $html=getFormHtml();
    } else {
        $html=getCardsHtml($content);
    }
    echo ($html);
}

最后的最后,想替汉典网站做个广告,就中文的字、词、词组、成语信息来说,他们的收集整理工作非常周到细致,当我遇到汉字相关问题时,绝对是首选的信息源。希望大家能正当地使用我的代码,不要做任何损害汉典利益的事情。这个“关于我们”页面里是汉典网站自己的介绍信息及捐助方法,如果大家欣赏汉典的内容,请多多支持!