![見出し画像](https://assets.st-note.com/production/uploads/images/159069262/rectangle_large_type_2_1a55f0cdfc331ba1eab17e72c74e815d.png?width=1200)
900行のコードをノーミスで出力するClaude 3.5 Sonnet (New) やるなお主
こんにちは!
ノーリーです。ClaudeやChatGPT、Gemini使ってますか?
今朝リリースされた、Claude 3.5 Sonnet (New)のコード生成能力を味う記事です。
では、まったり参りましょう!
1.公式情報
Claude 3.5 Sonnetは、コーディング能力において大きな進化を遂げたAIモデルだそうです。このモデルの新機能と改善点は以下の通りです。
強化されたコーディング支援: Claude 3.5 Sonnetは、JavaScriptやPythonなどの様々なプログラミング言語でコード生成する能力に優れています。簡単なコード補完から複雑な問題解決シナリオまで対応可能で、開発プロセスを大幅に効率化できます。
問題解決能力の向上: HumanEvalベンチマークで64%の問題を解決する能力を示し、前バージョンのClaude 3 Opusの38%から大幅に向上しました。これにより、コーディングロジックの理解力と文脈認識が強化されています。
文脈理解の向上: 200,000トークン(約150,000語)のコンテキストウィンドウを持ち、長い対話を通じて文脈を維持し、プロジェクト要件を深く理解することが可能です。
構造化されたコーディングプロンプト: コードレビュー、計画、出力生成、セキュリティレビューなど、コーディングプロセスの各段階をガイドする構造化されたシステムプロンプトが導入されています。これにより、AIは文脈を維持しつつ、高品質なコードを生成します。
リアルタイムコラボレーション機能: 実験的な機能として、サイドバーインターフェースでコードを生成し実行する機能が含まれました。これにより、開発環境内でのリアルタイムのコラボレーションとテストが容易になり、開発者が迅速にコードを反復することが可能だそうです。
2.実力チェック
公式の能書きはこれくらいにして、実際に今朝私が体験したコーディング体験を共有します。900行のPythonプログラムをほぼノーミスで出力することができていました。900行と言えば、A4で15ページくらいです。ついこないだまで、せいぜいA4で1ページくらいなら出せるかな、というところでしたが、一気に15倍の能力になっています。
(1)目的
本校、清風情報工科学院の就職活動を支援すべく、目標企業の情報を集めるエージェントを作りたいと考えました。
就活に必要となる企業の情報や、求人情報、できれば口コミ情報などを集めたい。集めておいて、その情報はGoogleのNotebookLMに入れて、企業情報とチャットできるようにしたい。そうなると、就活時に企業のことを効率的に調べて、面接の準備などがしやすくなる。そういう発想です。
NotebookLMに必要なのは、企業の情報のURLです。残念なことにNotebookLMはURLが示す1ページの情報しかとってくれませんので、必要なページのURLは細かくリストアップしておかなければなりません。
ということで、企業名を入れたら、関連情報のURLリストを引っ張ってきてくれるエージェントをつくってみました。
(2)プロンプト
コーディングのプロンプトはどうしたんだ?ということろですが、だらだらとしたいことを書いて、やりとりしました。一発出しにこだわったわけではなく、設計を対話的に行った感じです。では、早速参ります。
専門学校生の就職活動を支援するため、目標とする企業についての情報を集めるエージェントを作りたい。まず公式情報のURLを集める。企業の基本情報やプレスリリース、主な事業のページ、求人情報のページなどである。この際、ページ単位で集める。求人情報についてはさらに細かいページについてもそのURLを集める。企業の公式ページ内の情報だけでなく、リクナビやマイナビなどの就活サイトにあるその企業の公式情報も集める。動画サイトにある就活向け動画もURLを集める。 更に、その企業の労働環境やインターン情報、就活で聞かれる情報や採用プロセスなどの口コミ情報もそれぞれURLを集める。 最終的に、公式情報と非公式情報に分けて、URLとそのページの概要(20字以内)をリストにしてユーザーに提示する。 URLは合計10以上50以内にする。 こういうエージェントの設計方針を考えて。
すると、クロさん(私はClaudeをこう呼んでます)は、設計方針を語りつつ、さっさとコードも書いてくれました。コードの方は骨格だけです(省略)。
このエージェントの主な特徴と設計方針について説明します:
基本構造
・データクラスとEnumを使用して情報を厳格に型付け
・情報タイプ(公式/非公式)とコンテンツカテゴリを明確に分類
・ページ情報を統一されたフォーマットで管理
収集方針
・公式情報と非公式情報を明確に区別して収集
・複数の情報源から横断的に情報を収集
・カテゴリごとにURLの収集方法を最適化
品質管理
・URLの重複を除去
・10-50件の制限を遵守
・カテゴリバランスを考慮した選択アルゴリズム
拡張性
・新しい情報源の追加が容易
・クローリングルールのカスタマイズが可能
・異なる就活サイトへの対応が容易
実装する際の注意点:
-クローリング制限の考慮
・robots.txtの遵守
・アクセス間隔の適切な設定
・サイトごとの利用規約確認
-情報の鮮度
・最終更新日のチェック
・定期的な情報更新の仕組み
-エラー処理
・サイトアクセス失敗時の適切な処理
・不完全なデータの扱い
このエージェントを改善するための提案はありますか?
ぱっと目にしたコードの次の部分が気になりました。
def _crawl_company_site(self, domain: str) -> None:
"""企業サイトのクローリング(実装例)"""
basic_paths = {
"/company/": ContentCategory.COMPANY_PROFILE,
"/news/": ContentCategory.PRESS_RELEASE,
"/business/": ContentCategory.BUSINESS,
"/recruit/": ContentCategory.RECRUITMENT,
"/internship/": ContentCategory.INTERNSHIP
}
# 実際のクローリング処理を実装
パスを決め打ちなのかなと思ったからです。そこで次のようにプロンプトを出しました。
crawlcompany_siteについては、決め打ちのサブページリンクだけでなく、ホームページを解析して、リンクとそのラベルからも情報を得た方がいい。その中で、就職活動に役立つ情報を中心に抽出したい。
すると、改善したようで「はい、その提案はとても重要です。より柔軟で効果的な情報収集のために、クローリング部分を改善しましょう。」といいつつ、コードを書き直してきました(省略)。よくわからなかったですが、まぁ多分いいんだろ、ということで、こちらのリクエストを追加します。
出力時には、URLは完全URLで出力し、
https://www.ugu.com/recruit/info/ うぐ社採用情報の企業情報
などのようにURLが見える形で出力させて。
こちらは、NotebookLMに情報を持っていく際に、コピペしやすいようにするためのリクエストです。すると、出力例を作ってきました。
【公式情報】
≪企業概要≫
https://www.ugu.com/company/ うぐ社の企業理念
https://www.ugu.com/about/history/ うぐ社のあゆみ
≪採用情報≫
https://www.ugu.com/recruit/info/ うぐ社採用情報
https://www.ugu.com/recruit/newgraduate/ 新卒採用情報
https://www.ugu.com/recruit/career/ キャリア採用情報
≪求人詳細≫
https://www.ugu.com/recruit/newgraduate/tech/ 技術職募集要項
https://www.ugu.com/recruit/newgraduate/sales/ 営業職募集要項
【非公式情報】
≪社風・労働環境≫
https://www.vorkers.com/company/ugu/ うぐ社の評判・口コミ
https://www.kaisha-hyouban.com/ugu/ 社員の声・評価
≪面接情報≫
https://job.mynavi.jp/conts/2025/interview/ugu/ うぐ社の面接体験記
うん、ええ感じ、ということで、GOサインを出します。
いいね。これで、コード全体を出力して。
これでずらずらーーーーーーとPythonのコードが吐き出されました(省略)。長すぎて出力が途中で中断したので、「つづけて」と促して、最後まで出してもらいました。これを、Cursorに張り付けて、実行してみると動くようでした。しかし、「https://www.うぐ社.co.jp/」にアクセスしようとしていたので、修正することにしました。
最初に、ユーザーから日本語で社名が与えられるので、その公式サイトを調べる必要がある。その公式サイトを元に調べて行ってはどうか?他にいい方法があれば提案して。
すると、公式サイトの特定方法を提案し、またそのコードを書いてきます(省略)。
この改善版の主な特徴は:
複数の手法の組み合わせ
・法人番号データベースの活用
・Google検索の利用
・サイトの内容検証
・スコアリングによる順位付け
詳細な企業情報の収集
・公式サイトURL
・法人番号
・企業の別名
・SNSアカウント
堅牢性の向上
・複数の検証方法
・エラーハンドリング
・タイムアウト設定
・アクセス間隔の制御
拡張性
・新しい検証基準の追加が容易
・異なる情報源の追加が可能
・スコアリング基準の調整が可能
このCompanySiteFinderクラスを既存のJobSearchAgentクラスと組み合わせて使用することで、より信頼性の高い情報収集が可能になります。
え、こんなすごい話になってんのと思いつつ、もっともらしいので、次のように唱えました。
方法はこれでいい。 この公式HPの特定を元のシステムに統合して。
なんか、めちゃぶり上司ですね!
すると、「統合版就活情報収集エージェント」というコードが生成されました。使用例として次の様なものも生成されています。
>>> agent = JobSearchAgent("サイバーエージェント")
>>> agent.collect_all_information()
1. サイバーエージェントの公式サイト情報収集を開始...
2. 就活サイトからの情報収集を開始...
3. 口コミ情報の収集を開始...
4. SNSアカウントからの情報収集を開始...
>>> agent.print_results()
「このように統合することで、より確実で包括的な情報収集が可能になりました。改善点や追加機能のご要望はありますか?」と問われたので、よくわからんけど、ゴーサインを改めて出しました。
これでいい。実際に動かしてみる。 コードに省略箇所がある場合、省略なしで全部出して。
「コードに省略箇所がある場合、省略なしで全部出して。」と追記してあるのは、出力されていたコードは省略箇所のあるものだったからです。
するとどうでしょう。コードは長くなっていたようで、コードブロック3回分にわたる長大なプログラムが生成されるではありませんか。クロさんからも分割するねという断りが入っています。
エージェントのコード全体を、省略なしで完全なものとして出力するために、メソッドの詳細実装まで含めて提供したいと思います。コードが長くなるため、分割して投稿します。まずは前半部分です:
これも先のと同様「続けて」「続けて」と促して、3部に分かれたコード全体を出力させました。
ご丁寧に、クロさんからは次のようにダメ押しが。
これで完全な実装が完了しました。実際に使用する際の注意点:
・必要なライブラリのインストール:
bash
pip install requests beautifulsoup4
・各サイトのロボット規約を確認し、必要に応じてアクセス間隔を調整してください。
・実際の使用例:
python
agent = JobSearchAgent("サイバーエージェント") agent.collect_all_information() agent.print_results()
改善や追加機能のご要望はありますか?
ということでコード生成は終わりです。以下、生成されたコードを示します。
(3)生成されたコード
コードは900行もありましたが、とりあえず動くようでした。
コードは、WEBクローラーとして動作し、特にLLMを利用するコードにはなっていません。また、特に外部サイトのAPIキーはなくとも動くプログラムになっていました。
from dataclasses import dataclass
from typing import List, Optional, Dict, Set
from enum import Enum
import urllib.parse
import re
from bs4 import BeautifulSoup
import requests
from urllib.parse import urljoin, urlparse
import time
import json
from datetime import datetime
# --- 基本データ構造の定義 ---
@dataclass
class CompanyInfo:
name: str
official_url: str
corporate_number: Optional[str] = None
alternative_names: List[str] = None
sns_links: Dict[str, str] = None
class InfoType(Enum):
OFFICIAL = "公式情報"
UNOFFICIAL = "非公式情報"
class ContentCategory(Enum):
COMPANY_PROFILE = "企業概要"
PRESS_RELEASE = "プレスリリース"
BUSINESS = "事業内容"
RECRUITMENT = "採用情報"
JOB_LISTING = "求人詳細"
INTERNSHIP = "インターン"
COMPANY_CULTURE = "社風・労働環境"
INTERVIEW = "面接情報"
RECRUITING_PROCESS = "採用プロセス"
PR_VIDEO = "企業PR動画"
@dataclass
class PageInfo:
url: str
title: str
info_type: InfoType
category: ContentCategory
source_site: str
class CompanySiteFinder:
"""企業の公式サイトを特定するクラス"""
def __init__(self):
self.google_search_url = "https://www.google.com/search"
self.corporate_search_url = "https://api.houjin-bangou.nta.go.jp/4/name"
self.official_patterns = [
r'会社概要',
r'企業情報',
r'IR情報',
r'採用情報',
r'リクルート',
r'recruit',
r'about\s*us',
]
self.exclude_domains = {
'facebook.com', 'twitter.com', 'linkedin.com', 'youtube.com',
'instagram.com', 'bloomberg.co.jp', 'wikipedia.org'
}
def find_company_site(self, company_name: str) -> CompanyInfo:
"""複数の手法を組み合わせて企業の公式サイトを特定"""
company_info = CompanyInfo(name=company_name, official_url="")
# 1. 法人番号データベースで検索
corporate_info = self._search_corporate_number(company_name)
if corporate_info:
company_info.corporate_number = corporate_info.get('corporate_number')
if 'official_url' in corporate_info:
if self._verify_official_site(corporate_info['official_url']):
company_info.official_url = corporate_info['official_url']
return company_info
# 2. Google検索による特定
google_results = self._google_search(company_name)
# 3. 検索結果の検証と順位付け
ranked_sites = self._rank_potential_sites(google_results, company_name)
if ranked_sites:
top_site = ranked_sites[0]
company_info.official_url = top_site
# 4. SNSアカウントの収集
company_info.sns_links = self._collect_sns_links(top_site)
# 5. 企業の別名収集
company_info.alternative_names = self._collect_alternative_names(top_site)
return company_info
def _search_corporate_number(self, company_name: str) -> Optional[dict]:
"""法人番号公表サイトAPIを使用して企業情報を検索"""
try:
params = {
'name': company_name,
'type': '12', # 文字列に変更
'format': 'json'
}
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(
self.corporate_search_url,
params=params,
headers=headers,
timeout=10
)
response.raise_for_status()
data = response.json()
if data.get('count', 0) > 0:
return data['corporations'][0]
except requests.exceptions.RequestException as e:
print(f"法人番号検索エラー: {str(e)}")
return None
def _google_search(self, company_name: str) -> List[str]:
"""Google検索を使用して候補となるURLを収集"""
search_query = f"{company_name} (会社概要 OR 企業情報 OR 採用情報)"
urls = []
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
params = {
'q': search_query,
'num': 10,
'hl': 'ja',
'cr': 'countryJP'
}
response = requests.get(
self.google_search_url,
params=params,
headers=headers,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
for result in soup.find_all('a'):
href = result.get('href', '')
if href.startswith('/url?q='):
url = href.split('/url?q=')[1].split('&')[0]
if not any(domain in url for domain in self.exclude_domains):
urls.append(url)
return urls
except Exception as e:
print(f"Google検索エラー: {str(e)}")
return []
def _rank_potential_sites(self, urls: List[str], company_name: str) -> List[str]:
"""候補サイトを検証し、順位付け"""
site_scores = []
for url in urls:
score = 0
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# スコアリング基準
# 1. URLにcompany_nameが含まれる
domain = urlparse(url).netloc
if company_name.lower() in domain.lower():
score += 5
# 2. 公式サイトパターンの存在
for pattern in self.official_patterns:
if re.search(pattern, response.text, re.IGNORECASE):
score += 2
# 3. HTMLの構造評価
if soup.find('meta', {'name': 'copyright'}):
score += 2
if soup.find('link', {'rel': 'canonical'}):
score += 1
# 4. 会社名の出現頻度
company_name_count = len(re.findall(company_name, response.text))
score += min(company_name_count // 5, 3)
# 5. セキュリティ評価
if url.startswith('https://'):
score += 1
site_scores.append((url, score))
time.sleep(1)
except Exception as e:
print(f"サイト検証エラー {url}: {str(e)}")
# スコアで並び替えて上位のURLを返す
return [url for url, score in sorted(site_scores, key=lambda x: x[1], reverse=True)]
def _verify_official_site(self, url: str) -> bool:
"""URLが有効な公式サイトかどうかを確認"""
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return True
except:
return False
def _collect_sns_links(self, url: str) -> Dict[str, str]:
"""公式サイトからSNSアカウントのリンクを収集"""
sns_links = {}
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
sns_patterns = {
'twitter': r'twitter\.com/([^/]+)',
'facebook': r'facebook\.com/([^/]+)',
'linkedin': r'linkedin\.com/company/([^/]+)',
'youtube': r'youtube\.com/([^/]+)',
'instagram': r'instagram\.com/([^/]+)'
}
for link in soup.find_all('a', href=True):
href = link['href']
for sns, pattern in sns_patterns.items():
match = re.search(pattern, href)
if match:
sns_links[sns] = href
except Exception as e:
print(f"SNSリンク収集エラー: {str(e)}")
return sns_links
def _collect_alternative_names(self, url: str) -> List[str]:
"""公式サイトから企業の別名を収集"""
names = []
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 英語名の探索
english_name = soup.find('meta', {'name': ['company_english', 'company-english']})
if english_name:
names.append(english_name['content'])
# 略称の探索
for tag in soup.find_all(['p', 'div']):
text = tag.get_text()
# 略称パターンの例: "○○(略称:××)"
match = re.search(r'略称[::]\s*(\S+)', text)
if match:
names.append(match.group(1))
# 英語表記パターン
match = re.search(r'[((]([A-Za-z\s,\.]+)[))]', text)
if match and len(match.group(1)) > 2:
names.append(match.group(1).strip())
except Exception as e:
print(f"別名収集エラー: {str(e)}")
return list(set(names)) # 重複を除去
class LinkAnalyzer:
"""リンクとそのコンテキスト情報を分析するクラス"""
def __init__(self):
# 就活関連のキーワードパターン
self.recruitment_patterns = {
ContentCategory.COMPANY_PROFILE: [
r'企業情報', r'会社概要', r'私たちについて', r'about\s*us',
r'ビジョン', r'理念', r'mission', r'philosophy',
r'企業理念', r'経営理念'
],
ContentCategory.PRESS_RELEASE: [
r'ニュース', r'プレスリリース', r'news', r'press',
r'お知らせ', r'リリース', r'media'
],
ContentCategory.BUSINESS: [
r'事業紹介', r'サービス', r'製品', r'business', r'service',
r'ソリューション', r'solution', r'product', r'事業案内',
r'取り扱い', r'サービス一覧'
],
ContentCategory.RECRUITMENT: [
r'採用', r'求人', r'キャリア', r'recruit', r'career',
r'新卒採用', r'中途採用', r'job', r'採用情報',
r'エントリー', r'募集要項'
],
ContentCategory.INTERNSHIP: [
r'インターン', r'intern', r'体験', r'短期留学',
r'サマーインターン', r'就業体験', r'学生インターン'
],
ContentCategory.PR_VIDEO: [
r'動画', r'ムービー', r'video', r'チャンネル',
r'採用動画', r'企業紹介.*動画', r'会社紹介.*動画'
]
}
# 無視するパターン
self.ignore_patterns = [
r'login', r'contact', r'inquiry', r'support',
r'privacy', r'terms', r'sitemap', r'search',
r'faq', r'help', r'guide', r'download'
]
def categorize_link(self, url: str, link_text: str, surrounding_text: str) -> Optional[ContentCategory]:
"""リンクのカテゴリを判定"""
# URLと文字列を結合して一つのコンテキストとして扱う
context = f"{url} {link_text} {surrounding_text}".lower()
# 無視すべきパターンをチェック
if any(re.search(pattern, context) for pattern in self.ignore_patterns):
return None
# カテゴリごとのパターンマッチング
for category, patterns in self.recruitment_patterns.items():
if any(re.search(pattern, context, re.IGNORECASE) for pattern in patterns):
return category
return None
def extract_surrounding_text(self, link_element) -> str:
"""リンクの前後のテキストを抽出"""
if not link_element.parent:
return ''
# リンクの親要素からテキストを抽出
surrounding_text = []
# 前の要素のテキスト
prev_sibling = link_element.find_previous_sibling()
if prev_sibling:
surrounding_text.append(prev_sibling.get_text(strip=True))
# 親要素のテキスト
parent_text = link_element.parent.get_text(strip=True)
if parent_text:
surrounding_text.append(parent_text)
# 次の要素のテキスト
next_sibling = link_element.find_next_sibling()
if next_sibling:
surrounding_text.append(next_sibling.get_text(strip=True))
return ' '.join(surrounding_text)
class JobSearchAgent:
"""就活情報収集エージェント"""
def __init__(self, company_name: str):
"""
企業名から情報収集を開始する際の初期化
"""
print(f"エージェントの初期化を開始: {company_name}")
self.company_name = company_name
self.pages: List[PageInfo] = []
self.visited_urls: Set[str] = set()
self.link_analyzer = LinkAnalyzer()
# 公式サイトの特定
print("公式サイトの特定を開始...")
finder = CompanySiteFinder()
self.company_info = finder.find_company_site(company_name)
if not self.company_info.official_url:
raise ValueError(f"企業 {company_name} の公式サイトが特定できませんでした。")
print(f"公式サイトを特定: {self.company_info.official_url}")
# 就活関連サイト
self.job_sites = {
"リクナビ": "https://job.rikunabi.com/",
"マイナビ": "https://job.mynavi.jp/",
"type就活": "https://type.jp/",
"LinkedIn": "https://www.linkedin.com/"
}
# 口コミサイト
self.review_sites = {
"キャリコネ": "https://careerconnection.jp/",
"転職会議": "https://jobtalk.jp/",
"みんなの就職活動": "https://www.minsyukatsu.jp/"
}
# ログ用のタイムスタンプ
self.start_time = datetime.now()
def collect_all_information(self) -> None:
"""全ての情報収集を実行"""
try:
print(f"\n1. {self.company_name}の公式サイト情報収集を開始...")
self._crawl_company_site(self.company_info.official_url)
print("\n2. 就活サイトからの情報収集を開始...")
self.collect_job_site_info()
print("\n3. 口コミ情報の収集を開始...")
self.collect_review_site_info()
print("\n4. SNSアカウントからの情報収集を開始...")
if self.company_info.sns_links:
self.collect_sns_info()
print("\n情報収集が完了しました。")
self._log_collection_stats()
except Exception as e:
print(f"\nエラーが発生しました: {str(e)}")
raise
def _crawl_company_site(self, base_url: str) -> None:
"""企業の公式サイトをクロール"""
try:
queue = [(base_url, 0)] # (URL, depth)
max_depth = 3
processed_count = 0
while queue and len(self.visited_urls) < 100: # 最大100ページまで
current_url, depth = queue.pop(0)
if current_url in self.visited_urls or depth > max_depth:
continue
print(f"クロール中: {current_url}")
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(current_url, headers=headers, timeout=10)
response.raise_for_status()
self.visited_urls.add(current_url)
processed_count += 1
soup = BeautifulSoup(response.text, 'html.parser')
page_title = soup.title.string if soup.title else ""
# リンクの分析
for link in soup.find_all('a', href=True):
href = link.get('href')
absolute_url = urljoin(current_url, href)
if not self._is_same_domain(absolute_url, base_url):
continue
link_text = link.get_text(strip=True)
surrounding_text = self.link_analyzer.extract_surrounding_text(link)
category = self.link_analyzer.categorize_link(
absolute_url, link_text, surrounding_text
)
if category:
# ページ情報を保存
page_info = PageInfo(
url=absolute_url,
title=self._create_page_title(link_text, page_title),
info_type=InfoType.OFFICIAL,
category=category,
source_site="公式サイト"
)
self.pages.append(page_info)
# 採用ページは優先的に探索
priority = 1 if category == ContentCategory.RECRUITMENT else 0
if (absolute_url, depth + 1 - priority) not in queue:
queue.append((absolute_url, depth + 1 - priority))
time.sleep(1) # アクセス間隔の制御
except requests.RequestException as e:
print(f"ページアクセスエラー {current_url}: {str(e)}")
continue
print(f"公式サイトのクロール完了: {processed_count}ページを処理")
except Exception as e:
print(f"公式サイトのクロール中にエラー: {str(e)}")
raise
def collect_job_site_info(self) -> None:
"""就活サイトからの情報収集"""
for site_name, base_url in self.job_sites.items():
try:
print(f"{site_name}から情報収集中...")
search_url = self._build_job_site_search_url(site_name, base_url)
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(search_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
self._parse_job_site(site_name, soup, base_url)
time.sleep(2) # サイト間のアクセス間隔
except Exception as e:
print(f"{site_name}からの情報収集中にエラー: {str(e)}")
continue
def collect_review_site_info(self) -> None:
"""口コミサイトからの情報収集"""
for site_name, base_url in self.review_sites.items():
try:
print(f"{site_name}から口コミを収集中...")
search_url = self._build_review_site_search_url(site_name, base_url)
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(search_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
self._parse_review_site(site_name, soup, base_url)
time.sleep(2)
except Exception as e:
print(f"{site_name}からの口コミ収集中にエラー: {str(e)}")
continue
def collect_sns_info(self) -> None:
"""SNSアカウントからの情報収集"""
for sns_type, sns_url in self.company_info.sns_links.items():
try:
print(f"{sns_type}から情報収集中...")
if sns_type == "twitter":
self._collect_twitter_info(sns_url)
elif sns_type == "facebook":
self._collect_facebook_info(sns_url)
elif sns_type == "linkedin":
self._collect_linkedin_info(sns_url)
time.sleep(2)
except Exception as e:
print(f"{sns_type}からの情報収集中にエラー: {str(e)}")
continue
def _build_job_site_search_url(self, site_name: str, base_url: str) -> str:
"""就活サイトの検索URL構築"""
encoded_name = urllib.parse.quote(self.company_name)
if site_name == "リクナビ":
return f"{base_url}2025/company/r893431089/"
elif site_name == "マイナビ":
return f"{base_url}25/search/company/{encoded_name}/"
elif site_name == "type就活":
return f"{base_url}company/search?keyword={encoded_name}"
else:
return f"{base_url}search?keywords={encoded_name}"
def _build_review_site_search_url(self, site_name: str, base_url: str) -> str:
"""口コミサイトの検索URL構築"""
encoded_name = urllib.parse.quote(self.company_name)
if site_name == "キャリコネ":
return f"{base_url}company/search?keyword={encoded_name}"
elif site_name == "転職会議":
return f"{base_url}search?k={encoded_name}"
else:
return f"{base_url}search/company/?keyword={encoded_name}"
def _parse_job_site(self, site_name: str, soup: BeautifulSoup, base_url: str) -> None:
"""就活サイトの解析"""
# 企業情報ページの特定
company_info_elements = soup.find_all(class_=re.compile(r'company|corporation|employer'))
for element in company_info_elements:
links = element.find_all('a', href=True)
for link in links:
href = link.get('href')
absolute_url = urljoin(base_url, href)
link_text = link.get_text(strip=True)
# カテゴリの判定
if re.search(r'企業情報|会社概要', link_text):
category = ContentCategory.COMPANY_PROFILE
elif re.search(r'採用情報|募集要項', link_text):
category = ContentCategory.RECRUITMENT
elif re.search(r'インターン', link_text):
category = ContentCategory.INTERNSHIP
else:
continue
page_info = PageInfo(
url=absolute_url,
title=self._create_page_title(link_text, ""),
info_type=InfoType.OFFICIAL,
category=category,
source_site=site_name
)
self.pages.append(page_info)
def _parse_review_site(self, site_name: str, soup: BeautifulSoup, base_url: str) -> None:
"""口コミサイトの解析"""
# 口コミ情報の抽出
review_sections = soup.find_all(class_=re.compile(r'review|comment|feedback|kuchikomi'))
for section in review_sections:
try:
# タイトルの取得
title = section.get_text()[:20] # 20文字以内に制限
# URLの取得
link = section.find('a', href=True)
if not link:
continue
url = urljoin(base_url, link['href'])
# カテゴリの判定
category = self._determine_review_category(title, section.get_text())
page_info = PageInfo(
url=url,
title=title,
info_type=InfoType.UNOFFICIAL,
category=category,
source_site=site_name
)
self.pages.append(page_info)
except Exception as e:
print(f"口コミ解析エラー: {str(e)}")
continue
def _determine_review_category(self, title: str, content: str) -> ContentCategory:
"""口コミの内容からカテゴリを判定"""
text = f"{title} {content}".lower()
category_patterns = {
ContentCategory.INTERVIEW: [
r'面接', r'選考', r'interview', r'質問', r'グループディスカッション',
r'筆記試験', r'適性検査'
],
ContentCategory.COMPANY_CULTURE: [
r'環境', r'文化', r'雰囲気', r'働き方', r'残業', r'福利厚生',
r'社風', r'職場', r'評判'
],
ContentCategory.RECRUITING_PROCESS: [
r'プロセス', r'採用フロー', r'選考フロー', r'エントリー',
r'応募', r'説明会', r'インターン'
]
}
for category, patterns in category_patterns.items():
if any(re.search(pattern, text) for pattern in patterns):
return category
# デフォルトは社風・労働環境
return ContentCategory.COMPANY_CULTURE
def _collect_twitter_info(self, twitter_url: str) -> None:
"""Twitterからの情報収集"""
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(twitter_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 採用アカウントかどうかを判定
is_recruiting = any(keyword in twitter_url.lower() for keyword in ['recruit', 'hiring', 'career', '採用'])
page_info = PageInfo(
url=twitter_url,
title=f"{self.company_name} {'採用情報' if is_recruiting else '公式'}Twitter",
info_type=InfoType.OFFICIAL,
category=ContentCategory.RECRUITMENT if is_recruiting else ContentCategory.COMPANY_PROFILE,
source_site="Twitter"
)
self.pages.append(page_info)
except Exception as e:
print(f"Twitter情報収集エラー: {str(e)}")
def _collect_facebook_info(self, facebook_url: str) -> None:
"""Facebookからの情報収集"""
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(facebook_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 採用ページかどうかを判定
is_recruiting = any(keyword in facebook_url.lower() for keyword in ['recruit', 'hiring', 'career', '採用'])
page_info = PageInfo(
url=facebook_url,
title=f"{self.company_name} {'採用情報' if is_recruiting else '公式'}Facebook",
info_type=InfoType.OFFICIAL,
category=ContentCategory.RECRUITMENT if is_recruiting else ContentCategory.COMPANY_PROFILE,
source_site="Facebook"
)
self.pages.append(page_info)
except Exception as e:
print(f"Facebook情報収集エラー: {str(e)}")
def _collect_linkedin_info(self, linkedin_url: str) -> None:
"""LinkedInからの情報収集"""
try:
headers = {
'User-Agent': 'CompanyResearchBot/1.0'
}
response = requests.get(linkedin_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
page_info = PageInfo(
url=linkedin_url,
title=f"{self.company_name} LinkedIn企業ページ",
info_type=InfoType.OFFICIAL,
category=ContentCategory.COMPANY_PROFILE,
source_site="LinkedIn"
)
self.pages.append(page_info)
except Exception as e:
print(f"LinkedIn情報収集エラー: {str(e)}")
def _is_same_domain(self, url: str, base_url: str) -> bool:
"""同一ドメインかどうかをチェック"""
try:
parsed_url = urlparse(url)
parsed_base = urlparse(base_url)
return parsed_url.netloc == parsed_base.netloc or \
parsed_url.netloc.endswith('.' + parsed_base.netloc)
except:
return False
def _create_page_title(self, link_text: str, page_title: str) -> str:
"""ページタイトルの生成(20文字以内)"""
# リンクテキストを優先
title = link_text if link_text else page_title
# 空白文字の正規化
title = ' '.join(title.split())
if len(title) <= 20:
return title
return title[:19] + "…"
def filter_results(self, min_urls: int = 10, max_urls: int = 50) -> List[PageInfo]:
"""結果のフィルタリングと制限"""
# 重複除去
unique_pages = list({page.url: page for page in self.pages}.values())
# URLの数を制限
if len(unique_pages) > max_urls:
return self._select_balanced_urls(unique_pages, max_urls)
elif len(unique_pages) < min_urls:
print(f"Warning: Only {len(unique_pages)} URLs found")
return unique_pages
def _select_balanced_urls(self, pages: List[PageInfo], max_urls: int) -> List[PageInfo]:
"""カテゴリバランスを考慮したURL選択"""
# カテゴリごとに分類
category_pages = {}
for category in ContentCategory:
category_pages[category] = [p for p in pages if p.category == category]
# 優先カテゴリの設定
priority_categories = [
ContentCategory.RECRUITMENT,
ContentCategory.JOB_LISTING,
ContentCategory.COMPANY_PROFILE,
ContentCategory.INTERNSHIP
]
selected_pages = []
remaining_urls = max_urls
# 1. 優先カテゴリから選択
for category in priority_categories:
if category in category_pages and category_pages[category]:
pages_to_select = min(
len(category_pages[category]),
max(3, remaining_urls // (len(priority_categories) + 1))
)
selected_pages.extend(category_pages[category][:pages_to_select])
remaining_urls -= pages_to_select
del category_pages[category]
# 2. 残りのカテゴリから均等に選択
remaining_categories = [c for c in category_pages if category_pages[c]]
if remaining_categories and remaining_urls > 0:
urls_per_category = remaining_urls // len(remaining_categories)
for category in remaining_categories:
pages_to_select = min(len(category_pages[category]), urls_per_category)
selected_pages.extend(category_pages[category][:pages_to_select])
return selected_pages
def _log_collection_stats(self) -> None:
"""収集状況のログを出力"""
end_time = datetime.now()
duration = end_time - self.start_time
print("\n=== 情報収集統計 ===")
print(f"収集時間: {duration.total_seconds():.1f}秒")
print(f"収集URL数: {len(self.pages)}")
print(f"訪問ページ数: {len(self.visited_urls)}")
# カテゴリごとの集計
category_counts = {}
for page in self.pages:
category_counts[page.category] = category_counts.get(page.category, 0) + 1
print("\nカテゴリ別URL数:")
for category, count in category_counts.items():
print(f"- {category.value}: {count}")
def format_results(self) -> str:
"""結果を見やすい形式で出力"""
filtered_pages = self.filter_results()
output = []
# 基本情報の出力
output.append(f"企業名: {self.company_name}")
output.append(f"公式サイト: {self.company_info.official_url}")
if self.company_info.corporate_number:
output.append(f"法人番号: {self.company_info.corporate_number}")
if self.company_info.alternative_names:
output.append(f"別名: {', '.join(self.company_info.alternative_names)}")
if self.company_info.sns_links:
output.append("\n【SNSアカウント】")
for sns, url in self.company_info.sns_links.items():
output.append(f"{sns}: {url}")
# 公式情報の出力
output.append("\n【公式情報】")
official_pages = [p for p in filtered_pages if p.info_type == InfoType.OFFICIAL]
for category in ContentCategory:
category_pages = [p for p in official_pages if p.category == category]
if category_pages:
output.append(f"\n≪{category.value}≫")
for page in category_pages:
output.append(f"{page.url} {page.title}")
# 非公式情報の出力
output.append("\n\n【非公式情報】")
unofficial_pages = [p for p in filtered_pages if p.info_type == InfoType.UNOFFICIAL]
for category in ContentCategory:
category_pages = [p for p in unofficial_pages if p.category == category]
if category_pages:
output.append(f"\n≪{category.value}≫")
for page in category_pages:
output.append(f"{page.url} {page.title}")
return "\n".join(output)
def print_results(self) -> None:
"""結果を標準出力に表示"""
print("\n=== 収集結果 ===")
print(self.format_results())
# 使用例
if __name__ == "__main__":
try:
company_name = input("企業名を入力してください: ")
agent = JobSearchAgent(company_name)
agent.collect_all_information()
agent.print_results()
except Exception as e:
print(f"エラーが発生しました: {str(e)}")
(4)実行結果
=== 情報収集統計 ===
収集時間: 78.8秒
収集URL数: 764
訪問ページ数: 60
カテゴリ別URL数:
採用情報: 585
企業概要: 71
プレスリリース: 77
インターン: 31
=== 収集結果 ===
企業名: くら寿司
公式サイト: https://www.kurasushi.co.jp/company/company/aboutus.php
【公式情報】
≪企業概要≫
https://www.kurasushi.co.jp/company/company/philosophy.php コンセプト
https://www.kurasushi.co.jp/company/company/aboutus.php 会社概要
https://www.kurasushi.co.jp/company/company/aboutus.php#top ページトップへ
https://www.kurasushi.co.jp/company/company/philosophy.php#top ページトップへ
https://www.kurasushi.co.jp/company/ 企業情報
≪プレスリリース≫
https://www.kurasushi.co.jp/company/news/ir.php IRニュース
https://www.kurasushi.co.jp/upload/220228ir_release.pdf 譲渡制限付株式報酬としての自 己株式の処…
https://www.kurasushi.co.jp/upload/211221ir_release_01.pdf 役員報酬の改定及び譲渡制限付株式報酬制…
https://www.kurasushi.co.jp/upload/211221ir_release_02.pdf 剰余金の配当に関するお知らせ
https://www.kurasushi.co.jp/upload/210910ir_release.pdf 業績予想の修正に関するお知ら せ
https://www.kurasushi.co.jp/upload/210721ir_release.pdf 連結子会社の増資に関するお知 らせ
https://www.kurasushi.co.jp/upload/210331ir_release.pdf 譲渡制限付株式報酬としての自 己株式の処…
https://www.kurasushi.co.jp/upload/210318ir_yutai.pdf 株主優待制度の内容変更に関する お知らせ
https://www.kurasushi.co.jp/upload/210318ir_kabushiki.pdf 株式分割及び定款の一部変更 並びに配当予…
https://www.kurasushi.co.jp/upload/201217ir_release.pdf 剰余金の配当に関するお知らせ
https://www.kurasushi.co.jp/upload/kurair_201202_release.pdf 2020年10月期通期連結実績値と前期…
https://www.kurasushi.co.jp/upload/200917ir_taiwan.pdf 台湾子会社のTaipei Exchan…
https://www.kurasushi.co.jp/upload/kurair_200626_saienki.pdf 2020年10月期第2四半期決 算発表の…
https://www.kurasushi.co.jp/upload/kurair_200605_syusei.pdf 2020年10月期第2四半期決算発表の…
https://www.kurasushi.co.jp/upload/kurair_200331_kabusiki_hosyu.pdf 譲渡制限付株式報 酬としての自己株式の処…
https://www.kurasushi.co.jp/company/news/ir.php#top ページトップへ
https://www.kurasushi.co.jp/author/ くら寿司プレスリリース
https://www.kurasushi.co.jp/policy/socialmedia.html ソーシャルメディアポリシー
https://www.kurasushi.co.jp/policy/socialmedia.html#top ページトップへ
≪採用情報≫
https://www.kurasushi.co.jp/company/recruit/ 採用情報
https://www.kurasushi.co.jp/company/recruit/graduate/ 新卒採用サイト
https://www.kurasushi.co.jp/company/recruit/career/ 中途採用者の皆様へ
https://www.kurasushi.co.jp/company/recruit/employee/ アルバイト正社員登用について
https://www.kurasushi.co.jp/company/recruit/employee_SP/ アルバイト正社員登用について
https://www.kurasushi.co.jp/company/recruit/#top ページトップへ
https://www.kurasushi.co.jp/company/recruit/graduate/#cta 募集要項/エントリー
https://www.kurasushi.co.jp/company/recruit/graduate/#food_revolution 食の変革
https://www.kurasushi.co.jp/company/recruit/graduate/#world_wide グローバル戦略
https://www.kurasushi.co.jp/company/recruit/graduate/#newest IT企業としての顔
≪インターン≫
https://www.kurasushi.co.jp/company/ir/internal-control.php 内部統制
https://www.kurasushi.co.jp/upload/220106ir_release_internet.pdf 第26期定時株主総会招集ご通知に際して…
https://www.kurasushi.co.jp/upload/210106ir_release_internet.pdf 第25期定時株主総会招集ご通知に際して…
https://www.kurasushi.co.jp/company/ir/internal-control.php#top ページトップへ
【非公式情報】
<この部分何も出力されていない>
(5)評価
「非公式情報」の部分が出力されていません。これはどこに原因があるのか探る必要があります。
3.まとめ
まがりなりにも、900行のPythonプログラムが生成され、エラーなしで実行終了しました。これまでの生成AIによるコードは、大概途中でエラーで停止するようなコードでしたので、それに比べると、900行エラーなしで出せるというのはすごくないでしょうか。
職場では、これが一気出しできるということは、ああいうこととか、こういうこととかも出せるってことだよね、ということは、こういう使い方とかああいう使い方とかできそうで、それが多分数時間で実質的に書けるということでは?と盛り上がった所です。
みなさんも、とりあえず、いろいろ出力させてみてはいかがでしょうか。
今日は、驚き体験を共有させていただきました。
最後までお読みいただき、ありがとうございます。
あ、「いいね」ボタンクリックお願いします。
励みになりますので!
いいなと思ったら応援しよう!
![平岡憲人(ノーリー)](https://assets.st-note.com/production/uploads/images/127217855/profile_ff8e817d3b66900e4d167f10e436c572.jpg?width=600&crop=1:1,smart)