次の記事を作成するため、必要なデータを用意します。
データのありかはこちらです。
記録は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年の記録は残っていないようです。
上の表は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
- 順序を保持して重複を削除したリストを返す
list(sorted(set(response.xpath('//a[contains(./text(),"10000m")]/@href').extract())))
- 参考:Stack Overflow: Get unique values from a list in python
- 5000m:
- 各組結果ページを開き、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
以降を抽出します。
- csvファイルで用意済み
-
meet_name
: 大会名 -
meet_num
: 大会回数 -
meet_date
: 大会開催日
-
- 文字列を抽出後、改行で分割する
- 正規表現で判別
-
cat_name
: 競技種目名 -
cat_num
: 組 -
cat_gen
: 性別
-
- タイムの位置を基準に判別
-
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.py
をhiukirokukai.py
に名前変更して、クロールさせるスパイダーを作成します。
チュートリアル参考:More examples and patterns
ライブラリをインポートします。
import scrapy
import pandas as pd
import re
スパイダー本体のクラスを作成します。名前name
はhiukirokukai
とします。
class HiukirokukaiSpider(scrapy.Spider):
name = 'hiukirokukai'
csvを読み込み、読み込むURLのリストをstart_urls
に設定します。無用なサーバー負荷を避けるため、最初はスライサを[:1]
に指定して1大会分だけ読み込んで確認します。問題なければスライサを外し、全大会を読み込むことにします。
df = pd.read_csv('hiukirokukai_to_read.csv')
start_urls = df['url'].values.tolist()[:1]
読み込む種目名のリストcatlist
を作っておきます。
catlist = ['5000m', '10000m']
setfield()
メソッドを定義します。urlref
を含む行の対応する値をfield
に設定します。
parse()
メソッドの中で速報ページトップを開いたときと、各組結果ページを開いたときに呼び出します。
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()
メソッドが呼び出されます。
def parse(self, response):
フィールドを初期化して、URLから設定するmeet_*
を設定します。
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を抽出します。
for cat in self.catlist:
queryt = '//a[contains(./text(),"'+ cat +'")]/text()'
query = '//a[contains(./text(),"'+ cat +'")]/@href'
テキストを1行ずつ処理し、正規表現を使ってcat_*
を設定します。
# 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組」のような表記になることあり、少しやっかいです。
まずは該当部分のリストを得ます。
# set cat_num
ma1 = re.findall('[0-9組〜~\- ]+組',l)
リストを結合し、数字部分のみのリストを得ます。
if ma1:
ma1j = ''.join(ma1)
ma1jf = re.findall('[0-9]+',ma1j)
数字のはじめからおわりまで組数を設定し、フィールドを出力します。
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()
メソッドに渡します。
# 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で出力できるようにします。
def parse_record(self, response):
フィールドを設定します。親フォルダのURLをsetfield()
へ渡し、meet_*
を設定します。デバッグのため、最後にprint()
で表示させることにします。
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)
記録を含む部分を全部読み取り、不要な改行を削除します。
# 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で確認したように、各行を読んで各項目を設定します。読んだ行に種目名と組数が含まれているか正規表現で検索します。
# 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
は見つかった場合のみ、設定します。
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()
を適用し、各行の先頭や末尾に含まれることのあるスペースを削除しておきます。
# 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行の項目数で、最も多い項目数を項目数として採用します。
# 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
にします。そして、時間のフィールドを設定します。
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()
リストの中での時間の位置からレーンのありなしを判定し、レーン数を設定します。
# 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]
項目数とリストの中での時間の位置から、登録陸協があるか判定します。
そして、後ろからのインデックス位置を指定して、各フィールドを設定します。
# 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])
最後に、フィールドを出力します。
yield field
以上で、スパイダーの作成完了です。
実際のところ、次でクロールさせた出力結果を確認したり可視化すると、意図しない結果に何度もなりました。
その都度scrapy shellを活用しながらスクレイピングのスクリプトを何度も修正し、上のスパイダーを作りました。
クロール
大量のページを一括で読みに行くとサーバへの攻撃になりかねません。迷惑をかけないために、次のAutoThrottle設定でクロールする速度を自動調整します。デフォルトだとコメントアウトされて無効になっています。
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
- csvから設定した大会名
- 最初と最後の項目のみ
- タイム
ath_time
- 名前
ath_name
所属ath_team
登録陸協ath_reg
- タイム
続きは別記事にします。
- スクレイピングした5000mと10000mの記録を解析用に整形する(平国大記録会)
まとめ
Scrapyを活用することで、用意したURLとそこからリンクされているURLのページを一度にスクレイピングできました。
ここまででうまくできた部分や、苦労しながらできた部分をまとめておきます。
- スクリプトを書き始める前に、ページ構造確認して表にまとめておくのは良かった
- scrapy shellで1行ずつ確認することで、スパイダーの作成がスムーズだった
- scrapy shellへの入力が意図通りに動作したとき、随時スパイダーに反映させた
- 逆に、出力結果を確認して意図しない結果が出た場合、デバッグと修正が容易だった
- 出力結果を確認すると、意図しない結果になることが何度かあって修正をかけた
- 各結果ページの文字の並びが必ずしも同じではなかった
- 性別の記載ありなしが混在していた
- どこまで各ページを考慮できているか、見ているかが手戻りを防ぐのに重要だった