背景
最近写了一个看笑话的 Android 应用,数据来源于一个半免费的 API 接口,每个 app_key 每天有访问次数限制,并且认证机制也过于简单,仅仅是在 HTTP GET 请求的参数里明文传输 app_key,这样的话,就不能直接让客户端来发起请求,一是容易暴露 app_key,二是请求次数很快就会用完。
想解决第一个问题,可以使用一个自己的服务器作中转,客户端向我们自己的服务器发起请求,不需要携带 app_key,我们自己的服务器再添加 app_key 向 API 接口发起请求,将得到的结果返回给客户端。
不过上面的做法解决不了请求次数限制的问题,最终的解决方法是,每天允许的访问次数全部留给服务器自己来用,访问得到的数据缓存到服务器的数据库上,客户端则是访问服务器的数据库。
下面就来分享一种基于 Laravel 框架的实现方案,可以让服务器每天定时通过 API 接口获取数据,更新到数据库中。
老规矩,先上代码:
https://github.com/zhongchenyu/jokes-laravel
1. 创建命令
首先创建一个启动数据获取的命令,直接在 Laravel 项目路径下运行:
php artisan make:command JokeSpider--command=spider:joke
这样就在 app/Console/Commands 路径下自动创建了一个 JokerSpider 文件,–command 参数表示执行的命令名称。要调用此命令时,在项目跟路径下执行:
php artisan spider:joke
注:老版本的创建命令有所不同,不是 make:command ,而是 make:console
在 app/Console/Kernel.php 文件中,编辑 $commands 变量,添加刚创建的命令文件:
protected $commands = [
Commands\MultithreadingRequest::class,
Commands\JokeSpider::class,
Commands\ImageSpider::class,
Commands\Test::class,
];
2. 编辑命令
接下来编辑命令执行逻辑,打开 JokeSpider 文件,编辑 handle() 函数,这就是命令要执行的内容。
public function handle()
{
...
}
2.1 初始化 URL,LOG路径
$uri = 'http://****.cn/joke/content/';
$logPath = 'joke_spider/spider_log';
$timeStorePath = 'joke_spider/earliest_time';
$appKey = env('JUHE_API_KEY');
if (Storage::disk('local')->exists($timeStorePath)) {
$time = Storage::disk('local')->get($timeStorePath);
if($time == null) $time = time();
} else {
$time = time();
}
$earliestTime = $time;
$totalPage = 50;
首先初始化一些变量,
接下来获取上次更新时间,如果获取失败,则以当前时间为更新时间。
2.2 创建 client
请求数据需要用到 PHP 的 HTTP client 库,这里我们用的是 Guzzle 库,在文件开头导入:
use GuzzleHttp\Client;
在 handle() 函数再添加一下代码:
$client = new Client([
'base_uri' => $uri,
'timeout' => 2.0
]);
$this->info('Begin to get data before ' . date('Y-m-d H:i:s', $time) . ' with ' . $totalPage . ' pages data, 20 data per page, total' . 20 * $totalPage . 'data');
for ($page = 1; $page <= $totalPage; $page++) {
...
}
以
2.3 请求数据
$this->info('requesting data of page ' . $page);
$response = $client->request('GET', 'list.from', [
'query' => [
'sort' => 'desc',
'page' => $page,
'pagesize' => 20,
'time' => $time,
'key' => $appKey
]
]
);
$res = \GuzzleHttp\json_decode($response->getBody()->getContents());
向 api 接口发送请求,并将响应的 body 部分经过 json 解码后,存储到变量 $res 中。
2.4 解析并存储数据
if ($res->error_code != 0) {
Storage::disk('local')->append($logPath, date('Y-m-d H:i:s', $time)." ".$res->reason);
$this->info($res->reason);
continue;
}
$jokes = $res->result->data;
foreach ($jokes as $key => $joke) {
$params['content'] = $joke->content;
$params['hashId'] = $joke->hashId;
$params['origin_unix_time'] = $joke->unixtime;
$params['origin_update_time'] = $joke->updatetime;
if(Joke::where('hashId', $params['hashId'])->get()->isEmpty()) {
try {
Joke::create($params);
} catch (QueryException $queryException) {
$this->warn($queryException->getMessage());
Storage::disk('local')->put($logPath, '['.date('Y-m-d H:i:s', time()).']'.$queryException->getMessage());
}catch (Exception $exception) {
$this->warn($exception->getMessage());
Storage::disk('local')->put($logPath, '['.date('Y-m-d H:i:s', time()).']'.$exception->getMessage());
}finally {
$this->info('Stored page ' . $page . '\'s ' . ($key + 1) . 'th data');
$earliestTime = $params['origin_unix_time'];
Storage::disk('local')->put($timeStorePath, $earliestTime+100);
}
} else {
Storage::disk('local')->append($logPath, '['.date('Y-m-d H:i:s', time()).']'." ignore repeated data, hashId:".$params['hashId']);
$this->info(" ignore repeated data, hashId:".$params['hashId']);
}
}
$this->info("wait 10 seconds...");
sleep(2);
这个过程也比较简单,主要是排除掉各种异常后,使用命令 Joke::create($params)
将数据存储到 Joke 类对应的 数据表中,这里用到了 Laravel 的 ORM 方式。
其中的异常包含这些情况:
api 接口返回的 error_code != 0,代表 api 返回数据有错。
数据重复,判断依据是单条数据的 hashId 是否已存在,如果存在则忽略此条数据。
向数据库写数据时出错,通过 catch 捕获。
以上这些异常都会在屏幕打印,并存储到 log 文件中。
2.5 更新时间
最后将数据最新更新时间存储到文件中,方便下次执行时调用。
Storage::disk('local')->put($timeStorePath, $earliestTime+100);
$this->info('Complete, update data to ' . date('Y-m-d H:i:s', $earliestTime));
Storage::disk('local')->append($logPath, '['.date('Y-m-d H:i:s', time()).']'.'Complete, update data to' . date('Y-m-d H:i:s', $earliestTime));
3. 定时执行命令
Laravel 框架也提供了调度机制,和 服务器的 crontab 结合,可以实现任务定时执行。
首先编辑 app/Console/Kernel.php 文件的 schedule() 函数:
protected function schedule(Schedule $schedule)
{
$schedule->command('test')->dailyAt('12:35');
$schedule->command('spider:joke')
->dailyAt('0:03');
$schedule->command('spider:image')
->dailyAt('0:03');
}
这里代表在每天 0:03 执行 spider:joke 命令,也可以用 daily() ,代表在每天 0:00 执行。使用dailyAt() 时,注意参数是 h:i 格式的字符串,两段数字通过 ‘:’ 连接,分别代表小时和分钟,格式不符时,第一段取为小时,分钟默认为0 。这个我当时就遇到坑了,开始写了3段,0:30:00,实际效果是在 0:00 执行,结果等到 0:30 没有执行,后来看了 dailyAt() 的源码才明白。
$schedule->command('spider:joke')->dailyAt('0:03');
另外要注意一下服务器上 php 时区的配置,如果和你期望的不一致,执行实际也会不对。
最后,编辑服务器的 crontab,命令行输入:
crontab -e
添加下面内容:
* * * * * /usr/local/php71/bin/php /data/wwwroot/default/test/jokes/artisan schedule:run 1>> /dev/null 2>&1
其中第一段为你自己服务器的 php 路径。
这样就完成了一个每天定时更新数据库的功能了。