Python

【Python】自分専用のクローラーを作ろう【AI活用】

「Webスクレイピングツールを自分でゼロから構築したい」こうしたニーズが近年急増しています。Webスクレイピングツールは、インターネットの情報を収集・保存・分析に役立ちます。そのWebスクレイピングツールで、重要な役目を持つのが「Webクローラー」です。

本記事ではプログラミング初心者の方でも、かんたんに作れるWebクローラーの構築方法について解説します。実際に構築をしなくても、Webクローラーの仕組みを理解することは、ITリテラシーを高める上でとても大切です。ぜひ参考にしてみてください。

Webクローラーとは?

Webクローラーとは、インターネットからWebサイトのコンテンツをダウンロードし、それらをインデックス付けすることによって動作するインターネットボットです。Webクローラーの目的は、Webサイト全体に存在するWebページの内容を把握し、必要なときに必要な情報だけを取得することです。

例えるならば、書籍が整理されていない図書館で、すべての本を調べてカードカタログにまとめるような役割を持ちます。カードカタログがあれば、図書館に訪れた人は目的の書籍を容易に見つけられるようになります。

実際のWebクローラーの多くが、検索エンジンによって運営されています。検索エンジンは、Webクローラーによって収集されたデータに独自の検索アルゴリズムを適用することで、ユーザーの検索クエリ(キーワード)に応じた関連リンクや検索結果一覧を生成します。

Webクローラーで何が実現できるのか?

Webクローラーを構築すると次のようなことを実現できます。

1.コンテンツ集約(コンテンツアグリゲーション)
さまざまなリソースからの情報を1つのプラットフォームに集約します。そのため、人気のあるWebサイトをスクレイピングして、プラットフォームの内容を充実させる必要があります。

2.感情分析(センチメント分析)
感情分析とは製品とサービスに対するユーザーの態度や感情を分析することです。分析を行うためには、関連するメディアのレビュー、コメントを抽出する必要があります。

3.見込み客獲得
見込み客を獲得するには、従来であれば積極的に展示会やセミナーなどに参加することや、飛び込み営業などで名刺交換を行う必要がありました。入手した名刺情報・顧客情報からアプローチを行い、将来的に顧客化へとつなげていきます。しかし、こうしたアナログなやり方をしなくとも、Webクローラーであれば、人気のWebサイト(リクナビ・食べログ)や、iタウンページなどから企業情報を自動で収集できます。

Webクローラーをゼロから構築する方法とは?

Webクローラーをゼロから構築する方法には大きく2つあります。どちらの手法を用いるかは、現在のプログラミングスキルや、Webスクレイピングの目的・用途にもよりますが、どちらも知っておくことで、状況に合わせた使い分けができるようになります。

プログラミング言語を使い自分で実装

プログラミング言語を使ってWebクローラーを構築したいと思っている非プログラマーにとって、PHP、Java、C / C ++などの取得は容易ではありません。比較的簡単なプログラミング言語として人気なのが、「Python」です。Pythonの文法は、かんたんな英語がわかる方であれば、理解しやすいのが特徴です。

今回は「python」を使って実装します。

Webスクレイピングツールを活用する

数ヶ月間のプログラミング学習をする時間がなかったり、複雑なコード記述をせずに手っ取り早くWebクローラーを構築したい方であれば、Webスクレイピングツールの活用が最適です。

たとえば、Webスクレイピングツールの「Octoparse(オクトパス)」はコード記述を一切行わないノーコードツールです。独自のエディタ画面から、ドラッグ&ドロップでの直感的な操作だけで、任意のWebサイトから欲しい情報を自動取得を実現します。

さらにOctoparseには数十種類のテンプレートが備わっているため、そのテンプレートを選択するだけで、立ち上げから数分でWebスクレイピングを実行します。

テンプレートは、食べログ、リクナビ、Amazon、Indeed、Instagram、Twitter、YouTubeなど国内でも人気のあるサイトをカバーしています。

Pythonで実装

今回作るクローラーは普通にスクレイピングするだけでは芸がないので、以下の要件のものを作ってみたいと思います。

  • 検索ワードを引数に渡す
  • カレントディレクトリに「検索ワード」をフォルダ名としてディレクトリを作成
  • googleの検索結果、上位5件のデータ取得
  • 各ページの要約をそれぞれテキストファイルにまとめて、先に作成したフォルダに保存

