4
3

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 5 years have passed since last update.

平国大長距離競技会速報ページからScrapyで記録を全部スクレイピングする

Posted at

次の記事を作成するため、必要なデータを用意します。

データのありかはこちらです。

記録はHTMLで記載されています。csvやjsonなど、直接解析可能な形式ではないため、スクレイピングして必要なデータを抽出します。

1回のスクレイピングで収集したデータは他の問題でも使えるようにしたいので、前もって他に解決したい問題をいくつか考えておきます。

  • 出場する大会を選定するため平国大記録会と日体大長距離競技会の記録分布を可視化する
  • 駅伝チームの実力把握のためチームごとの記録分布を可視化する
  • 記録会エントリー申請タイム検討のため組ごとの記録を可視化する
  • 練習方法の参考にするため記録を伸ばしている人やチームを抽出する
  • フルマラソン2時間30分以内で走った人のタイム分布から10000mと5000mの目標タイムを設定する

これらに必要なデータを考慮の上、5000mと10000mの記録を入手します。任意の開催日の全ての組の記録を入手するとします。

スクレイピングにはScrapyを使用して、次の項目のフィールドをjsonで保存します。

  • meet_name: 大会名
  • meet_num: 大会回数
  • meet_date: 大会開催日
  • cat_name: 競技種目名
  • cat_num: 組数
  • cat_gen: 性別
  • ath_lane: レーン
  • ath_time: タイム
  • ath_name: 名前
  • ath_team: 所属
  • ath_reg: 登録陸協

実行環境

  • Mac OS X HighSierra 10.13
  • Scrapy

手順

次の手順をたどります。

  • 表で項目を整理する
    • ページ構造の確認
    • 速報ページのURLとフィールドの各項目の対応
      • meet_name: 大会名
      • meet_num: 大会回数
      • meet_date: 大会開催日
  • scrapy shellで確認する
    • 各組の結果へのリンクを抽出するxpath
    • 記録を含む文字列を抽出するxpath
    • フィールドの各項目の設定
      • cat_name: 競技種目名
      • cat_num: 組数
      • cat_gen: 性別
      • ath_lane: レーン
      • ath_time: タイム
      • ath_name: 名前
      • ath_team: 所属
      • ath_reg: 登録陸協
  • スクレイピングの実施
    • プロジェクトの作成
    • スパイダーの作成
    • クロール
  • 出力結果確認
    • 書式の確認
    • 欠損値の修正
    • 誤入力データの修正

ページ構造とフィールド各項目の確認

ページ構造を確認し、どのページからどのフィールドを設定するか整理します。
また、スクレイピングを開始するページのURL一覧表を作り、URLから一意に決めるフィールドを対応させます。

ページ構造と各フィールドの対応

5000mと10000mの各組の結果記載のページについては、
全体のトップページ>各開催日の速報ページトップ>各組の結果一覧 の順に、たどることができます。
各開催日の速報ページトップのみ、URLを用意するとします。

  • 全体のトップページ
  • hiukirokukai.web.fc2.com
    • 各開催日の速報ページトップ
    • hiukirokukai.web.fc2.com/YYYY/MMDD/top.html
      • 各組の結果一覧
      • hiukirokukai.web.fc2.com/YYYY/MMDD/50or10-ページ番号.html

各ページから、次のフィールドを設定します。

  • 各開催日の速報ページトップ
    • cat_name: 競技種目名
    • cat_num: 組数
    • cat_gen: 性別
  • 各組の結果一覧
    • cat_name: 競技種目名
    • cat_num: 組数
    • cat_gen: 性別
    • ath_lane: レーン
    • ath_time: タイム
    • ath_name: 名前
    • ath_team: 所属
    • ath_reg: 登録陸協

速報ページトップと各組の結果一覧で項目が重複する部分ありますが、
大会日によっては性別の項目が速報ページトップにしかない場合ありました。
両方のページでスクレイピングした結果は残し、出力結果の確認時に合わせます。

