Python 研究部

【Python】BeautifulSoup を使ってニュースサイトの記事タイトルを取得する。【初学者向け】

Pythonを使って、ニュースサイトの記事タイトル名を取得する方法を纏めてみます。

今回使うニュースサイトは東海地方のニュースがまとまったロキポのニュースページを使います。

(このページにした意味は特にありません)

https://locipo.jp/article/

このトップページから、ニュースの記事名と、記事のURL、記事の発行元を抽出します。

必要なライブラリ

import requests
from bs4 import BeautifulSoup as bs
import pandas as pd

まずは requests 。これは、url 先の情報を取得してくれるライブラリです。

天気API を使う方法を紹介した記事でも登場しました。

そして、BeautifulSoup というライブラリ。

requests は、url 先の情報をHTMLの文字列で取得してくるだけなので、それをBeautifulSoupが解析してくれます。

名前の由来は、サイトのHTMLタグの海(スープ)から、必要な情報をうまく抽出して綺麗なスープを作る、みたいな感じだったと記憶してます(違うかな)

pandas は、今回は取ってきた情報を csv ファイル形式にして保存するために使います。

csvファイル形式にすれば、取得した情報をエクセルなどで読むことができます。

まずはサイトのソースを見てみる

まずはソース見てみる必要性があります。

といっても、自分自身そこまで HTML には詳しくありませんが。

データを取得したいページにて、右クリックして検証を選択することでページのソースを見れます。

このページでは、「article」というタグに、それぞれの記事が入っているようです。

とり会えず、article タグの情報を全て取得してみます。

BeautifulSoupを使って記事情報の取得

それでは、早速やってみましょう。

まずは requests を使って、 url 先の情報を取得します。

url = 'https://locipo.jp/article'
res = requests.get(url)

requests.get で得た情報を、変数 res で受けます。

次にこの情報を BeautifulSoup (ここでは bs としてます) を使って解析します。

解析した結果は変数 soup で受けます

soup = bs(res.text)

res.text のままではただの文字列だったものを、soup で意味を持って情報を取得できるものに変換するという感じです。

次に article というタグ内に記事情報がありそうだということをみたので、find_all()メソッドを用いて、article の要素を全て取得します。

articles = soup.find_all('article')
print(len(articles)) #20

find_all()メソッドを用いて得た結果はリスト形式で帰ってきますので、len()を用いて、要素数をみれます。

今回は20個の記事を取得しました。

リストarticles の最初の要素を見て見ましょう。

articles[0].text
#新着勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 石川昂弥も猛アピール02.22(月)20:07

「.text」を使うことで、そのタグ内の文字情報を全て得ることができます。

今回取得できた情報は

「新着勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 石川昂弥も猛アピール02.22(月)20:07」

しかし、今回は、「新着というトップ記事にでる文字 + 記事名 + 日にち時間」が同時に得られています。

これで良い場合もあるかもしれませんが、なるべく記事名だけ得たいという場合が多いでしょう。

そこで、より絞り込んでテキスト化する必要があります。

print(articles[0]) で、詳しく内容を見てみると、記事名は、

print(articles[0])
#~省略~
#<h2 class="title_13taR">勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 
#石川昂弥も猛アピール</h2>
#~省略~

のように、タイトルは h2 タグで囲まれています。なので、、

title0 = articles[0].find('h2').text
print(title0)
#'勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 石川昂弥も猛アピール'

とすると、記事名だけ取得することができます。

では、2番目の記事以降も。。

articles[1].find('h2').text
AttributeError: 'NoneType' object has no attribute 'text'

とやると、エラーが出てしまいました。要するに、2番目以降のarticle には、h2 タグが内容です。

最初の記事は、ページトップに大きめに表示され、それ以降は少し小さめに表示されるという、サイトでよくある表示の仕方ですが、

それによってフォーマットも変わってしまいます。

これも、ソースをみて、タイトルがどのタグ内にあるかを確認する必要があります。

articles[1].find('h3').text
#'成人式・嫁入り…思い出の着物を再利用\u3000世界でただ一つの「マイお雛様」'

最初の記事以外は、h3 タグに記事名が入っているようです。

最初の記事以外をまとめると、、

