こんにちは、某SNSに文章作品を投稿している系のエンジニアです。
書きたいものを書いているんだと言い張りながらも、投稿して公開する以上、自作品の閲覧数とかいいね数は気になります。
某SNSではログインしていると、そのアカウントで投稿した作品の閲覧数やいいね数が閲覧できます。
今回はそれらの情報を記録して、数値の変化を閲覧しやすいようにする、ための、下準備をしてみたいと思います。
データの蓄積にはmicroCMSを使いたいと思います。
すでにアカウントを持っていること、使用した経験があること(親戚の経営している店舗のお知らせ欄に組みこんだ)が理由です。
ベースとなるイメージ図
ということで、最初のイメージ図は以下のようになります。
蓄積したデータには、
- 自分だけが見られるようにしたい
- Webページとかで気軽に閲覧できるようにしたい
という要望がありますが、この部分は後でJavaScriptなりPHPなりで適当になんとかしたいと思います。
ここを悩むのは後です。
今は、図で言えば、「何か1」というモクモクを考えます。
microCMSはCMSで、人間がデータを入力しやすい仕組みになっています。
ですから、もちろん、「何か1」=「人間」にしてもいいわけです。
これなら、某SNSのデータを見ながら、microCMSの管理画面からコツコツデータ入力するだけで済みます。
でも、これはめんどくさい。
こういうめんどくさいことが嫌だから、わたしはプログラミングを始めたのです。
ということで、わたしの代わりをしてくれるプログラム(またはシステム)を開発することにします。
microCMSにはデータ入力をすることのできるAPIがあるため、プログラムを介する以上、これを使うことは自動的に決定します。
この入力するためのAPIをどこから呼び出すかが問題です。
わたしの細かな要望のひとつに、データ入力が自分のパソコンであればどこからでも実行できることがあります。
つまり、プログラム開発環境の整った母艦のデスクトップパソコンはもちろんのこと、ふつうにネットサーフィン用にしか使っていないノートパソコンからもデータ入力をしたい。
ということで、microCMS APIを叩くためのプログラム実行は、特別な開発環境なしでも行いたいです。
最初に思いついたのは、Webページを作成して、インターネット上へデプロイして、そこへアクセスし、そこから入力するという仕組みです。
でも、インターネット上でアクセス可能になるということは、それ相応のセキュリティ管理が必要となります。
それらを気にするのは嫌です。
めんどくさいです。
次に思いついたのは、GitHubを利用することです。
GitHub Actionsでプログラムを動かせることは知っていたため、これを利用してはどうだろうかと。
GitHub使うなら、どうせなら、生データもGitHubで管理したい。さらに「某SNSの作品データ掲載ページのHTMLファイルを保存して、それらをパースしてデータを抽出する」という仕組みもほぼ決まっているので、このファイルをGitHubのリポジトリへ集約しておけたらいいな。
ということは、
- GitHubのリポジトリへHTMLファイルをコミット
- それをトリガーとして、ファイルを処理しデータ抽出
- 最後にmicroCMSへデータを送信する
という感じを検討してみます。
これなら、
- 入力部分のセキュリティはGitHub任せ
- APIトークン等の情報だけ適切に管理すればよい
- 生データは後で再利用可能
- 使用するサービスはmicroCMSとGitHubだけに限定できる
という特徴になります。
なんとなくよさそうなので、この案をベースに開発を始めました。
全体像
HTMLページ保存処理
ここは手動で行います。
おそらく、自動で定期実行もできるのでしょうが、ログインが必要なページを保存しなければならない点、およびスクレイピングの制御に失敗した場合の暴走のリスクを考え、自分で明示的に操作して保存することにします。
手軽さを追及し、Chromeのブックマークレットへ以下を追加しました。
javascript: (function () {
const date = new Date().toLocaleDateString("ja-JP", {
year: "numeric", month: "2-digit", day:
"2-digit"
}).replaceAll('/', '');
const time = new Date().toLocaleTimeString("ja-JP", {
hour: "2-digit", minute:
"2-digit", second: "2-digit"
}).replaceAll(':', '');
const html = document.documentElement.outerHTML;
const blob = new
Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const filename = date + time + '.html';
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
})();
某SNSの作品データ掲載ページを開いた状態で、このブックマークレットを実行すると、現在のページのHTMLデータをYYYYMMDDHHMMSS.html
で保存するウィンドウが表示されます。
某SNSでは、この方法であれば入手したい形のHTMLファイルが保存できました。
おそらく、ページのデータ表示の仕方によって、保存されるHTMLファイルに違いが出ると思います。
今は、SPAとかSSGとか色んなページの表示の仕方がありますから、ここらへんは試行錯誤が必要なところだと思いますし、某SNSの表示方法が変わった場合は再度検討しなければならない点です。
ですが、今はこれでいきましょう。
コミット処理
続いては、コミット処理ですが、特別なことは一切なく、GitHubでリポジトリを作成し、HTMLファイルをコミットするだけです。
肝心なのは、このコミット処理をトリガーとするGitHub Actionsの設定です。
GitHub Actionsの設定
まずは完成形のGitHub Actionsのyamlファイルです。
仕組みは、
- コミットする時のメッセージに、HTMLファイル名を記載
- コミットしたファイル(複数OK)とコミットメッセージを照合
- 一致したファイル名を、データ処理を行うPythonプログラムへ渡す
となっています。
name: YYYYMMDDHHMMSS.htmlファイルを処理する
on:
push:
paths:
- "test/1/pi_raw/*.html"
jobs:
process-files:
runs-on: ubuntu-latest
steps:
# 1. リポジトリをチェックアウト
- name: Checkout repository
uses: actions/checkout@v3
# 2. Python環境をセットアップ
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
# 3. 必要なパッケージをインストール
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r test/1/requirements.txt
# 4. コミットメッセージを取得
- name: Get commit message
id: commit_message
run: |
commit_message=$(git log -1 --pretty=%B)
echo "commit_message=$commit_message" >> $GITHUB_ENV
# 5. 変更されたファイルをリストアップ
- name: Get list of changed files
id: changed_files
run: |
git fetch --depth 1 origin ${{ github.event.before }}
committed_files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }})
echo "committed_files<<EOF" >> $GITHUB_ENV
echo "$committed_files" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# 6. コミットメッセージのファイル名と一致する、変更されたファイル名を取得
- name: Match commit message with committed files
id: match_file
run: |
echo "committed_files=$committed_files"
matching_file=""
IFS=$'\n' # 改行で区切る
for file in $committed_files
do
if [[ "$file" == *"${{ env.commit_message }}" ]]; then
matching_file=$file
break
fi
done
echo "matching_file=$matching_file" >> $GITHUB_ENV
# 7. Pythonスクリプトを実行
- name: Run Python script
env: # Or as an environment variable
MICROCMS_PI_API_KEY: ${{ secrets.MICROCMS_PI_API_KEY }}
run: |
echo "Changed files: ${{ env.matching_file }}"
python test/1/test.py
# 6. 結果を出力 (オプション)
- name: Output results
run: echo "File processing completed."
GitHub Actionsに関する知識が一切ないため、ベースはChatGPTで作成し、そこから試行錯誤しました。
明示的に処理したいファイルを指定する形です。
今回の要件は、
- 1日1回などの気づいたタイミングで1ファイルだけ処理できればよい
- 処理するプログラム自体も同じリポジトリで管理するため、HTMLファイルと同時についでにコミットできればうれしい
であったため、この形に落ち着きました。
データ処理をするPythonファイルでは、microCMSのAPIキーが必要なため、GitHubのSecretsから取得しています。
次に、Pythonでの処理です。
Pythonでのデータ処理
このプログラムは大きく二段階に分かれます。
- HTMLファイルを解析して、データを取得
- データをmicroCMSへ送信
まずは全体像を提示します。
import requests
import os
from dotenv import load_dotenv
import json
from bs4 import BeautifulSoup
import re
from datetime import datetime
load_dotenv()
html_file_name = os.getenv("matching_file")
url_work_logs = "https://xxxxxx.microcms.io/api/v1/xxxxxx"
api_key = os.getenv("MICROCMS_PI_API_KEY")
headers = {"X-MICROCMS-API-KEY": api_key, "Content-Type": "application/json"}
file = open(html_file_name, "r", encoding="utf-8")
htmldata = file.read()
soup = BeautifulSoup(htmldata, "html.parser")
def microcms_post_work_logs(post_data):
res = requests.post(url_work_logs, data=json.dumps(post_data), headers=headers)
print(res.json())
def get_title():
ret_arr = []
arr = soup.find_all("a", href=re.compile("/novel/show.php"))
sorted_arr = []
for obj in arr:
m = re.match(r"^(?!.*comment).+$", obj["href"])
if m:
if obj.get_text():
sorted_arr.append(obj)
for obj in sorted_arr:
ret_arr.append(obj.get_text())
return ret_arr
def get_iine():
ret_arr = []
arr = soup.find_all("a", href=re.compile("section=rating"))
for obj in arr:
ret_arr.append(obj.get_text())
return ret_arr
def get_bookmark():
ret_arr = []
arr = soup.find_all("a", href=re.compile("bookmark_detail.php"))
for obj in arr:
ret_arr.append(obj.get_text())
return ret_arr
def get_view():
ret_arr = []
arr = soup.find_all("a", href=re.compile("section=access"))
for obj in arr:
ret_arr.append(obj.get_text())
return ret_arr
def get_comment():
ret_arr = []
arr = soup.find_all("a", href=re.compile("#comment"))
for obj in arr:
ret_arr.append(obj.get_text())
return ret_arr
def get_saveDate():
dt = html_file_name.split("/")[-1:][0].replace(".html", "")
dati = datetime.strptime( dt, '%Y%m%d%H%M%S')
return dati.isoformat()
titles = get_title()
iines = get_iine()
bookmarks = get_bookmark()
views = get_view()
comments = get_comment()
saveDate = get_saveDate()
# データの整合性を確認するため、リストの長さを照らし合わせる
if (
len(titles) != len(iines)
or len(titles) != len(bookmarks)
or len(titles) != len(views)
or len(titles) != len(comments)
):
exit()
for index, title in enumerate(titles):
new_obj = {}
new_obj["title"] = str(title)
new_obj["iine"] = int(iines[index])
new_obj["bookmark"] = int(bookmarks[index])
new_obj["view"] = int(views[index])
new_obj["comment"] = int(comments[index])
new_obj["saveDate"] = saveDate + "+09:00"
microcms_post_work_logs(new_obj)
Python初心者なので散らかったコードですが、これでmicroCMSへデータが記録されることを確認しました。
終わりに
今回は作品1件につき、データを1件投稿しているため、記録を繰り返していると無料プラン分のコンテンツ数をはみ出そうな予感もします。
これはもう少し改善が必要です。
また、蓄積したデータを活用する方法も考えないとならないため、フロントエンドやグラフ描画などを勉強していきたいと思います。
ここまで読んでいただきありがとうございました。