Webスクレイピング備忘録(T11 イントロより)
トークトリアル11はオンラインAPI/サービスを利用しています。T1~10と比べてCADD以前にプログラミングの部分の敷居が高いという印象です。・・・正直コードが何をしているかプログラミングのできない私には分からなかったです(レベル低くてすみません)。
と、いうわけで素人による適当解説をしますよ!!WEBスクレイパーに俺はなる!!
題材
T11の初っ端、イントロから敷居の高い単語が連発でしたが、一番気になった「WkipediaProteinogenic amino acidからのアミノ酸のテーブルを取得する部分」を見ていきます。
行われていることは以下の通りです。
手順 | ライブラリ | やってること |
---|---|---|
データの取得 | requests | Wikipediaの該当ページにアクセス HTMLデータを取得 |
データの抽出 | BeautifulSoup | HTMLの文字列から 目的のテーブルの場所を指定して 情報を抜き出す |
テーブルの再構成 | Pandas | 抜き出した情報をDataFrameで テーブルに再構成して表示 |
では順番に見ていきます。。*1
requestsによるHTMLデータの取得
「入門 Python3 9章」によると、WWWの単純な形としてウェブクライアントが
- ウェブサーバーにHTTP(Hypertext Transfer Protocl)で接続し
- サーバーにURL(Uniform Resource Locator)を要求し、
- HTML(Hypertext Markup Language)を受け取る
という流れになっているそうです。で、Pythonはこれが得意なんだそうな。
クライアントがサーバーに要求(リクエスト)を送って応答(レスポンス)を受け取るウェブのデータの交換の標準プロトコルがHTTPで、そのHTTPメソッドにGETやPOSTというものがあるそうです。
では、Pythonのrequests
を使って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の情報をちゃんと取得できているようです。
BeautifulSoupによる構文解析
取得したHTMLの情報から該当の場所を探し出します。といってもHTMLの書式が分からない私にはさっぱりです。でも大丈夫! ......そう、「Google Chrome」ならね! *2
「右クリック → 検証」だ!
HTMLをただ表示するだけではありません。HTML上でマウスオーバーするだけで、通常の表示ページでの該当箇所がハイライトされて対応を確認することができます。
ここを頼りに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 > | テーブルのデータセルを作成 |
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)の位置に相当します。
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個じゃなかったっけ?と思いましたが、テーブルを見直すとSelenocysteine、Pyrrolysineが含まれていました。こんなアミノ酸知らなかった。。。
あとはデータフレームにするだけです。
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まであるのですが先が思いやられますね!
色々間違っていそうなのでご指摘いただければ幸いです。
ではでは
*1: 参考にさせていただいた記事
図解!PythonでWEB スクレイピングを極めろ!(サンプルコード付きチュートリアル)
図解!Python BeautifulSoupの使い方を徹底解説!(select、find、find_all、インストール、スクレイピングなど)
図解!PythonのRequestsを徹底解説!(インストール・使い方)
Pythonでスクレイピングをする最初の一歩、Webページを丸ごと取得する方法
PythonでWebページを取得できたかどうかのエラーチェックと安全な中止の仕方
*2: とてもわかりやすい記事 Python Webスクレイピング テクニック集「取得できない値は無い」JavaScript対応
*4: テーブルの中では書式の問題で<>とタグにスペースを挟んでいますが、実際は不要です。念のため
*5:初めからtableタグで指定すればよかったのでは?とも思いましたが、同じページにある複数のテーブルでtableタグとその属性が同じになっていたので指定しづらいから使わなかったのだと思います。多分