titles = [article.find('h3').text for article in articles[1:]]
titles
'''
['新着勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 石川昂弥も猛アピール02.22(月)20:07',
 '成人式・嫁入り…思い出の着物を再利用\u3000世界でただ一つの「マイお雛様」',
 '「おじさん顔」で人気に…アザラシ「ニコ」22日が1歳の誕生日\u3000鳥羽水族館',
 '物流会社の倉庫が全焼…火を使った作業が原因か\u3000愛知・武豊町',
 'リコール署名偽造問題…高須院長会見「こんな貧乏たらしいことするわけない」',
 'バイトの受注したとされる社長「闇は深いと思いますよ…」リコール署名の偽造疑惑 見えてきた不正の構図',
 '大村・愛知県知事のリコール署名偽造、リコール運動の事務局が発注か',
 '東日本大震災から10年\u3000小学校で命を守る避難訓練\u3000三重・尾鷲市',
 '中部空港の「フライト・オブ・ドリームズ」有料エリア、来月末で営業終了へ',
 '福留二番起用で打線に厚みを!井端、本音トークでドラゴンズ戦力チェック!',
 'ワクチン接種後の副反応は…病院長「発熱は78人中1人。診療に影響なし」接種によって「社会の免疫を」',
 '愛知\u3000緊急事態宣言「2月28日まで」\u3000政府に解除要請\u3000',
 'リコール運動主導の高須院長「佐賀は1度ヘリで行った事あるだけ 何の関係もない」不正署名への関与否定',
 '起こされ「うるせぇ」と蹴りも…障害者施設での虐待事件 元職員は「注意聞き入れぬ入所者に立腹し暴力」か',
 '名古屋の先行接種…軽症ながら4人副反応の疑い\u3000三重ではLINEでアンケート',
 '下着などがイヤリングやヘアバンドに…廃棄予定だった下着や部屋着を専門学生がリメイク 女子高生に贈呈',
 '黒煙立ち上り屋根が崩落…鉄骨平屋建ての倉庫・約1千平米が全焼 プラスチック製のパレットなど保管',
 '自殺志願の女性と男をつないだのは「ツイッター」連絡取り合い2日後に嘱託殺人未遂の疑い 男は金銭目的か',
 'ドラゴンズ“立浪塾”が熱い!根尾そして岡林ら“塾生”たち覚醒の手応え',
 '「落合ノック」から10年 ドラゴンズ沖縄キャンプに見る令和時代の指導者像~最強の内野守備を目指して~']
'''
print(len(titles)) #19

こんな感じでかけます。うまく全ての記事名が得られています。「\u3000」という文字列は全角スペースを表します。

最初の記事のタイトル「title0」をinsertすれば、全ての記事名を一つのリストにできます。

titles.insert(0,title0)

記事の発行元を取得する。

今度は記事の発行元を取得してみます。

このサイトは、中京テレビ、東海テレビ、CBCテレビ、テレビ愛知 という4社が記事を制作しており、どの会社が作った記事かを取得してみます。

どこの会社が作った記事かは、img タグの alt 属性が持っています。

articles[0].find_all('img')
[<img alt="" class="eyecatch_wN9Ea" src="https://dophkbxgy39ig.cloudfront.net/store/articles/54789228/original-83d2b3819374a34ee797c895fc7c0fa4.jpg"/>,
 <img alt="東海テレビ" src="https://service.locipo.jp/images/logo_tohkaitv.png"/>]

find_all('img') をすると、二つの要素が得られます。

一つ目は、記事のメイン画像で、二つ目は、記事制作会社のロゴ画像のようです。

この二つ目の画像要素が、alt 属性に社名を持っているようです。

なので、二つ目の画像の alt 属性を得られば、どこの会社制作の記事かがわかります。

articles[0].find_all('img')[1].get('alt')
#'東海テレビ'

属性の内容は get() メソッドで取得可能です。

二つ目の画像要素に、社名を持っているのは、トップ記事も後続の記事も同様なので、

authors = [art.find_all('img')[1].get('alt') for art in articles]
#['東海テレビ', 'CBCテレビ', 'CBCテレビ', ....]

とすれば、トップ記事から順に制作会社が得られます。

記事のURLと投稿日時も得る

URLは、a タグの href が持ちます。これも、get を用いて、

print(articles[0].find('a').get('href'))
#/article/99bb4301-2735-4054-8054-5592dc2d5f7b

日時はトップ記事では、

