『7回目の出直し🌻』

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

はてなブログのAPIを使って、全ての記事を更新するだけのスクリプトを作りました

久しぶりのプログラミング。ないなら作る!
先日、書いた画像の仕組みが変わったことによる記事更新作業を自動化するスクリプトです。

何を作ったか、何を解決したか

はてなブログの(下書きを除く)公開中の全記事を何も変更せずに更新するスクリプトです。 実行すると、更新日が新しくなるだけです。記事データは何も変わりません。

なぜ作ったか

前回の記事にも書きましたが、はてなブログの画像タグの出力する仕組みが変わりました。その恩恵を受けるには、全部の記事を強制的に更新する必要がありました。

2つのブログ合わせて500記事以上もあり、手でポチポチするのがさすがに面倒だったため、プログラムで解決しました。

やっていること

はてなブログ公式が出しているAPI(はてなブログAtomPub)を使って、ブログのデータを操作します。
developer.hatena.ne.jp

実際にやっていることは、

  • 「ブログエントリの一覧取得」のAPIを使って、記事情報を10個ずつ取り出します。(マニュアルには7件って書いてあったけど10件返ってきた)
  • 10個の記事データにに対して、「ブログエントリの編集」のAPIを使って、記事を更新をしています。

これだけです。

まったく同じ情報を入れて更新しているので、記事の内容に変更はありません。記事のedittedだけが変わり、HTMLのコンテンツが再作成されます。

データ構造と仕組みの理解

ブログの詳細設定を確認

自分専用のAPIのURLとパスワードがありますので、まずは詳細設定を確認しておきます。 f:id:kanaxx43:20210724000140p:plain

黄色の部分がはてなID、青部分がブログID、赤の部分がAPIのキー(パスワード)です。

記事の一覧取得

まずは、記事の全量を知るために一覧をとります。

APIの接続先は、以下になります。
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry

上のURLはプログラムを使わなくてもブラウザでアクセスできます。
Basic認証のIDとパスワードの入力が必要です。実行するとこのようなXMLデータが返ってきます。

記事一覧の全体構造

記事一覧取得をすると、このようなデータが取得できます。

<?xml version="1.0" encoding="utf-8"?>
<feed 
  xmlns="http://www.w3.org/2005/Atom" 
  xmlns:app="http://www.w3.org/2007/app">
  <link rel="first" href="https://blog.hatena.ne.jp/kanaxx43/kanaxx43.hatenablog.com/atom/entry" />

  <!--※1  10個目以降の記事の一覧をとるときにnextのhrefが必要-->
  <link rel="next" href="https://blog.hatena.ne.jp/kanaxx43/kanaxx43.hatenablog.com/atom/entry?page=1604754648" />

  <title>kanaxx43’s diary</title>
  <link rel="alternate" href="https://kanaxx43.hatenablog.com/"/>
  <updated>2021-07-22T17:31:20+09:00</updated>
  <author>
    <name>kanaxx43</name>
  </author>
  <generator uri="https://blog.hatena.ne.jp/" version="3e5ec5d7e0f06cda0c488784bbd5d252">Hatena::Blog</generator>
  <id>hatenablog://blog/26006613586349043</id>
  
  <!--※2 1個目の記事-->
  <entry>
  (省略)
  </entry>

  <!--※2 2個目の記事-->
  <entry>
  (省略)
  </entry>

</feed>
  • 全体として大事なものは、※1のnextのURLです。
  • 1回目に取得したら新しい順に10個の記事情報が手に入ります。次の10件をとるためには1回目のAPI実行で戻ってきたnextのURLへ行く必要があります。
  • 2回目のAPI実行で11番目~20番目の記事情報+3回目のURLが戻ってくる。
  • 3回目のAPI実行で21番目~30番目までの記事情報+4回目のURLが戻ってくる。
  • nextがなくなればもうとるものが無いということです。

次は、記事のデータ<entry>で必要なものを押さえておきます。

記事一覧に含まれるentryの構造(複数出現するものの1つ分)

1つの記事につき、この塊が返ってきます。1つずつ読んでいきます。

