Author: handt
涉及到的脚本和 SQL 文件 点此下载
=========== 正文部分 ==============
思路分析:
1. 获得高校数据
地址:http://s.xnimg.cn/a44177/allunivlist.js (通过分析页面请求确定为该文件)
对该文件进行人工分析,结合页面弹出框,可以得知,该js文件包含了 国家、城市、学校
{id:1,univs:[{id:1001,name:"\u6e05\u534e\u5927\u5b66"}
标准 JSON 格式的id,univs,name 都应该用双引号包围。关于JSON格式的简单介绍可以参见《PHP 与 JSON》
因此,第一步 应该是格式化数据文件,转换为标准JSON格式,以便直接用PHP的 json_decode 函数处理。
接下来,具体分析该文件的字段信息。
出于提高访问速度的考虑,allunivlist.js 文件被压缩在同一行。为了快速分析该文件,提供一个小技巧:将文件另存到本地,用vim打开,使用 vim 的括号匹配功能(shift+5)从最外层逐渐开始分析。(vim更多知识可以参见《Vimtutor拾遗》)
经过分析,文件整体结构为
[{国家1},{国家2},{国家3}....]
国家定义:{id:xxx, univs:xxxx, name:xxxx, provs:xxxx, country_id:xxx} (provs表示省份)
provs:[{省份1},{省份2},{省份3}....]
省份定义:{id:xxx, univs:xxx, country_id:xxxx, name:xxxx} (univs表示大学)
univs: [{大学1},{大学2},{大学3}....]
大学定义:{id:xxx, name:xxxx}
通过该文件,能够获取到中国所有省份的高校信息。
对应的,可以建立数据表school:
1 CREATE TABLE `school` (
2 `id` int(11) NOT NULL AUTO_INCREMENT,
3 `sid` int(11) DEFAULT NULL COMMENT '学校id',
4 `cid` int(11) DEFAULT NULL COMMENT '所在城市id',
5 `name` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '名字',
6 PRIMARY KEY (`id`)
7 ) ENGINE=InnoDB AUTO_INCREMENT=3198 DEFAULT CHARSET=latin1;
2. 获取各高校的院系信息
人人网查询高校院系的接口为:http://www.renren.com/GetDep.do?id=高校ID
这里的 高校ID
该接口的返回信息很简单,一个<select>元素,其中的<option> value 就是院系信息。
对于院系信息,可以建立数据表 department:
1 CREATE TABLE `department` (
2 `id` int(11) NOT NULL AUTO_INCREMENT,
3 `sid` int(11) NOT NULL COMMENT 'school id',
4 `did` int(11) NOT NULL COMMENT 'department id',
5 `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'department 名字',
6 PRIMARY KEY (`id`)
7 ) ENGINE=InnoDB AUTO_INCREMENT=30044 DEFAULT CHARSET=latin1;
确立了思路之后,下面开始动手。采用 PHP + MYSQL 完成编码部分内容。
1. 获得高校数据:
1.1 读取文件并将初始化为标准 JSON 格式
将 http://s.xnimg.cn/a44177/allunivlist.js 文件另存到本地。
因为这里只需要中国的数据,可以将 [{国家1},{国家2},{国家3}....] 中其他国家的内容删除。同时,开头的 ‘ var allUnivList=’ 也删掉。简单处理后,文件在括号匹配上是符合json格式的。
接下来读取文件,对形如 ‘#(?P<name>\w+):#i’ 的数据,进行替换,在前后均添加双引号。
读取文件和初始化代码:
1 function x_readfile($filename)
2 {
3 $handle = fopen($filename, 'r');
4 $contents = fread($handle, filesize($filename));
5 fclose($handle);
6 return $contents;
7 }
8
9 $content = x_readfile('allunivlist.js');
10 // 将文件初始化为标准JSON格式
11 $pat = '#(?P<name>\w+):#i';
12 $replacement = '"${1}":';
13 $str = preg_replace($pat, $replacement, $content);
14 $ret = json_decode($str, true); // true 返回的是关联array类型,否则为class
1.2 生成要插入数据库的语句:
通过循环,生成形如 ‘insert into school(sid, cid, name) values(xxx, xxx, xxx)’的语句。
$ret 变量是 json_decode 后的关联数组,回顾 1 中分析的数据结构:
provs:[{省份1},{省份2},{省份3}....]
省份定义:{id:xxx, univs:xxx, country_id:xxxx, name:xxxx} univs表示大学
univs: [{大学1},{大学2},{大学3}....]
大学定义:{id:xxx, name:xxxx}·
利用如下代码进行处理:
1 // 提取 城市 - 学校 信息
2 // 省份{id, univs, country_id, name}
3 // 学校{id, name}
4 foreach($ret as $k => $v)
5 {
6 // $k : order id
7 // $v : detail array(id, univs, conntry_id, name)
8 // table: id, cid, sid, name (city, school, name)
9 $name = my_decode($v['name']);
10 create_insert_sql($v['id'], 0, $name); // $v['univs']表示当前省份下的所有高校
11 foreach($v['univs'] as $university){
12 $name = my_decode($university['name']); // 学校{id, name}
13 create_insert_sql($university['id'], $v['id'], $name);
14 }
15 }
这里用到了my_decode函数,对形如‘\uaaaa\ubbbb’的字符进行编码处理:
1 function replace_unicode_escape_sequence($match) {
2 return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UTF-8');
3 }
4 // solution from:http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha
5 function my_decode($str)
6 {
7 return $str = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $str);
8 }
create_insert_sql 函数用来生成进行插入的sql 语句。为了方便,这里直接将sql语句用echo显示在网页上。再将网页文字直接复制到sql 客户端中(笔者使用的navicat)执行。
1 function create_insert_sql($sid, $cid, $name)
2 {
3 $sql = 'insert into ' . TABNAME . '(sid, cid, name) values (' . $sid . ',' . $cid . ',"' . $name . '");';
4 echo $sql . '<br/>';
5 return $sql;
6 }
获取高校信息的完整代码如下(点击展开):
<?php
define('TABNAME', 'school');
function x_readfile($filename)
{
$handle = fopen($filename, 'r');
$contents = fread($handle, filesize($filename));
fclose($handle);
return $contents;
}
function replace_unicode_escape_sequence($match) {
return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UTF-8');
}
// solution from:http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha
function my_decode($str)
{
return $str = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $str);
}
function create_insert_sql($sid, $cid, $name)
{
$sql = 'insert into ' . TABNAME . '(sid, cid, name) values (' . $sid . ',' . $cid . ',"' . $name . '");';
echo $sql . '<br/>';
return $sql;
}
$content = x_readfile('cu.js');
// 将文件初始化为标准JSON格式
$pat = '#(?P<name>\w+):#i';
$replacement = '"${1}":';
$str = preg_replace($pat, $replacement, $content);
$ret = json_decode($str, true);
// 提取 城市 - 学校 信息
// 省份{id, univs, country_id, name}
// 学校{id, name}
foreach($ret as $k => $v)
{
// $k : order id
// $v : detail array(id, univs, conntry_id, name)
// table: id, cid, sid, name (city, school, name)
$name = my_decode($v['name']);
create_insert_sql($v['id'], 0, $name);
foreach($v['univs'] as $university){
$name = my_decode($university['name']);
create_insert_sql($university['id'], $v['id'], $name);
}
}
?>
2. 获得院系信息
在 1 中已经成功生成了 省份 - 高校 数据。院系信息只需要通过接口,循环访问即可。
2.1 获取学校ID 集合
标准的数据库连接操作。利用while循环,将所有sid存储在 $sids 数组中。
1 // 获取所有学校id
2 $link = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS);
3 if (!$link) {
4 die('Could not connect: ' . mysql_error());
5 }
6 $db_selected = mysql_select_db(DATABASE, $link);
7 if (!$db_selected) {
8 die ('Can\'t use : ' . mysql_error());
9 }
10 mysql_query("set names utf8;");
11 $sql_statement = 'select sid from school where cid != 0';
12 $result = mysql_query($sql_statement, $link);
13 $sids = array();
14 while($row = mysql_fetch_array($result)){
15 $sids[] = $row['sid'];
16 }
2.2 调用接口,循环访问各高校院系数据
这里使用到了存储过程进行数据库读写操作,具体分析参看下文的 A) 部分。
1 // 根据学校 id 去发起请求,一共3163个学校,可见请求之多
2 $baseurl = 'http://www.renren.com/GetDep.do?id=';
3 for($i = 0; $i < count($sids); $i++){
4 $content = x_readfile($baseurl . $sids[$i]);
5 $pat = '#<option[^>].*?>(.*?)</option>#i';
6 $matches = array();
7 preg_match_all($pat, $content, $matches);
8 // 抛弃第一条结果:<option value=''>院系</option>
9 array_shift($matches[1]);
10 try {
11 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
12
13 $dbh->beginTransaction();
14 $dbh->exec('set names utf8;');
15 foreach($matches[1] as $k => $v){
16 $dbh->exec(create_insert_sql($sids[$i], $k + 1, mb_convert_encoding($v, 'UTF-8', 'HTML-ENTITIES')));
17 }
18 $dbh->commit();
19
20 } catch (Exception $e) {
21 $dbh->rollBack();
22 echo "Failed: " . $e->getMessage();
23 }
24 }
因为是访问网络文件,x_readfile有所改变:
1 function x_readfile($filename)
2 {
3 $handle = fopen($filename, 'r');
4 $contents = stream_get_contents($handle);
5 return $contents;
6 }
没有过于复杂的思路,不过细节上要提到两点。
A) 使用存储过程写入数据库
在1.2中生成的sql语句,是直接使用mysql客户端执行的。对于3000多条数据,一条条执行,速度很慢。考虑到院系数据可能是高校数据的10倍,应该用更快的方式写数据库。因此,使用到了存储过程。
PHP中存储过程的使用可以参看PHP手册,这里给出示范代码:
1 // 初始化 PDO 存储过程
2 try {
3 $dsn = 'mysql:dbname=' . DATABASE . ';host=127.0.0.1';
4 $dbh = new PDO($dsn, MYSQL_USER, MYSQL_PASS,
5 array(PDO::ATTR_PERSISTENT => true));
6 } catch (Exception $e) {
7 die("Unable to connect: " . $e->getMessage());
8 }
9 .......
10 .......
11 try {
12 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
13
14 $dbh->beginTransaction();
15 $dbh->exec('set names utf8;');
16 foreach($matches[1] as $k => $v){
17 $dbh->exec(create_insert_sql($sids[$i], $k + 1, mb_convert_encoding($v, 'UTF-8', 'HTML-ENTITIES')));
18 }
19 $dbh->commit();
20
21 } catch (Exception $e) {
22 $dbh->rollBack();
23 echo "Failed: " . $e->getMessage();
24 }
B) 数据库乱码
因为院系信息返回的格式形如”&#xxxxx;&#xxxxx“,进行了十进制编码,需要进行转换。
mb_convert_encoding($v, 'UTF-8', 'HTML-ENTITIES')
在插入数据库的时候,发现 department.name字段 全部是乱码,而数据表的format中已将name字段设置为 UTF-8 字符集。经验分析,是在写入数据库时的编码出了问题,因此,在每次写数据库前加一条语句:
$dbh->exec('set names utf8;');
这样就没有乱码了。
获取院系信息的完整代码如下(点击展开):
1 <?php
2 define('DATABASE', 'test');
3 define('TABNAME', 'department');
4 define('MYSQL_HOST', 'localhost');
5 define('MYSQL_USER', 'root');
6 define('MYSQL_PASS', 'root');
7
8 function x_readfile($filename)
9 {
10 $handle = fopen($filename, 'r');
11 $contents = stream_get_contents($handle);
12 return $contents;
13 }
14
15 function create_insert_sql($sid, $did, $name)
16 {
17 $sql = 'insert into ' . TABNAME . '(sid, did, name) values (' . $sid . ',' . $did . ',"' . $name . '");';
18 //echo $sql . '<br/>';
19 return $sql;
20 }
21 // 获取所有学校id
22 $link = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS);
23 if (!$link) {
24 die('Could not connect: ' . mysql_error());
25 }
26 $db_selected = mysql_select_db(DATABASE, $link);
27 if (!$db_selected) {
28 die ('Can\'t use : ' . mysql_error());
29 }
30 mysql_query("set names utf8;");
31 $sql_statement = 'select sid from school where cid != 0';
32 $result = mysql_query($sql_statement, $link);
33 $sids = array();
34 while($row = mysql_fetch_array($result)){
35 $sids[] = $row['sid'];
36 }
37 // 初始化 PDO 存储过程
38 try {
39 $dsn = 'mysql:dbname=' . DATABASE . ';host=127.0.0.1';
40 $dbh = new PDO($dsn, MYSQL_USER, MYSQL_PASS,
41 array(PDO::ATTR_PERSISTENT => true));
42 } catch (Exception $e) {
43 die("Unable to connect: " . $e->getMessage());
44 }
45 // 根据学校 id 去发起请求,一共3163个学校,可见请求之多
46 $baseurl = 'http://www.renren.com/GetDep.do?id=';
47 for($i = 0; $i < count($sids); $i++){
48 $content = x_readfile($baseurl . $sids[$i]);
49 $pat = '#<option[^>].*?>(.*?)</option>#i';
50 $matches = array();
51 preg_match_all($pat, $content, $matches);
52 // 抛弃第一条结果:<option value=''>院系</option>
53 array_shift($matches[1]);
54 try {
55 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
56
57 $dbh->beginTransaction();
58 $dbh->exec('set names utf8;');
59 foreach($matches[1] as $k => $v){
60 $dbh->exec(create_insert_sql($sids[$i], $k + 1, mb_convert_encoding($v, 'UTF-8', 'HTML-ENTITIES')));
61 }
62 $dbh->commit();
63
64 } catch (Exception $e) {
65 $dbh->rollBack();
66 echo "Failed: " . $e->getMessage();
67 }
68 }
69 ?>
最后附上两张最终SQL文件结果图:
一个直接使用这些数据的例子:
《制作一个选择中国大学的弹框 (数据、步骤、代码)》