Laravel 实现自动更新服务器数据

来源:互联网 发布:淘宝店铺套现处罚 编辑:程序博客网 时间:2024/06/05 15:10

背景

最近写了一个看笑话的 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;

首先初始化一些变量,uriapilogPath 用来记录数据获取结果,异常等日志信息, timeStorePath便appKey 为api接口需要的认证key,从 .env 文件中获取。

接下来获取上次更新时间,如果获取失败,则以当前时间为更新时间。

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++) {    ...    }

uribaseuriclient,通过 $this->info() 向屏幕打印启动请求的信息。接下来启动循环来请求每一页的数据。

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 路径。

这样就完成了一个每天定时更新数据库的功能了。

原创粉丝点击