magattacaのブログ

日付以外誤報

Webスクレイピング備忘録(T11 イントロより)

トークトリアル11はオンラインAPI/サービスを利用しています。T1~10と比べてCADD以前にプログラミングの部分の敷居が高いという印象です。・・・正直コードが何をしているかプログラミングのできない私には分からなかったです(レベル低くてすみません)。

と、いうわけで素人による適当解説をしますよ!!WEBスクレイパーに俺はなる!!

題材

T11の初っ端、イントロから敷居の高い単語が連発でしたが、一番気になった「WkipediaProteinogenic amino acidからのアミノ酸のテーブルを取得する部分」を見ていきます。

f:id:magattaca:20200509235206p:plain

行われていることは以下の通りです。

手順 ライブラリ やってること
データの取得 requests Wikipediaの該当ページにアクセス
HTMLデータを取得
データの抽出 BeautifulSoup HTMLの文字列から
目的のテーブルの場所を指定して
情報を抜き出す
テーブルの再構成 Pandas 抜き出した情報をDataFrameで
テーブルに再構成して表示

では順番に見ていきます。。*1

requestsによるHTMLデータの取得

「入門 Python3 9章」によると、WWWの単純な形としてウェブクライアントが

  1. ウェブサーバーにHTTP(Hypertext Transfer Protocl)で接続し
  2. サーバーにURL(Uniform Resource Locator)を要求し、
  3. HTML(Hypertext Markup Language)を受け取る

という流れになっているそうです。で、Pythonはこれが得意なんだそうな。

クライアントがサーバーに要求(リクエスト)を送って応答(レスポンス)を受け取るウェブのデータの交換の標準プロトコルHTTPで、そのHTTPメソッドにGETPOSTというものがあるそうです。

