Pythonクローリング＆スクレイピング 第2章まとめ

#前回まで
前回は環境構築やunixコマンドでのクローリング・スクレイピングを行った
見出しは本文に準ずる

###第2章Pythonではじめるクローリング・スクレイピング
##Pythonを使うメリット
標準ライブラリもサードパーティライブラリも充実していて、データ分析に役立つライブラリもいっぱい揃っているよ！という話
##Python3.7のインストール
Ubuntu 18.04ではPython3.6.8がインストールされているが、本書では3.7の新機能を使うとのことでインストール
`$ sudo apt install -y python3.7 python3.7-venv libpython3.7-dev python3-pip build-essential` 
apt install のあとの -y は 「問い合わせがあった場合はすべて「y」と答える」のオプション
[【 apt-get 】 パッケージを取得してインストール/アップデートする](https://tech.nikkeibp.co.jp/it/article/COLUMN/20060227/230709/)

##仮想環境（venv）の使用
Pythonでは同じライブラリの別バージョンを同一環境でインストール出来ないため、ライブラリの依存関係による誤作動を防ぐために仮想環境を利用する。Pythonでは標準の仮想環境としてvenvがある。

`$ python3.7 -m venv 仮想環境名` でカレントディレクトリに仮想環境名と同じ名前のフォルダが作成される。-mオプションは指定したモジュールをスクリプトとして実行する

注釈としてVirtualBoxの仮想マシン上のubuntuで上記コマンドを実行した場合の不具合が書かれてあったが、WSL上のubuntuでの場合は問題なく動いた

`$ . 仮想環境名/bin/activate` で仮想環境を有効化
.コマンドは引数で指定したファイルを読み込み、現在のシェルで実行する
仮想環境が有効化されるとプロンプトの先頭に
`(仮想環境名)ユーザー名$`と表示される
`(仮想環境名) $ deactivate`で仮想環境を無効化

##Pythonの基礎知識
この辺は自分では理解出来ていると思うので流し読み
でもfを先頭に付けた文字列で
`f"{変数名} hogehoge"` みたいに変数内の値を文字列に置き換えられるというのは知らなかった(3.6から導入された模様)

##Webページを取得する

###Requests

サードパーティライブラリのRequestsを使ってWebページを取得する
`(仮想環境名) $ pip install requests` で**仮想環境内に**インストール

`import requests`して`r = requests.get('<URL>')`でWebページを取得
get()関数はHTTPメソッドのGETに対応している
get()関数はキーワード引数を指定することで、HTTPヘッダーをリクエストに追加,Basic認証を指定,URLパラメータを指定といったことができる

その他post(),put(),patch(),delete(),head(),options()関数がそれぞれ関数名大文字のHTTPメソッドに対応している。

- `r.status_code` →ステータスコードを取得
- `r.headers['content-type'] `→HTTPヘッダーの辞書を取得
- `r.encoding`→エンコーディングを取得
- `r.text`→str型でレスポンスボディ(HTMLのソース)を表示
- `r.content`→ ↑のbytes型版
- `r.json()`→JSON形式で取得

複数のページを連続してクローリングするときは**Session**オブジェクトを利用すると良い
設定を複数のリクエストで使いまわしたり、HTTP Keep-Aliveという接続方式を使うことで相手側のサーバーの負担を軽減することができる
get()、post()などのメソッドはrequets.get()と同じように使える
`s = requests.Session()`
`r = s.get('<URL>')`

###文字コードの扱い
Webページのエンコーディングを取得・推定する3つの方法

1.HTTPレスポンスのContent-Typeヘッダーからそのページのエンコーディングを知ることができる

```
url = sys.argv[1] # 第1引数からURLを取得する。
r = requests.get(url) # URLで指定したWebページを取得する。
print(f'encoding: {r.encoding}', file=sys.stderr) # エンコーディングを標準エラー出力に出力する。
print(r.text) # デコードしたレスポンスボディを標準出力に出力する。
```
2.`r.encoding = r.apparent_encoding`でバイト列の特徴からそのWebページのエンコーディングを推定した結果を得ることができる

3.HTMLのmetaタグで指定されたエンコーディングを取得する
Requestsは**HTTP通信のためのライブラリ**でHTMLなどのコンテンツに依存したエンコーディングを取得することが出来ないため、正規表現を使ってmetaタグのエンコーディングの指定を取り出す

###Webページからデータを抜き出す
####1.正規表現(reモジュール)を使ったスクレイピング
コードは割愛
re.findall()を使って抜き出したい部分(本文では出版社のサイトの書籍のタイトルと書籍ページのURL)を取得して、タグをスペースに置き換えたり、取り除いたりといったところ

####2.lxmlを使ったスクレイピング

lxml → PythonのC拡張モジュール。HTMLのパースにはlxml.htmlを使う。

>パースとは、文法に従って分析する、品詞を記述する、構文解析する、などの意味を持つ英単語。
[パースとは - IT用語辞典](http://e-words.jp/w/パース.html)

- XPathとCSSセレクター
    - XPath →XMLで特定の要素を指定するための言語。多機能で細かな指定ができる。
        - 例:`//body//h1` body要素の子孫のh1
    - CSSセレクター→CSSで装飾する要素を指定するための表記方法
        - 例:`body  h1` body要素の子孫のh1
    - Chromeの開発者ツールでは選択した要素のXPathとCSSセレクターをコピーできる
        - このとき、**tableタグ直下にtrタグがある場合、ブラウザがこの2つの間にtbodyを補完してしまうのでそのままコピーするとlxmlで思うように要素を取れなくなるので注意** 
- lxmlではCSSセレクターをXpathに変換して実行する

#####lxml.htmlのメソッド色々
- `lxml.html.parse('HTMLファイル') `→HTMLファイルをパースする。URLやファイルオブジェクトを指定することもできる。
    -  ↑のパース結果は _ElementTreeオブジェクトとして得られる
    -  ↑ にgetroot()メソッドでHtmlElementオブジェクトが得られる
- fromstring()で文字列をパースできる
    - `lxml.html.fromstring("""<html> ....... </html>""")`
- HtmlElementのxpath()/cssselect()メソッドでXPath/CSSセレクタにマッチする要素のリストを取得できる
    - リストの各要素(Elementオブジェクト)はtag属性でタグの名前を,textで要素のテキストを、get("属性名(idとかclassとか)")メソッドでHTMLタグの属性の値、attrib属性で全属性を表すdict-likeな要素, getparent()メソッドで親要素, tail属性で要素の直後のテキストを取得できる。text_content()メソッドで要素内のすべてのテキストを結合した文字列を得られる

###データをファイルに保存する
####CSV形式での保存
```hogehoge_csv.py
print('hoge,hugo,hego') #ヘッダー
#各行の要素
print('aaa,bbb,ccc')
print(','.join(['aaaa','bbbb','cccc']))
```
みたいなファイルを作って
`$python hogehoge.py > hogehoge.csv` でCSV形式で保存できる
または

```hogehoge2.py
import csv

with open('hogehoge_csv2.csv', 'w', newline='') as f:
writer = csv.writer(f) #引数はファイルオブジェクト
writer.writerow(['hoge', 'hugo', 'huga']) #ヘッダーを出力
writer.writerows([
    [1, 'aaa', 111],
    [2, 'bbb', 222],
    [3, 'ccc', 333],
    [4, 'ddd', 444],
    [5, 'eee', 555],
])
```

辞書形式では

```hogehoge_csv3.py
import csv
with open('hogehoge3.csv', 'w', newline='') as f:
    # 第1引数にファイルオブジェクトを、第2引数にフィールド名のリストを指定する。
    writer = csv.DictWriter(f, ['id', 'name', 'age'])
    writer.writeheader() # 1行目のヘッダーを出力する。
    # writerows()で複数の行を一度に出力する。引数は辞書のリスト。
    writer.writerows([
        {'id': 1, 'name': 'aaa', 'age': 11},
        {'id': 2, 'name': 'bbb', 'age': 12},
        {'id': 3, 'name': 'bbb', 'age': 13},
        {'id': 4, 'name': 'bbb', 'age': 14},
    ])
```

みたいな方法で出力できる

####JSON形式での保存

```hogehoge_json.py
import json
hogehoge = [
    {'hoge': 1, 'huga': 'あああ', 'huge': 111},
    {'hoge': 2, 'huga': 'いいい', 'huge': 222},
    {'hoge': 3, 'huga': 'ううう', 'huge': 333},
]
print(json.dumps(hogehoge, ensure_ascii=False, indent=2)) #後ろ2つのオプションで読みやすく出力できる
```

を実行するとJSON形式の文字列が表示される
保存する場合には最後の行を
`with open('hogehoge.json', 'w') as f:
    json.dump(cities, f)` とする

###Pythonによるスクレイピングの流れ
サンプルプログラムではここまでの内容をまとめてWebページの取得、スクレイピング、保存をの処理ごとに3つの関数に分けて行っている。
この節に関しては**別の記事で自分で書いたプログラムをアウトプットしたい。**
ちなみにサンプルプログラムに関しては取得先のURLがなぜか403エラーになり正常に動作しなかった(´・ω:;.:...
(追記:[取得先のURLが変更されているようです](https://teratail.com/questions/138318)。コメントで教えてくださった@n0ahcodeさんに感謝。)

###URLの基礎知識
URLの構造や絶対URL、相対URLなどについて
Pythonで相対URLを絶対URLに変換するには標準ライブラリurllib.parseのurljoin()関数を使う

(第3章につづく)