はじめまして。tatsukoizumiと申します。現在大学生で、エンジニアを目指して学習中の駆け出しです。人生初のQiita投稿で緊張しています。よろしくお願いいたします。
記念すべき初投稿のテーマはRubyのライブラリであるnokogiriを使用したスクレイピングです。早速本題に入っていきましょう。
概要
今回スクレイピングの対象とするサイトは世界中のサッカーに関する情報が網羅されている、Worldfootball.netというサイトです。
大学の研究でデータ分析を行っている友人がプレミアリーグの全選手の詳細データをexcelにまとめるから手伝ってほしいとのことだったので、「それコード書けば一瞬で終わるんじゃね?」と言って引き受けました。人生初のプログラミングを用いた仕事です。(完全なるボランティアですが…)
このサイトは基本データがテーブル形式になっている一覧ページ(ページネーションあり)と、各選手のより詳細なデータが記された詳細ページという構成になっています。まずは、一覧ページから全選手の基本データと各選手の詳細ページへのリンクを取得する作業を行い、次にそれぞれの詳細ページからデータを取得するというのが大まかな手順です。基本データと詳細データはそれぞれ以下のような項目からなります。
-
基本データ
- 名前
- 誕生日
- 身長
-
詳細データ
- 出場数
- 得点数
- イエローカード、レッドカードの枚数
- 途中交代の回数
etc...
↓選手一覧ページ
https://www.worldfootball.net/players_list/eng-premier-league-2019-2020/nach-name/1/
↓詳細ページ(例:南野選手の詳細ページ)
https://www.worldfootball.net/player_summary/takumi-minamino/
*今回は2019/2020シーズンのみという制約付きです。
0. 下準備
*Rubyはインストール済みという前提で進めていきます。
以下のコマンドを打ってnokogiriインストールしましょう。
gem install nokogiri
続いて今回必要なライブラリを読み込みます。
require 'nokogiri'
require 'open-uri' #URLにアクセスするためのライブラリです。
require 'csv' #csvファイルを読み書きするためのライブラリです。
1. 基本データの取得
まず対象とするのは基本データです。つまり一覧ページから取得できるデータです。一覧ページはページネーションされており複数のURLを取得することになります。そこで、以下のように各URLを配列に格納していきます。
urls = []
(1..14).each do |num|
urls.push("https://www.worldfootball.net/players_list/eng-premier-league-2019-2020/nach-mannschaft/#{num}/")
end
これによって14ページの一覧ページが取得できたので、対象となる全選手の基本データを取得する準備が整いました!!
ここからは、正直自分がやった方法が正しいのかどうか自信がありませんが、一応目的を果たす事ができたのでご紹介させていただきます。なにかご指摘いただければ非常に嬉しいです。
次にやったことは各選手の基本データをそれぞれのデータごとに配列で取得していくということです。nokogiriでは以下のようにして、サイトにおける目的となるデータをCSSセレクターを用いて取得することができます。
doc = Nokogiri::HTML(URI.open(url)) #対象となるURLからデータを取得
target = doc.css(".container > div > a") #containerクラス直下にあるdiv直下のa要素を取得。
link = target[:href] #a要素内のhref属性を取得。
text = target.inner_html #a要素内のテキストを取得。
これを踏まえて一覧ページから各選手の基本データを取得していきます。URLの配列はすでにできているので、これに対して繰り返し処理を行い、すべてのページからデータを取得します。
players_pages = [] #各選手の詳細URLを格納
names = [] #各選手の名前 〃
teams = []#各選手の所属チーム 〃
birthdays = [] #各選手の誕生日 〃
height_data = [] #各選手の身長 〃
urls.each do |url|
doc = Nokogiri::HTML(URI.open(url))
doc.css(".standard_tabelle td:nth-child(1) > a").each do |name|
players_pages.push(name[:href])
names.push(name.inner_html)
end
doc.css(".standard_tabelle td:nth-child(3) > a").each do |team|
teams.push(team.inner_html)
end
doc.css(".standard_tabelle td:nth-child(4)").each do |born|
birthdays.push(born.inner_html)
end
doc.css(".standard_tabelle td:nth-child(5)").each do |height|
height_data.push(height.inner_html)
end
end
これで全選手の基本データを取得する事ができました。この段階でp names
と記述して実行すれば全選手の名前が吐き出されると思います!!
csvライブラリではcsvファイルの書き出しと読み込みを行う事ができます。まずは選手の詳細ページのリンク一覧のcsvファイルを書き出したいと思います。このファイルは次のステップ(詳細データの取得)で読み込んで使用します。
CSV.open('players_pages.csv', 'w') do |csv|
csv << players_pages
end
最後に、取得した基本データをcsvファイルの形でエクスポートしたいと思います。といっても非常に簡単で、以下の5行を追加するだけです。headerはなくてもいいですが、あったほうがexcelやスプレッドシートにそのまま出力できるのでいい気がします。
CSV.open('data_1.csv', 'w') do |csv|
headers = %w(name team born height)
csv << headers
names.zip(teams, birthdays, height_data).each { |data| csv << data }
end
これで、全選手の基本データがまとまったファイルの完成です。csvファイルは作業しているディレクトリに自動で作成されます。ひとまず、基本データの取得がこれで完了しました!!
require 'nokogiri'
require 'open-uri'
require 'csv'
urls = []
(1..14).each do |num|
urls.push("https://www.worldfootball.net/players_list/eng-premier-league-2019-2020/nach-mannschaft/#{num}/")
end
players_pages = []
names = []
teams = []
birthdays = []
height_data = []
urls.each do |url|
doc = Nokogiri::HTML(URI.open(url))
doc.css(".standard_tabelle td:nth-child(1) > a").each do |name|
players_pages.push(name[:href])
names.push(name.inner_html)
end
doc.css(".standard_tabelle td:nth-child(3) > a").each do |team|
teams.push(team.inner_html)
end
doc.css(".standard_tabelle td:nth-child(4)").each do |born|
birthdays.push(born.inner_html)
end
doc.css(".standard_tabelle td:nth-child(5)").each do |height|
height_data.push(height.inner_html)
end
end
CSV.open('players_pages.csv', 'w') do |csv|
csv << players_pages
end
CSV.open('data_1.csv', 'w') do |csv|
headers = %w(name team born height)
csv << headers
names.zip(names, teams, birthdays, height_data).each { |data| csv << data }
end
2. 詳細データの取得
ここからはかなり苦戦しました。理由はデータの取得方法がバラバラだったからです。どういうことかというと、詳細ページにははぞの選手のすべてのシーズン、あるいは代表や他のリーグにおける情報なども収められており、ある選手の目的のデータにアクセスしてそれを繰り返し処理で回すといったことができなかったのです。例えば、選手Aの詳細ページの目的となるデータ(19/20シーズンのプレミアリーグのデータ)にアクセスするためにはtableの一列目にアクセスすればよかったものが、選手Bのページで同様にアクセスすると一列目は別のシーズンになってしまっている事があり、これでは意図したデータを収集する事ができません。このように苦戦したのですが、条件分岐などを使ってなんとか実装する事ができたのでそれをご紹介したいと思います。
今回は別のファイルを作成しましたが、同じファイルでも問題ありません。まずは、基本データのときと同様に必要なライブラリを読み込み、各選手の詳細ページのURLを前回作成したcsvファイルから読み込んでいきます。
require 'nokogiri'
require 'open-uri'
require 'csv'
urls = CSV.read('players_pages.csv') #csvファイルを読み込んで配列に格納する。
これで全選手の詳細ページのURLが入った配列(urls)を作成することができました。これらすべてに対してデータを取得する作業をeach文を用いて行っていくのですが、問題はeach文の中で行う処理です。今回私は各選手のデータを項目別にハッシュで取得してそれらをまとめたハッシュの配列を作成していくことにしました。まずは全てのデータを収める配列を定義します。
details = []
これから行う処理では条件分岐を行います。どういった条件で分岐させるかというと、まずはプレミアリーグのデータが存在するかどうか、次に19/20シーズンのプレミアリーグのデータがあるかどうかで分岐させます。
サッカーに詳しい方でないとどういったことかよくわからないかもしれませんが、今回対象となる選手の中には一試合も出場してい選手なども含まれます。そういった選手はプレミアリーグのデータがないということがありえます。また、登録されながら対象となる19/20シーズンのデータがない選手もいて、これらの選手のページには目的となるテーブルが存在しないことがあります。そのため、こういった分岐が必要になるのです。
まず、プレミアリーグのデータがあるかどうかで分岐する処理を書いていきます。
urls.each do |u|
url = u[0] #urls[0][0]というイメージです。単にuの場合["htpps://~~~~.com"]といった形で出力されてしまいます。
doc = Nokogiri::HTML(URI.open(url))
prLeagues = []
doc.css("td > a").each do |a| #すべてのtd以下のa要素に対して処理を行います。
if a.text == "Pr. League"
prLeagues.push(a)
end
end
tds = []
if prLeagues.any?
prLeagues.each { |league| tds.push(league.parent) } #a要素(プレミアリーグ)の親要素であるtd要素を取得します。
else #プレミアリーグがない場合。すべてのデータを0としてハッシュを作成します。
data = { appearances: 0, scores: 0, yellow: 0, red_with_2yellow: 0, red: 0 }
details.push(data)
next #終了して次の処理に移ります。
end
# appearances=出場試合数, scores=得点数
次に19/20シーズンのプレミアリーグのデータが有るかどうかで条件分岐をしていきます。tdsという配列には、プレミアリーグという文字列("pr. League")が含まれたtd要素が格納されています。リーグが示されているtdの直後にはシーズンが示されているのでそこにアクセスして有効なテーブルのみを抽出していきます。今回の場合有効になりうるテーブルは一行のみです。そこで、横に広がるデータを取得していくために「直後」を表すCSSセレクタである+を多用しました。
valid_table = nil #初期化
tds.each do |td|
valid_table = td if td.css(" + td > a").text == "2019/2020" #19/20シーズンのデータがあれば
end
if valid_table
scores = valid_table.css("+ td + td + td + td").text
yellow_cards = valid_table.css("+ td + td + td + td + td + td + td + td").text
red_cards_with_2yellow = valid_table.css("+ td + td + td + td + td + td + td + td + td").text
red_cards = valid_table.css("+ td + td + td + td + td + td + td + td + td + td").text
data = { appearances: appearances, scores: scores, yellow: yellow_cards, red_with_2yellow: red_cards_with_2yellow, red: red_cards }
details.push(data)
else #19/20シーズンのテーブルがない場合(nilの場合)。この場合も全てを0としてデータを作成します。
data = { appearances: 0, scores: 0, yellow: 0, red_with_2yellow: 0, red: 0 }
details.push(data)
end
end #each文の終了
これで一応すべてのデータの取得が完了しました!! かなり苦戦しましたし、かなりごちゃごちゃしてしまった印象ですがなんとか狙い通りのデータを取得することができました。+を多用して「直後の直後の…」というのはさらに親要素を取得した上でnth-childでも良い気がします。分岐のさせ方ももっといい方法がありそうです。未熟者の奮闘記として受け取っていただけると幸いです。
最後に再びcsvファイルの書き出しを行っていきます。ハッシュのvalueは以下のようにして取り出すことができます。
CSV.open('data_2.csv', 'w') do |csv|
headers = %w(appearances score yellow red(2yellow) red(only) )
csv << headers
details.each { |detail| csv << detail.values }
end
これで一通り完成しました!!
require 'nokogiri'
require 'open-uri'
require 'csv'
urls = CSV.read('players_pages.csv')
details = []
urls.each do |u|
p details.count
url = u[0]
doc = Nokogiri::HTML(URI.open(url))
prLeagues = []
doc.css("td > a").each do |a|
if a.text == "Pr. League"
prLeagues.push(a)
end
end
tds = []
if prLeagues.any?
prLeagues.each { |league| tds.push(league.parent) }
else
data = { appearances: 0, scores: 0, yellow: 0, red_with_2yellow: 0, red: 0 }
details.push(data)
next
end
valid_table = nil
tds.each do |td|
valid_table = td if td.css(" + td > a").text == "2019/2020"
end
if valid_table
appearances = valid_table.css("+ td + td + td > a").text
scores = valid_table.css("+ td + td + td + td").text
yellow_cards = valid_table.css("+ td + td + td + td + td + td + td + td").text
red_cards_with_2yellow = valid_table.css("+ td + td + td + td + td + td + td + td + td").text
red_cards = valid_table.css("+ td + td + td + td + td + td + td + td + td + td").text
data = { appearances: appearances, scores: scores, yellow: yellow_cards, red_with_2yellow: red_cards_with_2yellow, red: red_cards }
details.push(data)
else
data = { appearances: 0, scores: 0, yellow: 0, red_with_2yellow: 0, red: 0 }
details.push(data)
end
end
CSV.open('data_2.csv', 'w') do |csv|
headers = %w(appearances score yellow red(2yellow) red(only) )
csv << headers
details.each { |detail| csv << detail.values }
end
完成した2つのcsvファイルをexcelやスプレッドシートで読み込めば画像のような表が作れます。
*チーム別にソートした状態でデータを取得したため、デフォルトの順番とは異なっていると思います。
最後に
今回初めてスクレイピングに挑戦してみましたが、苦戦しながらも非常に楽しかったです。時間がかかりながらも手動で作業するよりは遥かに速くできたのではないかと思います。全く同じようなことをしたい方は少ないかもしれませんが、スクレイピングをしたいと思っているどなたかのお役に立てれば幸いです。(スクレイピングをする際は規約等に違反しないようお気をつけください)