では、Pythonrequestsを使ってGETリクエストをおくり、指定したURLのHTML情報を受け取ります。(requests.get(URL)

import requests

r = requests.get("https://en.wikipedia.org/wiki/Proteinogenic_amino_acid")

requests.get関数の戻り値として得たResponseオブジェクトにはリクエストがうまくいったかどうかの情報(ステータス)も含まれています。HTTPステータスコードという3桁の数字で表されており、成功した場合は「2XX」、失敗すると「4XX」や「5XX」といったコードになります(Xも数字)。

今回のリクエストはうまくいったのか?Responseオブジェクトのstatus_code属性を調べます。

print(r.status_code)
#  200

200なので上手くいっているようです。URLをわざと間違えて失敗してみます。

r_error = requests.get("https://en.wikipedia.org/wiki/shippai")

print(r_error.status_code)
# 404

404 Not Found : 未検出 ! 存在しないページを要求したのでちゃんと失敗しました。4XXはクライアント側の誤りで、サーバー側がおかしい時は5XXというステータスコードになるそうです。

リクエストが失敗した時に備えて、エラーを認識してスクリプトを停止する(例外処理)メソッドも用意されています(raise_for_status())。こちらもT11で使われていました。

r_error.raise_for_status()

"""
    ---------------------------------------------------------------------------

    HTTPError                                 Traceback (most recent call last)

    <ipython-input-13-af32409c08e5> in <module>
    ----> 1 r_error.raise_for_status()
    ~~~ 省略~~~

        939 
        940         if http_error_msg:
    --> 941             raise HTTPError(http_error_msg, response=self)
        942 
        943     def close(self):

    HTTPError: 404 Client Error: Not Found for url: https://en.wikipedia.org/wiki/Shippai
"""

「URLが見つからない」というエラーが出ました。親切ですね!

では上手くいった場合どのような情報が取得できているのでしょうか? Resposeオブジェクトのtext属性で確認できるそうです。長いので最初の300文字取り出します。

print(r.text[:300])

"""
    <!DOCTYPE html>
    <html class="client-nojs" lang="en" dir="ltr">
    <head>
    <meta charset="UTF-8"/>
    <title>Proteinogenic amino acid - Wikipedia</title>
    <script>document.documentElement.className="client-js";RLCONF={"wgBreakFrames":!1,"wgSeparatorTransformTable":["",""],"wgDigitTransformTable":["",""],"wg
"""

Wikipediaの該当ページProteinogenic amino acidソースを表示させてみると上の出力と一致しました。HTMLの情報をちゃんと取得できているようです。

f:id:magattaca:20200509235557p:plain

BeautifulSoupによる構文解析

取得したHTMLの情報から該当の場所を探し出します。といってもHTMLの書式が分からない私にはさっぱりです。でも大丈夫! ......そう、「Google Chrome」ならね! *2

「右クリック → 検証」だ!

f:id:magattaca:20200509235709p:plain

HTMLをただ表示するだけではありません。HTML上でマウスオーバーするだけで、通常の表示ページでの該当箇所がハイライトされて対応を確認することができます。

f:id:magattaca:20200509235744p:plain

ここを頼りにHTML構文解析を行なって該当のテーブルの情報を抜き出していきます。関係するHTMLのタグを先に確認しておきましょう。HTMLクイックリファレンスがとても便利なページでした。*3

初歩の初歩で恐縮ですがタグはHTMLの目印で、「<●●>(開始タグ)~<\/●●>(終了タグ)」という形で目印をつけたい部分を囲んで指定します。(タグも知らずに偉そうに解説かいてごめんなさい。)*4

タグ 内容
< h1 > ~ < h6 > 見出し。hはheadingの略で、
数字1~6は見出しの上位、下位を示す(1が最上位)
< span > ひとかたまりの範囲として定義。
囲んだ範囲にスタイルシートを適用(cf. DIV)
style属性、id属性、class属性など
< table > テーブルを作成
< thead > テーブルのヘッダ行を定義
< tbody > テーブルのボディ部分を定義
< tr > テーブルの一行を定義
< th > テーブルの見出しセルを作成
< td > テーブルのデータセルを作成

f:id:magattaca:20200510000139p:plain

HTMLの書式もだいたいわかったのでBeautifulSoupを使ってデータを抽出していきます。先に見たようにrequestsで取得したHTMLテキストはResponseオブジェクトの.text属性でした。これをBeautifulSoupに渡してやります。

from bs4 import BeautifulSoup

html = BeautifulSoup(r.text)

BeautifulSoupのHTML解析では、HTMLタグをメソッドfind()find_all()を使って検索し、該当箇所を検索、指定します。find(タグ)は引数に一致する最初の一つfind_all(タグ)は一致する全ての要素を取得します。

まずは、目的のテーブル(General chemical properties)の見出しを探します。span要素で、id属性「id="General_chemical_properties"」 となっていました。一つしかないのでfind()で探します。

header = html.find('span', id="General_chemical_properties")

print(type(header))
print(header)

# <class 'bs4.element.Tag'>
# <span class="mw-headline" id="General_chemical_properties">General chemical properties</span>

望みのタグの箇所を指定できているようです。テーブルはこの見出しの下にあるので、順に辿っていきます。find_all_next()を使うことで以降の要素を全て取得することができ、さらにインデックスで要素を指定できます。

下図の通りtable要素は見出し以降の5番目(index 4)の位置に相当します。

f:id:magattaca:20200510000333p:plain

table = header.find_all_next()[4]

冗長なので省略しますが、print(table)として確認すると< table > ~ < /table >で囲まれる要素が取得されていることがわかります。*5

取り出したい情報はテーブルからヘッダを除いたボディ部分なのでtbodyタグを選択し抽出します。

table_body = table.find('tbody')

あとはここから取り出した情報をテーブルとして再構成すればOKです。

データの抽出とデータフレームの作成

DataFrameを作成する準備として、リストのリストを作成します。外側のリストはテーブルの各行を要素とするリストで、内部のリストは各行の各セルを要素とするリストです。

テーブルの各行はHTMLのtrタグを用いることで識別でき、各セルはtdにより識別できます。各行(row)、各セル(cell)についてforループを回すことでデータを順番に取り出していきます。forループでは、データ全体を格納するリストdataの中に、各行毎に空のリスト[]を追加したのち、各セルの中身を該当の行のリスト(リストdataの中で一番新しい最後の要素data[-1])に追加していきます。

data = []
for row in table_body.find_all('tr'):
    cells = row.find_all('td')
    if cells:
        data.append([])
    for cell in cells:
        cell_content = cell.text.strip()
        try:  # 可能であればfloatに変換します
            cell_content = float(cell_content)
        except ValueError:
            pass
        data[-1].append(cell_content)
print(len(data))

#  22

22の行がdataに格納されました。アミノ酸って20個じゃなかったっけ?と思いましたが、テーブルを見直すとSelenocysteinePyrrolysineが含まれていました。こんなアミノ酸知らなかった。。。

あとはデータフレームにするだけです。

import pandas as pd

pd.DataFrame.from_records(data)
0 1 2 3 4 5
0 A Ala 89.09404 6.01 2.35 9.87
1 C Cys 121.15404 5.05 1.92 10.7
2 D Asp 133.10384 2.85 1.99 9.9
3 E Glu 147.13074 3.15 2.1 9.47
4 F Phe 165.19184 5.49 2.2 9.31
5 G Gly 75.06714 6.06 2.35 9.78
6 H His 155.15634 7.6 1.8 9.33
7 I Ile 131.17464 6.05 2.32 9.76
8 K Lys 146.18934 9.6 2.16 9.06
9 L Leu 131.17464 6.01 2.33 9.74
10 M Met 149.20784 5.74 2.13 9.28
11 N Asn 132.11904 5.41 2.14 8.72
12 O Pyl 255.31000 ? ? ?
13 P Pro 115.13194 6.3 1.95 10.64
14 Q Gln 146.14594 5.65 2.17 9.13
15 R Arg 174.20274 10.76 1.82 8.99
16 S Ser 105.09344 5.68 2.19 9.21
17 T Thr 119.12034 5.6 2.09 9.1
18 U Sec 168.05300 5.47 1.91 10
19 V Val 117.14784 6 2.39 9.74
20 W Trp 204.22844 5.89 2.46 9.41
21 Y Tyr 181.19124 5.64 2.2 9.21

できました!

最初からPandasを使う

ちなみに最初からPandasを使って直接HTMLのtableを取得することも可能だそうです。*6

url = "https://en.wikipedia.org/wiki/Proteinogenic_amino_acid"

dfs = pd.read_html(url)

print("ページのテーブルの数: ", len(dfs))

print("最初のテーブルを出力")

dfs[0]

# ページのテーブルの数:  9
# 最初のテーブルを出力
Amino acid Short Abbrev. Avg. mass (Da) pI pK1(α-COOH) pK2(α-+NH3)
0 Alanine A Ala 89.09404 6.01 2.35 9.87
1 Cysteine C Cys 121.15404 5.05 1.92 10.70
2 Aspartic acid D Asp 133.10384 2.85 1.99 9.90
3 Glutamic acid E Glu 147.13074 3.15 2.10 9.47
4 Phenylalanine F Phe 165.19184 5.49 2.20 9.31
5 Glycine G Gly 75.06714 6.06 2.35 9.78
6 Histidine H His 155.15634 7.60 1.80 9.33
7 Isoleucine I Ile 131.17464 6.05 2.32 9.76
8 Lysine K Lys 146.18934 9.60 2.16 9.06
9 Leucine L Leu 131.17464 6.01 2.33 9.74
10 Methionine M Met 149.20784 5.74 2.13 9.28
11 Asparagine N Asn 132.11904 5.41 2.14 8.72
12 Pyrrolysine O Pyl 255.31000 ? ? ?
13 Proline P Pro 115.13194 6.30 1.95 10.64
14 Glutamine Q Gln 146.14594 5.65 2.17 9.13
15 Arginine R Arg 174.20274 10.76 1.82 8.99
16 Serine S Ser 105.09344 5.68 2.19 9.21
17 Threonine T Thr 119.12034 5.60 2.09 9.10
18 Selenocysteine U Sec 168.05300 5.47 1.91 10
19 Valine V Val 117.14784 6.00 2.39 9.74
20 Tryptophan W Trp 204.22844 5.89 2.46 9.41
21 Tyrosine Y Tyr 181.19124 5.64 2.20 9.21

・・・めっちゃ楽やん。

まとめ

以上、Webスクレイピングでした。ウェブのやりとりの仕組みからHTMLの書式までど素人には辛い内容ですね。T11の残り、本体パートA~Cまであるのですが先が思いやられますね!

色々間違っていそうなのでご指摘いただければ幸いです。

ではでは