私個人として何かを調べる際に、上位5件くらいのページを別タブで開いておき、後で読む事が非常に多く、それの代替となる機能を持ったアプリケーションがあると良いな、と考えていたので上位の様な機能を持ったアプリケーションを実装してみる事にしました。
カスタマイズ性などを考慮して実装するので、皆さんが実際に使用する際にはカスタマイズしてご利用ください。

プログラムの流れ

次のような手順で作成します。

ディレクトリの作成:

  • osモジュールを使用して、カレントディレクトリに検索ワードを名前とするディレクトリを作成します。

Google検索の実行:

  • requestsモジュールやbeautifulsoup4を使用してGoogle検索を実行し、上位5件のURLを取得します。
  • 注意: Googleの検索結果のスクレイピングはGoogleの利用規約に反する可能性があるため、googlesearch-pythonなどの公式APIを利用することをお勧めします。

ページ内容の要約:

  • 各URLに対してページの内容を取得し、beautifulsoup4を使用してテキストを抽出します。
  • テキストの要約には、sumygensimのようなライブラリを使用します。

要約の保存:

  • 要約されたテキストをそれぞれ別のテキストファイルとして保存します。

必要なモジュールは下記コマンドを実行してインストールして下さい。

pip install tkinter
pip install requests
pip install beautifulsoup4
pip install google
pip install sumy
pip install fpdf
pip install gensim
  • requests: Webページの内容を取得するためのライブラリです。
  • beautifulsoup4: HTMLやXMLを解析するためのライブラリです。
  • google: Google検索結果を取得するためのライブラリです。
  • python-sumy: テキストの要約を行うライブラリです。
  • fpdf: PDFファイルを生成するためのライブラリです。

基本機能のみ実装

import os
import requests
from bs4 import BeautifulSoup
from googlesearch import search
from gensim.summarization import summarize

def create_directory(search_word):
    if not os.path.exists(search_word):
        os.makedirs(search_word)
    return search_word

def get_google_search_results(query, num_results=5):
    return list(search(query, num_results=num_results))

