【JavaScript, PythonCGI】ホームページの自動更新,アクセス数をもとにしたオススメ投稿の選定, 記事一覧ページのページネーション化 の3本立てでお送りいたします。

最終更新日時を読み込み中です…

こんにちは。もう年末ですか。水野翔太です。

今年も、やることリストを完遂できずに、年を越してしまいそうです。

最後の悪あがきということで、ずっとできていなかったホームページの改修に手を付けようと思います。


はじめに


さて、前回のホームページ改修から、概ね半年が経ちました。

とりあえず、ホームページとして問題はなさそうですが、いくつか、当初から加えたい・変更したいと考えていた部分があります。

1つ目は最近の活動を表示する部分です。

トップページには、最近の活動を新しい順に表示する部分があります。

この表示欄、非常に面倒なんです。

というのも手動で更新してやる必要があり、新しく何か記事を投稿した場合には、いちいちトップページのHTMLを書き換え、アップロードして、という作業が必要なんです。

面倒なことは自動化しましょう。

さて、2つ目は最近の投稿欄です。

こちらも同じくトップページにあるやつです。

そして同じく手動更新をしています。面倒です。

3つ目はオススメの投稿欄です。

こちらも手動更新をしています。

自動化したいですが、何を基準にオススメと判断するのか、難しいところです。

4つ目は活動についてのページです。

活動についてのページでは、投稿した記事が一覧で表示されるようになっています。

ですが、すべてが一覧表示されるようになってしまっています。

これでは、将来的に読み込みに時間がかかるようになりそうな予感がしますね。

どのような表示方法にするかは、置いておいて少しずつ表示してやる方法に改修しましょう。


最近の活動を表示する部分


まず、どのように自動化するかですが、今回はPythonCGIを用いて、更新日順にファイルを取得できるプログラムを作成してみます。

流れとしては、

  1. トップページにアクセスされる
  2. サーバ上にある記事の更新日を取得する
  3. 更新日にしたがって日付順にソート
  4. トップページに更新日順にリンクを5つ表示する

さっそく、アクセスされる部分から作っていきましょう。

今回は、アクセスがあったらJavaScriptから通信を行い、サーバ上のPythonCGIを実行することにしようとおもいます。


function getRecentActivitiesInfo() {
    let url = CGIBIN + "get_recent_activities_info.py";
    let start = 0
    let length = 5
    $.ajax({
        contentType: 'application/json',
        data: JSON.stringify({
            start: start,
            end: start + length
        }),
        dataType: 'json',
        type: 'POST',
        url: url,
        success: function(result) {
            for (let ii = 0; ii < length; ii++) {
                $("#recent_activities_list_li_bottom").before(`<a href="${result.urls[ii]}"><li><budoux-ja>${result.filenames[ii]}</budoux-ja></li></a>`)
            }
            $("#recent_article").attr("src", result.urls[0])
            $("#go_iframe").attr("href", result.urls[0])

        },
        error: function(error) {
            console.log(error)
            $("#recent_activities_list_li_bottom").before(`<a href=""><li>エラー</li></a>`)

        }
    });
}
getRecentActivitiesInfo()
                        

JavaScriptはこんな感じです。これが、アクセスがあったときに実行されます。※jQuery使用

ajax通信で、サーバ上にあるPythonプログラムを実行しています。

いくつかデータをPOSTしています。これは、何件分の記事を取得するかを決めています。


#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import json
import glob
import os
import config
import re
import sys


def getChildrenDir(dir, basename="index.html"):
    res = glob.glob(f"{dir}/**")
    files = []
    for ele in res:
        ele = ele.replace("\\", "/")
        if os.path.isdir(ele):
            files.extend(getChildrenDir(ele))
            continue
        if os.path.basename(ele) == basename:
            files.append(ele)
            continue
    return files


def getTitle(file_path, pattern=".*<title>(.*)</title>.*", not_found="タイトルなし"):
    with open(file_path, "r", encoding="utf-8") as file:
        s = file.read()
    repattern = re.compile(pattern)
    match = repattern.match(s.replace("\n", ""))
    try:
        return match.group(1).replace("|水野翔太のホームページ", "")
    except:
        return not_found


def main():
    body = json.load(sys.stdin)
    start = body["start"]
    end =  body["end"] if body["end"] else None
    files = getChildrenDir(config.BLOG_DIR)
    files = sorted(files, key=lambda xx: os.path.getatime(xx), reverse=True)
    files = files[start:end]
    filenames = [getTitle(file) for file in files]
    urls = [file.replace(config.BLOG_DIR, config.BLOG_URL) for file in files]
    return {"filenames": filenames, "urls": urls}


if __name__ == "__main__":
    print("Access-Control-Allow-Headers: Origin, Content-Type\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS\r\nContent-Type: application/json\r\n")
    print(json.JSONEncoder().encode(main()))
                            
                        

こちらは、サーバ上のPythonプログラムです。

