UserAgentからOS、ブラウザ、バージョンを判定する

#はじめに
UserAgentという死ぬほどわかりにくい文字列からOS、ブラウザ、バージョンなどを判定するロジックをどうしても組みたかったので備忘録も兼ねて。

2020年7月現在、ITPやGDPRをはじめ、日本でも個人情報保護法が改正されるなどIPアドレス、cookieなどの情報が非常にセンシティブに扱われている。
こういった個人情報保護へ向けたアップデートは各種ブラウザやバージョンごとに実装されていく風潮となっており、これらの傾向や実挙動を見るべくUserAgentからしっかりと端末情報を分析しやすい形にすることは重要である。

Chromeでは、UserAgent文字列を凍結し、[Client-Hintという新しいリクエストヘッダー情報が追加されるという話](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/-2JIRNMWJ7s/yHe4tQNLCgAJ)もある。


大変便利な機能に見えるが、Client-Hintの他ブラウザでの対応状況や[コロナ対応によるリリース延長](https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/-2JIRNMWJ7s%5B76-100%5D)もあってUserAgentの判定ロジックは作ってしまった方がよいという結果になった。

#判定対象
本記事で紹介するロジックでは、それぞれ下記を分類する。
それ以外に該当するものはnullとする。
2020年７月現在、下記からのリクエストが大半を占めているため基本的なuser_agentの判定は問題ないが、アクセスログを取得しているサイトへの導線としてアプリを利用している場合など、webviewのuser_agentが多く存在する可能性があり、下記の判定対象では効果的でないケースもある。

###os
・windows
・mac
・iOS
・Android
・linux
・chromeOS

###ブラウザ
・chrome
　　-chorme
　　-CriOS
・firefox
　　-firefox
　　--FxiOS
・edge
　　--edge
　　--EdgiOS
　　--EdgA
・IE
・safari

#コピペ用

「user_agent」カラムを含むアクセスログ等のテーブルに対して、下記のカラムを算出（presto）
・os
・os_version
・browser
・browser_version


