スクレイピングとは
Webサイトから自分の知りたい情報を抽出すること。
ex) 文章、画像、動画など
今回の目標
Qiitaで「ruby」と検索して「いいね順」に並べた検索結果一覧をスクレイピングします。
1. URLのパスパラメータ・クエリパラメータを理解する
スクレイピングをするにはURLのパラメータについて理解する必要があります。
「そんなん余裕で知っとるわ!」という方は飛ばして次章へどうぞ!
パラメータの種類
URLでドメイン以降の/
で区切られたパス1つ1つがパスパラメータです
URLの**?
以降がクエリパラメータ**です(複数記述する場合は&
で繋ぎます)。
例えばこのURLは。
https://qiita.com/search?page=1&q=ruby&sort=like
以下のパラメータになります。
種類 | パラメータ名 | パラメータの値 |
---|---|---|
パスパラメータ | search | search |
クエリパラメータ | page | 1 |
クエリパラメータ | q | ruby |
クエリパラメータ | sort | like |
一般的に page
というパラメータ名は ページ番号 として使用されます。
検索ワード のパラメータ名はサービスによって様々です。
Qiitaの場合は q
というパラメータ名の値が 検索ワード として使用されていますが、
別サービスでは search_words
, keywords
, k
などを 検索ワード のパラメータ名として使用しているのを見たことがあります。
検索結果の並び順を表すパラメータ名は、一般的にはsort
やorder
が多い様に感じます。
※ 補足ですが、パラメータ名はサーバーサイド側で自由に設定できます。
以前「パラメータ名は時自由に設定できないの?」って質問がありましたが、そんなことないです!
パラメータ名は自由に決めれます。極端な話、検索ワードのパラメータ名を page
にすることも可能です。
しかし現実的な話、コードの保守性・可読性の観点から、誰が見てもわかりやすいパラメータ名が使用されます。
ついでに q
は query
の略です。DBに検索をかける時の命令文をQUERY(クエリ)と呼ぶことに由来します。
パラメータを直接変更して検索結果を変えてみよう!
試しに、以下のURLにアクセスしてください。
https://qiita.com/search?q=アイウエオ
検索窓に「アイウエオ」と記入されていることを確認してください。
次に、以下のURLにアクセスしてください。
https://qiita.com/search?q=アイウエオ
検索窓に「ruby」と記入されていることを確認してください。
また、Qiitaではsort
パラメータに続く値が並び順になっているようです。
受け取る値によって並び順が変わります。
パラメータ値 | 並び順 |
---|---|
like | いいね順 |
created | 新着順 |
rel | 関連順 |
stock | ストック順 |
上記に該当しない値の場合は、関連順
になるようです。
https://qiita.com/search?q=ruby&sort=テストトト
このように、URLと検索条件が連動していることが分かります。
各パラメータが何を意味しているか理解しないと、スクレイピングはできません。
ここを理解した上で、次の章にお進みください!
2. スクレイピングに必要なgem, classをrequire
する
# レスポンスをファイルのように扱えるgem
# HTTPのGETメソッドが使えるものであればなんでもOKです。
require 'open-uri'
# スクレイピングのgem
require 'nokogiri'
# CSV形式で色々できるclass
require 'csv'
3. スクレイピングしたいページのHTMLを取得
url = 'https://qiita.com/search?page=1&q=ruby&sort=like'
res = open(url)
=> #<Tempfile:/var/folders/cr/gy13f2r54mb742wtds7j87c40000gp/T/open-uri20211006-69612-or1ib>
open()
メソッドの引数にURLを渡すと、URLへリクエストを送信し、レスポンスを返します。
レスポンスはTempfileクラスのオブジェクトになっていることが分かります。
実際の中身を見るにはread
というメソッドを使います。
body = res.read
=> "<!DOCTYPE html><html xmlns:og=\"http://ogp.me/ns#\"><head><meta charset=\"UTF-8\" /><title>Search result of “ruby” - Qiita</title>....."
4. HTMLをパースする
HTMLを取得することができましたが、ここで一つ注意したいことがあります。
Ruby上では、取得したHTML要素がただの文字列(String型)として認識されていることです。
body.class
=> String
なので、ただの文字列をパースして文章とHTML要素を区別します。
パースとは、構文解析のことです。
今回のように、ただの文字列をHTML要素とそうでない文章に区別させることを指します。
charset = res.charset
html = Nokogiri::HTML.parse(body, nil, charset)
html.class
#=> Nokogiri::HTML::Document
これでただの文字列ではなくなり、HTML要素とそうでない文章を区別できるようになりました。
5. Nokogiriの使い方
css
メソッドの引数に取得したい文章のDOM要素を指定します。
DOM要素の記述方法は、CSSセレクタの記述方法と同じです。
例えば、検索結果一覧から「記事タイトル」を取得する方法は以下です。
html.css('h1.searchResult_itemTitle').text
#=> "Markdown記法 チートシートペアプログラミングして気がついた新人プログラマの成長を阻害する悪習プログラミングでよく使う英単語のまとめ【随時更新】非デザイナーエンジニアが一人でWebサービスを作るときに便利なツール32選【まとめ】これ知らないプログラマって損してんなって思う汎用的なツール 100超新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡もう保守されない画面遷移図は嫌なので、UI Flow図を簡単にマークダウンぽく書くエディタ作った翻訳: WebAPI 設計のベストプラクティス開設後3週間で収益10万円を得た個人開発サイトでやったことの全部を公開するエンジニアの情報収集法まとめ"
当然ですが、CSSセレクタと同じでヒットするDOM要素全てを取得します。
html.css('h1.searchResult_itemTitle').length
#=> 10
次は「タイトル」と「タグ」と「見出し」を取ってきましょう。
# 「.search」は「.css」のエイリアスメソッド
# 「.inner_text」は「text」のエイリアスメソッド
results = []
html.search('.searchResult_main').each do |node|
title = node.css('.searchResult_itemTitle').inner_text
tags = node.css('.tagList_item').map{ |article_tag| article_tag.inner_text }
detail = node.css('.searchResult_snippet').inner_text
results << { title: title, tags: tags, details: detail }
end
results.each.with_index(1) do |res, i|
puts "#{i}番目の検索結果"
puts "Title: #{res[:title]}"
puts "Tags: #{res[:tags]}"
puts "Details: #{res[:detail]}"
puts '-----------------------------------------'
end
#=> 1番目の検索結果
# Title: Markdown記法 チートシート
# Tags: ["Qiita", "Markdown"]
# Details: の挿入たとえば、Rubyで記述したコードをファイル名「qiita.rb」と ...
6. スクレイピング
では本格的なスクレイピングをしていきます。
本格的にスクレイピングsを始めるとなった場合、結果をCSVファイルに出力することが多いと思いますので、それもやります。
ruby
, php
, python
, perl
で検索した結果をLGTM順に並べて1ページ目から100ページ目までの「タイトル」「タグ」「見出し」「記事URL」「LGTM数」「コメント数」「著者」「」を
search_words = ['ruby', 'php', 'python', 'perl']
results = {}
search_words.each do |word|
results[word] = []
(1..100).each do |i|
puts "================== #{word} #{i} =================="
url = "https://qiita.com/search?page=#{i}&q=#{word}&sort=like"
res = open(url)
charset = res.charset
body = res.read
html = Nokogiri::HTML.parse(body, url = nil, encoding = charset)
# 「.searchResult」はタイトルやタグなどの親のDOM要素であり、各記事の情報がぶら下っているので、一度ここを取得し、ここから子DOM要素の情報を抽出します。
html.search('.searchResult').each do |node|
title = node.css('.searchResult_itemTitle').text
tags = node.css('.tagList_item').map{ |tag| tag.text }
detail = node.css('.searchResult_snippet').text
link = "https://qiita.com/" + node.css('.searchResult_itemTitle').css('a')[0][:href]
# [0]が「良いね」の数。[1]は「コメント」の数
stars = node.css('.list-unstyled.list-inline.searchResult_statusList li')[0].children.last.text.gsub(" ","")
# コメント数が0個の場合は、コメントのHTML要素が出力されない為、[1]のところでNilエラーになります。それを回避する為にアンパサンドを使用します。
comments = node.css('.list-unstyled.list-inline.searchResult_statusList li')[1]&.text&.gsub(" ","")
author = node.css('.searchResult_header').css('a')&.text
results[word] << { stars: stars, title: title, tags: tags, detail: detail, link: link, comments: comments, author: author }
end
end
end
これで、取得したい情報を全て、results
という変数に格納することができました。
7. CSV出力
それぞれ検索ワードごとにファイルを分けてCSV出力していきます。
header = ['LGTM数', '記事タイトル', 'タグ', '見出し', 'URL', 'コメント数', '著者']
# 「ruby」の検索結果をCSV出力
ruby_articles = results["ruby"].map{|res| [ res[:stars], res[:title], res[:tags], res[:detail], res[:link], res[:comments], res[:author] ] }
CSV.open('qiita_ruby.csv', 'w') do |csv|
# ヘッダーの設定
csv << header
# ボディの入力
ruby_articles.each do |r|
csv << r
end
end
# 「php」の検索結果をCSV出力
php_articles = results["php"].map{|res| [ res[:stars], res[:title], res[:tags], res[:detail], res[:link], res[:comments], res[:author] ] }
CSV.open('qiita_php.csv', 'w') do |csv|
# ヘッダーの設定
csv << header
# ボディの入力
php_articles.each do |r|
csv << r
end
end
# 「python」の検索結果をCSV出力
python_articles = results["python"].map{|res| [ res[:stars], res[:title], res[:tags], res[:detail], res[:link], res[:comments], res[:author] ] }
CSV.open('qiita_python.csv', 'w') do |csv|
# ヘッダーの設定
csv << header
# ボディの入力
python_articles.each do |r|
csv << r
end
end
# 「perl」の検索結果をCSV出力
perl_articles = results["perl"].map{|res| [ res[:stars], res[:title], res[:tags], res[:detail], res[:link], res[:comments], res[:author] ] }
CSV.open('qiita_perl.csv', 'w') do |csv|
# ヘッダーの設定
csv << header
# ボディの入力
perl_articles.each do |r|
csv << r
end
end
所感
rubyのスクレイピングは人気なさそう。。。
やっぱりライブラリや記事・文献の多さから、Pythonが圧倒的に人気っぽそうですよねぇ。。。
https://trends.google.co.jp/trends/explore?q=ruby%20scraping,python%20scraping,php%20scraping,perl%20scraping,javascript%20scraping
イイねやコメント、指摘をいただけると幸いです!!!