<entry>
    <id>tag:blog.hatena.ne.jp,2013:blog-kanaxx43-26006613586349043-26006613789722237</id>

    <!--※3 更新するときのAPIの送り先-->
    <link rel="edit" href="https://blog.hatena.ne.jp/kanaxx43/kanaxx43.hatenablog.com/atom/entry/26006613789722237"/>

    <link rel="alternate" type="text/html" href="https://kanaxx43.hatenablog.com/entry/2021/07/23/151421"/>
    <author>
      <name>kanaxx43</name>
    </author>

    <!--※4 記事のタイトルは更新時に必要-->
    <title>ここに記事のタイトル</title>

    <!--※5 updatedの値を送らないと記事作成日時が変わってしまうので注意-->
    <updated>2021-07-23T15:14:21+09:00</updated>

    <!--この日付とサマリーは不要-->
    <published>2021-07-23T14:38:55+09:00</published>
    <app:edited>2021-07-23T15:14:21+09:00</app:edited>
    <summary type="text">xxx</summary>
    
    <!--※6 記事の本文は更新時に必要-->
    <content type="text/x-markdown">
    記事の本文
    </content>
    
    <!--ここも本文だが不要-->
    <hatena:formatted-content type="text/html" 
      xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">
    こっちも記事の本文
    </hatena:formatted-content>
    
    <!--※7 カテゴリは複数の場合もあるので、全部取る必要あり-->
    <category term="カテゴリ1" />
    <category term="カテゴリ2" />
    <category term="下書き" />
    
    <!--※8 下書きかどうか判断する値。yesのものは対象外とする-->
    <app:control>
      <app:draft>yes</app:draft>
    </app:control>
</entry>
  • ※3は、この記事をAPIで更新するときのURLです。ここに投げ込まないといけないので覚えておく必要があります。
  • ※4は、記事のタイトルです。必要なのでこれも読んでおきます。
  • ※5は、updated(更新日)です。これを正しく指定しないと、記事の作成日が変わってしまうので、もらったものをそのまま返さないといけません。
    ほかにpublishedとeditedの日付がありますが、これは記事更新をするのには不要です。
  • ※6は、記事本文。これを読み取って変更してAPIに投げると記事の内容を変更することができます。今回は「記事の内容は変えずに更新」をするので、読んだままを返します。
  • ※7は、記事のカテゴリ。これを読んで渡さないとカテゴリ無しの記事になってしまうので、複数個必ず読んでおきます。
  • ※8は、下書きかどうかの判断。yesのときは下書きなので更新しないようにします。

長いですが、一度わかってしまうと簡単です。

記事更新

記事を一覧で10個取得したら、記事ごとに更新処理を実行します。

接続先はここです。
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/{記事のID}

記事のIDが必要ですが、記事ごとにeditのURLがもらえるので、IDを意識せずにもらったURLへ接続します。(※3の部分)

記事一覧で取得した各データを、下の更新用のXMLにはめ込んでAPIを実行します。そうすると記事が更新できます。

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" 
  xmlns:app="http://www.w3.org/2007/app">

  <title>ここにタイトル</title>
  <updated>ここに更新日</updated>

  <category term="ここにカテゴリ1"></category>
  <category term="ここにカテゴリ2"></category>

  <content type="text/plain"><![CDATA[
  ここに本文
  ]]></content>

</entry>

実際に作ったもの

今回はPHPで100行ほど書きました。

使ったもの

今回使ったものは、これだけです。

  • PHP8
  • はてなブログAtomPub

PCが新しくなったので、プログラム環境もやっと新しくなりました。

PHP 8.0.8 (cli) (built: Jun 29 2021 16:02:52) ( ZTS Visual C++ 2019 x64 )
Copyright (c) The PHP Group
Zend Engine v4.0.8, Copyright (c) Zend Technologies

標準のライブラリだけ作った素のPHPスクリプトです。ソースコードを持ってくるだけで動きます。

コード

PHPのコードです。gitにもおいておきます。
https://github.com/kanaxx/hatenablog-omocha/blob/main/entry-update/hatena-entry-update.php

<?php
//set your information
$hatenaId = '';
$hatenaDomain = '';
$password = '';

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$hatenaId:$password");

$result = [];
$url = "https://blog.hatena.ne.jp/$hatenaId/$hatenaDomain/atom/entry";
$next = '';

