質問: ### 前提・実現したいこと scala初心者でございます。 stockedge/netkeiba-scraperで競馬情報をスクレイピングしておりますが、終盤の動作の途中で、以下のエラーメッセージが発生しました。 ### 発生している問題・エラーメッセージ error] (run-main-0) java.lang.RuntimeException: class not found:サラ系3歳以上1勝クラス[指 error] java.lang.RuntimeException: class not found:サラ系3歳以上1勝クラス[指 [error] at scala.sys.package$.error(package.scala:26) [error] at Util$.str2cls(Main.scala:1855) [error] at FeatureGenerator.$anonfun$gradeChanged$1(Main.scala:1521) [error] at scala.runtime.java8.JFunction1$mcII$sp.apply(JFunction1$mcII$sp.java:12) [error] at scala.Option.map(Option.scala:146) [error] at FeatureGenerator.(Main.scala:1519) [error] at FeatureGenerator$.$anonfun$iterator$2(Main.scala:662) [error] at scala.collection.Iterator$$anon$10.next(Iterator.scala:455) [error] at scala.collection.Iterator$GroupedIterator.takeDestructively(Iterator.scala:1155) [error] at scala.collection.Iterator$GroupedIterator.go(Iterator.scala:1170) [error] at scala.collection.Iterator$GroupedIterator.fill(Iterator.scala:1207) [error] at scala.collection.Iterator$GroupedIterator.hasNext(Iterator.scala:1211) [error] at scala.collection.Iterator.foreach(Iterator.scala:937) [error] at scala.collection.Iterator.foreach$(Iterator.scala:937) [error] at scala.collection.AbstractIterator.foreach(Iterator.scala:1425) [error] at FeatureDao$.$anonfun$rr2f$1(Main.scala:1740) [error] at FeatureDao$.$anonfun$rr2f$1$adapted(Main.scala:1737) [error] at scalikejdbc.DBConnection.readOnly(DBConnection.scala:202) [error] at scalikejdbc.DBConnection.readOnly$(DBConnection.scala:200) [error] at scalikejdbc.DB.readOnly(DB.scala:60) [error] at scalikejdbc.DB$.$anonfun$readOnly$1(DB.scala:175) [error] at scalikejdbc.LoanPattern.using(LoanPattern.scala:18) [error] at scalikejdbc.LoanPattern.using$(LoanPattern.scala:16) [error] at scalikejdbc.DB$.using(DB.scala:140) [error] at scalikejdbc.DB$.readOnly(DB.scala:174) [error] at FeatureDao$.rr2f(Main.scala:1737) [error] at Main$.main(Main.scala:1995) [error] at Main.main(Main.scala) [error] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) [error] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) [error] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) [error] at java.lang.reflect.Method.invoke(Method.java:498) [error] Nonzero exit code: 1 [error] (Compile / run) Nonzero exit code: 1 [error] Total time: 5435 s, completed 2019/08/28 2:51:53 ### 該当のソースコード [info] Loading project definition from /Users/AAA/Desktop/netkeiba-scraper-master/project [info] Loading settings for project netkeiba-scraper-master from build.sbt ... [info] Set current project to netkeiba-scraper-master (in build file:/Users/AAA/Desktop/netkeiba-scraper-master/) [info] Running Main genfeature 01:21:20.287 [run-main-0] DEBUG scalikejdbc.ConnectionPool$ - Registered connection pool : ConnectionPool(url:jdbc:sqlite:race.db, user:null) using factory : 01:21:20.291 [run-main-0] DEBUG scalikejdbc.ConnectionPool$ - Registered singleton connection pool : ConnectionPool(url:jdbc:sqlite:race.db, user:null) ### 試したこと 当たり前のことではございますが、stockedge/netkeiba-scraperの使い方の通りに最初からコマンドを実行してみました。 (以下マニュアル引用) 以下のコマンドを上から順に実行していけば最後に素性が作成される。 1sbt "run collecturl" レース結果が載っているURLを収集して「race_list.txt」に保存する。 2sbt "run scrapehtml" レース結果のHTMLをスクレイピングしてhtmlフォルダに保存する。HTMLをまるごとスクレイピングするので結構時間がかかる。 3sbt "run extract" HTMLからレース結果を抜き出しSQLiteに保存する。 4sbt "run genfeature" レース結果を元にして素性を作りSQLiteに保存する。 この4のコマンドを実行後に上記のエラーが発生いたしました。
回答: 私も先ほど、まったく同様のエラーが出ました。 scalaはちょっとソースの意味がわからない部分がありましたが、若干修正を加えたり、DBをいじることで問題を回避することができました。 他の作業の合間に2,3時間でやったことを箇条書きでまとめたことを書いておきます。 本来ならば、ソースを移植してpythonで素性データを作成するようなことをしたいのですが、ちょっと忙しく手をつけられないので、とりあえずの措置です。 まず、基本的なことからわかる範囲で説明します。以下、万一間違いがあったら、どなたかご指摘ください。 素性(そせい)とは、特徴量のことです。特徴量を使って機械学習させ、予測なりなんなりするのが目的です。このデータを得るためにwebスクレイピングします。ただし、webから取得した生データはそのまま扱えないので、数値変換などを行うことで、特徴量をまとめたテーブルを作成するということをsbt "run genfeature"で行います。そのコマンドで作成されるテーブルは、featureテーブルです。 上の方(rysh様)が書かれたエラーの原因箇所ですが、これはクラス(グレード)になります。 競馬のグレードと、Main.scalaのソース内のstr2clsMapのArray配列の中身が対応しています。 競馬のルールレースのクラス分け を参照するとわかりますが、 オープン:0 1600万下:1 1000万下:2 500万下:3 未勝利:4 新馬:5 といったように、sbt "run genfeature"を実行すると、数値に変換されてfeatureテーブルに格納されます。 !イメージ説明 エラーの出た箇所は、サラ系3歳以上1勝クラス指となっており、1勝クラスであれば500万下になりますが、500万下という文字列が含まれないためにエラーとなっています(図は、race_infoテーブルのrace_classカラムの、エラーの出たのと同様のデータですが、すでにSQL文で置換してしまったものです)。 エラーを回避するには、ソースを修正するか、テーブルを修正するかします。 まず、準備として、DB Browser for SQLiteをインストールしてください。このアプリは、プラットフォームを選ばない上、使い方が簡単なので、もし導入されていないならば、是非お使いください。 インストールや使い方の説明は長くなるので省かせていただきますが、DB操作がわからないと話にならないので、もしご存知ないならば、調べるなり勉強するなりして覚えてください。 以下、私が行ったことを書きます。 ### DB Browserでの作業 **1DBのバックアップ** race.dbをコピーしてバックアップ用として別のフォルダへ貼り付けてください。 DB Browser for SQLiteを起動し、File>Open DataBaseを選択して、race.dbを開いてください。 **2SQL文を発行し、データを修正(その1)** ``` UPDATE race_info SET race_class=REPLACE(race_class,"サラ系3歳以上1勝クラス","サラ系3歳以上1勝クラス500万下"); ``` Execute SQLタブを開き、上記のコードを貼り付けて、実行してください。 !イメージ説明 ▶のアイコンを押し、successと下段(結果出力)に表示されれば、無事、500万下が挿入されています。エラーになった場合は、結果出力が赤くなります。 **3SQL文を発行し、データを修正(その2)** サラ系3歳以上2勝クラスでもエラーが出たので、 ``` UPDATE race_info SET race_class=REPLACE(race_class,"サラ系3歳以上2勝クラス","サラ系3歳以上2勝クラス1000万下"); ``` を実行する。 **4SQL文を発行し、データを修正(その3)** サラ系2歳1勝クラスでもエラーが出たので、 ``` UPDATE race_info SET race_class=REPLACE(race_class,"サラ系2歳1勝クラス","サラ系2歳1勝クラス500万下"); ``` を行う。 **5各テーブルをCSV出力** !イメージ説明 Browse Dataでテーブル(payoff,race_info,race_resultの各テーブル)を選択し、File>ExportでCSV形式でエクスポートする。 **6エラー箇所のrace_idの行番号を確認** !イメージ説明 featureテーブルの最後のrace_idに何頭立てのデータが入っているか確認する。 ここでエラーが出た。実際の頭数を満たしていないので、最後のrace_idを持つ行をすべて手動で削除する。 featureテーブルのrace_idが31581を右クリックでDelete Recordし、すべて削除する。 **7sqlite_sequenceテーブル** sqlite_sequenceテーブルのseqの値を、seqの値(34390)-31580=2810にする。 もしかしたら、必要ないかも。 **8payoff,race_info,race_resultテーブルのデータの削除** payoff,race_info,race_resultの各テーブルをそれぞれ、sql文でrace_idが1~31580までのデータを削除する。 上から順番に行うこと(そうしないと、外部キー参照エラーになる) ``` DELETE FROM payoff WHERE race_id < 31581; DELETE FROM race_result WHERE race_id < 31581; DELETE FROM race_info WHERE id < 31581; ``` **9sbt "run genfeature"を再実行** 以上で準備ができたので、DB Browserを閉じ、閉じる際に保存するかどうか聞いてくるので、Saveを必ず行う。 再度、sbt "run genfeature"を実行する。 **10重複行の確認** ``` SELECT * FROM feature WHERE (race_id, horse_id) in ( SELECT race_id, horse_id FROM feature WHERE race_id > 31580 GROUP BY race_id, horse_id HAVING COUNT(*) > 1 ); ``` DB Browserを起動し、念のため、race_idが31581以上の行で重複行がないか確認。何も表示されていなければOK **11出力したcsvのインポート** DB Browserから各テーブルのcsvをインポートして、元に戻す。 この場合は、8とは逆に、race_infoを一番先にインポートする。 ***注意:現時点までのスクレイピングは、sbt "run scrapehtml"を実行することで完了しているので、再び最初からnetkeiba-scraperのコマンドを行う必要はなく、上記の通りに行った時点で、素性データまで最新のものが作られている。** ### 次からのスクレイピングの方法 一定期間ごとにスクレイピングするとして、時間短縮のため、過去1年以内のURLデータ(html自体ではなく、sbt "run collecturl"で行う分)を取得し、スクレイピングを取得済みの一番番号が上のもの以上にする・・・ちょっと何言ってるかわからないw 以下の通りにしてください。 **1race_url.txt、race.db、htmlを削除する** race_url.txt、race.dbは、別フォルダにバックアップをコピーしておいてください。 htmlフォルダ内のファイル名で、一番最近のものをメモしておく(私の場合、201910021212.htmlだった)。このファイル名は、2で使う。 htmlを空にする。時間がかかるようであれば、コマンドプロンプトから !イメージ説明 のようにdel *でフォルダ内のファイルをすべて削除する。 なぜrace_url.txt、race.dbを削除するかというと、単純に、netkeiba-scraperの処理を行うと、race_url.txt、race.dbがすでに存在している場合、エラーになり停止するからです。 **2Main.scalaを修正する** Main.scalaを2か所修正する。以降、秀丸のようなgrepか正規表現で置換できるエディタがあると便利。 まず、143行目を下記のように書き換える ``` //タイム指数が表示される2006年以降のデータだけ使う //filter(file => FilenameUtils.getBaseName(file.getName).take(4) >= "2006"). filter(file => FilenameUtils.getBaseName(file.getName) > "201910021212"). ``` //は、コメントアウトの意味。 ここの処理は、1でメモしたファイル名を拡張子なしで書いているが、そのファイルよりも新しいもののみ、sbt "run scrapehtml"の対象とするということ。よって、それ以前のファイルがあっても、処理されないし、すでにrace.dbに最新の状態でデータが格納されているわけだから、問題はない。 次に、1993行目あたりを次のように書き換える。 ``` //過去10年分のURLを収集する //RaceListScraper.scrape(period = 12 * 10) RaceListScraper.scrape(period = 12 * 1) ``` すでに取得している過去10年分のurlを再び取得するのは馬鹿げているので、上記のように1年分にする。 これより短い期間を指定したかったが、わからなかった。 **3sbt "run collecturl"を実行する** 1年分のURLがrace_url.txtに格納される。 私の環境は、仮想環境内にubuntuを入れてwindows10との共有フォルダ内にnetkeiba-scraper一式を置いて実行しているが、デフォルトのCPU1個とメモリの割り当てで、3分ぐらいで終了した。 **4race_url.txtから最新分だけ抜き出す** ある文字が含まれない行を削除するを参考にして、秀丸で、1でメモしたファイル名の201910021212の201910が含まれる行だけ一覧として取り出す(201910となっていると、2019年10月かと思うが、実際は、9月。それ以降のものを取得したいわけ)。 一覧を修正して、race_url.txtの書式通りに直し、race_url.txtでファイル保存する。要するに、1年分のurlを再びスクレイプするのも面倒なので、取得したファイルよりも新しいものだけをスクレイプするということ。 秀丸がなければ、"ある文字が含まれない行を削除する"で検索して、適当なエディタなり正規表現を使うなりして、race_url.txtを加工すると良い。 **5sbt "run scrapehtml"を実行する** 4で加工したにもかかわらず、480個ものurlを取得。20分ほどで処理が終了。 **6sbt "run extract"を実行する** race.dbが作成されるが、取得済みのurlよりも新しいものがない場合は、データは空である。 DB Browserで確認のこと。 **6バックアップしたrace.dbから、csv出力し、新しく生成されたrace.dbへインポート** 6で新しく生成されたrace.dbにデータがなかった場合は、以降、やる必要なし。 1でバックアップを取っておいたrace.dbをDB Browserで開き、payoff,race_info,race_result各テーブルをcsv出力する。 新しく生成されたrace.dbに、バックアップしたrace.dbから出力された各テーブルのcsvを読み込む。 この際、『DB Browserでの作業』の11に書いた通り、race_id.csvから読み込む。 **7重複行の確認** 10重複行の確認で書いたのを参考に、各テーブルで重複行がないか確認する。 テーブルごとに、どのカラムがあれば一意にデータが決まるかを考えて、SQL文を発行して確認する。 ある行を特定できればよいので、なんなら、すべてのカラムをSQL文に書いてもよい。重複行があるなら、複数行結果表示されるはず。 重複行があったら、削除する。 **8sbt "run genfeature"を実行する** 最新のデータとの差分だけの処理となるので、短時間で済むはず。 以上です。