



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒競合サイトのコンテンツを定期的にチェックするのが大変です。AIで自動分析できれば楽なんですが……



LangChainのWebBaseLoaderを使えば、競合サイトのテキストを自動収集してAIで分析するツールが作れるんじゃ!URLを渡すだけでコンテンツ抽出・キーワード分析・比較レポート生成まで自動化できるぞ。今日はPython実装コードを全文掲載して完全解説するぞい!
LangChainのWebBaseLoaderを使えば、URLを渡すだけで競合サイトのテキストを抽出し、OpenAI APIで要約・キーワード抽出・比較レポートを自動生成できます。毎週数時間かかっていた競合リサーチが、Pythonスクリプトの実行数分に短縮されます。
本記事では、LangChainの概要・WebBaseLoaderの使い方・競合サイト自動分析ツールの実装コード・複数サイト比較・Embeddings活用・VectorStore・定期実行の自動化・コスト最適化・エラーハンドリングを完全解説します。
あわせて読みたい:
RAG構成でChatGPT APIを使った自社データ参照チャットボットの作り方(LangChain活用の基礎)
LangChainは、ChatGPTなどのLLM(大規模言語モデル)を活用したAIアプリケーションを効率的に構築するためのオープンソースフレームワークです。Pythonを中心に、外部データとLLMを柔軟に連携させる機能を多数持っています。2022年10月にHarrison Chase氏によって公開されて以来、わずか数年でGitHub Star数が9万を超える急成長プロジェクトになりました。
LangChainの最大の強みは「データソースとLLMをつなぐグルー(接着剤)としての機能」にあります。Webページ・PDF・データベース・Slack・Notionなど多様なデータソースからテキストを取り込み、それをOpenAI・Anthropic・Google Geminiなど複数のLLMで処理するパイプラインを、最小限のコードで構築できます。競合分析においては「Webページの自動収集 → テキスト正規化 → LLMによる分析 → レポート生成」というフローをシームレスに実現できます。
WebBaseLoaderはLangChain Communityパッケージに含まれるドキュメントローダーの一種です。内部的にはPythonの有名なHTMLパーサーライブラリ「Beautiful Soup 4(bs4)」を使用して、指定URLのHTMLからテキストを抽出し、LangChainの標準データ構造であるDocumentオブジェクトに変換します。
【WebBaseLoaderの役割】
WebBaseLoader = LangChainに組み込まれたWebスクレイピング機能
できること:
✅ URLを渡すだけでWebページのテキストを抽出
✅ 複数URLを一括処理
✅ Beautiful Soup ベースのHTMLパース
✅ LangChainのDocument形式に自動変換(RAGやChainと直接連携できる)
✅ BeautifulSoupのSoupStrainerで取得対象要素を絞り込める
✅ カスタムHTTPヘッダーでUser-Agentを偽装できる
✅ プロキシ設定に対応
競合分析への活用イメージ:
URL[] → WebBaseLoader → テキスト抽出 → LLM分析 → レポート生成WebBaseLoaderが返すDocumentオブジェクトには、page_content(テキスト)とmetadata(ソースURL・タイトルなど)が格納されています。このフォーマットはLangChainのすべてのコンポーネントと互換性があるため、テキストをロードしたらすぐにChain・Agent・VectorStoreと組み合わせられます。
| コンポーネント | 役割 | 競合分析での使い所 |
|---|---|---|
| WebBaseLoader | WebページのHTMLからテキストを抽出 | 競合サイトのコンテンツ収集 |
| TextSplitter | 長いテキストをチャンクに分割 | トークン制限を超えないよう分割処理 |
| Embeddings | テキストをベクトルに変換 | コンテンツの類似度比較・重複検出 |
| VectorStore | ベクトルをDBに保存・検索 | 大量ページの効率的な管理・検索 |
| Chains | 複数の処理をパイプライン化 | 「抽出→要約→比較→レポート」の一括実行 |
| Agents | AIが自律的にツールを選択・実行 | 「この競合の強みは何か?」への自動回答 |
| PromptTemplate | 変数付きプロンプトを再利用可能な形で定義 | 分析観点を統一したプロンプト管理 |
競合分析の作業を手動で行う場合、対象サイトを一つひとつ開き、内容を読んで、スプレッドシートに転記して、比較する——という工程に毎回数時間かかります。これをPythonスクリプトに落とし込むだけなら通常のスクレイピングでも対応できますが、「テキストを取得した後にAIで意味を読み取る」という工程でLangChainが圧倒的に効率的です。
BeautifulSoupだけを使ったスクレイピングでは、抽出したテキストをOpenAI APIに渡すための前処理(トークン数の管理・チャンク分割・プロンプト設計)を自前で実装する必要があります。LangChainのWebBaseLoaderを使えば、この前処理が既にパッケージ化されており、load_summarize_chainなどの高レベルAPIを呼ぶだけで「要約」「キーワード抽出」「比較分析」ができます。
LangChainはPython 3.9以上を対象としています。2024年以降はパッケージがlangchain(コアロジック)とlangchain-community(外部連携)とlangchain-openai(OpenAI専用)に分割されているため、必要なものをすべてインストールします。
# 仮想環境を作成して有効化
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 必要なパッケージをインストール
pip install langchain langchain-community langchain-openai \
beautifulsoup4 requests python-dotenv openai \
faiss-cpu schedulefaiss-cpuはFacebook AI Research製のベクトル検索ライブラリです。後述するEmbeddingsを使った類似度比較に使います。GPUがある環境ではfaiss-gpuを使うとさらに高速化できます。
# .env ファイルに APIキーを設定(Gitに含めないこと)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxx# ファイル構成
competitor-analyzer/
├── main.py # メイン処理・実行エントリーポイント
├── analyzer.py # 分析ロジック(要約・キーワード抽出・比較)
├── embeddings_search.py # Embeddingsを使った類似度分析
├── notifier.py # Slack通知
├── schedule_analyzer.py # 定期実行スケジューラ
├── .env # APIキー(Gitに含めない)
├── .gitignore # .env・venv・reports/ を除外
├── requirements.txt # pip freeze した内容
└── reports/ # レポート出力先(.gitignore推奨)# requirements.txt
langchain>=0.3.0
langchain-community>=0.3.0
langchain-openai>=0.2.0
beautifulsoup4>=4.12.0
requests>=2.31.0
python-dotenv>=1.0.0
openai>=1.40.0
faiss-cpu>=1.8.0
schedule>=1.2.0# basic_loader.py
from langchain_community.document_loaders import WebBaseLoader
# URLを指定してローダーを作成
loader = WebBaseLoader("https://example-competitor.com/blog/")
# ページを読み込んでDocumentオブジェクトに変換
documents = loader.load()
# テキスト内容を確認
for doc in documents:
print(f"URL: {doc.metadata['source']}")
print(f"テキスト長: {len(doc.page_content)} 文字")
print(f"冒頭200文字: {doc.page_content[:200]}")
print("---")上記のコードでdocumentsにはDocumentオブジェクトのリストが返ります。page_contentにはHTMLタグを除去したテキスト、metadataには{'source': 'https://...'}が格納されます。
# multi_loader.py
from langchain_community.document_loaders import WebBaseLoader
# 競合サイトのURLリスト
competitor_urls = [
"https://competitor-a.com/services/",
"https://competitor-b.com/about/",
"https://competitor-c.com/blog/",
]
# 複数URLを一括ロード
loader = WebBaseLoader(competitor_urls)
documents = loader.load()
print(f"{len(documents)} ページを読み込みました")
for i, doc in enumerate(documents):
print(f"[{i+1}] {doc.metadata['source']} - {len(doc.page_content)}文字")デフォルトのWebBaseLoaderはナビゲーション・フッター・広告テキストも含めてすべての可視テキストを取得します。競合分析では本文テキストだけを抽出したい場合が多いため、bs_kwargsでSoupStrainerを使って取得対象要素を絞り込みます。
# clean_loader.py
import bs4
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=["https://competitor-a.com/services/"],
bs_kwargs={
"parse_only": bs4.SoupStrainer(
class_=("post-content", "article-body", "entry-content") # 本文エリアのみ取得
)
}
)
documents = loader.load()
# ナビゲーション・フッター・広告テキストを除外した本文のみを取得できる一部のWebサイトはデフォルトのPython User-Agent(python-requests/x.x.x)でアクセスするとブロックされる場合があります。header_templateでUser-Agentを設定できます。
# custom_header_loader.py
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=["https://competitor-a.com/blog/"],
header_template={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/122.0.0.0 Safari/537.36",
"Accept-Language": "ja,en-US;q=0.9,en;q=0.8",
}
)
documents = loader.load()
print(f"取得成功: {len(documents[0].page_content)} 文字")なお、User-Agentのカスタマイズはサイトの利用規約で禁止されている場合があります。robots.txtの確認と合わせて、適切なリクエスト間隔(後述)を設けることを強く推奨します。
LLMにはトークン数の上限があります(GPT-4o miniで128Kトークン)。Webページのテキストが長い場合、そのままAPIに渡すと上限を超えてエラーになります。LangChainのTextSplitterを使ってテキストをチャンクに分割します。
# text_splitter_demo.py
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 競合分析向けの設定例
splitter = RecursiveCharacterTextSplitter(
chunk_size=2000, # 1チャンクあたりの最大文字数
chunk_overlap=200, # チャンク間のオーバーラップ(文脈の連続性を保つ)
separators=["\n\n", "\n", "。", "、", " ", ""]
# 日本語コンテンツの場合、句点・読点を優先的な分割点として設定
)
# Documentを分割
chunks = splitter.split_documents(documents)
print(f"分割後のチャンク数: {len(chunks)}")
for i, chunk in enumerate(chunks[:3]):
print(f"\n--- チャンク {i+1} ---")
print(f"文字数: {len(chunk.page_content)}")
print(chunk.page_content[:200])| TextSplitter | 分割方法 | 向いているケース |
|---|---|---|
| RecursiveCharacterTextSplitter | 段落→改行→句点の順に試みて分割 | 一般的なWebページ・ブログ記事(推奨) |
| CharacterTextSplitter | 指定文字で単純に分割 | 区切り文字が明確なテキスト |
| TokenTextSplitter | トークン数で分割 | OpenAIのトークン数を正確に管理したい場合 |
| HTMLHeaderTextSplitter | HTMLの見出しタグで分割 | 構造化されたHTML(見出しベースの分割) |
| MarkdownHeaderTextSplitter | Markdownの見出しで分割 | Markdown形式のドキュメント |
# analyzer.py
import os
import time
from dotenv import load_dotenv
from langchain_community.document_loaders import WebBaseLoader
from langchain_openai import ChatOpenAI
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain.prompts import PromptTemplate
load_dotenv()
class CompetitorAnalyzer:
"""
競合サイトのコンテンツを自動収集・分析するクラス
"""
def __init__(self, model: str = "gpt-4o-mini"):
self.llm = ChatOpenAI(
model=model,
temperature=0.1,
openai_api_key=os.getenv("OPENAI_API_KEY")
)
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=2000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", "、", " ", ""]
)
self.request_delay = 2.0 # リクエスト間隔(秒)
def load_competitor_content(self, url: str) -> list:
"""URLから競合サイトのコンテンツを取得する"""
try:
time.sleep(self.request_delay) # サーバー負荷軽減のための待機
loader = WebBaseLoader(
web_paths=[url],
header_template={
"User-Agent": "Mozilla/5.0 (compatible; CompetitorAnalyzer/1.0)"
}
)
documents = loader.load()
# 長いコンテンツをチャンク分割
return self.text_splitter.split_documents(documents)
except Exception as e:
print(f"⚠️ 読み込みエラー: {url} - {e}")
return []
def summarize_content(self, url: str) -> str:
"""競合サイトのコンテンツを要約する"""
docs = self.load_competitor_content(url)
if not docs:
return "コンテンツの取得に失敗しました"
# マップリデュース方式で要約(長文でも対応)
# map_reduce: 各チャンクを個別に要約してから最終要約を生成
chain = load_summarize_chain(
self.llm,
chain_type="map_reduce",
verbose=False
)
summary = chain.invoke({"input_documents": docs})
return summary["output_text"]
def extract_keywords(self, url: str) -> str:
"""競合サイトの主要キーワードを抽出する"""
docs = self.load_competitor_content(url)
if not docs:
return "キーワードの取得に失敗しました"
# 最初の数チャンクからキーワードを抽出
content = "\n\n".join([doc.page_content for doc in docs[:3]])
prompt = PromptTemplate(
input_variables=["content"],
template="""
以下のWebページのコンテンツから、SEOキーワードとなりそうな重要語句を20個抽出してください。
検索意図を考慮し、ビジネス価値の高いキーワードを優先してください。
コンテンツ:
{content}
出力形式:
- キーワード1(検索意図: xxx、月間検索ボリューム想定: xxx)
- キーワード2(検索意図: xxx、月間検索ボリューム想定: xxx)
...
"""
)
response = self.llm.invoke(prompt.format(content=content[:3000]))
return response.content
def analyze_service_features(self, url: str) -> str:
"""競合サイトのサービス特徴・強みを分析する"""
docs = self.load_competitor_content(url)
if not docs:
return "サービス情報の取得に失敗しました"
content = "\n\n".join([doc.page_content for doc in docs[:4]])
prompt = PromptTemplate(
input_variables=["content", "url"],
template="""
以下は {url} のWebページコンテンツです。
このサービス・企業の以下の情報を分析して日本語でまとめてください:
1. 主要サービス・製品
2. 強みとポジション
3. ターゲット顧客層
4. 価格帯・料金体系(記載がある場合)
5. 差別化ポイント
6. コンテンツ・情報発信の傾向
7. 弱みと改善余地(推測を含む)
コンテンツ:
{content}
"""
)
response = self.llm.invoke(
prompt.format(content=content[:4000], url=url)
)
return response.content
def analyze_content_strategy(self, url: str) -> str:
"""競合のコンテンツ戦略を分析する"""
docs = self.load_competitor_content(url)
if not docs:
return "コンテンツ戦略の分析に失敗しました"
content = "\n\n".join([doc.page_content for doc in docs[:5]])
prompt = f"""
以下のWebページのコンテンツから、このサイトのコンテンツマーケティング戦略を分析してください。
分析観点:
1. メインのターゲットキーワード・トピック
2. コンテンツの更新頻度(推測)
3. コンテンツの深さと品質
4. CTA(Call to Action)の設置場所と訴求内容
5. SEO対策の状況(見出し構造・内部リンクなど)
6. ユーザーエンゲージメントを高める工夫
コンテンツ:
{content[:5000]}
"""
response = self.llm.invoke(prompt)
return response.content
def compare_competitors(self, urls: list[str]) -> str:
"""複数の競合サイトを比較分析する"""
analyses = {}
for url in urls:
print(f" → 分析中: {url}")
analyses[url] = self.analyze_service_features(url)
# 比較プロンプト
comparisons = "\n\n".join([
f"【{url}】\n{analysis}"
for url, analysis in analyses.items()
])
compare_prompt = f"""
以下の複数の競合サービスの分析結果をもとに、詳細な比較レポートを作成してください。
{comparisons}
比較レポートの形式:
## 比較サマリー
(全体の傾向・共通点・差異の概要)
## 各社の強み・弱み
(各サービスの強みと弱みを箇条書きで)
## 市場のポジショニングマップ
(価格帯×サービス範囲の観点で整理)
## コンテンツ戦略の比較
(各社がどのようなトピック・キーワードで情報発信しているか)
## 競合に対する差別化戦略の示唆
(自社が差別化できる余地・ブルーオーシャンとなりうる領域)
## 短期・中長期の戦略提案
(3ヶ月・1年で実施すべき具体的なアクション)
"""
response = self.llm.invoke(compare_prompt)
return response.content# main.py
import datetime
import os
from analyzer import CompetitorAnalyzer
def main():
# 分析対象の競合サイトURLリスト
competitor_urls = [
"https://competitor-a.com/services/",
"https://competitor-b.com/pricing/",
"https://competitor-c.com/about/",
]
print("=" * 60)
print("🔍 競合サイト自動分析ツール")
print(f"実行日時: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 60)
analyzer = CompetitorAnalyzer(model="gpt-4o-mini")
# 1. 各競合サイトの要約
print("\n📝 1. コンテンツ要約")
summaries = {}
for url in competitor_urls:
print(f" → 要約中: {url}")
summaries[url] = analyzer.summarize_content(url)
# 2. キーワード抽出
print("\n🔑 2. SEOキーワード抽出")
keywords = {}
for url in competitor_urls:
print(f" → キーワード抽出中: {url}")
keywords[url] = analyzer.extract_keywords(url)
# 3. コンテンツ戦略分析
print("\n📈 3. コンテンツ戦略分析")
content_strategies = {}
for url in competitor_urls:
print(f" → コンテンツ戦略分析中: {url}")
content_strategies[url] = analyzer.analyze_content_strategy(url)
# 4. 競合比較レポート
print("\n📊 4. 競合比較レポート生成")
comparison_report = analyzer.compare_competitors(competitor_urls)
# 5. レポートをファイルに保存
os.makedirs("reports", exist_ok=True)
report_path = f"reports/competitor_analysis_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.md"
with open(report_path, "w", encoding="utf-8") as f:
f.write(f"# 競合サイト分析レポート\n\n")
f.write(f"実行日時: {datetime.datetime.now().strftime('%Y年%m月%d日 %H:%M')}\n\n")
f.write("## 各サイト要約\n\n")
for url, summary in summaries.items():
f.write(f"### {url}\n{summary}\n\n")
f.write("## SEOキーワード分析\n\n")
for url, kw in keywords.items():
f.write(f"### {url}\n{kw}\n\n")
f.write("## コンテンツ戦略分析\n\n")
for url, strategy in content_strategies.items():
f.write(f"### {url}\n{strategy}\n\n")
f.write("## 競合比較レポート\n\n")
f.write(comparison_report)
print(f"\n✅ レポートを保存しました: {report_path}")
if __name__ == "__main__":
main()# 実行
python main.py
# 出力例
# ============================================================
# 🔍 競合サイト自動分析ツール
# 実行日時: 2026-03-14 18:14
# ============================================================
# 📝 1. コンテンツ要約
# → 要約中: https://competitor-a.com/services/
# ...
# ✅ レポートを保存しました: reports/competitor_analysis_20260314_1820.mdテキストの要約や比較だけでなく、Embeddingsを使うことでコンテンツの「意味的な類似度」を数値として計算できます。「自社コンテンツと競合コンテンツの重複度を調べる」「競合が扱っていないトピックを発見する」といった応用が可能です。
EmbeddingsはテキストをN次元の浮動小数点ベクトルに変換する技術です。意味が似ているテキストは近いベクトルになるため、ベクトル同士のコサイン類似度を計算することで「テキストの類似性」を数値化できます。例えば「Pythonでスクレイピングする方法」と「Pythonを使ったWebデータ収集」は意味が近いため、高い類似度スコアになります。
# embeddings_search.py
import numpy as np
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
class ContentSimilarityAnalyzer:
"""
Embeddingsを使ったコンテンツ類似度分析クラス
"""
def __init__(self):
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50
)
def build_vectorstore(self, urls: list[str]) -> FAISS:
"""複数URLのコンテンツからVectorStoreを構築"""
all_docs = []
for url in urls:
loader = WebBaseLoader(url)
docs = loader.load()
chunks = self.splitter.split_documents(docs)
all_docs.extend(chunks)
print(f"✅ {len(all_docs)} チャンクをVectorStoreに格納")
return FAISS.from_documents(all_docs, self.embeddings)
def find_gaps(
self,
my_url: str,
competitor_urls: list[str],
top_k: int = 5
) -> str:
"""
自社コンテンツにないが競合にあるトピックを発見する
"""
# 競合のVectorStoreを構築
competitor_store = self.build_vectorstore(competitor_urls)
# 自社コンテンツを読み込み
my_loader = WebBaseLoader(my_url)
my_docs = my_loader.load()
my_chunks = self.splitter.split_documents(my_docs)
gaps = []
for chunk in my_chunks[:20]: # 最初の20チャンクをクエリとして使用
# 競合のVectorStoreで類似チャンクを検索
similar = competitor_store.similarity_search_with_score(
chunk.page_content, k=top_k
)
for doc, score in similar:
# スコアが低い(類似度が低い)=自社にないコンテンツ
if score > 0.5: # FAISSのL2距離、値が大きいほど非類似
gaps.append({
"competitor_content": doc.page_content[:200],
"source": doc.metadata.get("source", ""),
"similarity_gap": score
})
# ギャップが大きいものをソート
gaps.sort(key=lambda x: x["similarity_gap"], reverse=True)
result = "## コンテンツギャップ分析結果\n\n"
result += "自社にないが競合にあるトピック(類似度が低いもの上位):\n\n"
for i, gap in enumerate(gaps[:10], 1):
result += f"{i}. [{gap['source']}]\n"
result += f" {gap['competitor_content']}\n\n"
return result
def calculate_content_overlap(
self, url_a: str, url_b: str
) -> float:
"""
2つのサイトのコンテンツ重複率を計算する(0.0〜1.0)
"""
loader_a = WebBaseLoader(url_a)
loader_b = WebBaseLoader(url_b)
docs_a = self.splitter.split_documents(loader_a.load())
docs_b = self.splitter.split_documents(loader_b.load())
# 各テキストのEmbeddingsを取得
texts_a = [doc.page_content for doc in docs_a[:10]]
texts_b = [doc.page_content for doc in docs_b[:10]]
vectors_a = self.embeddings.embed_documents(texts_a)
vectors_b = self.embeddings.embed_documents(texts_b)
# コサイン類似度の平均を計算
similarities = []
for va in vectors_a:
for vb in vectors_b:
va_np = np.array(va)
vb_np = np.array(vb)
cos_sim = np.dot(va_np, vb_np) / (
np.linalg.norm(va_np) * np.linalg.norm(vb_np)
)
similarities.append(cos_sim)
return float(np.mean(similarities))分析レポートが生成されたときにSlackのチャンネルに通知を送ることで、チーム全体で競合情報を素早く共有できます。Slack APIのIncoming Webhookを使います。
# notifier.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
def send_slack_notification(report_summary: str, report_path: str) -> bool:
"""
競合分析レポートをSlackに通知する
事前準備:
1. Slack APIの「Incoming Webhooks」でWebhook URLを取得
2. .envに SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx を設定
"""
webhook_url = os.getenv("SLACK_WEBHOOK_URL")
if not webhook_url:
print("⚠️ SLACK_WEBHOOK_URL が設定されていません")
return False
payload = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🔍 競合サイト自動分析レポート",
"emoji": True
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*レポートファイル:* `{report_path}`"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*分析サマリー:*\n{report_summary[:500]}..."
}
}
]
}
response = requests.post(webhook_url, json=payload)
if response.status_code == 200:
print("✅ Slack通知を送信しました")
return True
else:
print(f"❌ Slack通知の送信に失敗: {response.status_code}")
return False# schedule_analyzer.py - cronやGitHub Actionsで定期実行する例
import schedule
import time
from main import main
# 毎週月曜日の朝9時に自動実行
schedule.every().monday.at("09:00").do(main)
print("⏰ 競合分析スケジューラを起動しました(Ctrl+Cで停止)")
while True:
schedule.run_pending()
time.sleep(60)常時起動のサーバーがない場合は、GitHub Actionsのscheduleトリガーを使って無料でクラウド実行できます。毎週月曜日の朝9時(JST)に自動実行し、レポートをアーティファクトとして保存します。
# .github/workflows/competitor-analysis.yml
name: 競合サイト自動分析
on:
schedule:
- cron: '0 0 * * 1' # 毎週月曜日 09:00 JST(UTC 0:00)
workflow_dispatch: # 手動実行も可能
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: python main.py
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- uses: actions/upload-artifact@v4
with:
name: competitor-report
path: reports/
retention-days: 30 # 30日間保存競合サイトのスクレイピングでは、ネットワークエラー・HTTPエラー・サイト構造の変化・OpenAI APIのレート制限など、さまざまなエラーが発生します。プロダクションレベルの実装では適切なエラーハンドリングが不可欠です。
# robust_loader.py
import time
import logging
from typing import Optional
from langchain_community.document_loaders import WebBaseLoader
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def load_with_retry(
url: str,
max_retries: int = 3,
delay: float = 2.0
) -> Optional[list]:
"""
リトライ機能付きのWebBaseLoader
Args:
url: スクレイピング対象のURL
max_retries: 最大リトライ回数
delay: リトライ間の待機時間(秒)
"""
for attempt in range(max_retries + 1):
try:
time.sleep(delay) # リクエスト間隔
loader = WebBaseLoader(url)
documents = loader.load()
if not documents or not documents[0].page_content.strip():
raise ValueError(f"コンテンツが空でした: {url}")
logger.info(f"✅ 取得成功: {url} ({len(documents[0].page_content)}文字)")
return documents
except Exception as e:
if attempt == max_retries:
logger.error(f"❌ 最大リトライ回数に達しました: {url} - {e}")
return None
wait_time = delay * (2 ** attempt) # 指数バックオフ
logger.warning(
f"⚠️ 取得失敗 (試行 {attempt + 1}/{max_retries}): {e} "
f"→ {wait_time}秒後にリトライ"
)
time.sleep(wait_time)
return None# rate_limit_handler.py
import time
import openai
from langchain_openai import ChatOpenAI
def create_llm_with_retry() -> ChatOpenAI:
"""
レート制限を考慮したLLMクライアントを生成する
OpenAI APIのレート制限(2026年3月時点):
- GPT-4o mini: 500 RPM(リクエスト/分)、150,000 TPM(トークン/分)
- GPT-4o: 500 RPM、30,000 TPM(Tier 1の場合)
"""
return ChatOpenAI(
model="gpt-4o-mini",
temperature=0.1,
max_retries=5, # LangChain組み込みのリトライ
request_timeout=60, # タイムアウト設定
)
def safe_llm_invoke(llm: ChatOpenAI, prompt: str, max_retries: int = 3) -> str:
"""
レート制限エラーを処理しながらLLMを呼び出す
"""
for attempt in range(max_retries):
try:
response = llm.invoke(prompt)
return response.content
except openai.RateLimitError as e:
if attempt == max_retries - 1:
raise
# レート制限エラーの場合は60秒待機
wait_time = 60 * (attempt + 1)
print(f"⏳ レート制限に達しました。{wait_time}秒待機します...")
time.sleep(wait_time)
except openai.APIConnectionError as e:
if attempt == max_retries - 1:
raise
print(f"🔌 接続エラー: {e}. 10秒後にリトライ...")
time.sleep(10)
return ""競合分析ツールを実際に運用するにあたって、OpenAI APIのコストは重要な考慮点です。競合サイトを毎週5社分析した場合のコストを試算してみましょう。
| 処理 | 使用モデル | 想定トークン数 | コスト(週次) |
|---|---|---|---|
| コンテンツ要約(5サイト) | GPT-4o mini | 10,000トークン | 約$0.015 |
| キーワード抽出(5サイト) | GPT-4o mini | 15,000トークン | 約$0.023 |
| サービス分析(5サイト) | GPT-4o mini | 20,000トークン | 約$0.030 |
| 比較レポート生成(1回) | GPT-4o mini | 8,000トークン | 約$0.012 |
| Embeddings(5サイト) | text-embedding-3-small | 50,000トークン | 約$0.005 |
| 合計(週次) | — | 103,000トークン | 約$0.085 |
月次換算で約$0.34(約50円)。非常に低コストで競合分析の自動化が実現できます。もし高精度な分析が必要な場合はGPT-4oを使っても、月次で1,500〜3,000円程度に収まるはずです。
# cost_optimizer.py - コスト削減テクニック集
import hashlib
import json
import os
from datetime import datetime, timedelta
class CachedAnalyzer:
"""
キャッシュ機能付きの競合分析クラス
同じURLの分析結果を一定期間キャッシュして API呼び出しを減らす
"""
CACHE_DIR = ".analysis_cache"
CACHE_TTL_HOURS = 24 # キャッシュ有効期限(時間)
def __init__(self, analyzer):
self.analyzer = analyzer
os.makedirs(self.CACHE_DIR, exist_ok=True)
def _get_cache_key(self, url: str, analysis_type: str) -> str:
return hashlib.md5(f"{url}:{analysis_type}".encode()).hexdigest()
def _load_cache(self, cache_key: str) -> str | None:
cache_file = os.path.join(self.CACHE_DIR, f"{cache_key}.json")
if not os.path.exists(cache_file):
return None
with open(cache_file, "r", encoding="utf-8") as f:
data = json.load(f)
# TTLチェック
created_at = datetime.fromisoformat(data["created_at"])
if datetime.now() - created_at > timedelta(hours=self.CACHE_TTL_HOURS):
return None
return data["result"]
def _save_cache(self, cache_key: str, result: str):
cache_file = os.path.join(self.CACHE_DIR, f"{cache_key}.json")
with open(cache_file, "w", encoding="utf-8") as f:
json.dump({
"result": result,
"created_at": datetime.now().isoformat()
}, f, ensure_ascii=False)
def analyze_with_cache(self, url: str, analysis_type: str) -> str:
cache_key = self._get_cache_key(url, analysis_type)
# キャッシュヒット確認
cached = self._load_cache(cache_key)
if cached:
print(f" 💾 キャッシュから取得: {url} ({analysis_type})")
return cached
# APIを呼び出して分析
if analysis_type == "summary":
result = self.analyzer.summarize_content(url)
elif analysis_type == "keywords":
result = self.analyzer.extract_keywords(url)
elif analysis_type == "features":
result = self.analyzer.analyze_service_features(url)
else:
raise ValueError(f"未知の分析タイプ: {analysis_type}")
# キャッシュに保存
self._save_cache(cache_key, result)
return resultSaaS企業のマーケティング担当者が、競合他社のランディングページを毎月分析してレポートにまとめる作業を自動化した例です。従来は1社あたり1〜2時間かかっていた分析が、スクリプト実行の20〜30分に短縮されました(API呼び出し時間を含む)。
特に有効だったのが「キャッチコピーの変化検知」です。前週と今週のコンテンツを比較することで、「競合がA/Bテストを開始した」「価格表の訴求を変更した」などの変化を素早く把握できるようになりました。
ブログメディアの編集チームが、競合メディアの記事一覧ページを定期的にクロールしてキーワードトレンドを分析する用途にも活用できます。競合が最近注力しているトピック(Embeddingsの類似度分析で検出)と自社コンテンツの重複率を把握することで、「競合が書いていないが検索需要がある領域」を狙った記事企画が立てやすくなります。
競合他社の採用ページをモニタリングすることで、「エンジニアの大量採用を開始した」「新しい事業領域(MLOps)の求人が出た」などの戦略的な変化を早期に察知できます。この情報は事業戦略の意思決定に活用できます。
WebBaseLoaderは静的HTMLの取得に特化しており、JavaScriptレンダリングが必要なSPAには対応していません。動的ページには SeleniumURLLoader や PlaywrightURLLoader(LangChain Community に含まれる)を使ってください。
# PlaywrightURLLoader の使用例(動的ページ対応)
# pip install playwright && playwright install chromium
from langchain_community.document_loaders import PlaywrightURLLoader
loader = PlaywrightURLLoader(
urls=["https://spa-competitor.com/services/"],
remove_selectors=["nav", "footer", ".ads"] # 除外する要素
)
documents = loader.load()robots.txtの指示には従う必要があります。Disallowに指定されたURLへのアクセスは利用規約違反や不正アクセスとみなされる可能性があります。また、短時間に大量のリクエストを送るとサーバーに負荷をかけるため、リクエスト間隔(time.sleep())を設けることを強く推奨します。自動化ツールを実装する際は、robots.txtの確認・適切なリクエスト間隔・User-Agentの正直な設定(自社ツールであることを示す)の3点を徹底してください。
LangChainはAgents・Chains・Tool連携など「LLMを使ったアプリケーション全般」を構築するフレームワークです。LlamaIndexはRAG(文書検索×LLM)の構築に特化しており、大量文書のインデックス管理が得意です。競合分析のような「Webからデータを取得→LLMで処理」というフローではLangChainが適しています。一方、大量の社内ドキュメントを検索可能にしてLLMで回答させるシステムにはLlamaIndexが向いています。
使えます。LangChainはAnthropicのClaude・GoogleのGemini・Meta Llama・MistralなどさまざまなLLMに対応しています。ChatOpenAIの部分をChatAnthropicなどに差し替えるだけでほぼ同じコードが動きます。ローカルで動作するOllama経由のモデルも利用できるため、APIコストを完全にゼロにすることも可能です。
# Claudeを使う場合
# pip install langchain-anthropic
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(
model="claude-3-5-sonnet-20241022",
temperature=0.1
)
# Ollamaでローカル実行する場合(APIコスト0円)
# ollama pull llama3.2 で事前にモデルをダウンロード
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3.2", temperature=0.1)精度向上のためには以下のアプローチが有効です。①プロンプトの改善:分析観点を具体的に指定し、出力フォーマットをJSONや箇条書きで制約する。②モデルのアップグレード:GPT-4o miniからGPT-4oに変更することで分析の深さが向上します。③コンテキストの充実:取得するチャンク数を増やすか、自社のサービス説明をシステムプロンプトに追加することで、より文脈を踏まえた分析になります。④Few-shotプロンプト:過去の分析レポートの例をプロンプトに含めることで出力品質が安定します。



競合サイトの分析をAIで自動化できるんですね!週次で自動レポートが届く仕組みを作りたいです。Embeddingsでコンテンツギャップを発見できるのも面白いです!



GitHub Actionsと組み合わせれば完全自動化できるんじゃ!一度設定すれば毎週月曜日に競合レポートが生成されてくるので、戦略立案に集中できるようになるぞい。コスト面でも月数十円〜数百円で運用できるから、費用対効果は抜群じゃ。WithCodeでPythonとAPI連携を学べば、このような自動化ツールを自分で作れるようになるんじゃ!
bs_kwargsでスクレイピング対象要素を絞り込み、header_templateでUser-Agentをカスタマイズできる。separatorsに句点・読点を指定したRecursiveCharacterTextSplitterが推奨。LangChain WebBaseLoaderで競合分析を自動化することで、毎週数時間かかっていたリサーチ作業を数分に短縮できます。Embeddingsを活用したコンテンツギャップ分析や、GitHub Actionsによる完全自動化まで、Pythonの基礎があれば今日から実装できます。


副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
公式サイト より
今すぐ
無料カウンセリング
を予約!