userAgentを使ってアクセス元のOSとブラウザを判定する(JavaScript)

# 前書き
基本的には色々なところから情報を集めただけだし、ソースコードもほぼ丸パクリ。web開発者ではないし、JavaScriptに精通しているわけではないが、事情があってJavaScriptでOSとブラウザの情報を取得してhtmlに反映させたかったので記録しておく。

# userAgentは近々廃止される
この方法は近いうちに別の方法に取って代わられることを最初に断わっておく。

web開発者にとって、userAgentからOSやブラウザの情報を読み取ることは常套手段だ。一方で、その書式は標準化されていないし、歴史の変遷を経てややこしいものになっていることもよく知られている。
こういう不確かなものが情報源になっていることや、プライバシー的に問題があるなどという理由でGoogle Chromeの開発チームはuserAgentを廃止して代替の仕組み(User-Agent Client Hints)を策定している。ブラウザ業界でシェアの高いChromeが言いだしたことで、他のメジャーなブラウザも追従する方針らしい。
というわけで、この数年の間にuserAgentを使う方法は無用になる。JavaScriptだと``navigator.userAgent``の代わりにAPIを使った``navigator.getUserAgent``を使うことになるらしい。
このあたりの事情は
[Intent to Deprecate and Freeze: The User-Agent string](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/-2JIRNMWJ7s/yHe4tQNLCgAJ)
[グーグル、「Chrome」でユーザーエージェント文字列の利用縮小へ--プライバシー保護強化の一環](https://japan.cnet.com/article/35147994/)
[Google Chrome、ユーザーエージェントを廃止する計画を発表 ](https://news.mynavi.jp/article/20200117-955292/)
[ユーザーエージェント（UA）文字列は時代遅れ？ ～「Google Chrome」で凍結・非推奨に](https://forest.watch.impress.co.jp/docs/serial/yajiuma/1229968.html)
[ChromeでUserAgentが凍結される日（User Agent Client Hintsの使い方）- Qiita](https://qiita.com/paddy-oti/items/fae9ecca9facc9797035)
[UserAgentからOS/ブラウザなどの調べかたのまとめ - Qiita](https://qiita.com/nightyknite/items/b2590a69f2e0135756dc)
に詳しい。

そうは言ってもCOVID-19の問題でスケジュール変更があり、とりあえずChromeでも2021年まではuserAgentが使えることもあり、執筆時(2020年7月)で使える方法を書いておく。

# OSの判定

最初に参考サイトを挙げておく。
[OS の判別 - あんぽんたん](http://www9.plala.or.jp/oyoyon/html/script/platform.html)
適宜このページに挙げられるサイトも参考にしている。

userAgentを読んで、正規表現でマッチさせている。判定するための関数をつくった。

```JavaScript
function CheckOs() {
var ua = navigator.userAgent;
var os;

//まずはWindowsをチェック
if (ua.match(/Win(dows )?NT 10\.0/)) {
	os = "Windows 10";
}
//NTのカーネルバージョンと名称のナンバリングは一致していない。
else if (ua.match(/Win(dows )?NT 6\.3/)) {
	os = "Windows 8.1";
}
else if (ua.match(/Win(dows )?NT 6\.2/)) {
	os = "Windows 8";
}
//7より古いものは一纏め。NTより以前は最後のOtherに入れる。
else if (ua.match(/Win(dows )?(NT [654]|9.)/)) {
	os = "Old Windows";
}
//Macの判定。userAgentでは"Mac OS X"になっている。バージョン番号を正規表現で取得。
else if (ua.match(/Mac OS X ([0-9]+)[_.]([0-9]+)[_.]([0-9]+)/)) {
	os = "macOS " + RegExp.$1 + "." + RegExp.$2 + "." + RegExp.$3;
}
//OS Xより古いものはまとめる。
else if (ua.match(/Mac|PPC/)) {
	os = "Classic Mac OS";
}
//スマホ対応。ちゃんと動作確認してない。
else if (ua.match(/iPhone|iPad/)) {
	os = "iOS";
}
else if (ua.match(/Android ([\.\d]+)/)) {
	os = "Android " + RegExp.$1;
}
//Linuxはディストリは見ない。
else if (ua.match(/Linux/)) {
	os = "Linux";
}
//*BSDも一応書いておく。
else if (ua.match(/^.*\s([A-Za-z]+BSD)/)) {
	os = RegExp.$1;
}
//ここまでに該当しないものは一纏め。
else {
	os = "Other OS";
}
return os;
}
```

Windowsは執筆時サポート対象になっている10、8.1、8までを決め打ちで返し、それ以前は古いWindowsにひとくくり。Windows NT以前のものは完全対象外にしている。Windowsでアクセスされる可能性が高いので丁寧に判定しているが、Macになるとバージョン番号を正規表現で取得するようにしているだけにしている。バージョン番号がわかればとりあえず名称(Catalinaなど)もわかる。表記もバージョンによってMac OS X xxxxだったりOS X xxxxだったりmacOS xxxxだったりしているらしい。面倒なのでmacOSで統一することにした。OS Xより古いものはClassicでまとめている。
スマホも一応対応させている。勢いでLinuxと*BSDまで対応させた。

# ブラウザのバージョンまで判定

参考サイト
[使用してるブラウザを判定したい - Qiita](https://qiita.com/sakuraya/items/33f93e19438d0694a91d)
[ブラウザのバージョンを調べる２ - パズルネット智慧](https://www.pazru.net/js/navigator/4.html)

ブラウザはOS以上に扱いが大変。業界のトレンドや技術革新にあわせて仕様が変わったり、バージョンアップも頻繁にある。過去の歴史をひきずってuserAgentの文字列はややこしくなっている。なのでブラウザではバージョン情報もとってくるようにしたり、文字列の書き方が変わったりするトレンドにもついていかないといけない。やはりUser-Agent Client Hintsが普及していくことが望ましい。

今回も関数をつくる。userAgentの文字列の複雑さを踏まえて、この順番で判定させる必要がある。バージョン番号を切り取りたいので文字列を読んで切ったり貼ったりしている。正規表現を駆使してやれなくてもなさそうだけど、簡単にやることにした。

```JavaScript
function CheckBrowser(){
var nvUA = navigator.userAgent;
//文字列を切る開始と終点の変数
var cutSt,cutEd;
//バージョン番号を格納する変数
var bwVer;
//ブラウザ名+バージョン番号にする変数
var bw;
//大抵はuserAgentの一番最後にそれぞれのブラウザ名とバージョン番号がある。
//なので文字列カット終点デフォルトは末尾にする。
cutEd   = nvUA.length;

//まずはIEのチェックから。最早11より前は一纏め。
if (nvUA.indexOf("MSIE") != -1)
{
	bw = "Old Internet Explorer";
}
//IE11は"Trident"という文字列を使っている。
else if (nvUA.indexOf("Trident") != -1)
{
	bw = "Internet Explorer 11";
}
//Edgeは古いものは"Edge"、Chromiumベースになってからは"Edg"。これも悪い歴史とノウハウ。
else if (nvUA.indexOf("Edge") != -1 || nvUA.indexOf("Edg") != -1)
{
	if (nvUA.indexOf("Edge") != -1)
	{
//Edgeという文字列を見つけたら、そこを合図にして"Edge/"の5文字先からバージョン番号がはじまる。
		cutSt = nvUA.indexOf("Edge"); 
		bwVer = nvUA.substring(cutSt+5,cutEd);
	}
	else
	{
//Chromiumベースになってからは"Edg/"の4文字。
		cutSt = nvUA.indexOf("Edg"); 
		bwVer = nvUA.substring(cutSt+4,cutEd);
	}
  bw = "Microsoft Edge "+bwVer;
}
else if (nvUA.indexOf("Firefox") != -1)
{
	cutSt = nvUA.indexOf("Firefox");
//"Firefox/"の8文字から先がバージョン番号。
  	bwVer = nvUA.substring(cutSt+8,cutEd);
	bw = "FireFox " + bwVer;
}
//Operaは最近は"OPR"を使っているらしい。
else if (nvUA.indexOf("OPR") != -1)
{
	cutSt = nvUA.indexOf("OPR");
  	bwVer = nvUA.substring(cutSt+4,cutEd);
	bw = "Opera "+ bwVer;
}
//Safariは厄介。Chromeも最後"Safari/xxxx"で終わるのでちょっと遠まわりする。
else if ( nvUA.indexOf("Safari") != -1 )
{
//"Safari"があっても"Chrome"の文字列があったらGoogle Chrome。
  if ( nvUA.indexOf("Chrome") != -1 )
  {
    cutSt = nvUA.indexOf("Chrome");
// "Chrome/xxxx Safari/xxxx"なので、スペースまでが末尾。
    cutEd   = nvUA.indexOf(" ",cutSt);
//"Chrome/"の7文字からスペース(cutED)までがバージョン番号
    bwVer =nvUA.substring(cutSt+7,cutEd);
	bw = "Google Chrome "+ bwVer;
  }
  else
  {
//Safariは"Version/xxx Safari/yyy"になっており、バージョン番号はxxxの方
	cutSt = nvUA.indexOf("Version");
    cutEd = nvUA.indexOf(" ",cutSt);
    bwVer = nvUA.substring(cutSt+8,cutEd);
	bw = "Safari "+ bwVer;
  }
}
//それ以外にもブラウザは山のようにある。
else {
	bw = "Other Browser";
}
return bw;
}
```


最近はChrome/Chromiumベースのブラウザが増えている。それでもSRware IronやSleipnirのなど、ブラウザ名を入れるものもあれば、不具合が起こってブラウザ名を消した(Google Chromeと同じ文字列になって区別できなくなった)VivaldiやBraveなどがある。さすがにここまで面倒見れない。

# 関数を呼びだして実行するHTMLをつくる

webで使うなら、JSファイルを呼び出すHTMLを埋め込めばいい。デバックや動作確認としても、JSを実行する環境をわざわざ構築するよりはHTMLファイルをつくってブラウザで実行した方が楽。webブラウザはJSの実行環境としても使える。

作成した``CheckOS()``と``CheckBrowser()``関数を例えば``check.js``のようなJSファイルに書き、HTMLへの書き込むところまで書いておく。

```JavaScript
//ここより上にCheckOS()とCheckBrowser()関数を書いておく。
document.write(CheckOs());
document.write("</br>");
document.write(CheckBrowser());
document.write("</br>");
document.write(navigator.userAgent);
```
ついでにuserAgentの元の文字列も最後に出している。デバックのため。

HTMLをつくる(ファイル名は例えばindex.html)

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>javascript test</title>
</head>
<body>
    <script src="check.js">
    </script>
</body>
</html>
```

このファイルをブラウザで開けばOS名、ブラウザ名、userAgentの文字列を表示する。

## おまけ(テキスト欄に埋め込みたい時)

おまけでHTMLのinputタグの中でこの関数を実行する方法を乗せておく。inputタグは1行のテキスト記入欄を表示するHTMLコード。
HTMLを読み込んだときにこの関数が実行されるようにJSを書く。

まず、index.htmlをinputタグを仕込んだバージョンに書き換える。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>javascript test</title>
</head>
<body>
    <script src="check.js">
    </script>
<br/>
    <table>
        <tr>
            <th>OS:</th>
            <td><input id="os" type="text"></td>
        </tr>
        <tr>
            <th>ブラウザ:</th>
            <td><input id="bw" type="text"></td>
        </tr>
    </table>
</body>
</html>
```

ちょっと凝ってテーブルをつくった。

inputタグの中にあるidを読んで、そのタグの中で関数を実行するようにすればいい。htmlを読み込んだタイミングで実行するようにちょっと手間をかける。

```JavaScript
//ここより上にCheckOSとCheckBrowserを書く

//CheckOsとCheckBrowserを実行してhtmlに書き込む関数を定義
function InitialInput(){
//htmlでinputタグにつけたidを読んで実行。
	document.getElementById("os").value = CheckOs();
	document.getElementById("bw").value = CheckBrowser();
}


//ページ表示の時に実行するプログラム。InitialInputを実行させている。
//IEなどブラウザによって挙動が違うことがあるのでちょっとテクニカル。

if (window.addEventListener) {
	window.addEventListener('load', InitialInput, true);
	} else {
	window.attachEvent('onload', InitialInput);
	}
```

これで上のindex.htmlを読み込めば、最初からテキスト欄にOSとブラウザが記入済み。

JavaScriptに詳しいわけではないので、もっと簡単で良い方法があるかもしれない。このソースを書くために調べた参考サイトもわからなくなってしまったので参考サイトが挙げられない体たらく。JavaScript入門的なサイトをいくつも見たので、それで何とかなるはず。

# 動作確認(to be continued)

動作確認しようとすると色々なOSやブラウザを持っていないといけない。ブラウザぐらいならともかくOSは厳しい。そういうわけで、ブラウザが送信するuserAgentを偽装する方法を採用する。これは別の記事にする。