do{
    echo "◆◆" . $url . PHP_EOL;
    $_ = getData($ch, $url);
    echo ''. $_['code'] . PHP_EOL;

    $xml = new SimpleXMLElement($_['result']);
    $ns = $xml->getNamespaces(true);
    
    $next = (string)getLinkHref($xml->link, 'next');
    echo 'next=' . $next . PHP_EOL;
    echo '--- entry ---' . PHP_EOL;

    foreach($xml->entry as $n=>$entry){
        $id =  (string)$entry->id;
        $atomurl = (string)getLinkHref($entry->link, 'edit');
        $title = (string)$entry->title;
        $content = (string)$entry->content;
        $updated = (string)$entry->updated;
        $categories = [];
        foreach($entry->category as $n=>$tag){
            $categories[] = (string)$tag['term'];
        }

        echo $title . PHP_EOL;
        echo $updated . PHP_EOL;
        echo $atomurl . PHP_EOL;
        
        $draft = (string)$entry->children("app", true)->control->draft;
        if($draft==='yes'){
            echo '[skip] this entry is DRAFT' . PHP_EOL;
            $cnt = $result['draft']??0;
            $result['draft']=$cnt+1;
            continue;
        }

        echo 'call update api' .PHP_EOL;
        $postResult = postDataToHatena($ch, $atomurl, $title, $content, $updated, $categories);
        $code = $postResult['code'];
        
        $cnt = $result[$code]??0;
        $result[$code]=$cnt+1;
        echo $code . '|' . $postResult['error'] .PHP_EOL.PHP_EOL;
    }
    if(!empty($next)){
        $url = $next;
    }
}while(!empty($next));

curl_close($ch);
echo '----' .PHP_EOL;
foreach($result as $code=>$cnt){
    echo "$code:$cnt" .PHP_EOL;
}
echo 'end of program.' . PHP_EOL;

//----------

function getLinkHref($links, $rel){
    foreach($links as $n=>$link){
        if($link['rel']==$rel){
            return $link['href'];
        }
    }
    return null;
}

function getData($curl, $url){
    curl_setopt($curl, CURLOPT_URL,$url);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($curl, CURLOPT_POST, false);

    $result = curl_exec($curl);
    $code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    return ['code'=>$code, 'result'=>$result];
}

function postDataToHatena($curl, $url, $title, $content, $updated, $categories){
    $categoryTag = '';
    foreach($categories as $n=>$name){
        $categoryTag .= "<category term=\"$name\"></category>";
    }

    $postxml = <<<EOD
<?xml version="1.0" encoding="utf-8"?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app">
<title>{$title}</title>
<updated>{$updated}</updated>
{$categoryTag}
<content type="text/plain"><![CDATA[ $content ]]></content>
</entry>
EOD;

    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $postxml);
    // curl_setopt($curl, CURLOPT_VERBOSE, 1);

    $result = curl_exec($curl);
    $code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    $error = curl_error($curl);
    
    return ['code'=>$code, 'result'=>$result, 'error'=>$error];
}

はまったところ

ドキュメントが少なかったので、この辺ははまりました。

<updated>のタグを入れないと、更新日がスクリプト実行日に変わってしまう。
更新日なのでプログラム実行日でいいかなと手抜きをしたら、全記事のブログの作成日も変わってしまいました。テスト時に気が付いてよかった。

<category>を送らないとカテゴリーがクリアされてしまう。
新しい<entry>にカテゴリが指定してない場合は、カテゴリを指定しないで更新したいという意味ですね。ここも手抜きせずにちゃんとやりました。

記事更新のAPIでPOSTを使ったら404が帰ってきて悩みました。
新規作成時がPOST、記事の更新時はPUTが正しいです。404じゃなくていいじゃん。1時間くらい無駄にしました。

記事更新で投げるXMLを作るロジックは、かなり手抜きです。まぁよい。

他に応用するアイデア

今、このブログには記事が雑多になってきて、特定のカテゴリの記事だけを別のブログに移そうと計画しています。 ブログから記事全体を取得してカテゴリーを確認していき、カテゴリが一致するときだけ引っ越し先のブログの新規登録のAPIを投げるように改造すると、簡単にコピーができそうです。

80個くらいあってどうやって引っ越ししようか考えていたのですが、これは応用できそう。

さいごに

今回は、PHPのプログラムの記事でした。前回見つけたはてなの仕様変更に対応すべく全記事の更新ボタンを押すクエストをやるのが嫌だったので、プログラムを書いてみました。 はてなのAPIは理解できたので、いろいろ自動化できそうです。

毎度のことですがいつもの注意書きです。
公開しているスクリプトを使って試していただくのは問題ありませんが、プログラムに何か問題があり記事が全部消えるようなことがあっても責任は負えません。実行前にバックアップするなど自衛&自己責任でお試しください。