【素人プログラマー】Blueskyへのブログ過去記事投稿をレンタルサーバーで自動化してみた

自動投稿サムネ 日記

ブログ以外に、SNSでも活動しています。そして、様々なSNSで様々な方と繋がれています。せっかくなので、SNSのフォロワーさんにもブログをアピールしていきたいです。

2024年2月に、これまで紹介制だった短文SNS「Bluesky」が一般開放されました。SNSの新興勢力ということで、新規ユーザー同士の交流が特に盛んに行われている印象です。気付けば僕のアカウントのフォロワー数も増えてきました。しかも、InstagramやThreadsとはまた違った層のユーザーさんと繋がれています。

Blueskyのフォロワーさんにもブログをアピールしたいところですが、ワードプレスからブログ過去記事をBlueskyのタイムラインに投稿できるプラグインは僕の知る限りではまだリリースされていません。

そこで、ブログの過去記事へのリンクをBlueskyに半自動的に投稿するPythonスクリプトを書いてみた… というのが先日の記事です。

結果、決まった時間にブログの過去記事へのリンクをランダムに投稿することには成功したのですが、スクリプトを走らせている間ずっとパソコンを点けっぱなしにしておかなければならないというあまりにも大きすぎる弱点が露呈したのでした。

ならば「ずっと点けっぱなしになっているパソコンからスクリプトを走らせれば良いのではないか」という発想の転換で、今回は、ブログ用に借りているレンタルサーバー(エックスサーバー)からBlueskyにポストを自動投稿する環境を構築してみました!

0.モチベーション

・サーバー上から完全に自動で、Blueskyにブログ過去記事へのリンクを含んだポストを自動投稿したい。

以前のスクリプトでは、ポストの投稿日を手動で設定したり、投稿したい過去記事のリンクを含んだCSVファイルを用意したりする必要がありました。特に後者は、ブログ記事の更新を繰り返していき「過去記事」がどんどん増えていくことを考えると、都度内容を手動で更新していくのがあまりに大変です。

そこで、投稿したい過去記事のリンクの取得から自動化することにしました。また、投稿日も手動で設定するのではなく、スクリプトを走らせた当日に投稿されるように、スクリプトを書き直しました。

1.サーバーへのFTP接続準備

自宅のパソコンからエックスサーバーにFTP接続するための準備を整えます(環境はWindows11です)。

秘密鍵生成

エックスサーバーSSH

まずはエックスサーバーのサーバーパネルにアクセスし「アカウント」の「SSH設定」より、SSHをONにします。

エックスサーバー秘密鍵

続いて、任意のパスフレーズを入力し、秘密鍵ファイルを生成・ダウンロードします。

秘密鍵ファイルを.ppk形式に変換

サーバーへのFTP接続に使用可能なターミナルソフトは様々ですが、今回はPuTTYを使用しました。

puttygen

先ほどダウンロードした秘密鍵ファイルをPuTTYにて使用可能な.ppk形式に変換するため、PuTTYgenを起動します。「Load an existing private key file」の「Load」ボタンを押し、秘密鍵ファイルを選択すると、秘密鍵ファイル生成時に入力したパスフレーズを求められるので、それを入力します。

その後「Save the generated key」の「Save private key」ボタンを押すと.ppk形式の秘密鍵を保存できます。

FTP接続

PuTTYを起動し「Connection」⇒「Auth」⇒「Credentials」と開きます。

putty2

一番上の「Browse」をクリックし.ppk形式の秘密鍵ファイルをロードします。

putty1

「Session」に戻り「Host Name」に(サーバーID).xsrv.jpと入力します(サーバーIDは各自のもの)。ポートはデフォルトでは22と入っていますが、そこを10022とします。

putty3

「Open」をクリックすると、黒い画面のターミナルが起動します。

login as: にサーバーIDを入力しエンターを押すと、今度は秘密鍵のパスフレーズを求められます。パスフレーズを入力して再びエンターを押すと、ここでようやくサーバーを操作できるようになるわけです。