print(articles[0].find('div',class_="date_3H64D").text)
# 02.22(月)20:07

それ以降の記事では

print(articles[1].find('p').text)
#02.22(月)19:16

とこんな感じで情報を得られます。

全ての情報を辞書のリストでまとめて、csvファイル形式に

ここまで、記事名、記事制作会社、日時、url の得方を見ました。

これらを記事それぞれで辞書に纏めて、その辞書のリストを作ります。

辞書はこんな感じです。

dic = {'title':title,
              'date':date,
              'author':author,
              'url':url}

この辞書を記事一つ一つに対して作り、article_list で一つに纏めます。

ここまでの処理を、ライブラリインポートからまとめると。。

from bs4 import BeautifulSoup as bs
import requests
import pandas as pd
from pprint import pprint
url = 'https://locipo.jp/article'
res = requests.get(url)
soup = bs(res.text)
articles = soup.find_all('article')
article_list = []
for i , article in enumerate(articles):
    if i == 0:
        title = article.find('h2').text
        date = article.find('div',class_="date_3H64D").text
    else:
        title = article.find('h3').text
        date = article.find('p').text
    author = article.find_all('img')[1].get('alt') 
    url = article.find('a').get('href')
    dic = {'title':title,
              'date':date,
              'author':author,
              'url':url}
    article_list.append(dic)
print(len(article_list)) # 20

articles を for 文で回していますが、トップ記事は他の記事と少し体裁が異なるため、条件分岐を持たせた処理を行います。

辞書がちゃんとできているかは、pprint というライブラリを用いると見やすいです。

from pprint import pprint
pprint(article_list[:2])
'''
[{'author': '東海テレビ',
  'date': '02.22(月)20:07',
  'title': '勝負強さは健在…球界最年長の中日・福留がDeNAとの二軍練習試合で2安打2打点 石川昂弥も猛アピール',
  'url': '/article/99bb4301-2735-4054-8054-5592dc2d5f7b'},
 {'author': 'CBCテレビ',
  'date': '02.22(月)19:16',
  'title': '成人式・嫁入り…思い出の着物を再利用\u3000世界でただ一つの「マイお雛様」',
  'url': '/article/20e7beb6-0cc6-40fa-829d-6caa2803e957'}]
'''

あとは、この辞書のリストを pandas を用いてデータフレームに変換します。

これにより辞書のキーである、title の列、date の列、author の列、、といった形式になります。

この変換を行うことで csv ファイル形式で保存できます。

data = pd.DataFrame(article_list)
data.head(3)

.head メソッドで、最初の何行かをみることができます。

しっかりと、title 列、date 列、、、とあります。

最後に、csv 形式で保存します。

data.to_csv('locipo.csv',index=False, encoding='utf-8-sig')

引数の index は先の画像の一番左の列の、番号をつけるかつけないかの引数です。

ちなみに、header という引数も指定でき、これは、title や date などの列名を削除するか否かを決めます。

エンコーディング形式は、'utf-8-sig' が無難なよう。これは環境によって適宜決めるのが良いかも。

できたcsvファイルをエクセルで開くと。。

うまくいってそうですね。

まとめ

今回やったこと全部まとめるとこんな感じ。

from bs4 import BeautifulSoup as bs
import requests
import pandas as pd
from pprint import pprint
url = 'https://locipo.jp/article'
res = requests.get(url)
soup = bs(res.text)
articles = soup.find_all('article')
article_list = []
for i , article in enumerate(articles):
    if i == 0:
        title = article.find('h2').text
        date = article.find('div',class_="date_3H64D").text
    else:
        title = article.find('h3').text
        date = article.find('p').text
    author = article.find_all('img')[1].get('alt') 
    url = article.find('a').get('href')
    dic = {'title':title,
              'date':date,
              'author':author,
              'url':url}
    article_list.append(dic)
data = pd.DataFrame(article_list)
data.to_csv('locipo.csv',index=False, encoding='utf-8-sig')

このくらいのスクレイピングならば簡単にできてしまいますね。

サイトの構造を理解するには、少しHTMLとかの知識が必要かもしれませんが、そんなに知識なくとも見てればなんとなくわかってきます。(スクレイピングに必要な知識程度は)

ただし、スクレイピングが禁止されているサイトとかもあるので、利用規約等を少し目を通しておくと安全かもしれません。

-Python, 研究部