Seleniumではじめる位置偽装入門 〜Googleの検索結果で旅しよう〜

![image.png](https://qiita-image-store.s3.amazonaws.com/0/13541/9d829658-8be7-d0b4-d865-300a81ecffda.png)

みなさーん！位置偽装してますかー！！位置偽装大好き <a href="https://twitter.com/M_Ishikawa">@M_Ishikawa</a> です！
位置偽装の魅力はなんといっても、いつでも世界中のどこにでも旅ができる、そんな気分を味わえることですよね！

というわけで、Seleniumを使って位置偽装しGoogle検索して、世界中を駆け巡りましょう！！

*これは [Selenium/Appium Advent Calendar 2018](https://qiita.com/advent-calendar/2018/selenium_and_appium) の10日目の記事です。*

## 位置偽装ってなあに？

**位置偽装 (Faking Geolocation)** とは、ブラウザがGPSやらIP(GeoIP)やらを元にユーザーのいる位置情報を偽装して、東京にいるのにサンフランシスコでググったときの検索結果を出したりすることです。

ググるといいましたが、これはGoogle検索に限らず位置情報の取得は、**Geolocation API**を利用することでいろんなサイトでも使われています。Google MapsだってスマホでもGPS内蔵していないPCでも、アクセスすれば現在地が分かっちゃいますよね。

## Geolocation API を確認してみる

ブラウザのコンソールで

```javascript
navigator.geolocation.getCurrentPosition(function(e) {console.log(e)})
```

と打てば確認できます。位置情報送信の許可ダイアログが出てくるので許可すると情報が出てきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/13541/d71524b1-ab3d-18cd-ad3d-acc3ab5b937c.png" width="300">

<img src="https://qiita-image-store.s3.amazonaws.com/0/13541/70d42b07-9573-4066-6f48-b726dccbcf06.png" width="500">

ぼくの位置情報(表参道駅のカフェで、公衆Wi-Fiに繋いでいます)もバレたところで、試しに「[焼肉](https://www.google.co.jp/search?q=焼肉)」で検索してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/13541/bc741966-502a-e9d5-1c12-5de934297bc3.png)

きっと[みなさんの結果](https://www.google.co.jp/search?q=焼肉)はこれとは違い、自分のいる場所に近い焼肉やさんが出てきたと思います。

仕組みの説明はここでは割愛します。
ブラウザで位置偽装するにはエクステンション等使って確認したりすることもできます。

## Seleniumで位置偽装する

Laravel + Docker + Selenium で位置偽装にチャレンジしてみました。
結論からいうと、seleniumではfirefoxの、しかもバージョンが3.8.1以前だと、位置偽装することに成功しました。
[selenium/firefox](https://hub.docker.com/r/selenium/standalone-firefox/tags/)の3.9以降ではサポートされなくなり動作しません。また、selenium/chromeでもチャレンジしたのですがうまくいきませんでした。 [^1]

[^1]: オプション(SE_OPTS)に`-enablePassThrough false`を付けないとfacebook/WebDriverが動かないが、`-enablePassThrough`は3.8.1で打ち切られた。



これはひょっとすると使用したライブラリの facebook/WebDriver に依存していることで、解消できるかもしれませんが、とりあえずぼくはこれで満足ですｗ (でも解決した人いたらぜひ教えてください！

* 使用したもの
    * PHP 7.1
        * Laravel 5.7
        * facebook/WebDriver 1.6
    * Docker
        * selenium/standalone-firefox-debug:3.8.1

selenium/standalone-firefox:3.8.1 だと日本語フォントが入っていないので、VNC接続できるdebug版を使用。VNCでも確認したいしね。
standalone版を使っているのは、ただ楽をしたかったからです。

## 実装: 博多駅で焼肉を検索

では博多駅で焼肉を探してみましょう！

docker-compose.yml

```yml
version: "2"
services:

  php:
    build: docker/php
    working_dir: /var/www/faking
    volumes:
      - .:/var/www/faking
      - /dev/shm:/dev/shm
    tty: true
    stdin_open: true
    privileged: true

  firefox:
    # -enablePassThrough が使えるのは3.8.1まで
    image: selenium/standalone-firefox-debug:3.8.1  # vnc付き日本語フォントあり
    ports:
      - "15905:5900"  # localhost:15905でローカルからvncアクセスできるように
    environment:
      - SE_OPTS=-enablePassThrough false  # これやらないとfacebook/WebDriverが動かない
    volumes:
      - /dev/shm:/dev/shm
```

app/Console/Commands/travel.php

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\Firefox\FirefoxProfile;
use Facebook\WebDriver\Firefox\FirefoxDriver;

class travel extends Command
{

    ・・・

    public function handle()
    {
        $this->info('start');
        $profile = new FirefoxProfile();

        // geolocationの準備
        $profile->setPreference('geo.prompt.testing', true);
        $profile->setPreference('geo.prompt.testing.allow', true);
        $profile->setPreference('geo.enabled', true);
        $profile->setPreference('geo.provider.use_corelocation', false);
        $profile->setPreference('geo.provider.ms-windows-location', false);

        // localeを日本、languageを日本語、に
        $profile->setPreference('intl.accept_languages', 'ja');

        // 博多駅
        $lat = 33.5897275;
        $lng = 130.4207274;

        // 緯度経度算出
        $profile->setPreference('geo.wifi.uri', sprintf(
                'data:application/json,{"location": {"lat": "%s", "lng": "%s"}, "accuracy": 10.0, "status": "OK"}',
                $lat,
                $lng
            ));

        $caps = DesiredCapabilities::firefox();
        $caps->setCapability(FirefoxDriver::PROFILE, $profile);

        $this->info('open browser');
        $driver = RemoteWebDriver::create('http://firefox:4444/wd/hub', $caps);

        // Googleを開く
        $this->info('open google');
        $driver->get('https://www.google.co.jp/');

        // 検索ボックスが現れるのを待つ
        $driver->wait(10, 500)->until(
            WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::name('q'))
        );

        // 検索ボックスにキーワードを入力して検索実行
        $this->info('send query');
        $element = $driver->findElement(WebDriverBy::name('q'));
        $element->sendKeys('焼肉');
        $element->submit();

        // ページ遷移を待つ
        $driver->wait(10, 500)->until(
            // ページ遷移したかどうかは #swml-upd = 「正確な現在地を使用」リンクがあるかどうかで判定
            WebDriverExpectedCondition::elementToBeClickable(WebDriverBy::id('swml-upd'))
        );

        // スクリーンショットを保存
        $this->info('save screenshot');
        $driver->takeScreenshot(storage_path('yakiniku-hakata.png'));

        // ブラウザを閉じる
        $this->info('close browser');
        $driver->close();

        $this->info('end');
    }
```

**実行キャプチャ** ※クリックでYoutube開きます
<a href="https://www.youtube.com/watch?v=Pg5RNIsRNdc"><img src="https://qiita-image-store.s3.amazonaws.com/0/13541/07dde631-2100-c2b1-d5e0-fc501acd3c0e.gif" width="300"></a>
**結果ページ**
![yakiniku-hakata.png](https://qiita-image-store.s3.amazonaws.com/0/13541/afae4fd1-9869-6a6e-5f2d-3de261410554.png)

博多駅を指定しているはずなのに、自分のいる場所(西麻布に移動しちゃいました)で検索されています。
実はこれではうまくいきません。コツがあります。

## 実装: 「正確な現在地を使用」を利用

Google検索結果ページの下部にある「**正確な現在地を使用**」を利用することで位置情報を更新できます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/13541/3b0f6ffb-8b55-aadd-6ac3-e742c54fb86c.png" width="500">
↓
<img src="https://qiita-image-store.s3.amazonaws.com/0/13541/02f9a67f-0f9d-cadc-a0bf-f146d6a214fb.png" width="500">

```php
        // 「正確な現在地を使用」をクリック
        $this->info('click swml-upd');
        $driver->findElement(WebDriverBy::id('swml-upd'))->click();

        // 現在地更新されるまで待つ
        $driver->wait(10, 500)->until(
            WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector('#loc.known_loc'))
        );

        // reload
        $this->info('reload');
        $driver->navigate()->refresh();

        $driver->wait(10, 500)->until(
            // ページ遷移したかどうかは #swml-upd = 「正確な現在地を使用」リンクがあるかどうかで判定
            WebDriverExpectedCondition::elementToBeClickable(WebDriverBy::id('swml-upd'))
        );

        // スクリーンショットを保存
        $this->info('save screenshot');
        $driver->takeScreenshot(storage_path('yakiniku-hakata-retry.png'));
```

**実行キャプチャ**
<a href="https://www.youtube.com/watch?v=FvNlkH59kBA"><img src="https://qiita-image-store.s3.amazonaws.com/0/13541/dacfabc7-2faf-df01-50e7-8c80e6b6ae20.gif" width="300"></a>
**結果ページ**
![yakiniku-hakata-retry.png](https://qiita-image-store.s3.amazonaws.com/0/13541/9ce2c52f-5c01-e93e-0d67-99453b7a6b0f.png)

うまくいきました！
え、そんなの「博多駅 焼肉」で検索すればいいだろって？
じゃあ検索しにくいので試してみましょう。

例えばカリフォルニアにあるFacebook本社周辺の焼肉を探してみます。「Facebook 焼肉」では流石にぱっとは出てこないですからね。
緯度経度を調整して、

**実行キャプチャ**
<a href="https://www.youtube.com/watch?v=bpKLqH_W9os"><img src="https://qiita-image-store.s3.amazonaws.com/0/13541/4372971b-86e6-d295-4f04-df7be015c2bd.gif" width="300"></a>
**結果ページ**
![yakiniku-facebook.png](https://qiita-image-store.s3.amazonaws.com/0/13541/5b3b04bd-11a3-678c-0ce8-aa025d504b5d.png)

やったね！
これでFacebook本社に行った(つもりになった)ときも焼肉に困りません！

## サンプル

Githubにサンプル上げてますのでご利用ください〜
https://github.com/ishikawam/faking-geolocation-sample

REDME.mdとかMakefileとか読めば一瞬で動作できるようにしてます。dockerバンザイ！
上記の実装の流れもcommitごとに分けてますので確認しやすいかと。