def fetch_page_content(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    return soup.get_text()

def save_summary_to_file(directory, url, content):
    summary = summarize(content, word_count=100)  # 100 words summary
    filename = os.path.join(directory, f"{url.split('//')[-1].replace('/', '_')}.txt")
    with open(filename, 'w', encoding='utf-8') as file:
        file.write(summary)

def main():
    search_word = input("Enter the search word: ")
    directory = create_directory(search_word)
    
    search_results = get_google_search_results(search_word)
    
    for url in search_results:
        content = fetch_page_content(url)
        save_summary_to_file(directory, url, content)

if __name__ == "__main__":
    main()
  • create_directory: 検索ワードをディレクトリ名として作成します。
  • get_google_search_results: Google検索の上位5件のURLを取得します。
  • fetch_page_content: 各URLからページのテキスト内容を取得します。
  • save_summary_to_file: 要約されたテキストをファイルに保存します。

ファイルを保存してターミナルから実行すると、上記機能を実現できます。
ただ、これだと非常に使いにくいので、次の様な機能を実装します。

検索結果のフィルタリング機能:

  • ユーザーが検索結果の中から除外したいキーワードやドメインを指定し、フィルタリングできるようにします。

複数の検索ワードの同時処理:

  • 一度に複数の検索ワードを入力し、それぞれの結果を別々のフォルダに保存できるようにします。

保存ファイルのプレビュー機能:

  • 検索結果ファイルをリストで選択したときに、別ウィンドウでファイル内容をプレビュー表示する機能を追加します。

検索結果のランキング表示:

  • 検索結果をユーザーが評価できるようにし、ランキングに基づいて並べ替えたり、表示順序を変えられるようにします。

要約のカスタマイズ:

  • ユーザーが要約の長さ(単語数)を調整できるようにし、要約を自分好みにカスタマイズ可能にします。

PDF出力機能:

  • 要約結果をPDFとして保存できる機能を追加します。

自動更新機能:

  • 定期的に同じキーワードで検索し、結果を更新する機能。新しい情報がある場合に通知します。

また、GUIの方が直感的に操作可能なので、GUIも実装します。

GUIやその他機能を付加する

先に全てのコードを載せます。
次の項で解説を行います。

import os
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, scrolledtext, Listbox, SINGLE, Toplevel
import requests
from bs4 import BeautifulSoup
from googlesearch import search
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lsa import LsaSummarizer
from fpdf import FPDF
import threading
import time

class SummaryApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Web Page Summary App")
        self.create_widgets()
        self.excluded_keywords = []
        self.excluded_domains = []
        self.summary_word_count = 100
        self.ratings = {}
        self.auto_update_interval = 60 * 60 * 24  # デフォルト: 24時間
        self.auto_update_threads = {}

    def create_widgets(self):
        # 検索ワード入力
        self.search_label = tk.Label(self.root, text="検索ワード(カンマ区切り):")
        self.search_label.grid(row=0, column=0, padx=10, pady=10)
        self.search_entry = tk.Entry(self.root, width=40)
        self.search_entry.grid(row=0, column=1, padx=10, pady=10)

        # 検索ボタン
        self.search_button = tk.Button(self.root, text="検索", command=self.start_search)
        self.search_button.grid(row=0, column=2, padx=10, pady=10)

        # 結果表示用テキストエリア
        self.result_area = scrolledtext.ScrolledText(self.root, width=80, height=20)
        self.result_area.grid(row=1, column=0, columnspan=3, padx=10, pady=10)

        # 検索履歴の表示
        self.history_button = tk.Button(self.root, text="検索履歴を表示", command=self.show_history)
        self.history_button.grid(row=2, column=0, columnspan=3, pady=10)

        # 履歴リストボックス
        self.history_listbox = Listbox(self.root, selectmode=SINGLE, width=50)
        self.history_listbox.grid(row=3, column=0, columnspan=2, padx=10, pady=10)
        self.history_listbox.bind('<<ListboxSelect>>', self.display_selected_file)

        # フィルタ設定ボタン
        self.filter_button = tk.Button(self.root, text="フィルタ設定", command=self.set_filter)
        self.filter_button.grid(row=3, column=2, padx=10, pady=10)

        # 要約設定ボタン
        self.summary_button = tk.Button(self.root, text="要約設定", command=self.set_summary_length)
        self.summary_button.grid(row=4, column=0, padx=10, pady=10)

        # PDF出力ボタン
        self.pdf_button = tk.Button(self.root, text="PDF出力", command=self.save_summary_as_pdf)
        self.pdf_button.grid(row=4, column=1, padx=10, pady=10)

        # 自動更新設定ボタン
        self.auto_update_button = tk.Button(self.root, text="自動更新設定", command=self.set_auto_update)
        self.auto_update_button.grid(row=4, column=2, padx=10, pady=10)

    def create_directory(self, search_word):
        if not os.path.exists(search_word):
            os.makedirs(search_word)
        return search_word

    def get_google_search_results(self, query, num_results=5):
        results = []
        for url in search(query):
            if self.is_excluded(url):
                continue
            results.append(url)
            if len(results) >= num_results:
                break
        return results

    def is_excluded(self, url):
        for keyword in self.excluded_keywords:
            if keyword in url:
                return True
        for domain in self.excluded_domains:
            if domain in url:
                return True
        return False

    def fetch_page_content(self, url):
        response = requests.get(url)
        soup = BeautifulSoup(response.content, 'html.parser')
        return soup.get_text()

    def summarize_text(self, text):
        parser = PlaintextParser.from_string(text, Tokenizer("english"))
        summarizer = LsaSummarizer()
        summary = summarizer(parser.document, sentences_count=int(self.summary_word_count / 20))
        return " ".join([str(sentence) for sentence in summary])

    def save_summary_to_file(self, directory, url, content):
        summary = self.summarize_text(content)
        filename = os.path.join(directory, f"{url.split('//')[-1].replace('/', '_')}.txt")
        with open(filename, 'w', encoding='utf-8') as file:
            file.write(summary)
        return filename

    def start_search(self):
        search_words = self.search_entry.get().split(',')
        if not search_words:
            messagebox.showerror("エラー", "検索ワードを入力してください。")
            return

        self.result_area.delete(1.0, tk.END)
        for search_word in search_words:
            search_word = search_word.strip()
            directory = self.create_directory(search_word)
            search_results = self.get_google_search_results(search_word)

            self.result_area.insert(tk.END, f"検索ワード: {search_word}\n\n")

            for url in search_results:
                content = self.fetch_page_content(url)
                summary_file = self.save_summary_to_file(directory, url, content)
                self.result_area.insert(tk.END, f"URL: {url}\n要約ファイル: {summary_file}\n\n")

            self.auto_update_threads[search_word] = threading.Thread(target=self.auto_update_search, args=(search_word,))
            self.auto_update_threads[search_word].start()

        messagebox.showinfo("完了", "要約が完了しました。")

    def show_history(self):
        directory = filedialog.askdirectory(title="検索履歴を選択してください")
        if not directory:
            return

        files = os.listdir(directory)
        summary_files = [f for f in files if f.endswith(".txt")]

        if not summary_files:
            messagebox.showinfo("情報", "要約ファイルが見つかりません。")
            return

        self.history_listbox.delete(0, tk.END)
        for file in summary_files[:5]:  # 上位5件のファイルをリスト表示
            self.history_listbox.insert(tk.END, file)

    def display_selected_file(self, event):
        selected_file = self.history_listbox.get(self.history_listbox.curselection())
        directory = filedialog.askdirectory(title="検索履歴を選択してください")
        if not directory:
            return

        file_path = os.path.join(directory, selected_file)
        if os.path.exists(file_path):
            self.preview_file(file_path)
        else:
            messagebox.showerror("エラー", f"ファイルが見つかりません: {selected_file}")

    def preview_file(self, file_path):
        preview_window = Toplevel(self.root)
        preview_window.title("ファイルプレビュー")
        text_area = scrolledtext.ScrolledText(preview_window, width=80, height=20)
        text_area.pack(expand=True, fill='both')

        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        text_area.insert(tk.END, content)

    def set_filter(self):
        # キーワードフィルタ設定
        keywords = simpledialog.askstring("フィルタ設定", "除外するキーワード(カンマ区切り):")
        if keywords:
            self.excluded_keywords = keywords.split(',')

        # ドメインフィルタ設定
        domains = simpledialog.askstring("フィルタ設定", "除外するドメイン(カンマ区切り):")
        if domains:
            self.excluded_domains = domains.split(',')

        messagebox.showinfo("設定完了", "フィルタ設定が完了しました。")

    def set_summary_length(self):
        word_count = simpledialog.askinteger("要約設定", "要約の長さを単語数で指定してください:", minvalue=10, maxvalue=1000)
        if word_count:
            self.summary_word_count = word_count
            messagebox.showinfo("設定完了", f"要約の長さが {word_count} 単語に設定されました。")

    def save_summary_as_pdf(self):
        directory = filedialog.askdirectory(title="PDFを保存するディレクトリを選択してください")
        if not directory:
            return

        search_word = self.search_entry.get().strip()
        pdf = FPDF()
        pdf.set_auto_page_break(auto=True, margin=15)

        for filename in os.listdir(search_word):
            if filename.endswith('.txt'):
                file_path = os.path.join(search_word, filename)
                pdf.add_page()
                pdf.set_font("Arial", size=12)

                with open(file_path, 'r', encoding='utf-8') as file:
                    for line in file:
                        pdf.multi_cell(0, 10, line)

        output_path = os.path.join(directory, f"{search_word}_summary.pdf")
        pdf.output(output_path)

        messagebox.showinfo("完了", f"要約がPDFとして保存されました: {output_path}")

    def set_auto_update(self):
        interval = simpledialog.askinteger("自動更新設定", "自動更新の間隔を秒で指定してください:", minvalue=60, maxvalue=60 * 60 * 24)
        if interval:
            self.auto_update_interval = interval
            messagebox.showinfo("設定完了", f"自動更新の間隔が {interval} 秒に設定されました。")

    def auto_update_search(self, search_word):
        while True:
            time.sleep(self.auto_update_interval)
            directory = self.create_directory(search_word)
            search_results = self.get_google_search_results(search_word)
            for url in search_results:
                content = self.fetch_page_content(url)
                self.save_summary_to_file(directory, url, content)
            # 新しい情報がある場合、通知機能をここに実装できます。

if __name__ == "__main__":
    root = tk.Tk()
    app = SummaryApp(root)
    root.mainloop()

解説

import os
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, scrolledtext, Listbox, SINGLE, Toplevel
import requests
from bs4 import BeautifulSoup
from googlesearch import search
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lsa import LsaSummarizer
from fpdf import FPDF
import threading
import time
  • os: ファイルやディレクトリ操作に使用します。
  • tkinter: Pythonの標準ライブラリで、GUIアプリケーションを作成するために使用します。
  • requests: Webページの内容を取得します。
  • BeautifulSoup: 取得したHTMLデータを解析し、テキストを抽出します。
  • googlesearch: Google検索を行い、検索結果のURLを取得します。
  • PlaintextParser, Tokenizer, LsaSummarizer: sumyライブラリを使ってテキストの要約を行います。FPDF: 要約結果をPDFファイルとして保存します。threading: 自動更新機能を実装するために使用します。
  • FPDF: 要約結果をPDFファイルとして保存します。
  • threading: 自動更新機能を実装するために使用します。

クラスとメソッド

class SummaryApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Web Page Summary App")
        self.create_widgets()
        self.excluded_keywords = []
        self.excluded_domains = []
        self.summary_word_count = 100
        self.ratings = {}
        self.auto_update_interval = 60 * 60 * 24  # デフォルト: 24時間
        self.auto_update_threads = {}
  • SummaryApp クラスは、アプリケーション全体の機能を持つクラスです。
  • __init__ メソッドは、クラスの初期化を行います。ウィンドウのタイトルを設定し、ウィジェット(ボタンやテキストエリアなど)を作成します。
  • excluded_keywords, excluded_domains: 検索結果から除外するキーワードやドメインを指定するためのリストです。
  • summary_word_count: 要約の単語数を設定します。auto_update_interval: 自動更新の間隔を設定します(デフォルトは24時間)。
create_widgets メソッド
    def create_widgets(self):
        self.search_label = tk.Label(self.root, text="検索ワード(カンマ区切り):")
        self.search_label.grid(row=0, column=0, padx=10, pady=10)
        self.search_entry = tk.Entry(self.root, width=40)
        self.search_entry.grid(row=0, column=1, padx=10, pady=10)
        self.search_button = tk.Button(self.root, text="検索", command=self.start_search)
        self.search_button.grid(row=0, column=2, padx=10, pady=10)

create_widgets メソッドは、GUIの各種ウィジェット(検索ラベル、検索ボックス、検索ボタンなど)を作成し、配置します。

start_search メソッド
    def start_search(self):
        search_words = self.search_entry.get().split(',')
        if not search_words:
            messagebox.showerror("エラー", "検索ワードを入力してください。")
            return

ユーザーが入力した検索ワードを取得し、それに基づいてGoogle検索を行います。検索結果からページ内容を取得し、要約を生成してファイルに保存します。

動作の流れ

  • ユーザーが検索ワードを入力して「検索」ボタンをクリックします。
  • start_search メソッドが呼ばれ、検索ワードに基づいてGoogle検索を行います。
  • 検索結果からWebページを取得し、要約を生成します。
  • 生成した要約をファイルに保存し、結果をGUI上に表示します。

アプリケーションの拡張

現在の実装では、要約が思う程綺麗なものになりません。
これをChat GPTなどのAIツールを利用して、要約するようにすると更に便利なツールとして活用できるかと思います。

import os
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, scrolledtext, Listbox, SINGLE, Toplevel
import requests
from bs4 import BeautifulSoup
from googlesearch import search
from fpdf import FPDF
import threading
import time

class SummaryApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Web Page Summary App")
        self.create_widgets()
        self.excluded_keywords = []
        self.excluded_domains = []
        self.summary_word_count = 100
        self.ratings = {}
        self.auto_update_interval = 60 * 60 * 24  # デフォルト: 24時間
        self.auto_update_threads = {}

    def create_widgets(self):
        # 検索ワード入力
        self.search_label = tk.Label(self.root, text="検索ワード(カンマ区切り):")
        self.search_label.grid(row=0, column=0, padx=10, pady=10)
        self.search_entry = tk.Entry(self.root, width=40)
        self.search_entry.grid(row=0, column=1, padx=10, pady=10)

        # 検索ボタン
        self.search_button = tk.Button(self.root, text="検索", command=self.start_search)
        self.search_button.grid(row=0, column=2, padx=10, pady=10)

        # 結果表示用テキストエリア
        self.result_area = scrolledtext.ScrolledText(self.root, width=80, height=20)
        self.result_area.grid(row=1, column=0, columnspan=3, padx=10, pady=10)

        # 検索履歴の表示
        self.history_button = tk.Button(self.root, text="検索履歴を表示", command=self.show_history)
        self.history_button.grid(row=2, column=0, columnspan=3, pady=10)

        # 履歴リストボックス
        self.history_listbox = Listbox(self.root, selectmode=SINGLE, width=50)
        self.history_listbox.grid(row=3, column=0, columnspan=2, padx=10, pady=10)
        self.history_listbox.bind('<<ListboxSelect>>', self.display_selected_file)

        # フィルタ設定ボタン
        self.filter_button = tk.Button(self.root, text="フィルタ設定", command=self.set_filter)
        self.filter_button.grid(row=3, column=2, padx=10, pady=10)

        # 要約設定ボタン
        self.summary_button = tk.Button(self.root, text="要約設定", command=self.set_summary_length)
        self.summary_button.grid(row=4, column=0, padx=10, pady=10)

        # PDF出力ボタン
        self.pdf_button = tk.Button(self.root, text="PDF出力", command=self.save_summary_as_pdf)
        self.pdf_button.grid(row=4, column=1, padx=10, pady=10)

        # 自動更新設定ボタン
        self.auto_update_button = tk.Button(self.root, text="自動更新設定", command=self.set_auto_update)
        self.auto_update_button.grid(row=4, column=2, padx=10, pady=10)

    def create_directory(self, search_word):
        if not os.path.exists(search_word):
            os.makedirs(search_word)
        return search_word

    def get_google_search_results(self, query, num_results=5):
        results = []
        for url in search(query):
            if self.is_excluded(url):
                continue
            results.append(url)
            if len(results) >= num_results:
                break
        return results

    def is_excluded(self, url):
        for keyword in self.excluded_keywords:
            if keyword in url:
                return True
        for domain in self.excluded_domains:
            if domain in url:
                return True
        return False

    def fetch_page_content(self, url):
        response = requests.get(url)
        soup = BeautifulSoup(response.content, 'html.parser')
        return soup.get_text()

    def summarize_with_gemini(self, content):
        api_url = "https://api.gemini.com/v1/summarize"  # 仮のURL。実際のGemini APIのURLに置き換えてください。
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_API_KEY_HERE'  # APIキーを入力してください。
        }
        data = {
            "text": content,
            "summary_length": self.summary_word_count
        }
        response = requests.post(api_url, headers=headers, json=data)
        if response.status_code == 200:
            return response.json().get('summary', '要約に失敗しました。')
        else:
            return "Gemini APIの呼び出しに失敗しました。"

    def save_summary_to_file(self, directory, url, content):
        summary = self.summarize_with_gemini(content)
        filename = os.path.join(directory, f"{url.split('//')[-1].replace('/', '_')}.txt")
        with open(filename, 'w', encoding='utf-8') as file:
            file.write(summary)
        return filename

    def start_search(self):
        search_words = self.search_entry.get().split(',')
        if not search_words:
            messagebox.showerror("エラー", "検索ワードを入力してください。")
            return

        self.result_area.delete(1.0, tk.END)
        for search_word in search_words:
            search_word = search_word.strip()
            directory = self.create_directory(search_word)
            search_results = self.get_google_search_results(search_word)

            self.result_area.insert(tk.END, f"検索ワード: {search_word}\n\n")

            for url in search_results:
                content = self.fetch_page_content(url)
                summary_file = self.save_summary_to_file(directory, url, content)
                self.result_area.insert(tk.END, f"URL: {url}\n要約ファイル: {summary_file}\n\n")

            self.auto_update_threads[search_word] = threading.Thread(target=self.auto_update_search, args=(search_word,))
            self.auto_update_threads[search_word].start()

        messagebox.showinfo("完了", "要約が完了しました。")

    def show_history(self):
        directory = filedialog.askdirectory(title="検索履歴を選択してください")
        if not directory:
            return

        files = os.listdir(directory)
        summary_files = [f for f in files if f.endswith(".txt")]

        if not summary_files:
            messagebox.showinfo("情報", "要約ファイルが見つかりません。")
            return

        self.history_listbox.delete(0, tk.END)
        for file in summary_files[:5]:  # 上位5件のファイルをリスト表示
            self.history_listbox.insert(tk.END, file)

    def display_selected_file(self, event):
        selected_file = self.history_listbox.get(self.history_listbox.curselection())
        directory = filedialog.askdirectory(title="検索履歴を選択してください")
        if not directory:
            return

        file_path = os.path.join(directory, selected_file)
        if os.path.exists(file_path):
            self.preview_file(file_path)
        else:
            messagebox.showerror("エラー", f"ファイルが見つかりません: {selected_file}")

    def preview_file(self, file_path):
        preview_window = Toplevel(self.root)
        preview_window.title("ファイルプレビュー")
        text_area = scrolledtext.ScrolledText(preview_window, width=80, height=20)
        text_area.pack(expand=True, fill='both')

        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        text_area.insert(tk.END, content)

    def set_filter(self):
        # キーワードフィルタ設定
        keywords = simpledialog.askstring("フィルタ設定", "除外するキーワード(カンマ区切り):")
        if keywords:
            self.excluded_keywords = keywords.split(',')

        # ドメインフィルタ設定
        domains = simpledialog.askstring("フィルタ設定", "除外するドメイン(カンマ区切り):")
        if domains:
            self.excluded_domains = domains.split(',')

        messagebox.showinfo("設定完了", "フィルタ設定が完了しました。")

    def set_summary_length(self):
        word_count = simpledialog.askinteger("要約設定", "要約の長さを単語数で指定してください:", minvalue=10, maxvalue=1000)
        if word_count:
            self.summary_word_count = word_count
            messagebox.showinfo("設定完了", f"要約の長さが {word_count} 単語に設定されました。")

    def save_summary_as_pdf(self):
        directory = filedialog.askdirectory(title="PDFを保存するディレクトリを選択してください")
        if not directory:
            return

        search_word = self.search_entry.get().strip()
        pdf = FPDF()
        pdf.set_auto_page_break(auto=True, margin=15)

        for file in os.listdir(search_word):
            if file.endswith(".txt"):
                pdf.add_page()
                pdf.set_font("Arial", size=12)
                with open(os.path.join(search_word, file), 'r', encoding='utf-8') as f:
                    for line in f:
                        pdf.cell(200, 10, txt=line, ln=True)

        pdf_file_path = os.path.join(directory, f"{search_word}.pdf")
        pdf.output(pdf_file_path)
        messagebox.showinfo("保存完了", f"PDFが保存されました: {pdf_file_path}")

    def set_auto_update(self):
        interval = simpledialog.askinteger("自動更新設定", "更新間隔を秒で指定してください(デフォルト: 86400秒):", initialvalue=self.auto_update_interval)
        if interval:
            self.auto_update_interval = interval
            messagebox.showinfo("設定完了", f"自動更新間隔が {interval} 秒に設定されました。")

    def auto_update_search(self, search_word):
        while True:
            time.sleep(self.auto_update_interval)
            directory = self.create_directory(search_word)
            search_results = self.get_google_search_results(search_word)
            for url in search_results:
                content = self.fetch_page_content(url)
                self.save_summary_to_file(directory, url, content)
            messagebox.showinfo("更新完了", f"検索ワード '{search_word}' の要約が更新されました。")

