概要
GoogleのライブラリLangExtractで、書籍のメタデータを抽出してみます。
LangExtract
LangExtractはいわゆるNER(固有表現抽出)が簡単にできます。
環境
今回は、自分のPCが壊れたのでGoogle Colabで試しましたが、たぶん普通のノートPCでも動くには動くと思います。
Colabでは「ランタイムのタイプを変更」で適当なTPUを選択します。
私は、v6e-1 TPU を使いました。
なお、1日に数回試すだけなら無料枠でも問題なく動きますが、今回はいろいろ試行錯誤もしたので、Colab Pro を使いました。「1 か月あたり ¥1,179」なので、まぁいいかという感じです。
モデル
今回は、APIでGeminiをたたいてみるのと、Ollamaで軽量LLM(gemma3n:e4b、phi4-mini:latest)で試してみました。
Colabの準備
more_itertools
なぜかこれがないと、langextractで、「AttributeError: module 'more_itertools' has no attribute 'batched'」となり、途中で実行するとセッションの再起動が必要なので、colabでは最初にやっておきます。
!pip uninstall more_itertools
!pip install --upgrade more-itertools
APIキーの設定
あらかじめAPIキーをColab シークレットに保存しておきます(方法は省略)。
今回試す分には、Gemini 2.5 Flash(Lite) の無料枠で十分かと思います。
from google.colab import userdata
import os
api_key = userdata.get("GOOGLE_API_KEY")
os.environ["LANGEXTRACT_API_KEY"] = api_key
API認証のテスト代わりに
# SDK クライアントの初期化
from google.genai import Client
secret = os.environ.get("LANGEXTRACT_API_KEY")
client = Client(api_key=secret)
# モデルの選択
MODEL_ID = "gemini-2.5-flash"
# テキストプロンプトの送信
from IPython.display import Markdown
response = client.models.generate_content(
model=MODEL_ID,
contents="太陽系で最も大きな惑星は何ですか?"
)
Markdown(response.text) # 認証が成功していれば、ここで結果が表示されます。
必要なライブラリのインストール
!pip install langextract ollama google-generativeai requests beautifulsoup4
ollama と google-generativeai はどちらか試したい片方でも構いません。
Colabへのollamaのインストール
!curl https://ollama.ai/install.sh | sh
!echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
!sudo apt-get update && sudo apt-get install -y cuda-drivers
import os
# Set LD_LIBRARY_PATH so the system NVIDIA library
os.environ.update({'LD_LIBRARY_PATH': '/usr/lib64-nvidia'})
!nohup ollama serve &
Ollamaにモデルをpullして、modelに値を設定
!ollama pull phi4-mini
!ollama pull gemma3n:e4b
model = 'gemma3n:e4b'
単に、Colab上でOllamaを動かすだけなら、以下のようないろいろなモデルが使えますが、現状のLangExtractでは試した限り動かないようです。
# !ollama pull huggingface.co/janhq/Jan-v1-4B-GGUF
# !ollama pull qwen3:8b
# !ollama pull gpt-oss:20b
ウェブサイトのスクレイピング
import requests
from bs4 import BeautifulSoup
import json
import textwrap
from datetime import datetime
import re
# HTMLの取得(HTMLバージョン)
url = 'https://www.nippyo.co.jp/shop/book/7382.html'
# 実際のブラウザのユーザーエージェントに置き換える
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
}
response = requests.get(url)
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # HTTPエラーがあれば例外を発生させる
soup = BeautifulSoup(response.content, 'html.parser')
# ここにスクレイピングの処理を記述
print("スクレイピング成功!")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 503:
print("503エラーが発生しました。しばらく待ってから再試行します。")
time.sleep(60) # 60秒待機
else:
print(f"HTTPエラーが発生しました: {e}")
except Exception as e:
print(f"その他のエラーが発生しました: {e}")
# テキストの抽出(HTMLタグを除去)
raw_text = soup.get_text() #.replace(' ', '_').strip() #(strip=True) # get_text()
raw_text = re.sub(r'\s+', ' ', raw_text).strip()
raw_text = re.sub(r'[\[\]]', ' ', raw_text).strip()
# new_text = text.replace(' ', '_').strip() # .strip() で先頭と末尾の空白を削除
print(raw_text[:3000])
上記の例では、出版社のサイトからスクレイピングしていますが、国立国会図書館の検索サイトや、CiNii Researchなどでも似たような情報が得られます。
抽出
上段までが準備で、いよいよ実際の抽出です。まずollamaで、gemma3n:e4b を試します。
プロンプトの定義
ollamaとAPIで共通に定義するため、以下で定義しておきます。
prompt = textwrap.dedent("""\
次のテキストから書籍や論文のタイトル、著者、出版社などのメタデータ(目録情報)を抽出してください。
抽出するクラスは以下の3つです:
- title: コンテンツのタイトル(title)
- author: コンテンツの著者または責任表示(author)
- publisher: コンテンツの出版者と出版年月(publiser, date)
- topic: コンテンツの概要(summary)
- isbn: 数字10桁または末尾がXの場合がある。-で区切られていることがある(isbn)
必ず原文にある表現をそのまま使い、創作は禁止です。topicは50字以内で要約してください。
""")
import textwrap, os
import langextract as lx
import pandas as pd
import more_itertools
print('model:', model)
# --- プロンプト定義
prompt = prompt
ここが、LangExtractの肝の一つである few-shot の設定です。
examples = [
lx.data.ExampleData(
text="大英帝国という経験",
extractions=[
lx.data.Extraction(
extraction_class="title",
extraction_text="興亡の世界史 大英帝国という経験 コウボウノセカイシダイエイテイコクトイウケイケン 著: 井野瀬 久美惠 講談社学術文庫 ",
attributes={"title":"興亡の世界史 大英帝国という経験"}
),
lx.data.Extraction(
extraction_class="author",
extraction_text="興亡の世界史 大英帝国という経験 コウボウノセカイシダイエイテイコクトイウケイケン 著: 井野瀬 久美惠 講談社学術文庫 井野瀬, 久美惠 井野瀬 久美惠 [著]",
attributes={"author": "井野瀬 久美惠"}
),
lx.data.Extraction(
extraction_class="publisher",
extraction_text="[原本:『興亡の世界史16 大英帝国という経験』講談社 2007年刊]講談社学術文庫 2007. 5",
attributes={"publisher": "講談社", "date": "2007"}
),
lx.data.Extraction(
extraction_class="isbn",
extraction_text="ISBN9784768706725 ISBN 9784768706725 978-4-768-70672-5",
attributes={"isbn": "9784768706725"}
),
lx.data.Extraction(
extraction_class="topic",
extraction_text="アイルランドから、アフリカ、インド、香港まで、世界にその足跡を残した大英帝国。大陸の片隅の島国は、いかにして大帝国へと発展し、女王ヴィクトリアが治める最盛期へと至ったのか。「アメリカ植民地の喪失」をステップとし、多くのモノと文化と娯楽を手に入れ、女性たちが世界を旅したこの国は、なぜ、他国に先んじて奴隷制度を廃止することができたのか。解体と再編の歴史から、EU離脱に揺れるこの国の現代をも読み解く。",
attributes={"summary": "解体と再編の歴史から、EU離脱に揺れるこの国の現代をも読み解く。"}
),
],
)
]
ollama
以下、ollama(gemma3n:e4b)を使って、実際に抽出を実行します。
if not raw_text:
print("Scraped text is empty. Cannot perform extraction.")
else:
print("Scraped text:")
print(raw_text[:1000])
result = lx.extract(
text_or_documents = raw_text[:1000],
prompt_description=prompt,
examples=examples,
model_id= model,
model_url="http://localhost:11434",
temperature=1,
fence_output=False,
use_schema_constraints=True,
extraction_passes=2, # 包括的理解のため複数パス
max_workers=10, # 並列処理で高速化
max_char_buffer=8000, # 大きなチャンクサイズで文脈を保持
)
ollamaの場合、20秒から50秒くらい(結構まちまち)で結果が出ます。
なぜかたまに、「ResolverParsingError: Failed to parse content.」というエラーが出ることがありました。
また、LangExtractの以前のバージョンでは、以下の設定があったようですが、私がためしたバージョン(v1.0.9)では使用できません。
- chunk_size=2000, # チャンクサイズを指定
- overlap=200 # オーバーラップで情報欠落を防ぐ
結果を確認
単にざっと見るだけなら、
for extraction in result.extractions:
print(f"{extraction.extraction_class}: {extraction.extraction_text}")
とやると、
title: 経済学入門
author: 奥野, 正寛
publisher: 日本評論社
isbn: ISBN9784535806092
topic: 経済学の入門書。基礎知識から経済の仕組みまで幅広く解説。
topic: 経済学の入門書。基礎概念から応用まで幅広く解説。
という結果が確認できますが、あとで利用するため DataFrame に入れる場合、以下のようにします。
rows = [e.to_dict() if hasattr(e, "to_dict") else (e.model_dump() if hasattr(e, "model_dump") else e.__dict__)
for e in result.extractions]
df = pd.DataFrame(rows)
df = df[df["extraction_text"].notna() & (df["extraction_text"] != "")]
df = df[["extraction_class", "extraction_text", "attributes"]]
display(df)
]
JSONで出力した場合(インデントは整形しています):
[
{
"index": 0,
"extraction_class": "title",
"extraction_text": "経済学入門",
"attributes": "{'title': '経済学入門'}"
},
{
"index": 1,
"extraction_class": "author",
"extraction_text": "奥野, 正寛",
"attributes": "{'author': '奥野 正寛'}"
},
{
"index": 2,
"extraction_class": "publisher",
"extraction_text": "日本評論社",
"attributes": "{'publisher': '日本評論社', 'date': '2017.3'}"
},
{
"index": 3,
"extraction_class": "isbn",
"extraction_text": "ISBN9784535806092",
"attributes": ""
},
{
"index": 4,
"extraction_class": "topic",
"extraction_text": "経済学の入門書。基礎知識から経済の仕組みまで幅広く解説。",
"attributes": "{'summary': '経済学の入門書。基礎知識から経済の仕組みまで幅広く解説。'}"
},
{
"index": 5,
"extraction_class": "topic",
"extraction_text": "経済学の入門書。基礎概念から応用まで幅広く解説。",
"attributes": "{'summaary': '経済学の入門書。基礎概念から応用まで幅広く解説。'}"
}
]
上記は、'gemma3n:e4b'ですが、'gemma3:4b'の方が少し精度がよいかもしれません。'phi4-mini:latest'でもおおむね抽出できますが、若干精度は落ちる場合が多いようです(temperature=1 でも毎回結果が異なるのはなぜだろう)。
API(Gemini)
同じ入力(prompt、raw_text)、同じfew shot(examples)を使って、
'gemini-2.5-flash-lite'で抽出してみます。
if not raw_text:
print("Scraped text is empty. Cannot perform extraction.")
else:
print("Scraped text:")
print(raw_text[:1000])
result = lx.extract(
text_or_documents= raw_text[:1000], # input_text,
prompt_description=prompt,
examples=examples,
model_id = 'gemini-2.5-flash-lite',
fence_output=False, # TrueだとResolverParsingError: Failed to parse content.
use_schema_constraints=True, # FalseだとPursingエラーになる
extraction_passes=3, # 包括的理解のため複数パス
max_workers=20, # 並列処理で高速化
max_char_buffer=10000 # 大きなチャンクサイズで文脈を保持
)
geminiの場合、5から長くて10秒とollamaの場合の1/3から最大1/10の処理時間でした。
たまたまかもしれませんが、試した限り、gemini-2.5-flash より gemini-2.5-flash-lite の方が軽快に動くようです。
ollamaと同様に、結果を表示してみます。
[
{
"index": 0,
"extraction_class": "title",
"extraction_text": "経済学入門",
"attributes": "{'title': '経済学入門'}"
},
{
"index": 1,
"extraction_class": "author",
"extraction_text": "奥野正寛著",
"attributes": "{'author': '奥野正寛'}"
},
{
"index": 2,
"extraction_class": "publisher",
"extraction_text": "日本評論社 2017.3",
"attributes": "{'publisher': '日本評論社', 'date': '2017'}"
},
{
"index": 3,
"extraction_class": "isbn",
"extraction_text": "9784535806092",
"attributes": "{'isbn': '9784535806092'}"
},
{
"index": 4,
"extraction_class": "topic",
"extraction_text": "経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。",
"attributes": "{'summary': '経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。'}"
},
{
"index": 5,
"extraction_class": "topic",
"extraction_text": "経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。",
"attributes": "{'summary': '経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。'}"
},
{
"index": 6,
"extraction_class": "topic",
"extraction_text": "経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。",
"attributes": "{'summary': '経済学の入門書。書誌事項としてタイトル、別名、著者、出版者、出版年月、書籍サイズなどが記載されている。'}"
}
]
topicはちょっといまいちですが、おおむねよく抽出できています。
今後の展望
LLMを使ったメタデータの自動抽出(≒目録の自動作成)についてはいろいろな試行がなされていますが、誰でも(ちょっと試すだけならcolabの無料枠で十分)、比較的簡単にここまでできるようになったのは進歩が速いなと思いました。
上記の例では、国立国会図書館やCiNiiの検索結果ページなどを使ったので、そもそもそれら自体のAPIが使えるので、その方が早いのですが、メモ程度に本の情報が掲載されているページなどでも抽出が可能でしたが、ブログにちょっと本の情報が書かれているようなページではどれが本の情報か、LLMには区別がつかないこともありました。
本格的なメタデータ作成に使用するには、モデルの選定や、抽出したデータを、JSON SchemaのValidationを行うPythonライブラリなども使って、指定されたスキーマに厳密にフィッティングしていく必要はありますが、力業でよければ実装もわりとすぐできそうです。