0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

プレミアリーグの全選手のデータをRuby(nokogiri)によるスクレイピングで取得する

Last updated at Posted at 2020-10-14

 はじめまして。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

続いて今回必要なライブラリを読み込みます。

get_all.rb
require 'nokogiri' 
require 'open-uri' #URLにアクセスするためのライブラリです。
require 'csv' #csvファイルを読み書きするためのライブラリです。

1. 基本データの取得

 まず対象とするのは基本データです。つまり一覧ページから取得できるデータです。一覧ページはページネーションされており複数のURLを取得することになります。そこで、以下のように各URLを配列に格納していきます。

get_all.rb
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セレクターを用いて取得することができます。

sampe
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の配列はすでにできているので、これに対して繰り返し処理を行い、すべてのページからデータを取得します。

get_all.rb
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ファイルを書き出したいと思います。このファイルは次のステップ(詳細データの取得)で読み込んで使用します。

get_all.rb
CSV.open('players_pages.csv', 'w') do |csv|
  csv << players_pages
end

 最後に、取得した基本データをcsvファイルの形でエクスポートしたいと思います。といっても非常に簡単で、以下の5行を追加するだけです。headerはなくてもいいですが、あったほうがexcelやスプレッドシートにそのまま出力できるのでいい気がします。

get_all.rb
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ファイルは作業しているディレクトリに自動で作成されます。ひとまず、基本データの取得がこれで完了しました!!

get_all.rb
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ファイルから読み込んでいきます。

get_detail.rb
require 'nokogiri'
require 'open-uri'
require 'csv'

urls = CSV.read('players_pages.csv') #csvファイルを読み込んで配列に格納する。

 これで全選手の詳細ページのURLが入った配列(urls)を作成することができました。これらすべてに対してデータを取得する作業をeach文を用いて行っていくのですが、問題はeach文の中で行う処理です。今回私は各選手のデータを項目別にハッシュで取得してそれらをまとめたハッシュの配列を作成していくことにしました。まずは全てのデータを収める配列を定義します。

get_detail.rb
details = []

これから行う処理では条件分岐を行います。どういった条件で分岐させるかというと、まずはプレミアリーグのデータが存在するかどうか、次に19/20シーズンのプレミアリーグのデータがあるかどうかで分岐させます。

サッカーに詳しい方でないとどういったことかよくわからないかもしれませんが、今回対象となる選手の中には一試合も出場してい選手なども含まれます。そういった選手はプレミアリーグのデータがないということがありえます。また、登録されながら対象となる19/20シーズンのデータがない選手もいて、これらの選手のページには目的となるテーブルが存在しないことがあります。そのため、こういった分岐が必要になるのです。

 まず、プレミアリーグのデータがあるかどうかで分岐する処理を書いていきます。

get_detail.rb
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セレクタである+を多用しました。

get_detail.rb
  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は以下のようにして取り出すことができます。

get_detail.rb
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

これで一通り完成しました!!

get_detail.rb
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やスプレッドシートで読み込めば画像のような表が作れます。
スクリーンショット 2020-10-14 15.13.55.png
*チーム別にソートした状態でデータを取得したため、デフォルトの順番とは異なっていると思います。

最後に

 今回初めてスクレイピングに挑戦してみましたが、苦戦しながらも非常に楽しかったです。時間がかかりながらも手動で作業するよりは遥かに速くできたのではないかと思います。全く同じようなことをしたい方は少ないかもしれませんが、スクレイピングをしたいと思っているどなたかのお役に立てれば幸いです。(スクレイピングをする際は規約等に違反しないようお気をつけください)

0
2
1

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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?