なお、エックスサーバーの仕様上、入力を間違えすぎると一定時間アクセスが拒絶されターミナルを開けなくなるので気を付けてください(1敗)

2.サーバー上にPython環境構築

サーバー上で自動投稿用Pythonスクリプトを動かせる環境を構築します。

Anaconda(Miniconda)インストール

エックスサーバーにはデフォルトでPythonが入っていますが、バージョンは3.6です。そして、BlueskyのAPIであるatprotoパッケージを動かすにはPython3.7.1以降のバージョンが必須です。

しかしながら、ユーザーはエックスサーバーにroot権限でアクセスできないため、何らかの手段で別のPythonを入れる必要があるわけです。

そこで、今回はAnacondaの環境を作ることにします(MinicondaでもOK)。

wgetコマンドでAnacondaインストーラーをダウンロードし、インストーラーのシェルスクリプトを読み込んでインストールします。

wget https://repo.anaconda.com/archive/Anaconda3-2024.02-1-Linux-x86_64.sh
sh ./Anaconda3-2024.02-1-Linux-x86_64.sh

必要パッケージのインストール

仮想環境を構築し、そこに必要なパッケージをインストールします。

conda create -n (環境名)

conda activate (環境名)

conda install pandas
conda install numpy
conda install BeautifulSoup4
conda install requests
conda install pytz

するとここで一つ問題が発生します。肝心要のatprotoパッケージがcondaからインストールできないのです!!pipを使う必要があります。

「condaで環境を作った場合、pipのパッケージと混在すると環境が壊れる原因になる」と耳にタコができるほどに言い聞かされてきたので抵抗はありますが、今回の環境においてcondaで入れたパッケージと、これからpipで入れるatprotoとの間に依存関係はないので、幸い問題なくpipを使ってatprotoを入れることができます。

というわけで…

pip install atproto

ここまでできたら環境構築は完了です。

3.スクリプト

スクリプトは前回の記事にて紹介したものを一部改変しています。

#パッケージ読み込み
from bs4 import BeautifulSoup
import requests
import io
from atproto import Client
from atproto import models
from atproto import client_utils
import datetime
import time
import pytz
import pandas as pd
import numpy as np


#サイトマップよりURL取得
r = requests.get("http://サイトマップ.xml")
text=r.text
d = []

for line in text.split("\n"):
    if "<loc>" in line:
        p = line.strip()
        p = p.replace("<loc>","")
        p = p.replace("</loc>","")
        d.append(p)

d = np.array(d)


#Blueskyにログイン
client = Client()
client.login('ユーザー名.bsky.social', 'パスワード')


#リンクカード取得関数定義
def get_title_and_description(url:str):
    title : str = ''
    description : str = ''
    thumbnail : str = ''

    response = None
    try:
        # requestsで対象のURLに対してGET
        response = requests.get(url, timeout=60)
    except:
        return '', '', ''
    if response.status_code != 200:
        return '', '', ''

    # responseに含まれるテキストデータを、HTMLパーサで処理
    soup = BeautifulSoup(response.text, 'html.parser')
    # titleタグ内のテキストを取得
    result = soup.find('title')
    if result != None:
        title = result.text
    # metaタグdescription内のテキストを取得
    result = soup.find('meta', attrs={'name': 'description'})
    if result != None:
        description = result.get('content')
    # metaからサムネイルを取得
    result = soup.find('meta', attrs={'name': 'thumbnail'})
    if result != None:
        thumbnail = result.get('content')

    # タプルでまとめて返す
    return title, description, thumbnail


#サイトマップurlからランダムで記事を15つ取得
rng = np.random.default_rng()
link_selected = d[rng.choice(len(d), 15, replace = False)]


#自動ポスト時刻設定
posttime = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00",
            "14:00", "15:00", "16:00", "17:00", "18:00", "19:00",
            "20:00", "21:00", "22:00"]


#現在時刻取得
day = datetime.datetime.now(pytz.timezone("Asia/Tokyo")).date()
day = str(day)


