2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スクレイピングとGemini APIで会社の情報を見やすく整理してみた

Last updated at Posted at 2025-07-01

はじめに

先日、社内ツールとして「Webサイトから特定の情報を効率よく集め、整理して表示する」という要望がありました。
手作業では手間がかかるこのタスクを、スクレイピング技術とGemini APIを活用して自動化し、見やすく出力するシステムを構築してみたので、その過程をご紹介します。
本記事では、弊社の会社情報を例に、具体的な実装方法を解説します。

スクレイピングとは

Webスクレイピングは、インターネット上のWebサイトから、プログラムを使って必要な情報を自動的に収集する技術です。
例えば、企業の基本情報、製品リスト、ニュース記事など、手動でコピー&ペーストする手間を省き、効率的にデータを集めることができます。
ただし、スクレイピングを行う際には、以下の重要な注意点があります。
これらを無視すると、法的な問題や相手方サーバーへの迷惑行為となる可能性があるため、必ず確認しましょう。

  1. 著作権・肖像権の侵害:
    スクレイピングで取得した情報を無断で公開したり、二次利用したりすることは、著作権や肖像権の侵害にあたる可能性があります。利用目的を明確にし、権利者の許可を得ることが重要です。

  2. サーバー負荷の問題:
    短時間に大量のアクセスを行うと、Webサイトのサーバーに過度な負荷をかけ、サーバーダウンなどの問題を引き起こす可能性があります。適度なアクセス間隔を設ける、必要最低限の情報のみを取得するなどの配慮が必要です。

  3. 利用規約の確認:
    多くのWebサイトには「利用規約」が定められており、スクレイピング行為を禁止している場合があります。スクレイピングを行う前に、必ず対象サイトの利用規約を確認しましょう。

  4. robots.txtの確認:
    robots.txtは、Webサイトの管理者がクローラーやスクレイパーに対して、どのページへのアクセスを許可し、どのページへのアクセスを禁止するかを指示するファイルです。スクレイピングを行う前に、必ずrobots.txtの内容を確認し、指示に従いましょう。

実装

それでは、実際にスクレイピングを行うためのコードを見ていきましょう。
今回は以下の3つのステップで進めます。

1.Google検索で対象ページのURLを取得
2.robots.txtを確認し、スクレイピングの許可をチェック
3.対象URLからページの情報を取得

Google検索でURLを取得する

まずは、検索キーワードから対象のWebサイトのURLを見つけ出す関数です。
ここではPythonのgoogle-searchライブラリを使用しています。

def find_url(search_word):
    """
    Google検索を行い、ページのURLを返す。

    Args:
        search_word (str): 検索するワード。
    Returns:
        str or None: 最初に見つかった検索結果のURL。見つからない場合やエラー時はNone。
    """
    query = f"{search_word.strip()}"
    print(f"検索クエリ: {query}")
    try:
        # Google検索を実行し、結果のジェネレータを取得
        search_results_generator = search(query, lang='ja')
        # 最初の検索結果URLを取得 (見つからなければNone)
        first_url = next(search_results_generator, None)
        time.sleep(2)  # 連続アクセスを避けるための待機時間
        return first_url
    except Exception as e:
        print(f"Google検索中にエラーが発生しました: {e}")
        return None

robots.txtによるスクレイピング許可の確認

次に、取得したURLに対してrobots.txtを確認し、スクレイピングが許可されているかをチェックする関数です。
これにより、意図せずサイトに迷惑をかけたり、利用規約に違反したりすることを防ぎます。

