ai.get_paper_inf5 のソースコード

"""
学術論文のPDFからメタデータを抽出し、ファイルのリネームやExcelサマリーへの出力を行うスクリプト。

このスクリプトは、指定されたPDFファイルからテキストを抽出し、
OpenAI (GPT-4/GPT-5-nano) または Google Gemini などの生成AIモデルに送信して、
論文の著者、タイトル、ジャーナル、発行年などの情報をJSON形式で取得します。
取得したメタデータはExcelファイルに追記され、オプションでPDFファイルの名前を
AIが推奨するファイル名に変更することができます。

関連リンク: :doc:`get_paper_inf5_usage`
"""

import argparse
import os
import shutil
import glob
import io
try:
    from openpyxl import Workbook, load_workbook
except:
    print("Error: Failed to import openpyxl")
    input("Install: pip install openpyxl")
try:
    import PyPDF2
except:
    print("Error: Failed to import PyPDF2")
    input("Install: pip install pypdf2")

from tkai_lib import read_ai_config
from tkai_lib import query_openai4, openai_response_to_json
from tkai_lib import query_openai5, openai5_response_to_json
from tkai_lib import query_google, google_response_to_json


#=========================================================
# Global configuration
#=========================================================
config_path = "ai.env"

#=========================================================
# Read configuration
#=========================================================
read_ai_config(config_path)

max_tokens     = int(os.getenv("max_tokens", "2000"))

openai_api_key = os.getenv("OPENAI_API_KEY")
openai_model   = os.getenv("openai_model", "gpt-4o")
temperature    = float(os.getenv("temperature", "0.3"))

openai_model5  = os.getenv("openai_model5", "gpt-5-nano")
reasoning_effort = os.getenv("reasoning_effort", "low")

google_api_key = os.getenv("GOOGLE_API_KEY")
google_model   = os.getenv("gemini_model", "gemini-2.5-flash")


#=========================================================
# Parameters
#=========================================================
DEFAULT_TEMPLATE = "(author_first)_(author_last)_(short_title)_(shortest_name)_(year).pdf"

xlsx_labels = [
    "directory", "filename_original", "filename_rename", 
    "authors", "author_first", "author_last", 
    "title", "short_title", "journal", "short_name", "shortest_name", 
    "date_received", "date_accepted", "date_published_online", "date_published",
    "year", "volume", "issue", "pages", "doi",
    ]

# Prompt template: start
prompt_template = """
以下の学術論文のテキストから、指定された情報をJSON形式で抽出してください。
また、first author、last author、短縮したtitle (short_title)、shortest_name、発行年から
推奨ファイル名 {{template}} を作ってください。
short_titleの単語はCaptalFirstにして、空白、.、,などの文字は削除してください。

推奨ファイル名(filename_rename):
著者リスト(authors)   :
第一著者(author_first):
最終著者(author_last) :
論文題目(title)           :
論文題目略称(short_title) :
発行雑誌名(journal)       :
発行雑誌名略称(short_name):
発行雑誌名の最短の略称(shortest_name):
投稿受理日(date_received) :
採択日(date_accepted) :
オンライン発行日(date_published_online):
発行日(date_published):
発行年(year)   :
巻番号(volume):
号番号(issue) :
ページ(pages ):
DOI(doi):

情報が不明な場合は、空文字列 "" を使用してください。

--- 論文テキスト ---
{{text}}
"""
# Prompt template: end


