BeutifulSoupでWebスクレイピング

#はじめに
当記事はある目的を持って必要な技術を調べながら手を動かした記録を要素別にまとめたものの１つです。データ解析全体の流れはリンク先の記事を参照ください。('21/4/29時点未完成)

https://qiita.com/sckln/items/9af07be8df426cbcdb36


#やりたいこと
Web上の対象データを抜き出してにDataFrameに格納する。整形は後で考えるとして、いったんDataFrameに入れる。

#使用したライブラリ
###pandas
https://pandas.pydata.org/pandas-docs/stable/index.html

Documentationも充実してるし良記事に溢れているので特記はありません。
当記事で使うのは以下のようにリスト→Series→DataFrameの単純な処理くらい。

```py
import pandas as pd
list1=[1,2,3]
list2=[4,5,6]
sr1=pd.Series(list1, name='list1')
sr2=pd.Series(list2, name='list2')
df=pd.concat([sr1, sr2], axis=1)
print(df)
```
>   list1  list2
>0      1      4
>1      2      5
>2      3      6

###requests
https://requests-docs-ja.readthedocs.io/en/latest/

URLからWebページの内容を取得するのに使います。
``r = requests.get(url)``で得られるrはResponseオブジェクトと呼ばれ、いったん取得すれば様々な属性を取り出すことが出来ます。Webページの内容を取得したい場合はtext属性を使う。ブラウザで「ページのソースを表示」として出てくる文字列と同様のものです。
一方、この後に使うBeutifulSoupにはデコードされていないレスポンス（バイナリ形式、バイト列）を渡せば良いようです。バイナリ形式の内容はcontent属性で取得できます。

```py
import requests
url='https://ja.wikipedia.org/wiki/'
r=requests.get(url)
r.text #テキスト形式でページのソースを取得
r.content #バイナリ形式でデコードされていないバイト列を取得
```
###time
https://docs.python.org/ja/3/library/time.html#time.sleep
スクレイピングのマナーとしてスレッドの実行を一時停止するために``time.sleep(10)``が使いたいためtimeモジュールも呼び出しておくことにします。

```py
import time
time.sleep(10)#スレッドの実行を10秒停止する
```


###BeautifulSoup
https://www.crummy.com/software/BeautifulSoup/bs4/doc/

http://kondou.com/BS4/

HTMLをスキャンして、データ探索して取り出すためのライブラリです。
実際に使ったメソッドは``.find()``([リンク](http://kondou.com/BS4/#find))と``find_all()``([リンク](http://kondou.com/BS4/#find-all))くらい。

```py 
import requests
from bs4 import BeautifulSoup
url='https://ja.wikipedia.org/wiki/'
result = requests.get(url)
c = result.content
soup = BeautifulSoup(c)
```
実際に何かデータを取り出そうとするときはhtmlソースを眺めてどうやって抜き出したらいいか検討するところから始まります。
例えば、上記の例でWikipediaのトップページから、「今日は何の日」をリスト形式で取り出そうと思ったら、ソースコード上のどこにそれがあるか探します。``<div class="mainpage-content mainpage-onthisday" id="on_this_day">``だと分かれば、

```py
on_this_day=soup.find('div',{'id':'on_this_day'})
```
で該当するdivタグの中身を取り出すことが出来ます。さらに、イベントのリストはそのdivの中に、liタグでリストアップされている、ということが分かったので、それぞれのテキスト部分を抜き出します。

```py
events=on_this_day.find_all('li') 
#↑ここでのeventsはbs4.element.ResultSet形式
#各要素events[i]はbs4.element.Tag形式となっている
for i in range(len(events)):
  events[i]=events[i].text #この処理でTagを取り除いてtextをstr形式で取り出す。
print(events)
```
>['昭和の日', '国際ダンスデー', 'リーフデ号が臼杵湾漂着（1600年 - 慶長15年3月16日）', 'ジェームズ・クック一行がオーストラリア上陸（1770年）', 'ヘーチマンの政変（1918年）', '上海天長節爆弾事件（1932年）', '極東国際軍事裁判でA級戦犯起訴（1946年）', '国際オリンピック委員会 (IOC) が日本とドイツの五輪復帰を承認（1949年）', 'モハメド・アリが徴兵拒否を理由にタイトルを剥奪される（1967年）', '黒人による「貧者の行進」のデモがアメリカ各地からワシントンへ向けて出発（1968年）', 'ボードゲームのオセロがツクダから発売される（1973年）', '植村直己が世界初の北極点犬ゾリ単独行による北極点到達（1978年）', 'ロサンゼルス暴動発生（1992年）', '化学兵器禁止条約が発効（1997年）', 'シリア軍がレバノンから撤退完了（2005年）']

#実践
###下調べ(1)
今回、東京23区の中古マンション価格のデータセットを作って分析したいので、suumoさんのウェブサイトを見に行くことにしますが、一応利用規約をきちんと確認しました。データの私的利用は認められているし、対話型検索機能の利用条件の条項にも特に心配なことは書かれていません。
初心者なので慎重に対応したいですが、ResponseObjectの中を探索するのはGoogleColaboratory環境でやってる訳なので、suumoのwebサーバから見れば検索結果を最終ページまで丹念に閲覧してる人と同じですよね。
###下調べ(2)
suumoの検索結果のページのhtmlソースから、物件情報がどういったTagで記述されているか確認する必要があります。東京23区の中古マンション検索結果は以下のURLとなります。
``https://suumo.jp/jj/bukken/ichiran/JJ012FC002/?ar=030&bs=011&cn=9999999&cnb=0&ekTjCd=&ekTjNm=&kb=1&kt=9999999&mb=0&mt=9999999&sc=13101&sc=13102&sc=13103&sc=13104&sc=13105&sc=13113&sc=13106&sc=13107&sc=13108&sc=13118&sc=13121&sc=13122&sc=13123&sc=13109&sc=13110&sc=13111&sc=13112&sc=13114&sc=13115&sc=13120&sc=13116&sc=13117&sc=13119&ta=13&tj=0&bknlistmodeflg=2&pc=30&pn=1``
ソースを確認すると、「✔チェックした物件をまとめて資料請求する」以下の物件リストは

* 1セットの物件データ：``<div class="property_unit-content">``タグ
* 各物件の名称：``<h2 class="property_unit-title_wide">``タグ
* 販売価格・所在地その他のデータ：``<dd>``タグ

に格納されていることがわかりました。BeautifulSoupでこれらを順次取り出していくことになります。

```py
url='https://suumo.jp/jj/bukken/ichiran/JJ012FC002/?ar=030&bs=011&cn=9999999&cnb=0&ekTjCd=&ekTjNm=&kb=1&kt=9999999&mb=0&mt=9999999&sc=13101&sc=13102&sc=13103&sc=13104&sc=13105&sc=13113&sc=13106&sc=13107&sc=13108&sc=13118&sc=13121&sc=13122&sc=13123&sc=13109&sc=13110&sc=13111&sc=13112&sc=13114&sc=13115&sc=13120&sc=13116&sc=13117&sc=13119&ta=13&tj=0&bknlistmodeflg=2&pc=30&pn=1'
result = requests.get(url)
c = result.content
soup = BeautifulSoup(c)
content = soup.find_all("div",class_='property_unit-content')
data_table = content[0].find_all("dd") #1件目の物件のデータだけを取り出す。
data=[]
for i in range(len(data_table)):
  data.append(data_table[i].text)
print(data)
```
>['\n4990万円～5490万円\n', '46.65m2～50.43m2（壁芯）', '東京都世田谷区上野毛１-32-3リリファ世田谷上野毛', '3.64㎡～4.42㎡', '東急大井町線「上野毛」徒歩5分', '1LDK～2LDK', '\xa0', '1991年11月', '\n\n']

###対象のURLをリストに格納
検索結果は全部で627ページあるようなので、連番になっているURLを生成してリストに入れておきます。

```py
#URLを入れるリスト
urls = []
#URLの共通部分を格納
url='https://suumo.jp/jj/bukken/ichiran/JJ012FC002/?ar=030&bs=011&cn=9999999&cnb=0&ekTjCd=&ekTjNm=&kb=1&kt=9999999&mb=0&mt=9999999&sc=13101&sc=13102&sc=13103&sc=13104&sc=13105&sc=13113&sc=13106&sc=13107&sc=13108&sc=13118&sc=13121&sc=13122&sc=13123&sc=13109&sc=13110&sc=13111&sc=13112&sc=13114&sc=13115&sc=13120&sc=13116&sc=13117&sc=13119&ta=13&tj=0&bknlistmodeflg=2&pc=30'
#1ページ目から最後のページまでを格納
for i in range(627):#現実にはこの後の処理を考えると一気に全ページは待ち時間が大変
    pg = str(i+1)#iはゼロから始まるので1足しておく
    url_page = url + '&pn=' + pg
    urls.append(url_page)
```
###物件データを格納するリストの準備
```py
name = [] #マンション名
price = [] #所在地
space = [] #専有面積
address = [] #住所
balcony = [] #バルコニー面積
access = [] #沿線・駅（○○線「○○」徒歩○分）
rooms = [] #間取り
builtym = [] #築年月
```
###データ取得(これが本番)
むっちゃ時間かかります。主に``time.sleep(10)``のせいですが、気長に。実際には何回かに分けて実行して後でcsvで繋ぎました。

```py
#urlsに格納され散るurlの各ページで以下の動作をループ
for url in urls:
    result = requests.get(url)#urlのウェブページの情報を取得
    c = result.content#Responseオブジェクトからバイナリデータを取り出す
    soup = BeautifulSoup(c)#BeutifulSoupに入れてTagを探索
    
    #マンション名、住所、立地（最寄駅/徒歩~分）、築年数、建物高さが入っているproperty_unit-contentを全て抜き出す
    content = soup.find_all("div",class_='property_unit-content')

    #各propertyunitに対し、以下の動作をループ
    for i in range(len(content)):
        #マンション名取得
        title_wide = content[i].find("h2", class_='property_unit-title_wide')
        title= title_wide.find("a", href=True).text
        name.append(title)
        #その他データ取得
        data_table = content[i].find_all("dd")#
        data=[]
        for j in range(len(data_table)):
            data.append(data_table[j].text)
        price.append(data[0])
        space.append(data[1])
        address.append(data[2])
        balcony.append(data[3])
        access.append(data[4])
        rooms.append(data[5])
        builtym.append(data[7])       
    #プログラムを10秒間停止する（スクレイピングマナーらしい）
    time.sleep(10)
```
###DataFrameにしてcsvに書き出し

とにかくDataFrameに入れてcsvにしてひと段落つきたい。あまりスマートではないが、リスト→Series→DataFrameと順次変換します。
ついでにcolumnsに名前を付けておくことにします。

```py
#各リストをシリーズ化
name = Series(name)
price = Series(price)
space = Series(space)
address = Series(address)
balcony = Series(balcony)
access = Series(access)
rooms = Series(rooms)
builtym = Series(builtym)

#各シリーズをデータフレーム化
suumo_df = pd.concat([name, price, space, address, balcony, access, rooms, builtym], axis=1)

#カラム名
suumo_df.columns=["物件名", "価格(万円)", "専有面積(m2)", "住所", "バルコニー(m2)", "最寄り駅徒歩", "間取り", "築年月"]

#csvファイルとして保存
suumo_df.to_csv('suumo.csv', sep = '\t',encoding='utf-16')

```


#その他の参考記事
https://note.nkmk.me/python-pandas-dataframe-values-columns-index/
https://note.nkmk.me/python-requests-usage/
https://qiita.com/itkr/items/513318a9b5b92bd56185
https://www.analyze-world.com/entry/2017/10/09/062445