『7回目の出直し🌻』

好きなことを自分のペースで、のんびり更新

はてなブログのカテゴリーページのURLも、Google Indexing APIにpublishする

sitemap_indexファイルとsitemapファイルに載っているURLについては、URL単位でIndexing APIを投げることはできました。 心残りなのは、カテゴリページが対象にできていないことですね。

カテゴリページも対象にするためにプログラムを作りました。もちろん自動化します

カテゴリページとは

カテゴリの記事がまとまって、リスティング(一覧化)されているページです。

blogdomain/archive/category/カテゴリ名
というURLのパターンで作られるページです。URLの最後の部分が変えるだけで、カテゴリのページに行きつきます。

例えばここです(サイトマップのカテゴリ一覧)
https://kanaxx.hatenablog.jp/archive/category/sitemap

カテゴリページ自体の文字情報は多くないので、カテゴリページがインデックスされることやキャッシュが新鮮になることで、どれだけいいことがあるかは分かりませんが、データは新しいほうが良いですよね。

カテゴリページへのクローリング実績を見る

サイドバーからリンクをしているので、ブログ内のほぼ全てのページから内部リンクしている状態です。クローラーがどのページを見に来たとしても、ページ内のリンクとして認識される可能性はかなり高く、それなりの頻度でクローリングしてくれていると思っていました。


しかーし、Search Consoleで日付を見て愕然としました。
2020年4月30日に来たっきり、立ち寄ってないんです。2か月前だなんて。。

自然に来るのを待ってても、そんなに来ないのか、、、
それなら、呼び込んでみましょうか。

過去に作ったIndexingAPIのプログラムを使って、呼び込んでみます!

自分のブログのカテゴリを知る方法

過去に作ったサイトマップを読み込んで記事URLをGoogle Indexing APIに投げ込むPHPのプログラムの改造版です。Indexing APIに投げ込むところまでは実装済みなので、カテゴリーページのURLが分かれば簡単です。そう、URLさえ分かれば。。。

カテゴリページのURLを機械的に探す方法を見つけないといけません。

はてなブログAtomPub APIを使う

自分のブログ内に存在しているカテゴリの一覧を知るにはどうしたらいいか? カテゴリの一覧は増えたり減ったりするので、できればプログラマティックに解決したいですね。

いろいろ探し回ってみましたが、Hatena Developer Centerにある「はてなブログAtomPub」というAPIを使えば、ブログ内のカテゴリが取れそうです。

うちのブログの場合、
https://blog.hatena.ne.jp/kanaxx43/kanaxx.hatenablog.jp/atom/category  というURLです。

ということで、これで試してみます

認証を確認

はてなブログAtomPubは、記事投稿もできるものなので認証がかかっています。ドキュメントを見ると OAuth 認証WSSE認証Basic認証の種類に対応していると書いてありました。今回は、一番簡単なBasic認証で行くことにします。

Basic認証についてはユーザ名としてはてなIDを、パスワードとしてAPIキーを利用することで認証できます。Basic認証はAPIのURLがhttpsではじまる場合にのみ利用できます。

とあるので、自分の認証情報を調べておきます。

はてなIDは、
はてなにログインするときのIDですね。僕の場合はkanaxx43の部分です。

APIキーは、
ブログのサイドメニュー「設定」>「詳細設定」タブの最後のほうにある「AtomPub」に書いてあります。


ためしにブラウザでAPIを叩いてみましょう。

認証ダイアログが表示されるので、IDとパスワードがを入れます

認証をパスすれば、XMLファイルがダウンロードできます。このような形式です