```sql
SELECT
    CASE
        WHEN user_agent LIKE '%Android%' 
        THEN 'Android'
        WHEN user_agent LIKE '%Linux%' AND user_agent NOT LIKE '%Android%' 
        THEN 'Linux'
        WHEN user_agent LIKE '%Mac OS X%'
            AND user_agent NOT LIKE '%iPhone OS%'
            AND user_agent NOT LIKE '%CPU OS%' 
        THEN 'Mac'
        WHEN user_agent LIKE '%Windows%' 
        THEN 'Windows'
        WHEN user_agent LIKE '%iPhone%' 
        THEN 'iOS'
        WHEN user_agent LIKE '%iPad%' 
        THEN 'iPad'
        WHEN user_agent LIKE '%CrOS%' 
        THEN 'ChromeOS'
    END AS os,
    CASE
        WHEN user_agent LIKE '%Android%' 
        THEN regexp_extract(user_agent,'(Android )(.*?)(;)',2)
        WHEN user_agent LIKE '%Mac OS X%'
            AND user_agent NOT LIKE '%iPhone OS%'
            AND user_agent NOT LIKE '%CPU OS%' 
        THEN replace(regexp_extract(user_agent,'(Mac OS X )(.*?)([;)])',2),'_','.')
        WHEN user_agent LIKE '%Windows%' THEN
        regexp_extract(user_agent,'(Windows NT )(.*?)([;)])',2)
        WHEN user_agent LIKE '%iPhone%'
            OR user_agent LIKE '%iPad%'
        THEN replace(regexp_extract(user_agent,'(iPhone OS |CPU OS )(.*?)( )',2),'_','.')
        WHEN user_agent LIKE '%CrOS%' 
        THEN regexp_extract(user_agent,'(CrOS )(.*?)( )',2)
    END AS os_version,
    CASE
        WHEN user_agent LIKE '%Edge%'
            OR user_agent LIKE '%Edg%' 
        THEN 'Edge'
        WHEN user_agent LIKE '%Firefox%'
            OR user_agent LIKE '%FxiOS%' 
        THEN 'Firefox'
        WHEN user_agent LIKE '%Chrome%'
            OR user_agent LIKE '%CriOS%'
        THEN 'Chrome'
        WHEN user_agent LIKE '%Trident%' 
        THEN 'IE'
        WHEN (user_agent LIKE '%iPad%'
            OR user_agent LIKE '%iPhone%'
            OR user_agent LIKE '%Mac%')
            AND user_agent LIKE '%Safari%'
            AND user_agent LIKE '%Version%' 
        THEN 'Safari'
    END AS browser,
    CASE
        WHEN user_agent LIKE '%Edge%'
            OR user_agent LIKE '%Edg%' 
        THEN regexp_extract(user_agent,'(Edge/|Edg/|EdgA/)(.*)',2)
        WHEN user_agent LIKE '%Firefox%' 
        THEN regexp_extract(user_agent,'(Firefox/)(.{4})',2)
        WHEN user_agent LIKE '%Chrome%' 
        THEN regexp_extract(user_agent,'(Chrome/)(.*?)( )',2)
        WHEN user_agent LIKE '%Trident%' 
        THEN regexp_extract(user_agent,'(rv:|MSIE )(.*?)([;)])',2)
        WHEN user_agent LIKE '%CriOS%'
            OR user_agent LIKE '%FxiOS%' 
        THEN regexp_extract(user_agent,'(CriOS/|FxiOS/)(.*?)( )',2)
        WHEN user_agent LIKE '%iPad%'
            OR user_agent LIKE '%iPhone%'
            OR user_agent LIKE '%Mac%'
            AND user_agent NOT LIKE '%Chrome%'
            AND user_agent NOT LIKE '%CriOS%'
            AND user_agent NOT LIKE '%FxiOS%'
            AND user_agent LIKE '%Safari%' 
        THEN regexp_extract(user_agent,'(Version/)(.*?)( )',2)
    END AS browser_version, 
    user_agent
FROM YOUR_USER_AGENT_TABLE /*要編集*/
```
###結果sample
ちょっといい感じかも。

| os | os_version | browser | browser_version | user_agent |
|:-----------|:-----------|:-----------|:-----------|:-----------|
| Windows | 10.0 | Chrome | 83.0.4103.116 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 |
| iOS | 13.5.1 | Safari | 13.1.1 | Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1 |
| iOS | 13.5.1 | Safari | 13.1.1 | Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1 |
| Android | 10 | Chrome | 83.0.4103.106 | Mozilla/5.0 (Linux; Android 10; SCV41) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36 |

#誤判定について
user_agentを正規表現で精度100%で判定するのは不可能。
「判定対象」に含まれないものについては基本的にnullになる。
しかし、マイナーなos/browserやスマートデバイスのwebview等、user_agentが誤って上記のロジックに引っかかり異なったos/browser判定がされてしまうケースがある。

###確認済み誤判定ブラウザ
・Sleipnir　⇒　chrome
・cyberfox　⇒　firefox
・waterfox　⇒　waterfox
・Maxthon　⇒　chrome
・Vivaldi　⇒　chrome
・PaleMoon 　⇒　firefox
・RakutenWebsearch　⇒　chrome
・YJApp-ANDROID jp.co.yahoo.android.yjtop　⇒　chrome
・OPR　⇒　chrome
・SamsungBrowser　⇒　chrome

上記については確保しているアクセスログの中でもパーセンテージが低かったため、「誤差」とし、ロジックに含めていない。**必要であれば上記のロジックに正規表現等を追加して判定が必要。**

#終わりに
user_agentという最悪な発明の所為でアクセスログから端末情報の分析は非常に困難を極める。
chromeのclient-histも期待しているが、webサーバのデフォルト設定では取得できないらしく、今後も色々対応が必要になってきそうだ・・・