ローイングファンのプログラミング日記

ボート競技やプログラミングについて書きます

手続き型のコードをオブジェクト指向型のコードに書きかえる

ボートレース(レガッタ)の結果をボートクラブ別に検索できるコードを前に書いた。

ローイングファン version1 - ローイングファンの日記

このコードが手続き型だったのでオブジェクト指向型に書きかえてみた。
(2018-07-20修正 コードの実行部分にwhile文を追記)
目次


ローイングファンver1.0から1.1へ

今回書きかえた際、クローリング先のURLはあらかじめコードに仕込むようにした。
例として2017年全日本軽量級選手権の女子舵手なしクォドルプルと男子エイトのURLを登録した。

日本ボート協会 大会情報
第39回全日本軽量級選手権大会
女子舵手なしクォドルプル
https://www.jara.or.jp/race/2017/2017lightweight_w4x.html

男子エイト
https://www.jara.or.jp/race/2017/2017lightweight_m8+.html

<注意>


WindowsPCでレース結果の検索をためしたい場合はWebアプリ版を使ってください。
ローイングファン
https://rowingfan.herokuapp.com/

ローイングファンver1.1のコード

rowingfan_ver1_1.rb

require 'open-uri'

class RaceinfoCollector
    def initialize
        @raceInfoUrls = [ #行末のカンマを忘れずに
            "https://www.jara.or.jp/race/2017/2017lightweight_w4x.html",
            "https://www.jara.or.jp/race/2017/2017lightweight_m8+.html"
        ]
        @guidances = { #行末のカンマを忘れずに
            intro: "レース(レガッタ)情報をクローリングします。",
            yes: "つづける場合は 1 を入力",
            no: "中止する場合は 2 を入力",
            yesOrNo: "半角(1or2): ",
            wait: "クローリング中です。\n#{@raceInfoUrls.size}秒おまちください。",
            seeYou: "See you ...",
            decoHyphen: "----------"
        }
    end

    def crawl_results
        puts @guidances[:decoHyphen]
        puts @guidances[:intro]
        puts @guidances[:yes]
        puts @guidances[:no]
        puts @guidances[:decoHyphen]
        print @guidances[:yesOrNo]
        reply = gets.chomp.to_i
        if reply == 1
            puts @guidances[:wait]
            #全レース結果が入る配列を作成
            raceinfos_all = []
            @raceInfoUrls.each do |url|
                # HTMLをよみこむ
                # :utf-8オプションをつけないとバイト列になる
                opened_html = open("#{url}","r:utf-8")
                html_by_event = opened_html.read
                opened_html.close
                sleep(1) #<重要!>クローリングの間隔をあける

                #シングルスカル種目のクルー名にある余計なタブを削除
                html_by_event.gsub!(/<br.?\/><small>/,'')
                html_by_event.gsub!(/<\/small>/,'')

                # データを取得する
                # 大会名、年代、種目名(タグ付き)
                /<h1 class=\"title\">第\d+(?<tag_tourname>.+)大会<\/h1>/ =~ html_by_event
                /<a href=\"(?<tag_year>\d\d\d\d).+><i class=\"glyphicon glyphicon-file\"><\/i>/ =~ html_by_event
                /<div class=\"panel-heading\">(?<tag_event>.+)の組合せと結果<\/div>\n/ =~ html_by_event
                #レース記号(タグ付き)
                /\Ahttps:\/\/www.+\/(?<tag_urlyear>\d\d\d\d)(?<tag_urltour>.+)_(?<tag_urlevent>.+).html\z/ =~ url
                
                #---id情報---
                #RaceNo, 発艇日時, 組別
                raceno_array = html_by_event.scan(/Race No:\s(\d+)<\/div>/)
                starttime_array = html_by_event.scan(/発艇時刻:<\/b>\s(\d+\/\d+\s\d+:\d+)<\/div>/)
                group_array = html_by_event.scan(/<b>組別:<\/b> <a href=\"\d+.+_tt.html.day\d+_\d+_\d+\">(.+)<\/a><\/div>/)
                
                #---順位別情報---
                #順位, クルー名
                rank_array = html_by_event.scan(/<tr>\n.*<td class=\"text-right\">(\d?)<\/td>/)
                crew_array = html_by_event.scan(/<td class=\"crew\">(.*)<\/td>/)
                #ラップ500, 1000, 1500, 2000
                lap500_array = html_by_event.scan(/<td class=\"crew\">.*<\/td>\n.*<td class=\"text-center\">(\d*:?\d*.?\d*)<\/td>\n/)
                lap1000_array = html_by_event.scan(/<td class=\"crew\">.*<\/td>\n.+<\/td>\n.*<td class=\"text-center\">(\d*:?\d*.?\d*)<\/td>\n/)
                lap1500_array = html_by_event.scan(/<td class=\"text-center\">(\d*:?\d*.?\d*)<\/td>\n.*<td class=\"text-center\">.*<\/td>\n.*<td class=\"text-right\">\d*<\/td>\n/)
                lap2000_array = html_by_event.scan(/<td class=\"text-center\">(\d*:?\d*.?\d*)<\/td>\n.*<td class=\"text-right\">\d*<\/td>\n/)
                #レーンNo, 備考
                laneno_array = html_by_event.scan(/<td class=\"text-right\">(\d?)<\/td>\n.*<td class=\"qualify\">.*<\/td>\n/)
                qualify_array = html_by_event.scan(/<td class=\"qualify\">(.*)<\/td>\n/)

                #id情報の融合 レースNoを基準にしてレース日時,組別の行と列を入れ替える
                #=> [ [ [RaceNo1], [5/10 8:00], [予選A] ], [ [RaceNo2], [5/10 8:20], [予選B] ], [ [RaceNo3], [5/10 8:40,予選C] ], ....]
                identifiers = raceno_array.zip(starttime_array,group_array)

                #id情報に年, 大会名, 種目名を追加する
                identifiers.each do |identifier|
                    identifier.insert(1,tag_event)
                    identifier.insert(0,tag_year,tag_tourname,)
                    identifier.flatten!
                end

                #順位を基準にして,クルー名,ラップタイム,ゴールタイム,レーンNo,備考の行と列を入れ替える
                #=> [ [ [1], [crew], [500], [1000], [1500], [2000], [LaneNo], [備考] ], [ [2],....], [ [3],....] ]
                infos_by_rank = rank_array.zip(crew_array, lap500_array, lap1000_array, lap1500_array, lap2000_array, laneno_array, qualify_array)

                #1レース分の順位別情報を入れる配列を作成
                infos_rank1_6 = []
                #すべての順位別情報が入る配列を作成
                infos_rank1_6_all = []
                count = 0
                infos_by_rank.each do |info_by_rank|
                    count += 1
                    info_by_rank.flatten!
                    #6着の情報の次で配列を空にする
                    infos_rank1_6 = [] if count % 6 == 1
                    #1着から6着を1セットの配列にする
                    infos_rank1_6 << info_by_rank
                    #1着から6着が1セットになった配列を一つの配列に格納していく
                    infos_rank1_6_all << infos_rank1_6 if count % 6 == 0
                end

                #id情報と順位別レース結果の行と列を入れ替える
                #入れ替え後、ブロックに渡して配列に格納する
                identifiers.zip(infos_rank1_6_all) do |infos|
                    raceinfos_all << infos.flatten!
                end
            end #//@raceInfoUrls.each do |url|

            return raceinfos_all
        else
            puts ""
            puts @guidances[:seeYou]
            exit
        end #//if reply == 1
    end #def crawl_results
end

class RaceInfoManager
    attr_writer :race_results
    def initialize
        @race_results = []
        @guidances = { #行末のカンマを忘れずに
            intro: "ボートクラブ別にレース結果を検索します。",
            enterClubname: "ボートクラブの名称を入力してください。\n名称: ",
            hit: "レースを表示します。",
            tocsv: "CSVファイルをつくりました。",
            unhit: "見つかりませんでした。",
            decoHyphen: "----------"
        }
    end

    def find_race
        puts @guidances[:decoHyphen]
        puts @guidances[:intro]
        print @guidances[:enterClubname]
        gotten_clubname = gets.chomp
        found_races = []
        @race_results.each do |race_result|
            if  /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[7]  ||  
                /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[15] ||  
                /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[23] ||  
                /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[31] ||  
                /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[39] ||  
                /.*#{gotten_clubname}[A-Z]?\)?\z/i =~ race_result[47]
                found_races << race_result
            end
        end
        return found_races
    end

    def display_race_results(found_races)
        unless found_races.size == 0
            puts @guidances[:decoHyphen]
            print "#{found_races.size}"
            puts @guidances[:hit]
            puts @guidances[:decoHyphen]
            sleep(1.5)
            found_races.each do |race_result|
                #年, 大会名の出力
                puts "#{race_result[0]}#{race_result[1]}"
                #RaceNo, 種目, 組別, 日時の出力
                puts "RaceNo #{race_result[2]} #{race_result[3]} #{race_result[5]}\n#{race_result[4]}"
                #順位, クルー名, 2000mタイム, 備考の出力
                puts "#{race_result[6] }#{race_result[7] } #{race_result[11]} #{race_result[13]}" if race_result[7] != ""
                puts "#{race_result[14]}#{race_result[15]} #{race_result[19]} #{race_result[21]}" if race_result[15] != ""
                puts "#{race_result[22]}#{race_result[23]} #{race_result[27]} #{race_result[29]}" if race_result[23] != ""
                puts "#{race_result[30]}#{race_result[31]} #{race_result[35]} #{race_result[37]}" if race_result[31] != ""
                puts "#{race_result[38]}#{race_result[39]} #{race_result[43]} #{race_result[45]}" if race_result[39] != ""
                puts "#{race_result[46]}#{race_result[47]} #{race_result[51]} #{race_result[53]}" if race_result[47] != ""
                puts @guidances[:decoHyphen]
            end   
        else
            puts @guidances[:decoHyphen]
            puts @guidances[:unhit]
            puts @guidances[:decoHyphen]
        end #//unless found_races.size == 0
    end

    def write_csv(found_races)
        unless found_races.size == 0
            found_races.each do |race_result|
                #CSVファイル名につける記号を作成する
                case race_result[1]
                when "全日本軽量級選手権"
                    tourname_mark = "lightweight"
                when "全日本大学選手権"
                    tourname_mark = "incolle"
                when "全日本選手権"
                    tourname_mark = "alljapan"
                when "全日本新人選手権"
                    tourname_mark = "freshperson"
                end
                case race_result[3]
                    when "女子シングルスカル"
                        event_mark = "w1x"
                    when "男子シングルスカル"
                        event_mark = "m1x"
                    when "女子ダブルスカル"
                        event_mark = "w2x"
                    when "男子ダブルスカル"
                        event_mark = "m2x"
                    when "女子舵手なしペア"
                        event_mark = "w2-"
                    when "男子舵手なしペア"
                        event_mark = "m2-"
                    when "男子舵手つきペア"
                        event_mark = "m2+"
                    when "女子舵手なしクォドルプル"
                        event_mark = "w4x"
                    when "女子舵手つきクォドルプル"
                        event_mark = "w4x+"
                    when "男子舵手なしクォドルプル"
                        event_mark = "m4x"
                    when "男子舵手なしフォア"
                        event_mark = "m4-"
                    when "女子舵手つきフォア"
                        event_mark = "w4+"
                    when "男子舵手つきフォア"
                        event_mark = "m4+"
                    when "女子エイト"
                        event_mark = "w8+"
                    when "男子エイト"
                        event_mark = "m8+"
                end
                File.open("#{race_result[0]}_#{tourname_mark}_#{event_mark}.csv","a:shift_jis") do |csv|
                    csv.write "#{race_result[0]}#{race_result[1]} RaceNo #{race_result[2]}\n#{race_result[3]} #{race_result[5]}\n#{race_result[4]}\n"
                    csv.write "rank,crew,500m,1000m,1500m,2000m,lane,remarks\n"
                    csv.write "#{race_result[6] },#{race_result[7] },#{race_result[8] },#{race_result[9] },#{race_result[10]},#{race_result[11]},#{race_result[12]},#{race_result[13]},\n"
                    csv.write "#{race_result[14]},#{race_result[15]},#{race_result[16]},#{race_result[17]},#{race_result[18]},#{race_result[19]},#{race_result[20]},#{race_result[21]},\n"
                    csv.write "#{race_result[22]},#{race_result[23]},#{race_result[24]},#{race_result[25]},#{race_result[26]},#{race_result[27]},#{race_result[28]},#{race_result[29]},\n"
                    csv.write "#{race_result[30]},#{race_result[31]},#{race_result[32]},#{race_result[33]},#{race_result[34]},#{race_result[35]},#{race_result[36]},#{race_result[37]},\n"
                    csv.write "#{race_result[38]},#{race_result[39]},#{race_result[40]},#{race_result[41]},#{race_result[42]},#{race_result[43]},#{race_result[44]},#{race_result[45]},\n"
                    csv.write "#{race_result[46]},#{race_result[47]},#{race_result[48]},#{race_result[49]},#{race_result[50]},#{race_result[51]},#{race_result[52]},#{race_result[53]},\n"
                    csv.write "\n"
                end #//File.open
            end #//found_races.each do |race_result|
            puts ""
            puts @guidances[:tocsv]
        end #//unless found_races.size == 0
    end
end

#----ここからコードの実行-----
infoCollector = RaceinfoCollector.new
infoManager = RaceInfoManager.new

#crawllerを実行してレース結果を入手する
#crawlで入手したレース結果をinfoManagerに取り込む
result_collections = infoCollector.crawl_results
infoManager.race_results = result_collections

while true
    #クラブ名とマッチするレースを検索する
    #見つけた結果を表示してCSVファイルをつくる
    found_races = infoManager.find_race
    infoManager.display_race_results(found_races)
    infoManager.write_csv(found_races)
    puts ""
    puts "検索をつづける場合は 1 を入力"
    puts "検索を終了する場合は 2 を入力"
    print "半角(1or2): "
    reply = gets.chomp.to_i
    if reply != 1
        exit
    end
end


実行結果

結果をターミナルに表示させる

女子舵手なしクォドルプル、男子エイトともに優勝している明治大学で検索を試してみる。

まず適当なディレクトリをつくり、その中にRubyコードのファイルを保存する。

ターミナルでコードファイルが保存されているディレクトリに移動しておく。

コードを実行する。

ruby rowingfan_ver1_1.rb


確認表示が出るので12を入力する。

----------
レース(レガッタ)情報をクローリングします。
つづける場合は 1 を入力
中止する場合は 2 を入力
----------
半角(1or2):  


ボートクラブ名の入力を求められるので検索したいボートクラブ名を入力する。
今回は明治大学と入力。

半角(1or2): 1
クローリング中です。
2秒おまちください。
----------
ボートクラブ別にレース結果を検索します。
ボートクラブの名称を入力してください。
名称: 


検索結果がターミナルに表示されCSVファイルが種目ごとにつくられる。

ボートクラブ別にレース結果を検索します。
ボートクラブの名称を入力してください。
名称: 明治大学
----------
8レースを表示します。
----------
2017全日本軽量級選手権
RaceNo 39 女子舵手なしクォドルプル 予選C組
05/26 13:48
1着 明治大学 07:12.79 →Semi-Final
2着 立命館大学 07:22.76 
3着 早稲田大学 07:26.72 
----------
2017全日本軽量級選手権
RaceNo 111 女子舵手なしクォドルプル 準決B組
05/28 10:52
1着 仙台大学 07:01.51 →Final A
2着 明治大学 07:01.76 →Final A
3着 富山国際大学 07:04.14 →Final B
4着 日本体育大学A 07:15.03 →Final B
----------
2017全日本軽量級選手権
RaceNo 129 女子舵手なしクォドルプル 決勝
05/28 15:40
1着 明治大学 07:07.20 
2着 法政大学A 07:10.52 
3着 仙台大学 07:12.14 
4着 立命館大学 07:17.54 
----------
2017全日本軽量級選手権
RaceNo 45 男子エイト 予選A組
05/26 14:36
1着 一橋大学A 06:17.28 →Semi-Final
2着 明治大学B 06:26.61 
3着 早稲田大学 06:26.93 
4着 一橋大学C 06:45.94 
----------
2017全日本軽量級選手権
RaceNo 46 男子エイト 予選B組
05/26 14:44
1着 明治大学A 06:04.48 →Semi-Final
2着 東京大学 06:22.25 
3着 日本大学 06:25.30 
4着 成蹊大学 06:27.95 
----------
2017全日本軽量級選手権
RaceNo 91 男子エイト 敗復B組
05/27 15:10
1着 東京大学 06:26.79 →Semi-Final
2着 早稲田大学 06:29.00 →Semi-Final
3着 明治大学B 06:29.45 
4着 一橋大学C 06:48.27 
5着 慶應義塾大学 06:54.51 
----------
2017全日本軽量級選手権
RaceNo 114 男子エイト 準決A組
05/28 11:16
1着 明治大学A 06:08.44 →Final A
2着 東京大学 06:13.42 →Final A
3着 日本大学 06:16.40 →Final B
4着 一橋大学B 06:24.66 →Final B
----------
2017全日本軽量級選手権
RaceNo 133 男子エイト 決勝
05/28 16:30
1着 明治大学A 06:07.50 
2着 仙台大学 06:12.84 
3着 一橋大学A 06:18.60 
4着 東京大学 06:29.16 
----------

CSVファイルをつくりました。


CSVファイルを表計算アプリで開く

検索結果をターミナルに表示したあと種目ごとにCSVファイルがつくられる。
文字エンコーディングはShift-JISに設定してある。
ファイル名の書式は次の通り。

年_大会名(記号)_種目名(記号).csv


CSVファイルを表計算アプリで開いたところは次の通り。

2017_lightweight_w4x.csv

f:id:rowingfan:20180719071034p:plain

感想

オブジェクト指向型でコードを書くとメンテナンスがしやすくなる。
拡張もしやすい。データベースやTwitterと連動させればWebアプリ版ローイングファンになる。

今回のコードはRuby2.5.1 Windows環境では動かなかった。getsで取得する日本語処理のしかたがわからなかった。
解決したらコードを修正する予定。
そろそろWindowsPCに仮想環境をつくりたい。

今回の実行環境

macOS 10.13.5
Ruby 2.5.1p57

CentOS 7.5
Ruby 2.5.1p57