URL一覧表の作成

任意の開催日の速報ページトップのURL一覧を用意し、スクレイピングを開始するURLとしてスパイダーに設定します。
また、次の項目はスクレイピングを開始するURLから一意に設定するので一覧に入れます。

  • meet_name: 大会名
  • meet_num: 大会回数
  • meet_date: 大会開催日

速報ページのURLは手っ取り早く、Excelの文字列結合関数で作成しました。2015年から2018年まで、16回分のURLを用意できました。2017年の記録は残っていないようです。

meet_name meet_num meet_date url
平国大記録会 68 2018-06-23 http://hiukirokukai.web.fc2.com/2018/0623/top.html
平国大記録会 67 2018-05-19 http://hiukirokukai.web.fc2.com/2018/0519/top.html
平国大記録会 66 2018-04-29 http://hiukirokukai.web.fc2.com/2018/0429/top.html
平国大記録会 65 2018-03-24 http://hiukirokukai.web.fc2.com/2018/0324/top.html
平国大記録会 60 2016-12-18 http://hiukirokukai.web.fc2.com/2016/1218/top.html
平国大記録会 59 2016-11-27 http://hiukirokukai.web.fc2.com/2016/1127/top.html
平国大記録会 58 2016-10-23 http://hiukirokukai.web.fc2.com/2016/1023/top.html
平国大記録会 57 2016-06-25 http://hiukirokukai.web.fc2.com/2016/0625/top.html
平国大記録会 56 2016-05-29 http://hiukirokukai.web.fc2.com/2016/0529/top.html
平国大記録会 55 2016-04-29 http://hiukirokukai.web.fc2.com/2016/0429/top.html
平国大記録会 54 2015-11-29 http://hiukirokukai.web.fc2.com/2015/1129/top.html
平国大記録会 53 2015-10-24 http://hiukirokukai.web.fc2.com/2015/1024/top.html
平国大記録会 52 2015-09-27 http://hiukirokukai.web.fc2.com/2015/0927/top.html
平国大記録会 51 2015-05-30 http://hiukirokukai.web.fc2.com/2015/0530/top.html
平国大記録会 50 2015-04-29 http://hiukirokukai.web.fc2.com/2015/0429/top.html
平国大記録会 49 2015-04-11 http://hiukirokukai.web.fc2.com/2015/0411/top.html

上の表はhiukirokukai_to_read.csvとして保存し、後で参照します。

scrapy shellで確認する

各組の結果へのリンクや記録の部分を抽出するxpathをscrapy shellで確認します。scrapy shellを使うと、指定したURLを開いて各種scrapyのコマンドが使える状態でipythonが立ち上がり、対話的に各種コマンドとその結果を確認できます。記録を含む文字列を抽出できたら、文字列を分割してフィールドの各項目を設定します。

  • 各開催日速報ページトップを開き、xpathで各組結果ページのURLを抽出する
    • 5000m: //a[contains(./text(),"5000m")]/@href
    • 10000m: //a[contains(./text(),"10000m")]/@href
  • 各組結果ページを開き、xpathで記録を含む文字列を抽出する
    • 該当する部分全部を抜き出す://p/text()|//body/text()
  • 抽出した文字列を分割して、フィールドの各項目を設定する
    • meet_name: 大会名
    • meet_num: 大会回数
    • meet_date: 大会開催日
    • cat_name: 競技種目名
    • cat_num: 組数
    • cat_gen: 性別
    • ath_lane: レーン
    • ath_time: タイム
    • ath_name: 名前
    • ath_team: 所属
    • ath_reg: 登録陸協

各組結果ページのURLを抽出

一例として、2018年6月23日の10000mの記録を抽出します。まずは、2018年6月23日の速報ページを開き、xpathから各組結果ページのURLを抽出できるか確認します。

