Python初心者が初めてのWebスクレイピングで詰まったこと

# はじめに
Pythonの文法すら知らない状態でWebスクレイピングに挑戦したら色々と詰まったので、備忘録がてらまとめる。
実装したものは、とあるWebサイトからドリンクデータを取得して、CSVファイルに出力するプログラム。

## 環境
* windows 10
* Anaconda 3
* python 3.7.3
* BeautifulSoup 4.8.2

## 複数ページからデータを取ってくる
1つのページからデータを取る方法はすぐに見つかったが、どうやって複数ページから取得すれば良いのか？

```py
import requests
from bs4 import BeautifulSoup

import re

# 複数ページのURLを入れる配列
urls_ary = []

# トップページから、全てのaタグを検索して、そのhref属性を取得、配列に追加
url = 'http://hoge/top'
r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
for a in soup.find_all('a', href=re.compile('^huga')):
  urls_ary.append(a.get('href'))

# ドリンクデータ入れる配列
drinks_ary = []

# ループで回して、全ページにアクセス
for page in urls_ary:
  url = 'http://hoge/top/'
  r = requests.get(url + str(page))
  soup = BeautifulSoup(r.text, 'lxml')
  # spanタグにドリンク名が含まれているなら、spanタグとってくる
  tag = soup.find('span')
  drinks_ary.append(tag)
```

- ``find_all('a', href=re.compile('^huga'))``とすることで、全てのaタグのうち、リンクが``huga``から始まるもの（``<a href="huga...></a>"``）のみを取得するようにしている。
- 全てのaタグを検索したい場合は、このオプションなしの``find_all('a')``とすれば良い。``import re``も不要。

## 想定外のエラー時にもプログラムを止めたくない
上記のようにループで回している最中に想定外のエラーがあって処理が止まってしまい、また1からプログラムを回す、などということが起きて悲しかった。
エラー時でもとりあえず無視して処理を終わらせたい。
``try``と``except``を使えば、例外処理をできる。

```py
for page in urls_ary:
  url = 'http://hoge/top/'
  r = requests.get(url + str(page))
  soup = BeautifulSoup(r.text, 'lxml')
  # エラーが起きる可能性があるコードにtry, except
  try:
    tag = soup.find('span')
    drinks_ary.append(tag)
  # エラー時はこの処理を飛ばして次のループに入る
  except:
    continue
```

## ``.string``でテキストを取得できない
``.text``とすれば、ひとまず取得することができる。
``.string``と``.text``との違いについては、[この記事（BeautifulSoupでstringとtextの挙動の明確な違い – Python）](https://lets-hack.tech/programming/languages/python/bs4-text-or-string/)が個人的にわかりやすかった。

## n番目のタグを指定したい
```py
# <html>
# <body>
#   <ul>
#     <li>指定されない</li>
#     <li>指定されない</li>
#     <li>指定される</li>
#     <li>指定されない</li>
#   </ul>
# </body>
# </html>

import requests
from bs4 import BeautifulSoup

url = 'http://hoge/top'
r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')

# 3番目の<li>タグを取得
li_3rd = soup.select(body > ul > li:nth-of-type(3))
```

ちなみに、``nth-child()``でも指定可能だが、``nth-of-type``よりも挙動が複雑。

```py
# <html>
# <body>
#   <div>
#     <h1>指定しない</h1>
#     <p>指定しない</p>
#     <h2>指定しない</h2>
#     <p>指定する</p>
#   </div>
# </body>
# </html>

# 2つ目の<p>タグを取得したい
# nth-of-type
soup.select('body > div > p:nth-of-type(2)')
# nth-child
soup.select('body > div > nth-child(4)')
```

``nth-of-type``を使う場合は簡単で、``<div>``タグの中にある2つ目の``<p>``タグを取得したいので、
``nth-of-type(2)``とする。
一方、2つ目の``<p>``タグは、``<div>``の中にあるタグのうち4番目にあたると捉えることもできるので、``nth-child(4)``とも指定できる。

## cssセレクタが想定通りに効かない
Chromeの開発者ツール（？）を使ってWebサイトのhtmlを確認して、それをもとにCSSセレクタを指定すると稀に指定したものとは異なる要素を取得してしまう。
これはｓ、htmlの開始タグはあるのに閉じタグがないといったWebサイト側、``<table>``タグ内に勝手に``<tbody>``タグが挿入されているといったChrome側の問題であることが多い。
それらは、開発者ツールからは判別できないため、ページのソースを表示して閉じタグが不足していないかなどの確認をおこなう必要がある。

ちなみに、閉じタグが不足している場合は以下のように対処すると良い。

```py
# divの閉じタグが不足しているhtml
# <html>
# <body>
#   <div>
#     <h1>指定しない</h1>
#     <p>指定しない</p>
#     <h2>指定しない</h2>
#     <p>指定する</p>
# </body>
# </html>

# divタグを除去
for tag_div in soup.find_all('div'):
  tag_div.unwrap()

tag_p = soup.select('body > p:nth-of-type(2)')
```

## python3とwindowsと文字コードによる障壁
取得したデータをCSV形式でファイルに保存しようとした際に、``UnicodeEncodeError``に悩まされた。
[この記事（(Windows) Python3でのUnicodeEncodeErrorの原因と回避方法）](https://qiita.com/butada/items/33db39ced989c2ebf644)が特に参考になった。
その他にも色々とググったし参考にした記事も色々あったが、多すぎるのでここでは省く。

ちなみに、以下のコードで上手く保存することができた。

```py
import csv
import codecs

# 保存したいデータが入った配列
drinks_data = ['hoge', 'hogehoge', 'hugahuga']

# CSVで保存
f = codecs.open('data/sample.csv', 'wb', 'cp932', 'ignore')
writer = csv.writer(f)
writer.writerows(drinks_data)
f.close()
```

## おわりに
以上、Python初心者が初めてのスクレイピングで詰まった内容をまとめた。
完成したと勘違いして実行したプログラムが1時間後にエラーで止まって、また1からやり直したときの悲しさは忘れない…
例外処理…常に意識していく…

## 参考サイト
* https://lets-hack.tech/programming/languages/python/bs4-text-or-string/
* https://qiita.com/butada/items/33db39ced989c2ebf644