def check_scraping_permission(target_url: str) -> tuple[bool, str]:
    """
    指定されたURLのrobots.txtを確認し、スクレイピングが許可されているかを返す。

    Args:
        target_url (str): 確認対象のURL。
    Returns:
        tuple: (許可されているかのブール値, メッセージ文字列)
               対象URLがない場合は (False, "対象URLがありません。")
               robots.txtが存在しないか読み取れない場合は (True, "情報: robots.txt が存在しないか...")
    """
    if not target_url:
        return False, "対象URLがありません。"

    parsed_target_url_obj = urlparse(target_url)
    robots_url_str = f"{parsed_target_url_obj.scheme}://{parsed_target_url_obj.netloc}/robots.txt"

    rp = robotparser.RobotFileParser()

    try:
        response = requests.get(robots_url_str, headers={'User-Agent': USER_AGENT}, timeout=10, verify=True)
        response.raise_for_status() # HTTPエラーが発生した場合に例外を発生させる
        response.encoding = response.apparent_encoding
        fetched_robots_txt_content = response.text

        # 取得した内容の前処理
        cleaned_content = fetched_robots_txt_content
        if cleaned_content.startswith('\ufeff'):
            cleaned_content = cleaned_content[1:]
        cleaned_content = cleaned_content.replace('\r\n', '\n').replace('\r', '\n')
        lines = [line.strip() for line in cleaned_content.splitlines()]
        final_cleaned_content_for_parser = "\n".join(lines)

        # urllib.robotparser でパース
        rp.parse(final_cleaned_content_for_parser.splitlines())

        is_allowed = rp.can_fetch(USER_AGENT, target_url)

        if is_allowed:
            message = f"OK: {robots_url_str} でスクレイピングが許可されています。"
        else:
            message = (
                f"NG: {robots_url_str} により、このURLへのアクセスは禁止されています。"
                "Disallowルールを確認し、アクセスが禁止されているパスではないか確認してください。"
            )
        return is_allowed, message

    except requests.exceptions.RequestException as req_e:
        message = (
            f"情報: {robots_url_str} の取得に失敗しました (エラー: {req_e})。robots.txtが存在しないか、"
            "アクセスがブロックされている可能性があります。サイトの利用規約を直接確認してください。"
        )
        logging.warning(f"Failed to fetch robots.txt for {target_url}: {req_e}")
        return True, message # robots.txtが読めない場合は許可とみなす

    except Exception as e:
        message = f"情報: {robots_url_str} の処理中に予期せぬエラー ({e})。サイトの利用規約を直接確認してください。"
        logging.error(f"Unexpected error processing robots.txt for {target_url}: {e}")
        return True, message

Webページから情報を抽出する

最後に、許可されたURLからHTMLを取得し、会社概要のような構造化されたデータ(dlタグやtableタグなど)を効率的に抽出する関数です。
BeautifulSoupライブラリを活用します。

def scrape_structured_data(url: str) -> tuple[str, str]:
    """
    指定されたURLからHTMLを取得し、会社概要に関連する構造化データ(dlタグやtableタグ内の情報)を抽出する。

    Args:
        url (str): 対象のURL。
    Returns:
        tuple: (処理したURL, 抽出結果またはエラーメッセージの文字列)
               構造化データが見つからない場合は、ページの全テキストを返す。
    """
    try:
        headers = {'User-Agent': USER_AGENT}
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        response.encoding = response.apparent_encoding

        soup = BeautifulSoup(response.text, 'html.parser')
        profile_data = {}

        for dl in soup.find_all('dl'):
            for dt, dd in zip(dl.find_all('dt'), dl.find_all('dd')):
                key = dt.get_text(strip=True).replace(' ', '').replace(' ', '')
                value = dd.get_text(strip=True)
                if key and value:
                    profile_data[key] = value

        for table in soup.find_all('table'):
            for row in table.find_all('tr'):
                th = row.find('th')
                td = row.find('td')
                if th and td:
                    key = th.get_text(strip=True).replace(' ', '').replace(' ', '')
                    value = td.get_text(strip=True)
                    if key and value:
                        profile_data[key] = value

        if not profile_data:
            return url, "このページから構造化データ(会社名、住所など)を自動で抽出できませんでした。ページの全テキストを表示します。\n\n" + soup.get_text()

        formatted_text = "【抽出された主な情報】\n"
        for key, value in profile_data.items():
            formatted_text += f"{key}\n{value}\n\n"
        return url, formatted_text
    except requests.exceptions.RequestException as e:
        return url, f"エラー: サイトにアクセスできませんでした。\n詳細: {e}"
    except Exception as e:
        return url, f"エラー: スクレイピング中に予期せぬエラーが発生しました。\n詳細: {e}"

これで、Webページから必要な情報をプログラム的に取得できるようになりました。

GeminiAPIを利用して構造化出力する

Webページから生の情報を抽出できただけでは、まだデータとしての再利用性や見やすさに課題が残ります。
そこで、抽出したテキスト情報をGemini APIの構造化出力機能を使って、機械的に処理しやすいJSON形式に整形します。

Gemini APIについて

Gemini APIは、Googleの最先端AIモデル「Gemini」の機能をアプリケーションに統合するためのツールです。
テキスト生成、画像認識、多言語翻訳、コード生成といった多様なAI機能をプログラムから利用できます。
これにより、対話型AIアシスタント、コンテンツ自動生成ツール、データ分析支援システムなど、幅広いAI駆動型アプリケーションの開発が可能です。

APIキーの取得方法に関してはこちらの記事が参考になると思います。

構造化出力機能について

Geminiの構造化出力機能は、モデルが生成するテキストを、JSONやYAMLのような特定のフォーマットで出力させる機能です。
これにより、モデルが生成した回答をプログラムが直接パースし、データベースへの格納や次の処理に容易に連携できます。
例えば、ユーザーの質問から必要な情報を抽出し、あらかじめ定義されたスキーマ(データの型や構造)に従ってデータとして出力させることが可能です。

