外部のウェブサイトからOGPを取得したい時など、cURL等を使用してウェブサイトのソースコードを読み込む場合がある。 それを解析する際に適切な文字コードで処理しなければ文字化けを起こす可能性がある。 ではその文字コードはどこから取得すればよいのかという点について考えていきたい。
PHPにはmb_detect_encoding()
という文字コードを判別する関数があるが、これは癖が強く正直あまり役に立たない。
それ以外で文字コードを取得する手段だが、知る限りでは以下の3つがある。
レスポンスヘッダのContentTypeには文字コードが指定されている場合とされていない場合があるので、指定されていない場合はソースコード内の文字コード指定から取得を試みる。
これらがすべて未指定だった場合はSJIS-win
と見なす。
$url = 'https://...';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// 指定したURLからソースコードを読み込む
$result = curl_exec($ch);
// レスポンスヘッダからContent-Typeを取得する
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_exec()
を呼び出した後で、そのリクエストに使用したハンドラ(ここでは$ch)を使いcurl_getinfo($ch, CURLINFO_CONTENT_TYPE)
と呼び出すとレスポンスヘッダのContent-Typeを取得できる。
例えば以下のような値が取得できる。
このようにcharset
が指定されていればそこから文字コードを取得することができる。
多くの場合ここにきちんとcharset
が指定されているが、たまにtext/html
だけが指定されていてcharset
が指定されていない場合がある。
その場合はソースコード内から取得を試みる必要がある。
前述したように、HTMLのソースコード内には次のような形で文字コードを指定してある場合が多い。
<meta charset="..." />
があればそこから取得し、無ければ<meta http-equiv="Content-Type" content="..." />
を探す。
HTMLの文字列から正規表現で検索する手もあるが、HTMLは書式のばらつきが多いので正規表現で網羅するのは正直かなりめんどくさい。
なので少々回りくどいがXPathで検索する形を取ってみた。
function GetEncoding(string $html): string
{
$internalErrors = libxml_use_internal_errors(true);
// XPathを初期化する
$dom = new DOMDocument();
$dom->loadHTML($html);
$xpath = new DOMXPath($dom);
$charset = 'SJIS-win';
// <meta charset="..." />を検索
$node = $xpath->query('//meta/@charset');
if ($node->length > 0)
{
// 見つかればその値を取得
$charset = $node->item(0)->nodeValue;
}
else
{
// 見つからなければ<meta http-equiv="Content-Type" content="..." />を検索
$node = $xpath->query('//meta[@http-equiv="Content-Type"]/@content');
if ($node->length > 0)
{
// contentには"text/html; charset=UTF8"のような値が入る
$contentType = $node->item(0)->nodeValue;
// そのパターンにマッチしていればcharsetの値を取得
if (preg_match('/.+; ?charset=(.+)/', $contentType, $matches) === 1)
$charset = $matches[1];
}
}
libxml_use_internal_errors($internalErrors);
libxml_clear_errors();
// いずれも指定されていなければSJIS-winとみなす
return $charset;
}
<meta charset="..." />
と<meta http-equiv="Content-Type" content="..." />
がいずれも指定されていない場合はSJIS-win
と見なしている。
実際のところ「文字コードがどこにも指定されていないが実際はUTF-8が使われている」という可能性もある。 だがそこまで考慮していてはキリがないのでどこかで妥協することが必要なのではないだろうか。
正直あまりスマートな形ではないと思うが取り合えずこれでかなりの範囲をカバーできたのではないかと思う。 もっと簡潔に取得する方法があれば良いのだが、その辺りは追い追い調べていきたい。