scrapy shell "http://hiukirokukai.web.fc2.com/2018/0623/top.html"
In [1]: response.xpath('//a/@href').extract()
Out[1]:
['http://www2.hiu.ac.jp/~rikujo/KIROKUKAI/H30/68th-program.pdf',
 'http://www2.hiu.ac.jp/~rikujo/',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-1.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-1.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/15-1.html',
(省略)
 'http://hiukirokukai.web.fc2.com/2018/0623/50-6.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-2.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-2.html',
 'http://hiukirokukai.web.fc2.com/']
In [2]: response.xpath('//a/text()').extract()
Out[2]:
['こちら',
 '平成国際大学陸上競技部HP',
 '8:40 \xa0 \xa0 \xa0 10000m \xa0 \xa0 1組 \xa0\xa0 (男子)',
 '9:10 \xa0 \xa0 \xa0 10000m \xa0 \xa0 2組 \xa0\xa0 (男子)',
 '9:55 \xa0 \xa0 \xa0 1500m \xa0 \xa0 \xa0 1~2組 \xa0 (女子)',
(省略)
 '19:15 \xa0 \xa0 5000m \xa0 \xa0 \xa0 \xa0 11組\xa0 (男子)',
 '19:35 \xa0 \xa0\xa010000m \xa0 \xa0 \xa0 3組 \xa0\xa0 (男子)',
 '20:10 \xa0 \xa0\xa010000m \xa0 \xa0 \xa0 4組 \xa0\xa0 (男子)',
 'トップページに戻る']

10000mの結果を含むリンクのURLとテキストを確認できました。URLと文字列から、5000mと10000mの記録にアクセスできそうです。

リンクの文字列で10000mを含むURLを指定します。

In [3]: response.xpath('//a[contains(./text(),"10000m")]/@href').extract()  
Out[3]:
['http://hiukirokukai.web.fc2.com/2018/0623/10-1.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-1.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-2.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-2.html']

順序を保って重複を削除します。

In [4]: list(sorted(set(response.xpath('//a[contains(./text(),"10000m")]/@href').extract())))
Out[17]:
['http://hiukirokukai.web.fc2.com/2018/0623/10-1.html',
 'http://hiukirokukai.web.fc2.com/2018/0623/10-2.html']

これで、10000mの結果ページへのURLを抽出できました。次は、抽出したURLを開いて記録を抽出します。

記録を含む文字列を抽出

10000m1組目の結果一覧ページを開けたとして、xpathで記録を含む文字列を抽出できるか確認します。

scrapy shell "http://hiukirokukai.web.fc2.com/2018/0623/10-1.html"
In [1]: response.xpath('//p/text()').extract()
Out[1]:
['10000m1組(男子)',
 '19\xa032:56.09\xa0\xa0\xa0\xa0小柳\u3000泰治\xa0\xa0東京情報大学\xa0千葉',
 '25\xa033:18.82\xa0\xa0\xa0\xa0道岡\u3000聖\xa0\xa0東京大学\xa0東京',
 (省略)
 '30\xa038:19.78\xa0\xa0\xa0\xa0松丸\u3000大樹\xa0\xa0東京大学大学院\xa0千葉',
 '10000m2組\xa0 (男子)',
 '7\xa031:39.30\xa0\xa0\xa0\xa0藤本\u3000能有\xa0\xa0東京理科大学\xa0東京',
 '2\xa031:46.76\xa0\xa0\xa0\xa0井上\u3000卓哉\xa0\xa0上智大学\xa0千葉',
 (省略)
 '16\xa037:32.73\xa0\xa0\xa0\xa0宮澤\u3000賢太\xa0\xa0上智大学\xa0群馬']

レーン番号/空白文字/氏名/空白文字/所属/陸協 の順に抽出できました。

他の組でも確認します。

scrapy shell "http://hiukirokukai.web.fc2.com/2018/0519/50-1.html"
In [1]: response.xpath('//p/text()').extract()
Out[1]: ['5000m1組(男女混合)']

正しく抽出されませんでした。pタグではなく、bodyタグ内に直接打ち込まれていたためです。そこで、body部分のテキストも抽出することにします。