それでは、scrape_structured_data関数で取得した会社情報を、COMPANY_SCHEMAというスキーマに沿ってJSON形式で整形する実装を見ていきましょう。

def generate_company_json_with_gemini(info: str):
    """
    responseSchema を使って Gemini API にスキーマを渡し、
    情報をそのままスキーマ形式の JSON で返却させる。
    """
    client = genai.Client(api_key="APIキー")
    prompt = f"以下の情報からから会社情報を抜き出してください:{info}"

    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=prompt,
        config={
            "response_mime_type": "application/json",
            "response_schema": COMPANY_SCHEMA,
        },
    )
    my_recipes: list[Recipe] = response.parsed

このように、scrape_structured_data関数で抽出されたformatted_textをGemini APIに渡すことで、定義したスキーマに沿ったJSON形式で会社情報を効率的に整形できます。
これにより、後続のシステムでのデータ利用や表示が非常に容易になります。

実際に動かしてみる

では実際にこのコードを動かして弊社の会社情報を取得してみましょう。
今回Gemini APIに渡すスキーマです、弊社の会社情報ページからそのまま項目を持ってきました。

COMPANY_SCHEMA = {
  "type": "object",
  "properties": {
    "company_name":{"type": "string", "description": "会社名の正式名称を記述します。"},
    "representative_director":{"type": "string", "description": "代表者の氏名を記述します。"},
    "date_of_establishment":{"type": "string", "description": "会社設立日を記述します。"},
    "capital_stock":{"type": "number", "description": "資本金を円単位の整数で記述します。単位は円です。"},
    "employees":{"type": "number", "description": "従業員数を整数で記述します。単位は人です。"},
    "location1":{"type": "string", "description": "所在地1を記述します。無い場合無記入にします。"},
    "location2":{"type": "string", "description": "所在地2を記述します。無い場合無記入にします。"},
    "location3":{"type": "string", "description": "所在地3を記述します。無い場合無記入にします。"},
    "location4":{"type": "string", "description": "所在地4を記述します。無い場合無記入にします。"},
    "location5":{"type": "string", "description": "所在地5を記述します。無い場合無記入にします。"},
    "location6":{"type": "string", "description": "所在地5を記述します。無い場合無記入にします。"},
    "business_description":{"type": "string", "description": "事業内容を記述します。"},
  },
  "required": [
   "company_name","representative_director","date_of_establishment","capital_stock","employees",
    "location1","location2","location3","location4","location5","location6",
    "business_description"
  ]
}

事前準備が整ったので動かしていきます。
今回は"会社概要 エクス"のstrを渡し動かします。

# 検索クエリ
会社概要 エクス

# 取得したURL
https://www.xeex.co.jp/company/outline

# 出力されたjson
{
  "company_name": "株式会社エクス",
  "representative_director": "抱 厚志",
  "date_of_establishment": "1994年9月1日",
  "capital_stock": 100000000.00,
  "employees": 137.00,
  "location1": "〒531-0072 大阪市北区豊崎3-19-3 ピアスタワー20F",
  "location2": "〒101-0041 東京都千代田区神田須田町2-9-2 PMO神田岩本町ビル4F",
  "location3": "〒461-0004 名古屋市東区葵1-19-30 マザックアートプラザ3F",
  "location4": "〒812-0013 福岡市博多区博多駅東2-18-30 八重洲博多ビル6F",
  "location5": "〒900-0015 沖縄県那覇市久茂地1-7-1 琉球リース総合ビル2F",
  "location6": "〒577-0013 大阪府東大阪市長田中2-2-30 長田エミネンスビル6F 6D号室",
  "business_description": "生産管理パッケージ(Factory-ONE 電脳工場シリーズ)の開発・販売製造業向けクラウドサービスやプラットフォームの提供デジタルトランスフォーメーション(DX)関連サービスの開発、提供システム導入コンサルティングユーザー向けカスタマイズソフトの設計・開発・稼動支援・保守サービス製造業向け各種ソリューションの提供広域東大阪圏のビジネス&カルチャーニュース「東大阪経済新聞」の運営"
}

無事に弊社の会社概要ページのURL取得と、そのページから情報取得しjsonに整形ができました。
実際のページと是非見比べてどのように整形されているかを確認してみてください。

終わりに

今回の記事では、Webスクレイピング技術とGemini APIの構造化出力機能を組み合わせることで、Webサイトから会社情報を効率的に抽出し、機械的に扱いやすいJSON形式に整形する方法をご紹介しました。
今回の記事が、WebスクレイピングやGemini APIの活用に興味を持つ方の参考になれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?