#自動ポスト
for i in range(15):
    #タイトル等取得
    title, description, thumbnail = get_title_and_description(link_selected[i])

    #画像アップロード(画像をバイナリに変換しアップロード)
    response = requests.get(thumbnail)
    img = io.BytesIO(response.content)
    upload = client.com.atproto.repo.upload_blob(img)

    #リンクカード作成
    embed_external = models.AppBskyEmbedExternal.Main(
        external = models.AppBskyEmbedExternal.External(
            title = title,
            description = description,
            thumb = upload.blob,
            uri = link_selected[i]
        )
    )

    post = client_utils.TextBuilder().text('【ブログ過去記事】\n【自動投稿】\n\nたまには過去記事をどうぞ!').tag('#ブログ', 'ブログ')

    #投稿
    start_time = datetime.datetime.strptime(day + posttime[i] + "+0900", '%Y-%m-%d%H:%M%z')
    time_second = (start_time - datetime.datetime.now(pytz.timezone('Asia/Tokyo')))
    if time_second.days < 0:
        client.send_post(post, embed = embed_external)
    else:
        time.sleep(time_second.seconds)
        client.send_post(post, embed = embed_external)

具体的には、記事URLをサイトマップから直接取得するようにしたり、datetime.nowを加工してポスト投稿予定時刻を指定したり、ポストの本文を全て同じ文章にしたりしています(投稿一つ一つに対して別々の本文を投稿するのはあまりにカロリーが高すぎました)。

このスクリプトを「bsky_autopost.py」というファイル名で保存します。エックスサーバーのファイルマネージャー等で、スクリプトをサーバー上に保存するのをくれぐれもお忘れなく!(1敗)

4.Cronでスクリプトを定期実行する

上記のスクリプトをサーバー上で定期的に自動実行するにはCronという機能を使います。

エックスサーバーcron

サーバーパネルの「アカウント」⇒「Cron設定」より、定期的に実行するプログラムを追加できます。

分、時間、日、月、曜日単位で設定ができます。「毎時」や「毎日」実行したい場合は、時間や日のところに「*」を入力します。今回は分に30、時間に7、それ以外に*を入力したことで「毎日朝7時30分にコマンドを実行する」という設定になっています。

コマンドのところには、指定したい時間に実行するコマンドを入力します。

#!/bin/bash
source /home/サーバーID/anaconda3/etc/profile.d/conda.sh
conda activate (環境名)

python3 /home/サーバーID/bsky_autopost.py

今回は上記のシェルスクリプトを「autopost.sh」という名前で保存し、それをCronで定期実行させています。このシェルスクリプトは仮想環境の読み込みと、先ほどの自動投稿用Pythonスクリプト「bsky_autopost.py」の実行の両方を行うスクリプトとなっています。

うまくいけば、毎日朝7時30分に「autopost.sh」が実行されて「bsky_autopost.py」が読み込まれ、朝8時から夜10時までの間、毎時0分にブログ過去記事へのリンクを含むポストがBlueskyに自動投稿されるわけです。うまく動くことを祈りながら放置することしばし…

bluesky投稿

ポストの自動投稿を無事に確認することができたのでした!

5.おわりに

これでブログ過去記事へのリンクを自動投稿できる環境が完全に整いました。もちろんパソコンを点けっぱなしにせずに済みます(笑)

記事の投稿に連動して自動で更新されるサイトマップからURLを読み込んでいるのがポイントで、こうすることで新しい記事の投稿にも対応してくれますし、大昔に投稿した「更新休止のお知らせ」みたいな告知系記事が投稿されるのも防げます(その手の記事はサイトマップに載せていません)。

ブログ過去記事を定期的にBlueskyにポストするようになってからは、改めてブロガーとして認知してもらえるようになりましたし、Blueskyからブログへアクセスされる機会も今度こそ大幅に増えました。

ブロガーにとって過去記事は財産です。上手に告知して、アクセス増加に繋げていきましょう!

6.参考リンク

コメント

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