見出し画像

私は、noteのブログに関して、内容を要約したり、傾向を分析したり、関連する質問に答えたりできるAIボットを作成しました。どのようにして実現したのでしょうか?

みなさん、こんにちは!AIに関する面白いトピックを探すために、1〜2時間ほどnoteを眺めていたら、「ん〜、いいねが多いブログの情報を抽出して、要約してくれるAIボットを作ったら面白くない?」って思ったんです。そしたら、すぐに取りかかっちゃいました=))))。
このブログでは、そのAIボットを一から作る方法をステップごとに解説していきます!

以下は、私のボットが(直近20件のブログをチェックした後に)返答したいくつかの結果です。

いいねが一番多いブログはどれですか?
いいねが一番多いブログの内容を要約してください。
どんなテーマが一番多くのいいねを集めるでしょうか?

よし、始めよう!

AIボットはどのように動作するのか?

本質的には、このAIボットはウェブスクレイピング自然言語処理(NLP)、そしてOpenAIのGPTのような強力な言語モデルを組み合わせることで、ブログの内容を理解し、対話することができます。

  • ステップ1:ウェブスクレイピングによるデータ収集

  • ステップ2:データの整備と構造化

  • ステップ3:言語モデルによる質問応答

ウェブスクレイピングによるデータ収集

まず、ボットはウェブスクレイピング技術を使ってブログのデータを収集します。具体的には、プログラムでブログページにアクセスし、タイトル、著者、公開日、本文などの必要な情報を抽出します。
SeleniumBeautifulSoupを使えば、動的なページの読み込みと、必要なテキストの解析が可能です。
この処理を行うために、data_scrapper.pyというファイルを作成して、noteからのデータ収集を担当させます。

まず初めに、ウェブブラウザを自動操作し、HTMLコンテンツを解析するために必要なPythonライブラリをインポートします。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import time

Chromeをヘッドレスモードで実行するように設定します。ヘッドレスモードでは、ブラウザの画面(GUI)を表示せずに操作できるため、サーバー環境CIパイプラインで自動化スクリプトを実行する際に便利です。

# Chromeをヘッドレスモードで起動するための設定
options = Options()
options.add_argument('--headless')  # ヘッドレスモードを有効化(画面なしでブラウザを操作)
options.add_argument('--disable-gpu')  # GPUを無効化(ヘッドレスでの安定動作のため)
options.add_argument('--no-sandbox')  # サンドボックス機能を無効化(Linux環境で必要な場合あり)
options.add_argument('--disable-software-rasterizer')  # ソフトウェアラスタライザーを無効化
options.add_argument('--disable-dev-shm-usage')  # /dev/shmの使用を無効化(メモリ不足回避用)
options.add_argument('--disable-3d-apis')  # 3D APIを無効化
options.add_argument('--disable-webgl')  # WebGLを無効化
options.add_argument('--disable-features=VizDisplayCompositor')  # VizDisplayCompositor機能を無効化

設定したオプションを使って、Chromeブラウザを起動します。

driver = webdriver.Chrome(options=options)

これにより、Seleniumによって制御されるヘッドレスモードのChromeブラウザが起動します。

次に、基準となるURLと、スクレイピングしたい特定のページを定義します。

root = r"https://note.com"
link = r"https://note.com/interests"

各ブログ記事の詳細な内容を取得するために、ブログのURLへ移動し、ページソースを取得して本文の主要部分を解析する関数を定義します。この関数は、完全なURLと相対リンクの両方に対応しており、関連する段落や見出しを抽出して、ブログの読みやすいテキスト要約を作成します。

