猫でもわかるWeb開発・プログラミング

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

ショップディズニー1周年記念の 30% オフクーポンをプログラムから探す

f:id:yoshiki_utakata:20210701115712p:plain

ショップディズニーの、ネット通販サイトが1周年記念でキャンペーンをしていました。

その中で、「ページのどこかにクーポンが隠れている」というのがありました。

ショップディズニーのサイト内のどこかに、10%OFF、15%OFF、30%OFFの各クーポンが隠されています。

キャスト一同、一生懸命隠しましたので、ぜひチャレンジしてみてください!

30%OFFクーポンを見つけた方は、ショップディズニーマスターです!

しかし、6/30の夜にクーポンを探したのですが、30%オフのクーポンが全然見つからりません

twitterでも調べてみたのですが、マジで誰も見つけてなかったので、プログラム書いて探してみました(画像)。

作戦1: 画像のURLを推測する

10%と15%のクーポンはすぐ見つかる場所になったので、その画像のURLから、30%のクーポンのURLを推測します。

例えば、15%のクーポンはこのようなURLになっていました。

https://cdns7.shopdisney.disney.co.jp/is/image/ShopDisneyJP/treasure-hunt_210701_3200_400_15B_02?$xlargeFull$&fit=constrain&cropN=0,0,1,1

? 以下の部分はクエリパラメータなので無視できます。

重要なのは treasure-hunt_210701_3200_400_15B_02 この部分です。

10%や15%など、他の画像も見ていくと

treasure-hunt_210701_<幅>_<高さ>_<クーポンの割引率><アルファベット>_<数字>

となってることがわかりました。アルファベットの部分は、下記クニンした所 A, B, C がありました。また、最後の数字は、ついてない場合もあった気がします。

一応軽くコードを書いて検証はしたのですが、 URL から、 30%オフのクーポンを推測することは厳しそうでした。

軽く書いたコードはこちら

for ($w = 100; $w <= 2000; $w += 10) {
    for ($h = 100; $h <= 2000; $h += 10) {
        foreach (['', 'A', 'B'] as $suffix) {
            echo "({$w}, {$h})\n";
            $base = 'https://cdns7.shopdisney.disney.co.jp';
            $path = "/is/image/ShopDisneyJP/treasure-hunt_210701_{$w}_{$h}_30{$suffix}";
            echo $path . "\n";

            $uri = $base . $path;
            try {
                $this->client->get($uri);
                echo "アクセスできました\n";
                return 0;
            } catch (\Exception $e) {
                echo $e->getMessage() . "\n";
            }
            sleep(0.1);
        }
    }
}

これで探しましたが見つかりませんでした。

作戦2: ページをクロールする

ということで、ページをクロールすることにしました。

先程の画像解析から、画像に treasure-hunt という文字が入っているっぽいので、ページをランダムに移動しながら treasure-hunt を含む画像を探すことにします。

Laravel の Artisan Command で実装します。

ページの a タグを探し、ショップディズニー無いのページのURLを収集します。訪れたことある URL を管理しつつ、見つけた URL にランダムでアクセスしていきます。

訪れたページの img タグをみて、 treasure-hunt の画像を探していきます。

アクセス過多にならないように、アクセスごとに sleep を挟んで実行します

<?php

namespace App\Console\Commands;

use GuzzleHttp\Client;
use Illuminate\Console\Command;
use PHPHtmlParser\Dom;

class ShopCruise extends Command
{
    /**
     * @var Client
     */
    private $client;

    /**
     * @var Dom
     */
    private $dom;

    private $visited = [];

    private $candidate = [];

    public function __construct(
        ShopDisney $shop,
        Client $client,
        Dom $dom
    ) {
        parent::__construct();
        $this->shop = $shop;
        $this->client = $client;
        $this->dom = $dom;
    }

    public function handle()
    {
        ini_set('memory_limit', '8G');
        $this->cruise('');
        $this->error('finish');
    }

    private function cruise($path)
    {
        while(true) {
            $this->visited[] = $path;
            $uri = 'https://shopdisney.disney.co.jp' . $path;
            try {
                $response = $this->client->get($uri);
                $body = $response->getBody()->getContents();

                $this->dom->loadStr($body);

                // クーポンの画像があるかもしれないのでまずはそれを探す
                $imgDoms = $this->dom->find('img');
                foreach ($imgDoms as $imgDom) {
                    $src = $imgDom->src;
                    if (str_contains($src, 'treasure-hunt')) {
                        $this->error($src);
                    } else {
                        // $this->info($src);
                    }
                }

                // 見つからない場合はリンクをたどる
                sleep(0.1);
                $aDoms = $this->dom->find('a');

                foreach ($aDoms as $aDom) {
                    $next = $aDom->href;
                    if (substr($next, 0, 1) === '/') {
                        if (in_array($next, $this->visited)) {
                            continue;
                        }
                        if (in_array($next, $this->candidate)) {
                            continue;
                        }
                        $this->candidate[] = $next;
                    }
                }
            } catch (\Throwable $e) {
                // なにもしない
            }
            $r = random_int(0, count($this->candidate) - 1);
            echo count($this->candidate) . ', ' . $r . "\t";
            $path = $this->candidate[$r];
            unset($this->candidate[$r]);
            $this->candidate = array_values($this->candidate);
            $this->info($path);
        }
    }
}

結果

見つけることができました。