<?xml version="1.0" encoding="utf-8"?>
<app:categories
    xmlns:app="http://www.w3.org/2007/app"
    xmlns:atom="http://www.w3.org/2005/Atom"
    fixed="no">
  
  <atom:category term="Cloudinary" />
  <atom:category term="PROなのに" />
  <atom:category term="Screenpresso" />
  <atom:category term="SEO対策" />
  <atom:category term="sitemap" />
  <atom:category term="Windows" />
  <atom:category term="お買いもの" />
  <atom:category term="はてなブログ" />
  <atom:category term="アドセンス" />
  <atom:category term="アフィリエイト" />
  <atom:category term="サイト構築" />
  <atom:category term="テレワーク" />
  <atom:category term="ネットショッピング" />
  <atom:category term="プログラミング" />
  <atom:category term="作業効率UP" />
  <atom:category term="便利機能" />
  <atom:category term="初期設定" />
  <atom:category term="日本語化" />
  <atom:category term="未分類" />
  <atom:category term="画像編集" />
  <atom:category term="雑記" />
</app:categories>

これを、プログラムからHTTPSでデータを取りに行き、読みほどけばよさそうです。

事前の準備と確認はここまで。

プログラムを作る

さて、プログラムを作っていきます。

前回の記事で使った環境を引き続き使っているので、合わせて読んでください。変更箇所を中心に書いておきます。 kanaxx.hatenablog.jp

プログラムの流れ

プログラムの流れを簡単に説明します。

  • atomPubのAPIにアクセスし、全カテゴリ名が入ったxmlデータを取り出す
  • xmlデータを読みほどいて、カテゴリ名(日本語)一覧を作る
  • カテゴリ一覧をループして、カテゴリページURLを組み立てる
  • Google Indexing APIへpublish

これだけです

コード

80%くらいは前回のプログラムのコピーです。仕事でやったら怒られそうなコードですけど、今日のところは動けばいいです。
GuzzleというHTTP Clientのライブラリを使えば、Basic認証もらくらく突破です。

publish_category_url.php

<?php
//認証用のファイル
$credentialFile = './credential.json';

//制限事項 // https://developers.google.com/search/apis/indexing-api/v3/quota-pricing
//1分以内に60回以上投げてはいけないので間隔をあける
$intervalSecondsPerAPI = 1;
//1日で投げられるAPIの上限
$limitPublishPerDay = 200;

require_once './vendor/autoload.php';

//コマンドラインパラメータ
$hatenaId= $argv[1]??'';
$blogDomain = $argv[2]??'';
$apiKey = $argv[3]??'';

if(empty($hatenaId) || empty($apiKey) || empty($blogDomain)){
    echo "command line parameters are wrong." .PHP_EOL;
    echo "publish_category_url.php <hatenaId> <blogDomain without https://> <atom API key>". PHP_EOL;
    exit(1);
}

$atomEndpoint = "https://blog.hatena.ne.jp/$hatenaId/$blogDomain/atom/category";

//カテゴリの一覧
$categories = [];
$options = ['exceptions' => false,'debug' => false, 'auth' => [$hatenaId, $apiKey]];
$http = new GuzzleHttp\Client($options);
$response = $http->request('GET', $atomEndpoint);
$status = $response->getStatusCode();
if(200 != $status){
    echo "cant get category data from $atomEndpoint" . PHP_EOL;
    exit(1);
}

$body = $response->getBody()->getContents();
$xml = new SimpleXMLElement($body);
foreach($xml->children("atom", true) as $x){
    $cat = (string)($x->attributes()['term']??'');
    if(!empty($cat)){
        $categories[] = $cat;
    }
}

echo '=== Category ====' . PHP_EOL;
echo count($categories)   . PHP_EOL;
echo '============' . PHP_EOL;

//Indexing API
$client = new Google_Client();
$client->setAuthConfig($credentialFile);
$client->addScope(Google_Service_Indexing::INDEXING);
$httpClient = $client->authorize();
$endpoint = 'https://indexing.googleapis.com/v3/urlNotifications:publish';

