B L O G

【WordPress】投稿内の専門用語をリアルタイムで解説してくれる機能を作る【PHP】

最終更新日:

BableTechではコンピュータにまつわるあらゆる記事を投稿しているのですが、なんせ専門用語が多いです。それを記事内でいちいち解説するのも結構苦なものですから、自動的に専門用語を検出してそれを解説してくれるシステムとか作れないかなと考えましたので、早速作ってみようと思います。

システム構想

完全に思い付きでやろうとしているのですが、ある程度頭の中で筋道はたったので紹介していきます。

用語辞書

まず用語についてですが、他のサイト等からある程度情報系の用語の解説をスクレイピングして辞書に登録みたいな手もあるのですが、個人利用のスクレイピングはともかく、ブログでは解説として閲覧者に提供しつつ収益を得るといったスタイルになってしまい良くないので、辞書は自分で登録することにします。 あと、自動的に解説を取得する仕様にしてしまうとどれほど細かい用語まで辞書に登録すればいいのかの見極めがつかなくて、最悪解説だらけの見にくいサイトになってしまうので、選択的に用語を解説するためにもそこは手動でいきます。

用語の自動検出

専門用語の辞書ができたところで、投稿内に専門用語が出てきたタイミングで解説できないと使い物になりません。そこで、ページがロードされるたびに投稿の内容と辞書を照合して一致するものがあったらspanタグ等で囲ってマーキングをつけるというJavaScriptを使った手もあるのですが、一度ブラウザに辞書情報をすべてロードしないといけなくて負担がかかってしまうので、決められた時間にPHPで全投稿を取得して用語があるかどうか検査して、あったらそのHTMLを書き換えてあらかじめマーキングをつけておくという作戦にします。

用語の解説

そしてマーキングタグの属性に用語のIDかなんかを持たせておけば、その用語の部分を表示しているときにデータベースに問い合わせて解説を表示するJavaScriptのプログラムを作っていい感じにシステムを実現することができるでしょう。まだフワっとしている構想段階ですが、早速やってみようと思います。

早速実装してみる

それでは早速実装してみたいと思います。先ほど紹介したシステム構想のように段階を踏んで実装していきます。

用語辞書の作成

まずは用語辞書の作成です。これに関してはもはや力作業ですね。ただ、実は前からBableTechでも似たようなことをしていて、予め用語の解説をデータベースに登録しておいて、投稿の編集画面からショートコードで任意のタイミングで呼び出せるという機能を作っていました。

↑こういうやつですね。好きなタイミングで手軽に解説を挿入できるのでこれはこれで良いのですが、普通にショートコードを入力する手間すら惜しいってのと、本文中にこのくそでかい解説フィールドが表示されたら読むときに邪魔でしょうがないってのと、だいぶ昔に作ったやつ(高校生の時)で、データベースの構造が粗悪なものになっているので、作り直したいと思います。 ただ、すでに登録されている用語が100個くらいはあるので、一旦はこの辞書をそのまま活用したいと思います。

とりあえず適当にMySQLでテーブルを作ってみました。一意のIDと用語名、用語の説明、詳細説明をしている投稿のIDといった感じの属性を持っています。

投稿本文から用語を検出

続いて、既存の投稿を取得して、そこから用語を検出していきます。前回、関連記事を取得するシステムを作った時はPythonを使って、直接WordPressのデータベースから記事情報を取得しましたが、

【機械学習】WordPressでより高精度な関連記事を表示させたい【Python】

今回は(多分)相性が良いのでPHPを活用したいと思います。WordPressの関数を使うことも考えましたが、今回もWordPressのデータベースを直接いじりたいと思います。

<?php

//データベースに接続
$db = mysqli_connect("WordPressデータベース");
$term_db = mysqli_connect("用語データベース");

//投稿を取得するクエリ
$query = "SELECT ID,post_content FROM wp_posts WHERE post_type = 'post' AND post_status = 'publish'";

if($result = mysqli_query($db,$query)){
    foreach($result as $row){
        $post_id = $row["ID"];
        $content = $row["post_content"];

        $search = '/<span class="term" data-id="\d*">(.*?)<\/span>/';;
        $replace = '$1';
        
        //マーキングをつける前の状態に戻す
        while(preg_match($search,$content)){
            $content = preg_replace($search,$replace,$content);
        }

        //用語一覧を取得していく
        $query = "SELECT id,name FROM terms";
        if($term_list = mysqli_query($term_db,$query)){
            
            //それぞれの用語について
            foreach($term_list as $each_term){
                $name = $each_term["name"];
                $term_id = $each_term["id"];

                $term_search = "/<p>(.*?){$name}(.*?)<\/p>/";
                $term_replace = '<p>$1<span style="color:red;font-weight:bold;" class="term" data-id="'.$term_id.'">'.$name.'</span>$2</p>';
                $content = preg_replace($term_search,$term_replace,$content);
            }
        }

        echo $content;

    }
}


?>