if __name__ == "__main__":
    root = tk.Tk()
    app = SummaryApp(root)
    root.mainloop()

Gemini API を利用する様にして実装しました。
Gemini APIの利用方法は割愛しますので、別途検索してみて下さい。勿論、Chat GPTでも同様の実装は可能なので、下記部分をご自身の環境に合わせて調整して下さい。

Gemini APIの使用には、APIキーが必要となるので、コードの該当箇所にご自身のAPIキーを設定してください。APIのエンドポイントURLやリクエスト形式は、APIドキュメントを参照して適切に設定してください。

  def summarize_with_gemini(self, content):
        api_url = "https://api.gemini.com/v1/summarize"  # 仮のURL。実際のGemini APIのURLに置き換えてください。
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_API_KEY_HERE'  # APIキーを入力してください。
        }
        data = {
            "text": content,
            "summary_length": self.summary_word_count
        }
        response = requests.post(api_url, headers=headers, json=data)
        if response.status_code == 200:
            return response.json().get('summary', '要約に失敗しました。')
        else:
            return "Gemini APIの呼び出しに失敗しました。"

更なる改善のアイディア

私が思いつく限りで、いくつか改善点を記載します。

1. インターフェースの改善

  • 進行状況の表示: 検索や要約処理が進行中であることをユーザーに知らせるプログレスバーを追加します。これにより、アプリが動作していることを明確にし、処理が完了するまでの時間を予測しやすくします。
  • 検索結果のプレビュー: 検索結果一覧にリンクをクリックして、ブラウザを開くことなく、簡単なプレビューを表示する機能を追加します。
  • ドラッグ&ドロップ対応: テキストファイルやリンクをアプリにドラッグ&ドロップして、すぐに要約や他の処理を行えるようにします。

