
サンプルプログラムでスクレイピングができるようになっても、まだ重要なポイントが理解できていなくて不安に思っている人も多いでしょう。
HTMLを取得して、いくつかの情報を取得できるけど、サンプルプログラム以外のデータの取得をどうすればいいか分からないのではないでしょうか?
基本的には、DOM操作ができて、そのサイトの特性が理解できれば、スクレイピングはできてしまうのですが、動的サイトの対応や、
複雑なDOM構造の対応、リンクを辿っていくクローリング&スクレイピングはどうすればいいのか?
このような、スクレイピングの基本アルゴリズムについて、解説してみたいと思います。
スクレイピングの基本的なアルゴリズム構造
1. ターゲットURLの決定
一番最初に、スクレイピングしたいページのURLを決定します。
事前に見極めておく必要があるのは、1ページだけなのか、一覧ページ・詳細ページのような構造があるのかを整理する事です。
クエリパラメータ(例:?page=2)などでページを切り替えられるか確認してみるとよくわかりますし、対象のページ内を操作して、URLクエリがどのように変わっていくかを調査することも重要です。
2. HTMLの取得(クローリング)
URLのルールが分かれば、それに沿って繰返しのクローリングをする作業に移ります。
javascriptであれば、fetch()、UrlFetchApp.fetch()、requests.get()などを使い、
PHPであれば、file_get_contents()を使ってHTMLを取得。
他の言語でも結構簡単にURLからHTMLを取得する関数が揃っています。
記事下部におまけとして、いくつかの言語別にサンプルを紹介します。
ちなみに、日本語サイトや文字コードが特殊なサイトではエンコーディングに注意(UTF-8でないケースも要注意です)すること。
3. HTMLパース(DOM解析)
HTML文字列をDOMに変換する。
JavaScript:DOMParser()
Python:BeautifulSoup
欲しいデータがどこにあるか、class, id, tag構造 などで見極める事が重要です。
次に見極めたDOM構造を、querySelector()やselect()でピンポイントに抽出します。
これも、記事下部に、言語別サンプルプログラムを用意しておきました。
4. リスト処理(繰り返しの構造)
商品一覧、ニュース一覧など、似た構造の繰り返し要素は forEach, map, for で処理します。
各要素ごとに「名前・リンク・価格」などを1セットとして扱う(オブジェクトにまとめると便利)ようにします。
5. クロール処理(リンクの再帰的な辿り)
一覧ページ → 詳細ページ → さらにリンク先という構造がある場合は、
一度に全部取らず、リスト構築 → 再帰処理でリンクを辿るというルール設定も必要です。
スケジュールを組んで段階的にデータを蓄積する方法も検討してみましょう。
どのくらいのボリュームになるのか分からない場合に、取得スケジュールを作っていないと、クローリングのルール違反になる可能性もあるので、要注意です。
動的ページの対処法
最近は多くのサイトがJavaScriptで描画されており、HTMLだけを取得しても空っぽなケースがあります。
その場合は以下のような方法で対応できます。
Headlessブラウザ(例:Puppeteer, Playwright)で描画後のHTMLを取得
必要に応じてGASやNode.jsを使い分ける構成を考えるというのもアリです。
HTML取得操作いろいろ
レベル1/ 基本HTML取得
URLを指定してHTMLソースコードを取得するのは、最も基本的で簡易な、取得方法です。
これまでのサンプルコードで、対応できるでしょう。
レベル2/ リンク数珠繋ぎ
お知らせやニュースなどがトップページに掲載されているようなページの場合、
トップページでは、リンク先のURLの一覧を取得して、
その後、取得したURLリストを元に、1つずつURLからHTMLソースを取得していく手順になります。
これも、これまでのソースコードを少し改良すれば、対応できるレベルです。
レベル3/ 検索機能での情報取得
検索文字などを入力して、「検索」ボタンを押すことで、検索結果の一覧が表示されるページから、スクレイピングをするケースがあります。
Googleの検索が最もわかりやすいかもしれませんが、政府のサイトや、大手企業の商品ラインナップなど、ページ数が膨大なので、
絞り込みをするために検索機能を使うケースは、少し難易度が高くなる場合があります。
簡単なパターンとしては、検索結果が、URLのGETクエリでルールが判定できる場合は、
URLに検索文字を入れるだけで、結果ページが表示できて、簡単にリンク一覧の取得ができます。
でも、POSTクエリでの検索を必須としているようなサイトの場合は、サーバーサイドで、POST送信をする方法や、
Node.js系の、puppetterなどを使った、ブラウザライクな操作を実行する必要があります。
これは、これまでと違う環境を用意しなければいけないので、レベルの高いスクレイピングとして、次回以降の会で、解説をしたいと思います。
スクレイピング操作いろいろ
レベル1. 固定DOM構造の場合
HTMLの構造が毎回同じで、特定のIDやclass名が明確に定義されている場合は、最もシンプルで安定したスクレイピングができます。
たとえば、商品一覧のページで、毎回 <div class="item"> のようなタグで商品が並んでいる場合、
そのタグをループで処理するだけで、価格・商品名・リンク先などを一括で取得できます。
このようなケースでは、JavaScriptやPythonの基本的なDOM操作(`querySelectorAll`, `BeautifulSoup.select()`など)だけで対応できるので、初学者にもおすすめのパターンです。
レベル2. IDやclassの値が読み込むたびに変わるケース
最近のWebサービスでは、セキュリティや構造変更のために、IDやclass名が動的に生成されるケースが増えています。
例えば、`class="abc123"` のような、毎回変わるランダムな文字列がclass名に使われている場合、
単純にタグ名やID/class名を指定しても、うまく情報を取得できません。
このような場合には、
- HTML構造の
相対的な位置関係(兄弟・親子関係)を使って抽出する
-
`タグ構造+キーワード` などのテキストパターンマッチで探す
-
正規表現を併用する
など、少し高度なロジックが必要になります。
また、セレクタの変化に強いCSSの部分一致や属性セレクタ(例:`[class^="item_"]`)を活用することで、柔軟な抽出が可能です。
レベル3. DOM構造がリスト+詳細ページで構成されるケース
商品一覧ページやニュース記事の一覧ページなどで、一覧→詳細ページに遷移して情報を取得するような構造の場合です。
このときは、
1. 一覧ページから、詳細ページへのリンクURLをすべて抽出
2. それらのリンクを1つずつ巡回して、個別に詳細情報を取得
という「クローリング+スクレイピング」の組み合わせが必要になります。
この処理では、
- リンクの相対URLと絶対URLの変換
- リクエストの順序制御(負荷軽減のためのウェイトなど)
- ページネーションの処理(次のページへ進むロジック)
といった複数の技術的ポイントを意識しなければなりません。
比較的実務でよく使われるケースであり、効率的なロジック設計が求められます。
レベルMAX. Javascriptベースなページでの情報取得
最も難易度が高いレベルとして、
ページ内でJavascriptを使って、要素をドラッグしたり、順番にクリックしたりするようなページは、
かなり難しい操作になります。
検索サイトなどで使われがちな、「複数の画像の中で横断歩道や、オートバイの画像をクリックする」みたいな、人間操作確認の処理は、
スクレイピングを禁止しているサイトで行われている対応策なので、こうしたサイトからは、スクレイピングをするのを諦めた方が無難です。
ちなみに、やってできないことはありませんが、サーバーアタックやクラッキングに近い操作をする必要があるので、
あまりオススメできません。
スクレイピングのデメリット
データを取得するホームページが、リニューアルしたり、ページ構成を大きく変更されてしまうと、
それまでは正常に取得できていたスクレイピングデータが、ある日突然取得ができなくなります。
大きなサービスなどで流用しようとすると、ページ構成が変わった場合に、アラートを取得するなどの、
検知処理を入れることもありますが、簡易なスクレイピングをする場合は、そうした検知などは入れることも難しい場合が多いので、
後から気がつく事になります。
どうしても簡易に行いたいのであれば、GoogleAppsScriptで、定期スクレイピングを行って、同時にページ構成をチェックするような処理を追加しておくことで、
変更時に自分にメール送信をするようなプログラムを組むのが、独自にサーバー設置をしなくても、対応ができる最も簡単な方法ではないかと思われます。
または、自宅にRaspberryPieなどを使ってサーバーを立ち上げて、任意のタイミングで定期的にクローリング処理をするというシステムを作れば、安価に対応できるかもしれません。
あとがき
これまでの学習の復讐も兼ねて、
実際のコードも含めたスクレピングサンプルを掲載してみました。
レベルの低い簡単なスクレイピングであれば、すぐにでも実行できると思います。
でも、実際にやってみると、一筋縄ではいかない事の多く、そんな時にどうすればいいかというのを次回色々と紹介してみたいと思います。
おまけ : 言語別HTML取得サンプルプログラム
Javascript
fetch('https://example.com')
.then(res => res.text())
.then(html => console.log(html));
php
## File_get_contents
$html = file_get_contents('https://example.com');
echo $html;
## cURL
$ch = curl_init('https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$html = curl_exec($ch);
curl_close($ch);
echo $html;
python
import requests
html = requests.get('https://example.com').text
print(html)
Node.js(サーバーサイドJavascript)
import fetch from 'node-fetch';
const res = await fetch('https://example.com');
const html = await res.text();
console.log(html);
Ruby
require 'open-uri'
html = URI.open('https://example.com').read
puts html
Go
resp, _ := http.Get("https://example.com")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
Java
String html = Jsoup.connect("https://example.com").get().html();
System.out.println(html);
Google Apps Script
function getHtml() {
const html = UrlFetchApp.fetch('https://example.com').getContentText();
Logger.log(html);
}
おまけ : HTMLパースサンプルプログラム
Javascript
fetch('https://example.com')
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const title = doc.querySelector('title').textContent;
console.log(title);
});
PHP
$html = file_get_contents('https://example.com');
$doc = new DOMDocument();
libxml_use_internal_errors(true); // warning回避
$doc->loadHTML($html);
$xpath = new DOMXPath($doc);
$title = $xpath->query('//title')->item(0)->textContent;
echo $title;
Node.js
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
fetch('https://example.com')
.then(res => res.text())
.then(body => {
const dom = new JSDOM(body);
const title = dom.window.document.querySelector('title').textContent;
console.log(title);
});
Python
import requests
from bs4 import BeautifulSoup
res = requests.get('https://example.com')
soup = BeautifulSoup(res.text, 'html.parser')
title = soup.title.string
print(title)
Ruby
require 'open-uri'
require 'nokogiri'
doc = Nokogiri::HTML(URI.open("https://example.com"))
puts doc.title
Go
package main
import (
"fmt"
"net/http"
"golang.org/x/net/html"
)
func main() {
resp, _ := http.Get("https://example.com")
defer resp.Body.Close()
doc, _ := html.Parse(resp.Body)
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" {
fmt.Println(n.FirstChild.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
}
Java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
public class Main {
public static void main(String[] args) throws Exception {
Document doc = Jsoup.connect("https://example.com").get();
System.out.println(doc.title());
}
}
0 件のコメント:
コメントを投稿