# ブログの詳細コンテンツを取得する関数
def get_detail_content_from_blog(ブログのリンク):
    # フルURLでなければ、ルートURLを付けてアクセス
    if "https://note" not in ブログのリンク:
        driver.get(根 + ブログのリンク)
    else:
        driver.get(ブログのリンク)

    html = driver.page_source
    スクレイピング = BeautifulSoup(html, "html.parser")

    # 本文が含まれるdiv要素(クラス名note-common-styles__textnote-body)を取得
    セクションセット = スクレイピング.select_one(".note-common-styles__textnote-body")

    if セクションセット:
        # 本文中の<p>, <h2>~<h6>タグをすべて取得
        セクション = セクションセット.find_all(['p', 'h2', 'h3', 'h4', 'h5', 'h6'])
        
        # 各タグのテキストを改行で繋げて1つの文字列にまとめる
        テキスト = "\n".join([tag.get_text(strip=True) for tag in セクション])
        return テキスト
    else:
        # 本文が見つからなかった場合は空文字を返す
        return ""

指定したカテゴリーのブログデータを特定のウェブサイトからスクレイピングする関数です。Seleniumドライバーを使用してページにアクセスし、最大20件のブログエントリを抽出します。 各ブログから、公開時間、いいね数、著者、リンク、タイトル、コンテンツといった情報を取得します。 ブログ記事の詳細コンテンツは、補助関数 `get_detail_content_from_blog()` を使って取得されます。指定されたカテゴリーが存在しない場合や、スクレイピングに失敗した場合は、None を返します。

# 指定した分野のブログデータをスクレイピングする関数
def DataScrapper(分野):
    driver.get(リンク + "/" + 分野)
    
    time.sleep(3)  # ページ読み込みのため3秒待機

    html = driver.page_source

    try:
        スクレイピング = BeautifulSoup(html, "html.parser")

        # 特定のdiv要素(クラスhidden sm:grid gap-4)を取得
        セクションセット = スクレイピング.select_one("div.hidden.sm\\:grid.gap-4")

        # その中の最初の20個の<section>タグを抽出
        セクション = セクションセット.find_all('section')[:20] if セクションセット else []

        ブログセット = []

        for i, sec in enumerate(セクション):
            
            # print(f"Record {i}")
            ブログ  = {
                "公開時間": "",
                "いいね": "",
                "著者": "",
                "リンク": "",
                "タイトル": "",
                "コンテンツ": ""
            }

            # 公開時間を取得
            ブログ["公開時間"] = sec.find('time').get_text(strip=True)
            # いいね数を取得
            ブログ["いいね"] = sec.find('span', class_='pl-2 text-sm text-text-secondary').get_text(strip=True)
            # 著者名を取得
            ブログ["著者"] = sec.find('span', class_='o-noteItem__userText').get_text(strip=True)
            # 記事へのリンクを取得
            ブログ["リンク"] = sec.find('a')['href']
            # 記事タイトルを取得
            ブログ["タイトル"] = sec.find('h3', class_='m-noteBodyTitle__title').get_text(strip=True)

            # 記事の詳細コンテンツを取得する関数を呼び出す
            ブログ["コンテンツ"] = get_detail_content_from_blog(ブログ["リンク"])
            
            ブログセット.append(ブログ)
        
        print("データをスクレイピングしました。")

        return ブログセット
    except:
        print("この分野は見つかりません。")
        return None

データの整備と構造化

main.py を作成して、ユーザーが分野(興味のあるカテゴリ)とクエリ(質問)を入力できるようにします。最初に、DataScrapper 関数を使って、指定された分野に関連するブログデータをスクレイピングします。その後、無限ループに入り、ユーザーのクエリを繰り返し受け取り、LLM_Response 関数にクエリとブログデータを渡して回答を生成します。生成された答えはコンソールに表示され、ユーザーは新しい質問を続けて入力することができます。また、デバッグ目的で、ブログデータ全体を整形された JSON 形式で出力できるコメントアウトされた行も用意されています。

from data_scrapper import DataScrapper
from llm_response import LLM_Response
import json

# ユーザーに分野名を入力してもらう
分野 =  input("分野を入力してください: ")
# ユーザーに質問を入力してもらう
くえり = input("くえりを入力してください: ")

# 指定された分野のブログデータをスクレイピングして取得
ブログセット = DataScrapper(分野)

