
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、この間 【Laravel】ABテスト機能をつくる という記事を公開しました。記事内にも書きましたが、これは「経営者的な視点」から必要な機能として投稿しました。
そして、あるRPA
(ロボティック・プロセス・オートメーション ≒ 要するにプログラムによる自動化)の記事を読んでいて「これは面白そう!」と思ったのが今回のテーマです。
それが・・・・・・
競合の値段をスクレイピングして自社製品の金額を自動で変更する
機能です。
つまり、例えば「1日に1回自動で値段をチェックし、その金額から 10円 値引きする」とかそういう使い方ですね。
ということで今回は「経営者向け機能」の第2弾として、「スクレイピング + 金額設定」の自動化をしてみたいと思います。
ぜひ御社の役に立ちましたら光栄です
「地元のクラフトビール、
ウマウマです」
開発環境: Laravel 8.x
目次 [非表示]
やりたいこと
今回は、市販されているクラフトビールの中で一番私が好きな「インドの青鬼」の24缶セットを題材とします。
そして、私がこのビールを販売していると仮定し、架空サイトから最安値を取得&その値段より10円安くしてデータ保存してみたいと思います。
なお、もちろんスクレイピング先のサイトは商品によって違ってきますので、それぞれ「スクレイピング用クラス」を使って汎用的に実装できるようにします。
※ちなみに、インドの青鬼はこちらです。
↓ ↓ ↓
1缶280円ぐらいですが、350円でも迷わず買ってしまうと思います(笑)cheers!
商品テーブル&モデルを用意する
まずははじめに、「自社製品データ」を管理するitems
テーブルとそのテストデータを作成します。
以下のコマンドを実行してください。
php artisan make:model Item -m
すると、マイグレーションとモデルのファイルが作成されるので、中身を以下のように変更します。
database/migrations/****_**_**_******_create_items_table.php
// 省略
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('商品名');
$table->unsignedInteger('price')->comment('値段');
$table->string('scraping_class')->comment('スクレピング用クラス名');
$table->string('scraping_url')->comment('スクレイピング先URL');
$table->dateTime('scraped_at')->comment('スクレイピングした日時');
$table->timestamps();
});
}
// 省略
テストデータを用意する
次に、開発する際に何もデータがないと不便ですのでテストデータをつくります。
今回必要なデータは1件だけです。
以下のコマンドを実行してください。
php artisan make:seed ItemsTableSeeder
すると、Seeder
ファイルが作成されるので中身を以下のようにします。
database/seeders/ItemsTableSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Item;
use Illuminate\Database\Seeder;
class ItemsTableSeeder extends Seeder
{
public function run()
{
$item = new Item();
$item->name = 'インドの青鬼';
$item->price = 8000;
$item->scraping_class = 'App\Scrapes\LowestPriceScrape'; // 後でつくります
$item->scraping_url = 'http://l8x.test/scraping_page'; // スクレイピングするURL
$item->scraped_at = now();
$item->save();
}
}
変更が完了したら、Laravel
側へ登録します。
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(ItemsTableSeeder::class); //
ここを追加しました
}
}
では、今の状態でマイグレーションを実行します。
以下のコマンドを実行してDBを再構築しましょう。
php artisan migrate:fresh --seed
実行が完了すると、データベースは以下のようになります。
スクレイピング用のクラスをつくる
では、続いてスクレイピング用のクラスです。
このクラスを独自実装することで、他のどんなサイトにも対応させることができます。
架空のスクレイピング先ページをつくる
実際にクラスを作る前にスクレイピングする先の架空の販売ページをつくります。
まずはルートです。
routes/web.php
Route::get('scraping_page', function(){ return view('scraping_page'); });
そして、ビューです。
resources/views/scraping_page.blade.php
<html>
<head>
<title>インドの青鬼 350ml ×24缶</title>
</head>
<body>
<h1>インドの青鬼 350ml ×24缶</h1>
<table>
<tr>
<td class="store_name">商店 A</td>
<td class="price">¥6,670</td>
</tr>
<tr>
<td class="store_name">商店 B</td>
<td class="price">¥6,680</td>
</tr>
<tr>
<td class="store_name">商店 C</td>
<td class="price">¥7,040</td>
</tr>
</table>
</body>
</html>
これで、「http://******/scraping_page」にアクセスすると以下のように表示されるようになりました。
専用クラスをつくる
では、先ほどの架空ページからデータをスクレイピングするクラスをつくっていきますが、「やりたいこと」でも書いたようにどんなパターンでも使えるようにしたいので、今回は「抽象クラス」をつかって実装していきます。
抽象クラスとは、とてもシンプルに言うと「クラスのもと」のことで、メリットの1つに、必要なメソッドをセットしないとエラーになる、つまりセットのし忘れがなくなるという点があります。
app/Scrapes/BaseScrape.php
<?php
namespace App\Scrapes;
use App\Models\Item;
use Illuminate\Support\Facades\Http;
abstract class BaseScrape {
private $url;
abstract public function execute(); //
ここは継承先で必ず定義しないとエラーになります
public function setUrl($url) {
$this->url = $url;
}
protected function getContent() {
$url = $this->url;
$response = Http::get($url);
return $response->body();
}
}
そして、BaseScrape
を継承したクラスLowestPriceScrape
です。
app/Scrapes/LowestPriceScrape.php
<?php
namespace App\Scrapes;
class LowestPriceScrape extends BaseScrape {
public function execute()
{
$content = $this->getContent();
$pattern = '|<td class="price">¥([^<]+)</td>|';
if(preg_match_all($pattern, $content, $matches)) {
$prices = array_map(function($price_text){
return (int) filter_var($price_text, FILTER_SANITIZE_NUMBER_INT);
}, $matches[1]);
return min($prices);
}
return null;
}
}
これで、以下のようにするとスクレイピングが実行できるようになります。
$scraping = new \App\Scrapes\LowestPriceScrape();
$scraping->setUrl('http://l8x.test/scraping_page');
$lowest_price = $scraping->execute();
自動実行するコマンドをつくる
では、定期的にタイマー実行するための独自コマンドをつくっていきましょう。
以下のコマンドを実行してください。
php artisan make:command ScrapingCommand
するとファイルが作成されるので中身を以下のように変更します。
app/Console/Commands/ScrapingCommand.php
<?php
namespace App\Console\Commands;
use App\Models\Item;
use App\Scrapes\BaseScrape;
use App\Scrapes\LowestPriceScrape;
use Illuminate\Console\Command;
class ScrapingCommand extends Command
{
protected $signature = 'scraping';
protected $description = 'Scrape external website regularly';
public function handle()
{
$result = false;
$item = Item::oldest('scraped_at')->first(); // 一番前にスクレイピングされたデータを取得
$scraping_class = $item->scraping_class;
if(class_exists($scraping_class)) { // クラスが存在しているかチェック
$scraping = new $scraping_class();
if($scraping instanceof BaseScrape) { // 正しいクラスかチェック
$scraping = new LowestPriceScrape();
$scraping->setUrl($item->scraping_url);
$lowest_price = $scraping->execute();
$item->price = $lowest_price - 10; // 10円安くする
$result = $item->save();
}
}
if($result === true) {
$this->info('Done!');
} else {
$this->error('Failed...');
}
}
}
これで、php artisan scraping
を実行するとスクレイピングが実行され、値段が自動で更新されるようになりました。
では、このままではコマンドは実行できないのでLaravel
側に登録して使えるようにしましょう。
app/Console/Kernel.php
<?php
namespace App\Console;
use App\Console\Commands\ScrapingCommand;
// 省略
class Kernel extends ConsoleKernel
{
protected $commands = [
ScrapingCommand::class //
ここを追加しました
];
テストしてみる
では、実際にテストしてみましょう
まず先に現在データベースの「インドの青鬼」がいくらになっているか確認しておきましょう。
8,000円 ですね。
では、以下のコマンドを実行してうまくスクレイピング&値段変更ができるかをチェックしてみます。
php artisan scraping
すると・・・・・・
はい
(最安値 – 10円)の6,660円
になりました。
成功です
あとは、このコマンドをcrontab
で定期実行すれば定期的に自動更新できます
おわりに
ということで、今回はスクレイピングで取得した金額を使って自動的に価格設定できるようにしてみました。
ちなみに、当初は有名サイト「価格.com」を使ってスクレイピングするつもりでしたが、どうやら利用規約に引っかかるようでしたので途中で架空のページを使うよう変更しました。
また、今回開発した内容を拡張するアイデアとして、今回は全て10円引きでしたが、items
テーブルにdiscount_amount
という項目を用意し、いくら値引きするかを個別データで変更しても面白いかもしれません。
また、実際の運用では全く同じ商品でなくとも「競合の似た別の商品」をチェックしておいて、少しだけ安くするとか、なんなら株価や金の価格をチェックしていろいろなことができるかもしれません。
夢は広がるばかりですね
ではでは〜
「最近やっとベース音が
聞き取れるようになってきました。
ベンベン♪」