2. カスタマイズ機能の充実

  • 要約のスタイル選択: 要約を生成する際、ユーザーが要約スタイルを選択できるオプションを追加します(例えば、簡潔な要約、詳細な要約、技術的な要約など)。
  • 検索結果のフィルタリング: 検索結果の中で、特定のコンテンツタイプやドメイン、言語をフィルタリングする機能を強化します。これにより、ユーザーはより関連性の高い結果を得ることができます。
  • キーワードの強調表示: 要約結果で特定のキーワードやフレーズを強調表示し、ユーザーが重要な情報をすぐに確認できるようにします。

3. パフォーマンスと効率化の向上

  • 並列処理の最適化: 複数の検索ワードやページを同時に処理する際、非同期処理やマルチスレッドを活用して全体のパフォーマンスを向上させます。
  • キャッシュ機能の導入: 過去に要約したコンテンツをキャッシュし、再度要約する際に再処理を避けることで、効率を高めます。

4. 機能拡張

  • 多言語対応: 要約を複数言語で生成できるようにし、ユーザーが選択した言語に基づいて要約を生成します。これにより、国際的な利用にも対応できます。
  • クロスプラットフォーム対応: アプリをデスクトップ(Windows, macOS, Linux)だけでなく、モバイルデバイス(iOS, Android)にも対応させ、幅広いユーザーが利用できるようにします。

