1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初心者がスクレイピング、情報抽出を行った記録

Posted at

集めたい情報があったのでスクレイピングを初めて行った。さまざまな知見が得られたので記録したいと思う。

目的

スタートアップの資金調達情報に興味があったので海外のサイトからそれに関する情報を入手することを考えた。

得られた知見

  • request,selenium,BeautifulSoupの使い方
  • ブラウザの開発者用ツールの使い方
  • OpenAI APIでStructured Outputをさせる方法

対象サイト

使用モジュール

  • request
  • selenium
  • BeautifulSoup

手法の概要

  1. それぞれのサイトのスタートアップカテゴリーに属する記事をまとめているページから記事のURLを手にいれる
  2. 記事のURLから記事のタイトルと本文を入手する
  3. タイトルと本文からそれらが資金調達に関する記事であるか判断し、資金調達に関する記事のみを集める

難しかった点

  • それぞれのオブジェクトがどの位置(XML pathなど)に存在しているか調べること
  • 動的コンテンツをスクレイピングする方法
  • 動的コンテンツがどのスクリプトによって生成されているか調べること
  • OpenAI APIからStructured Outputをする方法

全体的にどのような流れで行ったのか解説する。特に難しかった点は個別記事にして解説する。(かも)

スタートアップカテゴリーの記事をまとめる

TechCrunch

TechCrunchは静的サイトであったためURLを入手しやすかった。
スタートアップカテゴリーに対して以下のコードを書くことでカテゴリーの記事のURLを入手した。
最初は記事のタイトルに"startup"や"raise"が入っているかどうかで資金調達の記事か判断しようかと思っていたが、その手法だと取りこぼしがありそうなので取りやめた。

import requests
from bs4 import BeautifulSoup

url = "https://techcrunch.com/category/startups/"

response = requests.get(url)

html = response.content

soup = BeautifulSoup(html, 'html.parser')

title_linknames = soup.find_all('a', class_='loop-card__title-link')

Ctech

https://www.calcalistech.com/ctechnews/category/5214
Ctechは動的にコンテンツを生成していたので少し難しかった。
Javascriptでアクセスの3000ms後にコンテンツを呼び出すことがわかったので3秒待機してからページのHTMLを取得するとうまくいった。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import time

url = "https://www.calcalistech.com/ctechnews/category/5214"

# WebDriver Managerを使用してChromeDriverを自動的にインストール
service = Service(ChromeDriverManager().install())

browser = webdriver.Chrome(service=service)

try:
	browser.get(url)

# ページが読み込まれてから3秒待機
	time.sleep(3)

# ページのHTMLを取得
	html = browser.page_source

finally:
	browser.quit()  

if html:
	print("Get HTML Successfully")

難しかった点

web driver

web driverとChromeのバージョンが一致してない?というようなエラーが出たのでservice変数を使わないと動かなかった。

JavaScriptを見つける

探し方がわからなかったので次のような工程で地道に行った。

  1. requestでどの階層まで出力されているかを見つける
    • requestで出力されていないところは動的に生成されているところであるのでそれを見つける。
    • 今回は#BkUaac0011xyJl > div > div.slotList まで存在していることがわかった
  2. Chromeの開発者ツールでURLを含むコンポーネント周辺の要素を含むコードがないか調べる
    • 地道に検索した。
    • #BkUaac0011xyJl > div > div.slotList > div:nth-child(1) > aに URLが存在するためdiv.slotListから一階層ずつclass名を検索してその部分のHTMLを出力していそうなものを探した。
    • 今回はslotItemを含むコードが目標のコードであった
目標のコード

Xpath = /html/body/script[18]/text()

setTimeout(function () {
    let strip_components = document.querySelectorAll(".ctech-infinite-headlines .slotItem");
    let strip_ar = Array.from(strip_components);
    for (let i in strip_ar) {
     strip_ar[i].addEventListener("click", function (event) {
        const clickText = strip_ar[i].querySelector(".slotTitle span:not(.roofTitle)").textContent;
        let a_link = strip_ar[i].querySelector("a").href;
        let split_link = a_link.split("/");
        let article_id = a_link.includes("article") ? split_link[split_link.length - 1]: "";         
    
        let authorName = strip_ar[i].querySelector(".author") ? strip_ar[i].querySelector(".author").innerText : "";
        let pageType = dataLayer && dataLayer[0] && dataLayer[0].contentPageType ? dataLayer[0].contentPageType : "";
        if (window.pushGa4DataLayer) {
             window.pushGa4DataLayer(event, {
                 event: "content_click",
                 click_text: clickText,
                 content_type: "componenta",
                 componenta_name: "ctech-infinite-headlines",
                 position_in_componenta: i,
                 article_id: article_id,
                 author_name: authorName,
                 page_type: pageType,
             });
         }
     });
    }
}, 3000);

記事のタイトルと本文の抽出

以下の関数を全てのリンクに対して適応した。
Techchrunch,Ctechの両方において記事ごとにHTML構造が全て同じだったので楽だった。
それぞれの位置に応じてsoup.find()の要素を入れ替えるだけで抽出できた。

def get_content(url):

response = requests.get(url)

html = response.content

  

soup = BeautifulSoup(html, 'html.parser')

titleAndContent = {}

title_tag = soup.find('title')

if title_tag:

title_text = title_tag.get_text()

titleAndContent["title"] = title_text

else:

titleAndContent["title"] = "None"

article_body = soup.find('div', class_='entry-content')

if article_body:

paragraphs = article_body.find_all('p')

extracted_text = "\n".join([para.get_text() for para in paragraphs])

titleAndContent["Main Content"] = extracted_text

else:

titleAndContent["Main Content"] = "None"

  

return titleAndContent

タイトルと本文から資金調達の記事であるか判断し、資金調達の記事のみを集めた。

単純にOpenAI APIに本文と記事を与えて判断させた。
出力をsructuredにしたかったので少し工夫した。

まず出力の形式を以下のように定義した。

from pydantic import BaseModel
class Summarize(BaseModel):
	fundingNews: bool
	
	companyName: str
	bussinessDomain: str
	fundraisingAmount: str
	leadInvestor: str
	fundingRound: str
	investor: str
	bussinessOverview: str
	subjectiveComment: str

次にプロンプトを以下のように定めた。
fundingNews == False つまりニュースが資金調達のものでない場合それ以降の要素を空白にするように指示した。

role = "You are good summarizer。"
prompt = f"Title: {titleAndContent['title']}\nContent: {titleAndContent['Main Content']}\n\nBase on the information, Please summarize it. Please express the bussiness domain as 1 phrase.If fundingNews == False, fill others with brank"

そして以下のようにAPIに入力して出力を得た。モデルはgpt-4o-miniを用いた。

def generate_response(role, prompt, model="gpt-4o-mini-2024-07-18"):
	completion = client.beta.chat.completions.parse(
		model=model,
		messages=[
		{"role": "system", "content": role},		
		{"role": "user", "content": prompt},		
		],		
		response_format=Summarize,		
		)	
	response = completion.choices[0].message.parsed

return response

注意事項

cient.beta.chatを用いる際pythonを3.12にしないと動かなかった。理由は定かでない。

まとめ

以上で出力を構造化して取得できた。
seleniumは使ったことがなかったので難しかったが、どのJαvascriptで動いているか見つけれた時は気持ちよかった。もうちょっと楽できる方法はないのか調べたい。
Structured outputでbool値も出力できるのは驚きだった。またプロンプトで指示するだけでそれ以降の出力もしないようになるのは面白かった。色々なところで使えそうな気がする。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?