こんな感じで、投稿のHTMLから既存のマーキングを一旦削除して、そのあとに辞書に登録されている単語が本文中のpタグの中に存在していらそこにマーキングをつけると言う様な処理をしています。 今、検証のためにマーキングのところのフォントを太字赤にして確認したのですが、ちゃんとできていました。

そしてこれをデータベースに再度格納するという流れですね。失敗したら全部の記事がおじゃんになってしまうって考えたら… ぞっとしますね。

//エスケープの処理
$content = mysqli_real_escape_string($db,$content);
$query = "UPDATE wp_posts SET post_content = '{$content}' WHERE ID = {$post_id}";
$result = mysqli_query($db,$query);

色々テストをした上で、意を決してすべての投稿のマーキングをつけてみました。そして実際に投稿を確認しつつ、Chromeの開発者ツールでマーキングがついているところを赤文字に変えてみたら、ちゃんとできていました。

赤いところを見てみると、用語辞書に登録したっぽいやつなので大丈夫そうですね。

解説を表示

ここからは主にJavaScriptの出番です。HTMLコード上にマーキングをつけることができたので、今度はその用語が表示されたときに自動的に横に解説を出すという処理を施していきます。

【jQuery】スクロールに追尾してくれる便利な目次を作る【WordPress】

この記事でスクロールに追従する目次を作った時と同じような感じにして、右側に用語説明フィールドを設置しました。

//現在一番近い用語を取得する
            var current_term_index = 0;

            term_list.each(function(index){//それぞれの用語について
                const this_y = jQuery(this).offset().top; //この用語のy座標
                if(standard_y > this_y) current_term_index = index; //用語を取得
            });

            const term_id = term_list.eq(current_term_index).data("id"); //用語IDを取得
            const term_name = term_list.eq(current_term_index).text(); //用語を取得
            term_list.css({background:'transparent'});//一度すべて背景を透明にする
            term_list.eq(current_term_index).css({backgroundColor:'#ff000040'});//現在の用語の背景を赤色にする

            if(term_field.find("h4").text() != term_name){

                term_field.find("h4").text(term_name);

                jQuery.ajax({
                    async: true,
                    type: "POST",
                    url: "PHPファイル",
                    data: {
                      "mode": "get_term_desc",
                      "id": term_id
                    }
                  }).done(function (data) {
                    data = JSON.parse(data);

                    const desc = data["desc"];

                    term_field.find("p").text(desc);
                  });

            }

後はこんな感じの処理をスクロール関数に記述しつつ、Ajax非同期通信でPHPファイルにPOSTして(インジェクション対策をしつつ) 用語データベースから用語解説を取得してそれを表示させれば完成ですね。

現在説明している用語に赤いマークをしてくれる仕様にしました。正直、いちいち解説を取得するためにPOST通信を行うくらいならspanタグ自体の属性の中(data-descとか)に説明文を埋め込んでしまうとか、それかそもそもWordPressデータベースを編集するという危険なことをしないでページが読み込まれたタイミングでJavaScriptで本文をスキャンして用語を検出させるという感じにしても良かった気がしますね。 あと、用語データベース自体の構造が結構シンプルなものなので、RDBにするのではなくてJSONファイルとかにして、それをJavaScriptで読み込む感じにしても良かったかなと。

というかセオリー的には、HTMLレンダリングを行うときにfunction.phpでフィルターをかけて用語の部分にマーキングをつけるというのが標準的かと思いますね。でも自分的に謎にfunction.phpでごちゃごちゃ処理をしたくないというのがありまして、データベースを直接書き換えるという奇行に走ってしまいました。

とりあえず作っちゃったので、しばらくはこれを使っていきたいと思います。

あと、スマホやタブレットで表示しているときはこの解説は表示されないようになっているので、本文中の用語の部分をタップしたらその解説がポップアップしてくるというシステムを作ろうと思います。

あと、次の課題として、同じ名前なのに意味が複数ある用語についてどのような対処するのかというものがありますね。右側に出てくる解説が間違っていたら良くないですし、正確に意味を伝えるためにも、名前空間という考え方が必要そうです。プログラミングなんかでよく耳にする言葉ですが、簡単に説明すると違うカテゴリなのに同じ名前のものがあったとしてそれらの衝突を防ぐために、最初に「今回はどのカテゴリについて述べるのか」という名前空間というものを定義することで、曖昧さを回避することができます。そこら辺はWikipediaとかでもうまくやっていますよね。 ただ、記事内にCPU(ハードウェア)みたいに記載するわけにもいかないので、投稿のカテゴリから名前空間を把握するという作戦があります。それはそれで落とし穴があったりはするのですがね… まぁそもそも曖昧にならないように記事内でしっかり説明しつつ、同じ名前の用語は登録しないという方針で固めていけばなんとかなりそうです。 用語名が被ってしまうほどたくさん用語を登録してしまったらそもそも記事が見づらくなってしまいますしね。


関連記事

    人気記事

    じゅんき
    10月頃BableTechが生まれ変わる予定です!

    記事内用語

    詳細ページ