5. データの保存と共有

  • クラウド同期: 要約結果や検索履歴をクラウドに同期し、複数デバイス間でデータを共有できるようにします。これにより、場所やデバイスを問わず利用できるようになります。
  • 共有機能: 要約結果を他のユーザーやチームメンバーと簡単に共有できる機能を追加します。共有リンクの生成や、メールやメッセージアプリ経由での送信が可能になります。

6. セキュリティとプライバシー

  • データ暗号化: 要約結果や検索履歴など、保存されるデータの暗号化を行い、ユーザーのプライバシーを保護します。
  • ユーザー認証: クラウド機能や共有機能を利用する際のユーザー認証を強化し、不正アクセスを防止します。

これらの改善を施すことで、より直感的で強力なツールとなり、多様なユーザーにとって便利で安全なアプリケーションに進化させることができます。
ご要望があれば、実装方法などを詳しく解説したいと思います。

まとめ

データ活用が求められるデータドリブン社会において、「このデータ欲しいな」と思った時に、すぐにデータを収集できるかどうかは、あなたのビジネスに大きな影響を与えるでしょう。

情報過多な時代で検索してページを読むだけでも時間がかかるので、AIを上手く利用し必要な情報を必要な時に取り出せる様にして、上手く利用する事が必要です。

今回紹介した自作のクローラーは簡単な設計で実用性は乏しいかもしれませんが、Chat GPTやGeminiなどのAPIを上手く利用することで、実用性の高いアプリケーションに生まれ変わるかもしれません。

皆さんも是非、プログラミングを楽しみ、色々なアプリケーションを作ってみて下さい。