$results = [];
foreach($categories as $n=>$cat){
    if($limitPublishPerDay <= $n){
        break;
    }
    $param = [];
    $param['type'] = 'URL_UPDATED';//'URL_NOTIFICATION_TYPE_UNSPECIFIED';
    $param['url'] = "https://$blogDomain/archive/category/" . urlencode($cat);
    
    $response = $httpClient->post($endpoint, ['json' => $param]);
    $body = $response->getBody()->getContents();
    $json = json_decode($body, true);
    
    $status = $response->getStatusCode();
    $results[$status] = ($results[$status]??0) + 1;

    if($status==200){
        $time = toJST($json["urlNotificationMetadata"]["latestUpdate"]["notifyTime"]);
        echo $status . ':' . $response->getReasonPhrase() . '|' . $param['url'] . '|'.$time .PHP_EOL;
    }else{
        $message = $json['error']['message']??'-';
        echo $status . ':' . $response->getReasonPhrase() . '|' . $param['url'] . '|-|' .$message .PHP_EOL;
    }

    sleep($intervalSecondsPerAPI);
}

echo '=== Result ===' . PHP_EOL;
foreach($results as $status=>$count){
    echo $status . ':' . $count . PHP_EOL;
}
echo '==============' . PHP_EOL;


//Google APIのタイムスタンプがnano秒まであるので正規表現で削り取る
function toJST($datetime){
    $p = '/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})\.[0-9]{9}Z/';
    if( preg_match($p, $datetime, $_)){
        $datetime = "$_[1]-$_[2]-$_[3]T$_[4]:$_[5]:$_[6]Z";
    }
    $t = new DateTime($datetime);
    $t->setTimeZone(new DateTimeZone('Asia/Tokyo'));
    return $t->format('Y-m-d H:i:s');
}

function startsWith($haystack, $needle){
     $length = strlen($needle);
     return (substr($haystack, 0, $length) === $needle);
}

コードはこちらに置いておきます
https://github.com/kanaxx/hatenablog-indexing/blob/master/publish_category_url.php

一番苦労したところは、
PHPのSimpleXMLElementを使って、ネームスペース付きのタグの属性値を取るところです。var_dumpをやってもXMLのデータが丸見えにならないので、構造を読み取るのとデータを引っ張り出すのが意外と面倒でした😛

動作確認

今回は、動作に必要な情報はコマンドラインパラメータから引き受ける形にしたので、使うときにPHPの編集は不要です。

php publish_category_url.php kanaxx43 kanaxx.hatenablog.jp password

第一引数に、はてなID
第二引数に、ブログのドメイン(httpsはいらない)
第三引数に、AtomPubのパスワード

動かすと、だらだらとログがでて終わります。

php publish_category_url.php kanaxx43 kanaxx.hatenablog.jp password
=== Category ====
21
============
200:OK|https://kanaxx.hatenablog.jp/archive/category/Cloudinary|2020-07-18 00:03:33
200:OK|https://kanaxx.hatenablog.jp/archive/category/PRO%E3%81%AA%E3%81%AE%E3%81%AB|2020-07-18 00:03:34
200:OK|https://kanaxx.hatenablog.jp/archive/category/Screenpresso|2020-07-18 00:03:36
200:OK|https://kanaxx.hatenablog.jp/archive/category/sitemap|2020-07-18 00:03:38

(省略)

=== Result ===
200:21
==============

sitemapカテゴリのpublishは、2020-07-18 00:03:38に送りました。

実行後のクローラ確認

1時間くらい放置していればくるかなと思ってましたが、2020/07/18 0:04:01 にクローラが来たみたいです。サーチコンソールの最終クロール日が変わりました。まじかい、早いな!

最後に自動化の設定

Herokuにコミットして、Heroku Schedulerにスケジューラに登録しておきます。 Indexing APIは1アカウントあたり1日200回と決まっているので、カテゴリのアップデートは1日1回にしておきましょう。

(手順は省略します)

まとめ

何もしないと2か月放置されていたページに、クローラーを呼び込んだら1分後に来てました。今回はたまたま1分くらいで来てくれましたけど、いままでの実績だとだいたい1時間以内では来てくれています。

そのうち来るだろうとGoogle任せにするのではなく、来てほしいと要求したほうがよさそうですね。

参考資料

Hatena Developer Center - はてなブログAtomPub
http://developer.hatena.ne.jp/ja/documents/blog/apis/atom

PHPでXMLの名前空間つきタグを読み込む色々
https://blog.mach3.jp/2010/12/14/various-xml-on-php.html