In [2]: response.xpath('//p/text()|//body/text()').extract()
Out[2]:
['\r\n',
 '5000m1組(男女混合)',
 '\r\n',
 '9\t16:35.08\t\t\t\t上野\u3000周平\t\t一橋大学\t東京\r\n',
 '7\t16:36.95\t\t\t\t毛利\u3000陽人\t\t一橋大学\t宮城\r\n',
(省略)
 '8\t\t欠場\t\t\t山中\u3000\t\t一橋大学\t東京\r\n10\t\t欠場\t\t\t井次\u3000恵一\t\t境港市陸協\t鳥取\r\n22\t\t欠場\t\t\t小泉\u3000建人\t\t聖学院大学\t埼玉\r\n25\t\t欠場\t\t\t山口\u3000海斗\t\t武蔵越生高校\t埼玉\r\n27\t\t欠場\t\t\t幸本\u3000裕司\t\t越谷競走倶楽部\t埼玉\r\n32\t\t欠場\t\t\t入鹿\u3000英洋\t\t防衛大学校\t神奈川\r\n35\t\t欠場\t\t\t須河\u3000沙央理\t\tオトバンク\t東京\r\n\r\n',
 '\r\n',
 '\r\n',
(省略)
 '\n',
 '\n']

イレギュラーな場合も同様に、抽出できました。

フィールド各項目の設定

抽出した文字列を分割し、フィールドを設定します。scrapy shellを使って、フィールドのうちcat_name以降を抽出します。

  1. csvファイルで用意済み
    • meet_name: 大会名
    • meet_num: 大会回数
    • meet_date: 大会開催日
  2. 文字列を抽出後、改行で分割する
  3. 正規表現で判別
    • cat_name: 競技種目名
    • cat_num: 組
    • cat_gen: 性別
  4. タイムの位置を基準に判別
    • ath_lane: レーン
    • ath_time: タイム
    • ath_name: 名前
    • ath_team: 所属
    • ath_reg: 登録陸協

Body要素に直接文字が打ち込まれているとき、うまくリストに分割されず、全員の結果がひとつの要素となることがあります。

In [3]: fetch('http://hiukirokukai.web.fc2.com/2018/0429/50-1.html')
In [4]: l_row = response.xpath('//p/text()|//body/text()').extract()
In [7]: l_row
Out[7]:
['\r\n',
 '5000m1組(男子)',
 '\r\n',
 '\r\n42\t42\t15:42.62\t\t\t    工藤\u3000\t\t花咲徳栄高)\r\n6\t6\t15:57.59\t\t\t\t高木\u3000拓郎\t\t武蔵野学院大学)\r\n3\t3\t15:59.67\t\t\t\t設永\u3000凱暉\t\t花咲徳栄高校\r\n14\t14\t15:59.97\t\t\t\t松浦\u3000友哉\t\t花咲徳栄高校\r\n35
  (省略)
  '\r\n',
 '\r\n',
 '\n',
 '\n',
 '\n',
 '\n']

改行文字\r\nを目印にリストを分割し、空要素と改行文字の要素を削除します。

In [12]: l_str = []
In [13]: for l in l_row:
    ...:     l_str.extend(l.split('\r\n'))
    ...:     
In [15]: while '' in l_str: l_str.remove('')
In [21]: while '\n' in l_str: l_str.remove('\n')
In [22]: l_str
Out[7]:
Out[22]:
['5000m1組(男子)',
 '42\t42\t15:42.62\t\t\t工藤\u3000\t\t花咲徳栄高)',
 '6\t6\t15:57.59\t\t\t\t高木\u3000拓郎\t\t武蔵野学院大学)',
(省略)
 '41\t41\t\t欠場\t\t\t角田\u3000寧々\t\t拓殖大学',
 '5000m2組(男子)',
 '7\t7\t15:20.46\t\t\t\t岩田\u3000大樹\t\tドリームアシストクラブ東京',
 '3\t3\t15:23.12\t\t\t\t西山\u3000隆将\t\t東京経済大学',
(省略)
 '36\t36\t\t欠場\t\t\t高橋\u3000和輝\t\t上水高校']