# 無限ループで質問に対する回答を繰り返し表示
while True:
    # LLMに質問とブログデータを渡して回答を取得
    respone = LLM_Response(くえり, ブログセット)
    # 回答を表示
    print("--------------------------------------------")
    print("答え:")
    print(respone)
    print("--------------------------------------------")

    # 次の質問をユーザーから入力してもらう
    くえり = input("くえりを入力してください: ")    

# ブログデータセットをJSON形式で整形して表示(デバッグ用)
# print(json.dumps(ブログセット, ensure_ascii=False, indent=2))

言語モデルによる質問応答

この関数は、指定されたプロンプトを使って OpenAI の API を呼び出し、応答を生成するための関数です。OpenAI クライアントの chat.completions.create メソッドを使用しています。プロンプトは2つのメッセージで構成されており、1つ目はアシスタントの役割を定義する「システムメッセージ」(この場合は大量のデータセットに関する質問に答えられる賢いアシスタント)、2つ目はユーザーからの実際の質問を含む「ユーザーメッセージ」です。

使用されるモデルは gpt-4o-mini で、応答は中程度の創造性(temperature=0.7)をもって生成されます。API 呼び出しが成功すれば、アシスタントの応答がプレーンテキストで返されます。一方、API 呼び出し時にエラーが発生した場合は、エラーメッセージが返されます。

from openai import OpenAI

client = OpenAI(api_key='YOUR_API_KEY')

# OpenAIのAPIを呼び出して応答を生成する関数
def generate_openai_response(システムプロンプト, model="gpt-4o-mini", max_tokens=1500):
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                # システムメッセージ(AIの役割を指定)
                {"role": "system", "content": "あなたは、大量のデータセットに関する質問に答えることができる賢いアシスタントです。"},
                # ユーザーメッセージ(質問内容)
                {"role": "user", "content": システムプロンプト}
            ],
            #max_tokens=max_tokens,  # 最大トークン数(必要に応じて使用)
            temperature=0.7,  # 創造性の度合い(0.7はややランダムな応答)
        )
        # 応答テキストを返す
        return response.choices[0].message.content.strip()
    except Exception as e:
        # API呼び出しでエラーが発生した場合の処理
        return f"Lỗi khi gọi API OpenAI: {e}"

この関数は、システムプロンプト(アシスタントの役割やできることの説明)とユーザーの質問(くえり)、そして提供されたブログデータ(ブログセット)を組み合わせて、完全なプロンプトを構築します。プロンプトの中では、以下のようなアシスタントの能力が定義されています:

  • 「いいね」が最も多いブログを特定する

  • 特定のブログ記事の内容を要約する

  • 複数のブログの傾向や比較に関する質問に答える

  • プレーンテキスト形式のブログデータを正確に解釈する

また、このプロンプトでは「提供されたブログデータの情報のみに基づいて回答するように」と明示的に指示されています。構築されたプロンプトは generate_openai_response() 関数に渡され、モデルからの適切な回答が取得されて返されます。

# ブログデータセットとユーザーの質問から、OpenAIに質問を投げて回答を得る関数
def LLM_Response(くえり, ブログセット):
    システムプロンプト = (
        f"""
        あなたは、ブログのリストを分析し、それに関する質問に答えることができる有能なアシスタントです。各ブログには、タイトル、内容、著者、いいね数、公開日時、リンクなどの情報が含まれています。

        あなたができること:
        - 最も多くの「いいね」がついているブログを特定すること
        - 特定のブログの内容を要約すること
        - ブログの中に見られる傾向や比較、詳細についての質問に答えること
        - プレーンテキスト形式で提供されたブログデータを正しく解釈すること

        会話の中で提供されたブログデータの情報のみに基づいて回答してください。


        **ブログデータセット**:
        {ブログセット}

        **ユーザーからの質問**:
        {くえり}

        """
    )
    # OpenAI API呼び出し関数にプロンプトを渡して応答を取得
    return generate_openai_response(システムプロンプト)

やったね!チャットボット完成おめでとう!🎉

こちらのコードを参考にしてください:https://github.com/MinhHung7/Note_blog_retrieval_chatbotAI

いいなと思ったら応援しよう!