記事をすべて取得し、更新日順にソートし、URLの生成、タイトルの取得を行っています。

このプログラムによって記事のタイトルとURLを更新日順で取得でき、前述のJavaScriptに返却されます。

そして、jQueryで指定の場所へと追加、表示をしています。


最近の投稿欄


こちらも最近の活動の記事をサーバ上から取得する必要がありますね。

しかも、同じトップページにあるので、前述のプログラムが使えます。

というか、あのプログラムに最近の投稿欄を更新する処理も書いています。

19・20行目が、更新の処理です。単に、返却された結果のURLが入っているリストの先頭要素を用いているだけです。

いやぁ、一石二鳥。


オススメの投稿欄


さて、このオススメですが、問題なのはどんな基準でオススメを選定するか。

ひとまず、今回はアクセス数を計測し、最も多くのアクセスがあった記事をオススメとすることにしました。

直近7日間のアクセス数を計測、みたいな感じにしようと思っていましたが、ここは簡易実装。

総数で行きましょう。年末なので。

必要な機能としては、

  • 記事へのアクセス数の記録
  • 最もアクセスされた記事の取得

こちらも、JavaScript、Pythonで実装していきます。


function access_counter() {
    $.ajax({
        contentType: 'application/json',
        data: JSON.stringify({
            path: location.pathname.replace("index.html", "")
        }),
        dataType: 'json',
        type: 'POST',
        url: "https://www.48v.me/~mizunoshota/cgi-bin/homepage/access_counter/main.py",
        success: function(result) {
            // console.log(result)
        },
        error: function(error) {
            console.log(error)
        }
    });
}
access_counter()
                        

JavaScriptはこれだけ。アクセスされた記事を特定するために、記事のPATHを送信しています。

とくに、返却された値を利用する必要はなく、サーバ上でアクセス数を記録してもらっているだけです。


#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import sys
import log
import json
import config
from control_sqlite import ControlSqlite3


def increment(url):
    controller = ControlSqlite3()
    res = controller.fileCheck()
    if not res["success"]:
        return {"message": res["message"], "success": False}
    controller.connect()
    TABLE = config.ACCESS_COUNTER_TABLE
    res = controller.checkTableExsist(TABLE)
    if not res["success"]:
        sql = f"CREATE TABLE IF NOT EXISTS {TABLE}(url TEXT PRIMARY KEY, count INTEGER)"
        controller.cursor.execute(sql)
    try:
        sql = f"""INSERT INTO {TABLE} VALUES("{url}", "0");"""
        controller.cursor.execute(sql)
    except:
        sql = f"""UPDATE {TABLE} SET "count" = "count" + 1 WHERE "url" = "{url}";"""
        controller.cursor.execute(sql)
    controller.close()


def main():
    try:
        body = json.load(sys.stdin)
        path = body["path"]
        increment(path)
        return {"success": 1}
    except Exception as e:
        log.output(e)
        return {"success": 0}


if __name__ == "__main__":
    print("Access-Control-Allow-Headers: Origin, Content-Type\r\nAccess-Control-Allow-Origin: https://www.48v.me \r\nAccess-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS\r\nContent-Type: application/json\r\n")
    print(json.JSONEncoder().encode(main()))
                            
                        

アクセスされるPythonCGIです。

データの記録にはSQLiteのデータベースを用いています。

次は、記録されているアクセス数の取り出しです。


#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import json
import config
import sys
import log
import config
from control_sqlite import ControlSqlite3


def getMaxAccessCount():
    controller = ControlSqlite3()
    controller.connect()
    TABLE = config.ACCESS_COUNTER_TABLE
    sql = f"""SELECT "url","count" FROM {TABLE} WHERE "count" = (SELECT max("count") from {TABLE});"""
    res = controller.cursor.execute(sql).fetchall()[0]
    controller.close()
    return res

def main():
    try:
        url,count=getMaxAccessCount()
        return {"success": 1,"url":config.ORIGIN+url, "count": count}
    except Exception as e:
        log.output(e)
        return {"success": 0}


if __name__ == "__main__":
    print("Access-Control-Allow-Headers: Origin, Content-Type\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS\r\nContent-Type: application/json\r\n")
    print(json.JSONEncoder().encode(main()))
                        

ここへとアクセスすれば、最多アクセス数のページへのURLとアクセス数を取得できるのです。

あとは、JavaScriptでオススメ欄を更新してやるという感じですね。


活動についてのページ


投稿した記事が一覧表示されているのですが、これを少しずつ表示する方式にしたいという話です。

どんな方式が良いか。最近は「さらに表示する」みたいなボタンを押すと、どんどんと表示数を増やせるというモノが多そうです。

yahooニュース

でも個人的に、ブログのイメージといえば、ページ数が書いてあってページ番号を選んでという感じなんですよね。

ということで、今回はページ番号を選択する方式にしてみましょう。

