LoginSignup
1
2

More than 5 years have passed since last update.

BeautifulSoupでノーベル賞受賞者を取得する(PJDV 5.7~5.7.1章)

Last updated at Posted at 2018-03-18

概要

使い方

  • サンプルコードを"pjdv_s5_7_s5_7_1.py"で保存
  • ipythonでget_nobel_laureates()を実行。
  • wikipediaにアクセスするので連続実行に注意。
In [70]: import pjdv_s5_7_s5_7_1
In [71]: get_nobel_laureates()
Out[71]: 
[{'category': 'Physics',
   'href': '/wiki/Wilhelm_R%C3%B6ntgen',
   'name': 'Wilhelm Röntgen',
   'year': 1901},
  {'category': 'Chemistry',
   'href': '/wiki/Jacobus_Henricus_van_%27t_Hoff',
   'name': "Jacobus Henricus van 't Hoff",
   'year': 1901},

サンプルコード

pjdv_s5_7_s5_7_1.py
# -*- coding: utf-8 -*-
# 概要
# [PJDV]s5.7, s5.7.1を理解するために作成したスクリプト

# 略語など
# [PJDV] : 「O'Reilly Japan - PythonとJavaScriptではじめるデータビジュアライゼーション」のこと
# [PJDV]s5.7: [PJDV]の5.7章のこと
# [PJDV]p124-p126 : [PJDV]の124ページからp126ページのこと。
# スクリプト内の補足への参照は、# [数値]
# スクリプト内の補足は、スクリプト末尾にまとめてある。

# 使い方
# ipythonでget_nobel_laureates()を実行。wikipediaにアクセスするので注意する。
# In [70]: import pjdv_s5_7_s5_7_1
# In [71]: get_nobel_laureates()
# Out[71]: 
# [{'category': 'Physics',
#   'href': '/wiki/Wilhelm_R%C3%B6ntgen',
#   'name': 'Wilhelm Röntgen',
#   'year': 1901},
#  {'category': 'Chemistry',
#   'href': '/wiki/Jacobus_Henricus_van_%27t_Hoff',
#   'name': "Jacobus Henricus van 't Hoff",
#   'year': 1901},

from bs4 import BeautifulSoup # https://www.crummy.com/software/BeautifulSoup/bs4/doc/
import requests
import requests_cache
import urllib
import re

requests_cache.install_cache()

def join_url(base_url, target):
    """ urlを結合する """
    return urllib.parse.urljoin(base_url, target)

def get_soup(url, parser="lxml", headers={'User-Agent': 'Mozilla/5.0'}):
    """ urlに対応したsoup(タグツリー)を返す """
    # print("requests.get(" + url + ")")
    response = requests.get(
        url, headers=headers
    )
    return BeautifulSoup(response.content, parser)

def get_column_titles(table):
    """ 列ヘッダディクショナリリストを得る。[PJDV]s5.7 タグの選択, [PJDV]s5.7.1 図5-2参照
    列ヘッダディクショナリ
    - タイトル(name)
    - リンク(href)
    """
    # 解析するhtml構造 : <tr><th><a href=リンク>タイトル</a></th><th><a href=リンク>タイトル</a></th>...</tr>
    th = table.select_one('tr').select('th') # [2]
    titles=[] # [3]
    # <th>を解析する
    for t in th: # [5]
        item = {
            "name" : t.text.replace("\n", " ").strip(), # [4]
            "href": None
        }
        # <a>を解析する
        a = t.select_one("a")
        if a:
            item["href"]=a.attrs["href"]
        titles.append(item)
    return titles

def get_nobel_categories(table):
    """ 受賞分野リスト (categories)を得る """
    titles = get_column_titles(table)
    categories = []
    for t in titles[1:]: # Yearを除外する
        item = {
            "category" : t["name"],
            "href": t["href"]
        }
        categories.append(item)

    return categories

def get_nobel_winners(table, categories):
    """ 受賞者ディクショナリリスト(nobel_winners)を得る。 [PJDV]s5.7.1 p128 get_Noble_winners()
    受賞者ディクショナリ
    - 受賞年 (year)
    - 受賞分野 (category)
    - 受賞した人物の名前 (name)
    - 受賞した人物のリンク (href) -- 注釈へのリンクは除く
    """
    # 解析するhtml構造 : [8]を参照 
    nobel_winners = []
    for r in table.select('tr')[1:-1]: # [6]
        year = int(re.sub(r'\[.+?\]', "", r.select('td')[0].text)) # [7]
        for i, td in enumerate(r.select('td')[1:]):
            for a in td.select('a'):
                if a.attrs["href"].startswith("#endnote"):
                    pass
                else:
                    # print(str(year) + " : " + str(i) + " " + categories[i]["category"] + " : " + a.text)
                    item = {
                        "year":year,
                        "category":categories[i]["category"],
                        "name":a.text,
                        "link":a.attrs["href"]
                    }
                    nobel_winners.append(item)
    return nobel_winners

def get_nobel_laureates():
    BASE = 'https://en.wikipedia.org'
    TARGET= 'wiki/List_of_Nobel_laureates'

    # ノーベル賞ページにリクエストを行う [0]
    soup = get_soup(join_url(BASE,TARGET))

    table = soup.select_one('.wikitable') # [1]
    categories = get_nobel_categories(table)
    nobel_winners = get_nobel_winners(table, categories)

    return nobel_winners

def main():
    print(get_nobel_laureates())

if __name__ == '__main__':
    main()

# [0]
# データ取得対象となるアドレス
# List of Nobel laureates - Wikipedia.html
# https://en.wikipedia.org/wiki/List_of_Nobel_laureates

# [1]
# このケースだけかもしれないが、以下でも同じ
# tbl = soup.select('table.sortable.wikitable')
# tbl = soup.select_one('table.sortable.wikitable')
# tbl = soup.select('.wikitable')

# [2]
# List of laureatesの列見出しは、上下に同じものが記載されているため
# [PJDV]p125-p126の流れで実装していくと、同じものが二度登録されてしまう。
# [PJDV]p127のget_column_titles()は、これが考慮されているようで、先にselect_one('tr')で
# 下部の列見出しを排除している様子

# [3]
# 得られるデータの例。
# titles = [{'category': 'Year', 'href': None}, {'category': 'Physics', 'href': '/wiki/List_of_Nobel_laureates_in_Physics'},]

# [4]
# Physiology or Medicineだけ、タイトルのテキストに改行コード(\n)が入っている
# [PJDV]s.5.7.1 p128のget_Nobel_winnersの受賞者ディクショナリリストでは改行コードを取り除いてないデータが出ている。
# 個人的な好みで.replace("\n", " ")で取り除いている。また、'.wikitable'の内容なら、.strip()は無くてもいい。

# [5]
# [PJDV]s.5.7.1 p127のget_column_titles()では、
# "テーブルヘッダをループし、最初の年の列([1:])を無視する。"
# とコメントされている。(.select('th')[1:]として意図的にYearを取り除いている)
# 処理の意味はわかるが、最終的に何をしたくてこうなっているかはわからない。
# [PJDV]s5.7.1 p128 get_Noble_winners()でcategoryとしてtitlesを参照するからYearの列が邪魔なのであろう。
# このスクリプトでは、get_nobel_categories()で、ラップすることにした。

# [6]
# [PJDV]s.5.7.1 p128のget_Noble_winners()のコメントでは、
# "図5-2に対応する、2行目から始まる年の全行を取得する"となっているが、
# おそらく"上下の列ヘッダを除く全行"という意図だと思う。そうでないと[1:-1]の-1が説明できない。

# [7]
# テキスト通り、year = int(r.select_one('td').text)としたいところだが、
# "2016[11]"というデータがあったので"[11]"を除去したあとでint()する。
#  2015     ・・・ OK.int()が成功する
#  2016[11] ・・・ NG.int()が失敗する。[11]はページ内のReferencesへのリンク
# select_one('td')ではなく、select('td')[0]は次のループとの関係からこういうほうがいいかなという好み

# [8]
# <tr><td>受賞年</td>
# <td>
#     <a href=受賞者1のリンク>受賞者名</a>
# </td>
# という構造を解析する(<span>もあるが気にしない)。[PJDV]s5.7.1 図5-2参照
# また、受賞した分野(category)については、categoriesリストから"順序的対応"で取得する。
# なお、複数の受賞者がいる場合、
# <tr><td>受賞年</td>
# <td>
#     <a href=受賞者1のリンク>受賞者名1</a>;
#     <a href=受賞者2のリンク>受賞者名2</a>
# </td>
# という構造になるので注意する。
# つまり、select_one('a')は利用できず、select('a')でループにより処理する必要がある。

補足の補足

[2]および[6]について

# [2]
# List of laureatesの列見出しは、上下に同じものが記載されているため
# ...
# [6]
# [PJDV]s.5.7.1 p128のget_Noble_winners()のコメントでは、
# "図5-2に対応する、2行目から始まる年の全行を取得する"となっているが、
# おそらく"上下の列ヘッダを除く全行"という意図だと思う。そうでないと[1:-1]の-1が説明できない。

title.png

table.select('tr')[-1]が何者かしらべてみる。

In [95]: soup = _get_nobel_soup(parser="lxml")
    ...: table = soup.select_one('.wikitable')
    ...: table.select('tr')[-1]
Out[95]: 
<tr>
<th>Year</th>
<th width="16%"><a href="/wiki/List_of_Nobel_laureates_in_Physics" title="List of Nobel laureates in Physics">Physics</a></th>
<th width="16%"><a href="/wiki/List_of_Nobel_laureates_in_Chemistry" title="List of Nobel laureates in Chemistry">Chemistry</a></th>
# 略

下段の列ヘッダだった

[4]について

# [4]
# Physiology or Medicineだけ、タイトルのテキストに改行コード(<br>, '\n')が入っている

br.png

In [82]: soup = _get_nobel_soup(parser="lxml")
    ...: table = soup.select_one('.wikitable')
    ...: th = table.select_one('tr').select('th')
    ...: test=[]
    ...: for t in th:
    ...:     test.append(t.text)

In [83]: test
Out[83]: 
['Year',
 'Physics',
 'Chemistry',
 'Physiology\nor Medicine', # <-- <br>が改行コード'\n'になっている
# 略

[7]について

# [7]
# テキスト通り、year = int(r.select_one('td').text)としたいところだが、
# "2016[11]"というデータがあったので"[11]"を除去したあとでint()する。

キャプチャ.PNG

In [76]: soup = _get_nobel_soup(parser="lxml")
    ...: table = soup.select_one('.wikitable')
    ...: categories = _get_novel_categories(table)

In [77]: def _get_years(table, categories):
    ...:     for r in table.select('tr')[1:-1]:
    ...:         print(r.select('td')[0].text)

In [78]: _get_years(table, categories)
# 略
2015
2016[11] # <-- "[11]"により、int()が失敗する
2017

[9]について


# [9]
# 複数の受賞者がいる場合、
# <tr><td>受賞年</td>
# <td>
#     <a href=受賞者1のリンク>受賞者名1</a>;
#     <a href=受賞者2のリンク>受賞者名2</a>
# </td>
# という構造になるので注意する。

anchor.png

html要素調査 with Firefox

[PJDV]s.4.3.3 ではFirebugを挙げているが、今はFirefox本体に統合されている
右クリック→要素を調査で実施できる。

List of Nobel laureates - Wikipedia

a1.png
a2.png

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2