phpでmetaタグとかOGP情報とかパースしたりしてみた

この記事は約14分で読めます。

近日公開予定のリンクをブログカード形式で表示する「Pz-LinkCard」。

外部サイトのタイトルや概要文(抜粋文)をリンク先から取得するのですが…。

この記事は自分用のメモなので、かなりワケ分かんないと思います(^-^;

一個ずつpreg_matchしていた

リンク先のHTMLを取得したあと、OGPの情報を取得しています。

本当は「OpenGraph.php」とかを使うとラクなんだと思うのですが、「OGP無かったらTITLEタグからタイトル取得したり、最初のPタグから抜粋文取りたい」とか思ったのと、仕様変更とかあったら対応しないといけないので、なるべく自前でやりたいと思いました。

もちろん、脆弱性とか見つかったときに自前じゃなかったら勝手に対策してもらえるメリットとかもあるのですけど。

 

というか、正規表現とかまだ慣れていないので、マッチしたときに「楽しい!Σ(゚ロ゚)o゙」というのが、一番正直なところだったりします。

 

さて、今までは、「og:title」とか「og:description」とか、一個ずつ preg_match() で取得していました。

// OGPパース(タイトル)
if (preg_match('/.*<meta\s+(.*property\s*=\s*"og:title"\s*.*?)>/si', $html, $m)) {
    $param = $m[1];
    if (preg_match('/.*content\s*=\s*"(.*?)".*/si', $param, $m)) {
        $title = $m[1];
    }
} else {
    // OGPが無いときは、titleタグから取得
    if (preg_match('/.*<title\s*.*>\s*(.*)\s*<\/title>.*/si', $html, $m)) {
        $title = $m[1];
    }
}

// OGPの概要文
if (preg_match('/.*<meta\s+(.*property\s*=\s*"og:description"\s*.*?)>/si', $html, $m)) {
    $param = $m[1];
    if (preg_match('/.*content\s*=\s*"(.*?)".*/si', $param, $m)) {
        $excerpt = $m[1];
    }
} elseif (preg_match('/.*<meta\s+(.*name\s*=\s*"*description"*\s*.*?)>/si', $html, $m)) {
    $param = $m[1];
    if (preg_match('/.*content\s*=\s*"(.*?)"/i', $param, $m)) {
        $excerpt = $m[1];
    }
} else {
    // OGPが無いときは、最初の p要素を最初の段落として
    if (preg_match('/<p>\s*(.*)<\/p>/si', $html, $m)) {
        $excerpt = $m[1];
    }
}

一回「og:title」でマッチさせてから、「content」をマッチさせて…とやっているのは、<meta property=”og:title” content=”タイトルですよ!”> としているサイトと、<meta content=”タイトルですよ!” property=”og:title”> という感じで、contentが先に来ているサイトがあったから。

「出現順がどっちでもマッチさせる(どちらも含む)」表現だと、キーと値の順番が分かんなくなっちゃうので、og:title が含まれていたら、とりあえず metaタグの中身をドーンと持ってきて、content の値を持ってくる方法を取っていました。

 

get_meta_tags を使うと name と content の組み合わせを取得できるので description とかは取得できるのですが、OGP は property と content で指定されているので取得できないので、検索するしか無い気がします。

preg_match_all をつかってみた

まずは、取得してきた HTML からヘッダ( <head>~</head> の間)を抜き出します。

// HEADタグ
$head = null;
if (preg_match('/&lt;head&gt;(.*)&lt;\/head&gt;/si', $html, $m)) {
    $head = $m[1];
}

今度は、そこから preg_match_all() を使って、metaタグをドーンと取ってきて、name をキー、content を値にして、連想配列にしてみました。

// metaタグ パース
$match = null;
preg_match_all('/\s*meta\s*name="?([^&gt;"]*)"?\s*content="?([^&gt;"]*)"?\s*\/?&gt;/si', $head, $match);
if (isset($match) &amp;&amp; is_array($match) &amp;&amp; count($match) == 3) {
    for ($i=0, $to_i=count($match[1]); $i &lt; $to_i; $i++) {
        $tags[$match[1][$i]] = esc_html($match[2][$i]);
    }
}

空白が入ったり入らなかったりするところは、 \s* で「空白0個以上」。

ダブルクォートはあったり無かったりするところは、 “? で「ダブルクォート0個か1個」。

「ダブルクォート ” 」か「大なり > 」があるところまでを取りたいので、 [^>”]* で、 > と ” 以外 を 0個以上。

もうなんか、正規表現って書いてるとワケ分かんなくなってきますけど、マッチすると気持ちいいです。

慣れてる人なんかだとスラスラ読めちゃうんだと思うんですけど。

マッチしたらけっこうワケ分かんない感じで配列に入ってくるので、for でぶんまわして、うまいことキーと値を連想配列にしています。

ちなみに、「https://popozure.info/pz-hatenablogcard」から property と content の組み合わせの meta タグを取得した配列がこんな感じです。

Array
(
    [0] => Array
        (
            [0] => meta property="og:type" content="article" />
            [1] => meta property="og:title" content="WordPressで「はてなブログカード」を利用する Pz-HatenaBlogCard" />
            [2] => meta property="og:url" content="https://popozure.info/pz-hatenablogcard" />
            [3] => meta property="og:description" content="外部リンクを「はてなブログカード」に置き換えて表示させるWordPressプラグイン「Pz-HatenaBlo…" />
            [4] => meta property="article:published_time" content="2015-08-09T02:04:18+00:00" />
            [5] => meta property="article:modified_time" content="2015-09-26T18:42:27+00:00" />
            [6] => meta property="og:site_name" content="ぽぽづれ。" />
            [7] => meta property="og:image" content="http://mailbox.conohawing.com/popozure/blog/wp-content/uploads/2015/08/pz-hatenablogcard-thumbnail1.png" />
            [8] => meta property="og:image:width" content="279" />
            [9] => meta property="og:image:height" content="280" />
            [10] => meta property="og:locale" content="ja_JP" />
        )

    [1] => Array
        (
            [0] => og:type
            [1] => og:title
            [2] => og:url
            [3] => og:description
            [4] => article:published_time
            [5] => article:modified_time
            [6] => og:site_name
            [7] => og:image
            [8] => og:image:width
            [9] => og:image:height
            [10] => og:locale
        )

    [2] => Array
        (
            [0] => article
            [1] => WordPressで「はてなブログカード」を利用する Pz-HatenaBlogCard
            [2] => https://popozure.info/pz-hatenablogcard
            [3] => 外部リンクを「はてなブログカード」に置き換えて表示させるWordPressプラグイン「Pz-HatenaBlo…
            [4] => 2015-08-09T02:04:18+00:00
            [5] => 2015-09-26T18:42:27+00:00
            [6] => ぽぽづれ。
            [7] => http://mailbox.conohawing.com/popozure/blog/wp-content/uploads/2015/08/pz-hatenablogcard-thumbnail1.png
            [8] => 279
            [9] => 280
            [10] => ja_JP
        )

)

$match[1][1]が「og:title」、$match[2][1]が「WordPressで「はてなブログカード」を利用する Pz-HatenaBlogCard」となっている感じです。

count($match[1]) で、拾ってきた行数が分かるので、for でまわして、$match[1][$i] をキーに、$match[2][$i] を値にしている感じです。

 

このやり方は、PHPマニュアルサイトの get_meta_tags のノートを参考にしました。

あとは取得するだけ

TITLE タグの内容とかも、一緒に連想配列に放り込んでおきました。

// TITLEタグ
if (preg_match('/&lt;\s*title\s*.*&gt;\s*(.*)\s*&lt;\s*\/title\s*&gt;/si', $head, $m)) {
    $tags['title'] = esc_html($m[1]);
}

あとは、取り出すだけです。

// タイトル
if (isset($tags['og:title'])) {
    $title = $tags['og:title'];
} elseif (isset($tags['title'])) {
    $title = $tags['title'];
}

大分シンプルな感じになりました。

使わないタグもいちいち配列に放り込むので処理時間はちょっと増えているかも知れませんが、2回目の表示からはDBから読み込んじゃうので問題無いと思います。

ファイル分けて、ちゃんと関数にして自分用のライブラリっぽくすると良さそうだなぁ。

引用符がダブルクォートじゃなくてシングルクォートのとき

[2015/10/06追記]

引用符がダブルクォートのときと、引用符が無いときは考えていたのですが、「シングルクォート」のときがありました(^-^;

つまり、ダブルクォートで囲まれていると、

<meta property=”og:title” content=”OGPパースを!正規表現で!”>

これでマッチさせて、

/\s*meta\s*property=”?([^>”]*)”?\s*content=”?([^>”]*)”?\s*\/?>/si

こんな結果になるのですが、

$match[0] meta property=”og:title” content=”OGPパースを!正規表現で!”>
$match[1] og:title
$match[2] OGPパースを!正規表現で!

シングルクォートで囲まれていると、

<meta property=’og:title’ content=’OGPパースを!正規表現で!’>

こんな感じで「’」が付いちゃいます。

 

$match[0] meta property=’og:title’ content=’OGPパースを!正規表現で!’>
$match[1] ‘og:title’
$match[2] ‘OGPパースを!正規表現で!’

これもマッチさせようとすると、こんな感じの正規表現になって、

/\s*meta\s*name=[\’\”]?([^>\’\”]*)[\’\”]?\s*content=[\’\”]?([^>\’\”]*)[\’\”]?\s*\/?>/si

今度はシングルクォートでもダブルクォートでも拾えるようになるけど、

<meta property=”og:title” content=”サマーフェスティバル’98”>

みたいに、「ダブルクォートで囲まれているけど、シングルクォート(というか、アポストロフィー)が入っている」というパターンの場合、途中で区切られてしまいます。

こんな感じで、シングルクォートもダブルクォートも「食う」感じにしておいて、

/\s*meta\s*property\s*=\s*([^>]*)\s+content\s*=\s*([^>]*)\s*\/?>/si

前後のクォートをトリムする!Σ(゚ロ゚)o゙

$match[1][$i] = trim($match[1][$i], '\'\"' );
$match[2][$i] = trim($match[2][$i], '\'\"' );

trim関数は、先頭の1文字目と最後の1文字目の空白やタブを取り除く関数ですが、第二パラメータに取り除きたい文字を書いておくと、先頭の1文字目と最後の1文字目から外してくれるようです。

これで対応できるパターンが増えた気がします(^-^;

 

では(^-^)o

コメント

タイトルとURLをコピーしました