すでに、更新日時順にURLを取得するプログラムはできています。前述の、「最近の活動を表示する部分」のPythonCGIですね。

なので、取得したものをうまいことページ切り替えできるようにすればよいのです。


function getActivities() {
    let url = CGIBIN + "get_recent_activities_info.py";
    let start = 0
    $.ajax({
        contentType: 'application/json',
        data: JSON.stringify({
            start: start,
            end: null
        }),
        dataType: 'json',
        type: 'POST',
        url: url,
        success: function(result) {
            let now_page = Number(getParam("p") ? getParam("p") : 1)
            let pages_length = Math.ceil(result.urls.length / SHOW_PAGES)
            if (now_page > 1) {
                $("#page_changer").append(` <a href = "${ACTIVITIES_URL}?p=${now_page-1}">前へ</a> `)
            }
            for (let ii = 1; ii < pages_length + 1; ii++) {
                if (now_page === ii) {
                    $("#page_changer").append(` <b>${ii}</b> `)
                } else {
                    $("#page_changer").append(` <a href = "${ACTIVITIES_URL}?p=${ii}">${ii}</a> `)
                }
            }
            if (now_page < pages_length) {
                $("#page_changer").append(` <a href = "${ACTIVITIES_URL}?p=${now_page+1}">次へ</a> `)
            }
            let start_index = now_page * 5 - 5
            let end_index = 0
            for (let ii = start_index; ii < start_index + SHOW_PAGES; ii++) {
                if (ii < result.urls.length) {
                    $("#articls_head").after(`<div class="wrapIframe"><iframe class="contentsIframe" scrolling="no" src="${result.urls[ii]}"></iframe><div class="moreRead"><a class="goIframe" href="${result.urls[ii]}"><div class="moreButton">もっと見る</div></a></div></div>`)
                    end_index = ii
                }
            }
            $("#page_changer").append(`<div>${start_index+1}~${end_index+1}件表示 (${result.urls.length}件中)</div>`)
            $("#loading").remove()
            if (now_page > 1) {
                $(".activityList")[0].scrollIntoView()
            }
        },
        error: function(error) {
            console.log(error)
        }
    });
}
getActivities()
                        

こちらはJavaScript。ページ番号がパラメータとして付与されるようになっています。それを取得し、5件ずつ表示するようになっています。

scrollIntoView()関数は、指定した要素の位置までWEBページをスクロールしてくれます。

はじめはURLフラグメントを使っていましたが、JavaScriptの実行が終わった後にページのスクロールをしてほしかったため、プログラムからの制御に切り替えました。


function getParam(name, url = window.location.href) {
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}
                        

こちらは、getParam()関数です。ほとんど 参考サイトのままですが、便利なのでどうぞ。


おまけ


この半年で、「Bootstrap」という超便利フレームワークを知ってしまいました。

このホームページを作るときには、名前を知っている程度でしたが、実際使ってみると、あまりにも簡単にレスポンシブ対応が可能であったりなど、すべて手書きでCSS・HTMLを書いていたのが、信じられなくなるほどでした。

どうやら、スライドショーなんかも簡単に作れるようなんですよ。

ついでということで、トップページをスライドショーにしてみましょう。

そうかこんな簡単に作れるのか。

Carousel (カルーセル) · Bootstrap v5.0

いや。

ここまで「Bootstrap」なしで頑張って作ってきたのに、いまさら使ってもいいのか?

ダメだろ!

契約悪魔が「Bootstrap」の方には申し訳ないのですが、今回は自作することで理解を深めようと思います。


function slideshow(images = shuffle(TOP_IMGS), ii = 0) {
    $("#top_img").attr("src", TOP_IMGS_RELATIVE_PATH + images[ii % images.length])
    $("#top_img").hide().fadeIn(1000)
    ii++
    setTimeout(() => { $("#top_img_background").attr("src", TOP_IMGS_RELATIVE_PATH + images[ii % images.length]) }, 1500)
    setTimeout(() => { $("#top_img").fadeOut(1000) }, 2000)
    setTimeout(slideshow, 5000, images, ii)
}
setTimeout(slideshow, 12000)
                        

CSSなどは省略しますが、こんな感じのJavaScriptでスライドショーを実装してみました。

imagesには、使用する画像のファイル名をリストで与えています。

仕組みとしては、2枚の画像を重ねて表示させておき、フェードアウトとフェードインで入れ替えを行うという感じです。

入れ替えは setTimeoutでうまいこと、タイミング調整しています。


おわりに


今回は、ホームページを改修してみました。

皆さんもどうですか、改修したくなりましたねホームページ。年末ですからぜひ。


参考


このサイトを作った人


水野翔太

福知山公立大学情報学部情報学科
2024年卒業予定

やまもとよしのふゼミ所属 3回生

連絡先:
32045088[at]fukuchiyama.ac.jp
(@マークに置き換えてご利用ください)