[ドキュメント] def initialize(): """ コマンドライン引数を解析するためのArgumentParserを設定します。 処理するPDFファイルのパス、使用するAI API、サマリーExcelのパス、 再帰検索の有無、ファイルリネームの有無、元のファイル削除の有無、 APIに送る最大テキストバイト数、ファイル名テンプレートなどの引数を定義します。 :returns: 設定済みのArgumentParserオブジェクト。 :rtype: argparse.ArgumentParser """ parser = argparse.ArgumentParser(description="学術論文のPDFからメタデータを抽出します。") parser.add_argument("--api", type=str, default='openai5', choices = ["openai", "openai5", "google"], help="使用する API を指定(openai5|openai|google: デフォルト: openai5)") parser.add_argument("input_file", type=str, help="処理するPDFファイルのパス") parser.add_argument("--summary_path", type=str, default = "summary.xlsx", help="summary.xlsxのパス") parser.add_argument("--recursive", type=int, default=0, help="1の場合、サブディレクトリまで処理する") parser.add_argument("--rename", type=int, default=0, help="1の場合、生成AIで推薦ファイル名を取得できた場合、リネームする") # parser.add_argument("--rename", action="store_true", # help="生成AIで推薦ファイル名を取得できた場合、リネームする") parser.add_argument("--delete_original", type=int, default=1, help="1の場合、renameしたら元のファイルを削除する") parser.add_argument("--max_bytes", type=int, default=10000, help="APIに送る最大テキストバイト数。論文全体を送る必要がない場合に指定。") parser.add_argument("--template", type=str, default=DEFAULT_TEMPLATE, help="ファイル名を変える際のテンプレート {{DEFAULT_TEMPLATE}}") return parser
[ドキュメント] def extract_text_from_pdf(pdf_path: str, max_bytes: int) -> str: """ 指定されたPDFファイルからテキストを抽出し、最大バイト数に制限します。 PyPDF2ライブラリを使用してPDFの各ページからテキストを抽出し、 指定されたmax_bytesを超えないように切り詰めます。 エラーが発生した場合は空文字列を返します。 :param pdf_path: 処理するPDFファイルのパス。 :type pdf_path: str :param max_bytes: 抽出するテキストの最大バイト数。 :type max_bytes: int :returns: 抽出されたテキスト文字列。エラーが発生した場合は空文字列。 :rtype: str """ text_buffer = io.StringIO() try: with open(pdf_path, 'rb') as f: pdf_reader = PyPDF2.PdfReader(f) num_pages = len(pdf_reader.pages) for page_num in range(num_pages): page = pdf_reader.pages[page_num] text_buffer.write(page.extract_text() or '') # バイト数チェック if text_buffer.tell() > max_bytes: break except Exception as e: print(f"PDFファイルの読み込み中にエラーが発生しました: {e}") return "" full_text = text_buffer.getvalue() # バイト数でテキストを切り詰める encoded_text = full_text.encode('utf-8', 'ignore') if len(encoded_text) > max_bytes: truncated_text = encoded_text[:max_bytes].decode('utf-8', 'ignore') return truncated_text return full_text
[ドキュメント] def get_metadata_from_openai(text: str, template: str) -> dict: """ OpenAI GPT-4 APIを使用して論文のメタデータを抽出します。 抽出されたテキストとファイル名テンプレートをプロンプトに組み込み、 OpenAI GPT-4モデルにJSON形式での情報抽出をリクエストします。 :param text: 論文から抽出されたテキスト。 :type text: str :param template: 推奨ファイル名の生成に使用されるテンプレート文字列。 :type template: str :returns: 抽出されたメタデータを含む辞書。API呼び出しが失敗した場合はFalse、レスポンスが空の場合は空の辞書を返します。 :rtype: dict or bool """ prompt = prompt_template.replace("{{text}}", text).replace("{{template}}", template) response = query_openai4(prompt, openai_model, max_tokens = max_tokens, response_format = { "type": "json_object" }, openai_api_key = openai_api_key) if not response: return False if response == {}: return {} json, json_list = openai_response_to_json(response) return json
[ドキュメント] def get_metadata_from_openai5(text: str, template: str) -> dict: """ OpenAI GPT-5-nano APIを使用して論文のメタデータを抽出します。 抽出されたテキストとファイル名テンプレートをプロンプトに組み込み、 OpenAI GPT-5-nanoモデルにJSON形式での情報抽出をリクエストします。 :param text: 論文から抽出されたテキスト。 :type text: str :param template: 推奨ファイル名の生成に使用されるテンプレート文字列。 :type template: str :returns: 抽出されたメタデータを含む辞書。API呼び出しが失敗した場合はFalse、レスポンスが空の場合は空の辞書を返します。 :rtype: dict or bool """ prompt = prompt_template.replace("{{text}}", text).replace("{{template}}", template) response = query_openai5(prompt, openai_model5, openai_api_key = openai_api_key, effort = reasoning_effort, max_output_tokens = max_tokens) if not response: return False if response == {}: return {} json, json_list = openai5_response_to_json(response) return json
[ドキュメント] def get_metadata_from_google(text: str, template: str) -> dict: """ Google Gemini APIを使用して論文のメタデータを抽出します。 抽出されたテキストとファイル名テンプレートをプロンプトに組み込み、 Google GeminiモデルにJSON形式での情報抽出をリクエストします。 :param text: 論文から抽出されたテキスト。 :type text: str :param template: 推奨ファイル名の生成に使用されるテンプレート文字列。 :type template: str :returns: 抽出されたメタデータを含む辞書。API呼び出しが失敗した場合はFalse、レスポンスが空の場合は空の辞書を返します。 :rtype: dict or bool """ prompt = prompt_template.replace("{{text}}", text).replace("{{template}}", template) response = query_google(prompt, google_model, role = None, generation_config = {"response_mime_type": "application/json"} , google_api_key = google_api_key) if not response: return False if response == {}: return {} json, json_list = google_response_to_json(response) return json
[ドキュメント] def append_xlsx(path, mode, labels, data_list): """ Excelファイルにデータを追記します。 指定されたパスのExcelファイルが存在しないか、`mode`が"w"の場合は新規作成し、 ヘッダー(`labels`)を追加します。それ以外の場合は既存のファイルにデータを追記します。 :param path: Excelファイルのパス。 :type path: str :param mode: ファイルモード("w"で新規作成/上書き、それ以外で追記)。 :type mode: str :param labels: ヘッダーとして使用する列名のリスト。 :type labels: list[str] :param data_list: Excelに追記するデータのリスト(各要素が1行に対応するリスト)。 :type data_list: list[list] :returns: なし。 :rtype: None """ if mode == "w" or not os.path.exists(path): wb = Workbook() ws = wb.active ws.append(labels) else: wb = load_workbook(filename = path) ws = wb.active for row in data_list: ws.append(row) wb.save(path)
[ドキュメント] def rename_file(input_file, new_filename, delete_original = False): """ ファイル名を変更またはコピーします。 `input_file`を`new_filename`に変更します。 `delete_original`が`True`の場合は元のファイルを削除し、 `False`の場合はコピーします。`new_filename`が既に存在する場合は処理をスキップします。 :param input_file: 元のファイルのパス。 :type input_file: str :param new_filename: 新しいファイルのパス。 :type new_filename: str :param delete_original: (オプション) 元のファイルを削除するかどうか。デフォルトはFalse。 :type delete_original: bool :returns: 処理が成功した場合はTrue、失敗した場合はFalse。 :rtype: bool """ if os.path.exists(new_filename): print(f"Error in rename_file(): File [{new_filename}] already exists. Skip.") return False try: directory = os.path.dirname(input_file) os.chdir(directory) if delete_original: os.rename(input_file, new_filename) print(f"Changed file name from [{input_file}] to [{new_filename}]") else: shutil.copy(input_file, new_filename) print(f"Copied [{input_file}] to [{new_filename}]") except Exception as e: print(f"Error in rename_file(): Could not change file name to [{new_filename}]") return True
[ドキュメント] def get_inf(input_file, summary_path, api = "openai", max_bytes = 10000, rename = False, delete_original = True, template = DEFAULT_TEMPLATE): """ 単一のPDFファイルから論文メタデータを抽出し、Excelに記録し、必要に応じてリネームします。 PDFからテキストを抽出し、指定されたAPI(OpenAI GPT-4, OpenAI GPT-5-nano, Google Gemini) を使用してメタデータを取得します。取得したメタデータは`summary_path`で指定された Excelファイルに追記されます。`rename`が`True`の場合、AIが推奨するファイル名に PDFファイルを変更します。 :param input_file: 処理するPDFファイルのパス。 :type input_file: str :param summary_path: メタデータを記録するExcelファイルのパス。 :type summary_path: str :param api: (オプション) 使用するAI API ("openai", "openai5", "google")。デフォルトは"openai"。 :type api: str :param max_bytes: (オプション) APIに送信するテキストの最大バイト数。デフォルトは10000。 :type max_bytes: int :param rename: (オプション) ファイル名をAIが推奨するものに変更するかどうか。デフォルトはFalse。 :type rename: bool :param delete_original: (オプション) `rename`がTrueの場合、元のファイルを削除するかどうか。デフォルトはTrue。 :type delete_original: bool :param template: (オプション) ファイル名変更時に使用するテンプレート。デフォルトはDEFAULT_TEMPLATE。 :type template: str :returns: 処理が成功した場合はTrue、失敗した場合はFalse、APIの指定が誤っている場合はNone。 :rtype: bool or None """ directory = os.path.dirname(input_file) filename = os.path.basename(input_file) print(f"Input PDF file: [{input_file}]") print(f" Directory: [{directory}]") print(f" File name: [{filename}]") extracted_text = extract_text_from_pdf(input_file, max_bytes) if not extracted_text: print("テキストの抽出に失敗したか、ファイルが空です。処理を終了します。") print("PyCryptodomeが必要というメッセージが出たら、以下のようにinstallしてください。") print("Install: pip install pycryptodome ") return False print(f" API: {api.upper()}") if 'openai5' in api: print(f" openai_model: {openai_model5}") print(f" effort : {reasoning_effort}") print(f" max_tokens : {max_tokens}") elif 'openai' in api: print(f" openai_model: {openai_model}") print(f" temperature : {temperature}") print(f" max_tokens : {max_tokens}") else: print(f" google_model: {google_model}") print(f" max_tokens: {max_tokens}") print(f" max bytes: {max_bytes}") print(f" 抽出したテキストを送信中...") if api == 'openai': metadata = get_metadata_from_openai(extracted_text[:max_bytes], template) elif api == 'openai5': metadata = get_metadata_from_openai5(extracted_text[:max_bytes], template) elif api == 'google': metadata = get_metadata_from_google(extracted_text[:max_bytes], template) else: print("Error in get_inf(): api が 'openai' または 'google' ではありません。") return None if metadata: print("\n--- 論文メタデータ ---") data = [directory, filename] for key, value in metadata.items(): print(f"{key}: {value}") if type(value) is list or type(value) is tuple: data.append(", ".join(value)) else: data.append(value) else: data = [directory, filename, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""] append_xlsx(summary_path, mode = "a", labels = xlsx_labels, data_list = [data]) if rename and metadata.get("filename_rename", None): new_filename = metadata["filename_rename"] if not new_filename.endswith('.pdf'): new_filename += '.pdf' ret = rename_file(input_file, new_filename, delete_original = delete_original) if not ret: return False
[ドキュメント] def main(): """ スクリプトのメインエントリポイント。コマンドライン引数を処理し、ファイル群に対してメタデータ抽出を実行します。 `initialize`関数を呼び出して引数を解析し、`input_file`の指定に基づいてPDFファイルを検索します。 見つかった各ファイルに対して`get_inf`関数を呼び出し、メタデータ抽出と処理を行います。 :returns: なし。 :rtype: None """ parser = initialize() args = parser.parse_args() fmask = args.input_file if args.recursive: directory = os.path.dirname(fmask) base_name = os.path.basename(fmask) if '*' not in directory: fmask = os.path.join(directory, '**', base_name) print() print(f"Search files for [{fmask}]") print(f"Recursive search? {args.recursive}") files = sorted(glob.glob(fmask, recursive = args.recursive)) print() if len(files) == 0: print(f"Error: No file found.") else: print(f"Files found for [{args.input_file}]") for f in files: print(f" {f}") for f in files: print() get_inf(f, args.summary_path, args.api, args.max_bytes, args.rename, args.delete_original, args.template) input("\nPress ENTER to terminate>>\n")
if __name__ == "__main__": main()