集めたい情報があったのでスクレイピングを初めて行った。さまざまな知見が得られたので記録したいと思う。
目的
スタートアップの資金調達情報に興味があったので海外のサイトからそれに関する情報を入手することを考えた。
得られた知見
- request,selenium,BeautifulSoupの使い方
- ブラウザの開発者用ツールの使い方
- OpenAI APIでStructured Outputをさせる方法
対象サイト
- TechCrunch https://techcrunch.com/
- Ctech https://www.calcalistech.com/ctechnews/
使用モジュール
- request
- selenium
- BeautifulSoup
手法の概要
- それぞれのサイトのスタートアップカテゴリーに属する記事をまとめているページから記事のURLを手にいれる
- 記事のURLから記事のタイトルと本文を入手する
- タイトルと本文からそれらが資金調達に関する記事であるか判断し、資金調達に関する記事のみを集める
難しかった点
- それぞれのオブジェクトがどの位置(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を見つける
探し方がわからなかったので次のような工程で地道に行った。
- requestでどの階層まで出力されているかを見つける
- requestで出力されていないところは動的に生成されているところであるのでそれを見つける。
- 今回は
#BkUaac0011xyJl > div > div.slotList
まで存在していることがわかった
- 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値も出力できるのは驚きだった。またプロンプトで指示するだけでそれ以降の出力もしないようになるのは面白かった。色々なところで使えそうな気がする。