ここから、リストのアイテムを1つづつ読んでフィールドを設定します。競技種目名と組数を設定するため、まずは正規表現を使って数字を抜き取ります。

文字列から数値を抽出し、合計したい

In [8]: field = {}
In [9]: field['categoty_num'] = 0
In [10]: import re
In [11]: l = l_row[0]
In [12]: l_num = re.findall('[0-9]*', l)
In [13]: l_num
Out[13]: ['5000', '', '6', '', '', '', '', '', '']

リストの空の要素を削除します。

空の要素を持つリストの要素を取り除きたい

In [14]: l_num = [x for x in l_num if x]
In [15]: l_num
Out[15]: ['5000', '6']

リストの1番目の要素が競技種目名で、2番目の要素が組数です。

In [16]: field['categoty_name'] = l_num[0]
In [17]: field['categoty_num'] = l_num[1]
In [18]: print(field['categoty_name'], field['categoty_num']  
Out[18]: '5000 6'

[レーン, タイム, 名前, 所属, 登録陸協]のリストを得るため、まずはスペースかタブで文字列を分割します。

In [19]: l = l_row[1]
In [20]: l_split = l.split()
In [21]: l_split
Out[21]: ['15:10.58', '村越', '凌太', '埼玉栄高校', '埼玉']

先頭にレーン番号があるときとないときがあります。先頭がタイムであれば、ダミーでレーン番号0を追加します。:が含まれればタイムとみなします。

In [22]: l_split_dummy = [0] + l_split if l_split[0].find(':') else l_split
In [23]: l_split_dummy
Out[23]: [0, '15:10.58', '村越', '凌太', '埼玉栄高校', '埼玉']

分割された名前を半角スペースで結合します。先頭要素2つと末尾要素2つ以外は名前とみなします。

In [24]: l_split_join = l_split_dummy[:2] + [" ".join(l_split_dummy[2:-2])] \
    ...:              + l_split_dummy[-2:]
In [25]: l_split_join
Out[25]: [0, '15:10.58', '村越 凌太', '埼玉栄高校', '埼玉']

リストから各フィールドを設定します。

In [26]: field['ath_lane'] = l_split_join[0]
In [27]: field['ath_time'] = l_split_join[1]
In [28]: field['ath_name'] = l_split_join[2]
In [29]: field['ath_team'] = l_split_join[3]
In [30]: field['ath_reg'] = l_split_join[4]
In [31]: field
Out[31]:
{'cat_name': '5000',
 'categoty_num': '6',
 'ath_lane': 0,
 'ath_time': '15:10.58',
 'ath_name': '村越 凌太',
 'ath_team': '埼玉栄高校',
 'ath_reg': '埼玉'}

フィールドの各項目を設定する手順を確認できました。次は、この手順をたどるスパイダーを作成します。

スクレイピングの実施

リクエストしたページにあるリンク先のページの記録を抜き出すspiderを名前hiukirokukaiとして作成します。そして、コマンドscrapy crawl hiukirokukai -o hiukirokukai.json でクロールさせ、jsonで保存します。

  • プロジェクトの作成
  • スパイダーの作成
    • 名前hiukirokukaiの設定
    • URLの読み込み
    • 種目名の設定
    • URLを開いてリンクをたどる
    • リンクを開いて文字列を抽出し、フィールドを設定する
  • クロール

プロジェクトの作成

プロジェクト名scrape_ldでプロジェクトを作成します。

scrapy startproject scrape_ld

次の構成でファイルが作成されます。

scrape_ld
├── scrape_ld
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── scrape_ld.py
└── scrapy.cfg

スパイダーの作成

scrape_ld.pyhiukirokukai.pyに名前変更して、クロールさせるスパイダーを作成します。

チュートリアル参考:More examples and patterns

ライブラリをインポートします。

hiukirokukai.py
import scrapy
import pandas as pd
import re

スパイダー本体のクラスを作成します。名前namehiukirokukaiとします。

hiukirokukai.py
class HiukirokukaiSpider(scrapy.Spider):
    name = 'hiukirokukai'

csvを読み込み、読み込むURLのリストをstart_urlsに設定します。無用なサーバー負荷を避けるため、最初はスライサを[:1]に指定して1大会分だけ読み込んで確認します。問題なければスライサを外し、全大会を読み込むことにします。

hiukirokukai.py
    df = pd.read_csv('hiukirokukai_to_read.csv')
    start_urls = df['url'].values.tolist()[:1]

読み込む種目名のリストcatlistを作っておきます。

hiukirokukai.py
    catlist = ['5000m', '10000m']

setfield()メソッドを定義します。urlrefを含む行の対応する値をfieldに設定します。
parse()メソッドの中で速報ページトップを開いたときと、各組結果ページを開いたときに呼び出します。

hiukirokukai.py
  def setfield(self, field, urlref):
      field['meet_name'] = self.df[self.df['url'].str.contains(urlref)]['meet_name'].values[0]
      field['meet_num'] = self.df[self.df['url'].str.contains(urlref)]['meet_num'].values[0].astype(str)
      field['meet_date'] = self.df[self.df['url'].str.contains(urlref)]['meet_date'].values[0]
      print(field['meet_num'], field['meet_date'])

parse()メソッドを定義します。クロールを実行すると、start_urlsに対してparse()メソッドが呼び出されます。

hiukirokukai.py
    def parse(self, response):

フィールドを初期化して、URLから設定するmeet_*を設定します。

hiukirokukai.py
        field = {
            'meet_name':'',      # set from df['meet_name']
            'meet_num':0,        # set from df['meet_num']
            'meet_date':'',      # set from df['meet_date']
            'cat_name':'',
            'cat_num':0,
            'cat_gen':''
                }
        self.setfield(field,response.url)

それぞれのcatlistについて、catlistが含まれるリンクのテキストとURLを抽出します。

hiukirokukai.py
        for cat in self.catlist:
            queryt = '//a[contains(./text(),"'+ cat +'")]/text()'
            query = '//a[contains(./text(),"'+ cat +'")]/@href'

テキストを1行ずつ処理し、正規表現を使ってcat_*を設定します。

hiukirokukai.py
            # set cat_name,num,gen
            for l in list(sorted(set(response.xpath(queryt).extract()))):
                print(l)
                # set cat_name
                ma0 = re.search('[0-9]+[a-zA-Z]+',l)
                field['cat_name'] = ma0.group()
                # set cat_gen
                ma2 = re.search('(男子)+|(女子)+|(男女混合)',l)
                if ma2:
                    field['cat_gen'] = ma2.group()

組数cat_numも正規表現を使って抽出するのですが、「4〜6組」のような表記になることあり、少しやっかいです。
まずは該当部分のリストを得ます。

hiukirokukai.py
                # set cat_num
                ma1 = re.findall('[0-9組〜~\- ]+組',l)

リストを結合し、数字部分のみのリストを得ます。

hiukirokukai.py
                if ma1:
                    ma1j = ''.join(ma1)
                    ma1jf = re.findall('[0-9]+',ma1j)

数字のはじめからおわりまで組数を設定し、フィールドを出力します。

hiukirokukai.py
                    if ma1jf:
                        nums = int(ma1jf[0])
                        nume = int(ma1jf[-1])
                        i = nums
                        while(i<=nume):
                            field['cat_num'] = i
                            yield field
                            i = i+1

これで、テキストからフィールドを出力する部分はおわりです。
次は、結果ページのURLをparse_record()メソッドに渡します。

hiukirokukai.py
            # follow links to each records' pages
            for href in list(sorted(set(response.xpath(query).extract()))):
                yield response.follow(href, self.parse_record)

parse_record()メソッドを定義します。辞書型のフィールドの各項目を設定し、jsonで出力できるようにします。

hiukirokukai.py
    def parse_record(self, response):

フィールドを設定します。親フォルダのURLをsetfield()へ渡し、meet_*を設定します。デバッグのため、最後にprint()で表示させることにします。

hiukirokukai.py
        field = {
            'meet_name':'',      # set from df['meet_name']
            'meet_num':0,        # set from df['meet_num']
            'meet_date':'',      # set from df['meet_date']
            'cat_name':'',
            'cat_num':0,
            'ath_lane':0,
            'ath_time':'',
            'ath_name':'',
            'ath_team':'',
            'ath_reg':''
                }
        url = response.url
        url_p = url[:url.rfind('/')]
        self.setfield(field,url_p)

記録を含む部分を全部読み取り、不要な改行を削除します。

hiukirokukai.py
        # read all texts without empty line
        l_row = response.xpath('//p/text()|//body/text()').extract()
        while '\n' in l_row: l_row.remove('\n')
        while '\r\n' in l_row: l_row.remove('\r\n')

scrapy shellで確認したように、各行を読んで各項目を設定します。読んだ行に種目名と組数が含まれているか正規表現で検索します。

hiukirokukai.py
        # set each item
        for j,l in enumerate(l_str):
            print('item: ' + l)
            print('cat_name:', field['cat_name'])
            print('cat_num:', field['cat_num'])
            ma = re.search('[0-9]*[a-zA-Z]*[0-9]*組',l)

種目名と組数が含まれていれば、種目名と組数、性別を正規表現で検索し、フィールドを設定します。
性別cat_genは見つかった場合のみ、設定します。

hiukirokukai.py
            if ma:
                ma0 = re.search('[0-9]+[a-zA-Z]+',l)
                ma1 = re.search('[0-9]*組',l)
                ma2 = re.search('(男子)+|(女子)+|(男女混合)',l)
                ma1n = re.match('[0-9]*',ma1.group())
                field['cat_name'] = ma0.group()
                field['cat_num'] = ma1n.group()
                if ma2:
                    field['cat_gen'] = ma2.group()
                print('set cat_name and cat_num')
                print(field['cat_name'],field['cat_num'])

次の3行を読み、項目数を調べます。[(lane), time, name1, (name2), team, (reg)]となるはずですが、()をつけた項目は全ての行で存在しなかったり、特定の行だけ存在したりします。

項目は半角全角スペースとタブだけでなく、'・'で区切られていることもあったので、正規表現で分割します。項目を数える各行にstrip()を適用し、各行の先頭や末尾に含まれることのあるスペースを削除しておきます。

hiukirokukai.py
                # look next 3 lines to determine length of l_split
                # l_split should be: [(lane), time, name1, (name2), team, (reg)]
                # sometimes, name2 lacks or more splitted
                if len(l_str)-j > 3:
                    l1 = len(re.split('[ \xa0\u3000\t・]+',l_str[j+1].strip()))
                    l2 = len(re.split('[ \xa0\u3000\t・]+',l_str[j+2].strip()))
                    l3 = len(re.split('[ \xa0\u3000\t・]+',l_str[j+3].strip()))

読んだ3行の項目数で、最も多い項目数を項目数として採用します。

hiukirokukai.py
                # lmax: [(lane), time, name1, name2, team, (reg)]
                # select most common length
                l123 = [l1,l2,l3]
                lmax = collections.Counter(l123).most_common()[0][0]

行に種目名が含まれず、時間'mm:ss.ff'の正規表現'[0-9]:[0-9].[0-9]*'が含まれれば行lを項目のリストl_splitにします。そして、時間のフィールドを設定します。

hiukirokukai.py
            else:
                # continue if l has ath_time, 'mm:ss.ff'
                ma3 = re.search('[0-9]*:[0-9]*\.[0-9]*',l)
                if ma3:
                    # split to list
                    l_split = re.split('[ \xa0\u3000\t・]+',l.strip())
                    # set ath_time
                    field['ath_time'] = ma3.group()

リストの中での時間の位置からレーンのありなしを判定し、レーン数を設定します。

hiukirokukai.py
                    # search index of ath_time
                    i_time = 0
                    for k, l_s in enumerate(l_split):
                        if ma3.group() in l_s:
                            i_time = k
                    # from index of ath_time, set ath_lane
                    if i_time == 0:
                        field['ath_lane'] = 0
                    else:
                        field['ath_lane'] = l_split[i_time-1]

項目数とリストの中での時間の位置から、登録陸協があるか判定します。
そして、後ろからのインデックス位置を指定して、各フィールドを設定します。

hiukirokukai.py
                    # from index of ath_time and lmax,
                    # determine if l_split has reg
                    #     l_split: [(lane), time, name1, (name2), team, (reg)]
                    #     index:     (0)   i_time
                    if lmax-(i_time+1) == 4:
                        # l_split: [(lane), time, name1, (name2), team, reg]
                        # set ath_team and ath_reg from last 2 items
                        field['ath_team'] = l_split[-2]
                        field['ath_reg'] = l_split[-1]
                        # set ath_name between i_time and last 2 items
                        field['ath_name'] = ' '.join(l_split[(i_time+1):-2])
                    else:
                        # l_split: [(lane), time, name1, (name2), team]
                        # set ath_team from last item
                        field['ath_team'] = l_split[-1]
                        field['ath_reg'] = 'NaN'
                        # set ath_name between i_time and last item
                        field['ath_name'] = ' '.join(l_split[(i_time+1):-1])

最後に、フィールドを出力します。

hiukirokukai.py
                    yield field

以上で、スパイダーの作成完了です。

実際のところ、次でクロールさせた出力結果を確認したり可視化すると、意図しない結果に何度もなりました。
その都度scrapy shellを活用しながらスクレイピングのスクリプトを何度も修正し、上のスパイダーを作りました。

クロール

大量のページを一括で読みに行くとサーバへの攻撃になりかねません。迷惑をかけないために、次のAutoThrottle設定でクロールする速度を自動調整します。デフォルトだとコメントアウトされて無効になっています。

AutoThrottle extension

settings.py
AUTOTHROTTLE_ENABLED = True

最後に、スクレイピングを実行して結果をjsonファイルhiukirokukai.jsonに書き出します。

scrapy crawl hiukirokukai -o hiukirokukai.json

出力結果確認

スクレイピングした結果が意図した結果になっているか、jsonをpandasのdataframeに読み込ませて確認します。各項目を昇順に数え上げ、次の項目をチェックします。

  • 全項目
    • csvから設定した大会名meet_name大会回数meet_num大会開催日meet_date
    • 各種目cat_name各組cat_numを組み合わせたもの
    • レーンath_lane
  • 最初と最後の項目のみ
    • タイムath_time
    • 名前ath_name所属ath_team登録陸協ath_reg

続きは別記事にします。

  • スクレイピングした5000mと10000mの記録を解析用に整形する(平国大記録会)

まとめ

Scrapyを活用することで、用意したURLとそこからリンクされているURLのページを一度にスクレイピングできました。
ここまででうまくできた部分や、苦労しながらできた部分をまとめておきます。

  • スクリプトを書き始める前に、ページ構造確認して表にまとめておくのは良かった
  • scrapy shellで1行ずつ確認することで、スパイダーの作成がスムーズだった
    • scrapy shellへの入力が意図通りに動作したとき、随時スパイダーに反映させた
    • 逆に、出力結果を確認して意図しない結果が出た場合、デバッグと修正が容易だった
  • 出力結果を確認すると、意図しない結果になることが何度かあって修正をかけた
    • 各結果ページの文字の並びが必ずしも同じではなかった
    • 性別の記載ありなしが混在していた
    • どこまで各ページを考慮できているか、見ているかが手戻りを防ぐのに重要だった
4
3
0

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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?