Webスクレイピング:ニュースサイトから本文を抽出する
前回まで,VOA Learning English から記事の本文を抽出しました。今回は,他のニュースサイトから記事の本文を抽出します。
段階的にURLを取得する
今回は,BBC のニュースサイトから記事の本文を抽出します。
まず,トップページのhtmlを抽出し,さらに文字列/news/
を含むURLを抽出します。
次に,抽出されたURLのhtmlから次のURLを抽出します。こうして,段階的に記事のURLを取得します。
pre_extracted_urls = []
pre_extracted_urls.append('https://www.bbc.com/news')
トップページのURLをリストpre_extracted_urls
に格納します。
for depth in range(2):
繰り返し処理を用いて段階的に記事のURLを取得します。depth
は0
から1
まで変化します。depth
の範囲をあまりに大きくすると,処理が完了するのに非常に長い時間がかかります。
extracted_urls = []
for i in range(len(pre_extracted_urls)):
リストextracted_urls
は抽出したURLを格納します。
はじめ,pre_extracted_urls
にはトップページのURLが一つ含まれています。したがって,処理は繰り返されません。しかし,次の段階ではトップページから抽出された複数のURLを含むため,URLの数に応じて処理が繰り返されます。
try:
res = requests.get(pre_extracted_urls[i], timeout=3.0)
except Timeout:
print('Connection timeout')
continue
try:
はいったん処理を実行し,例外が発生したときにexcept:
を実行します。requests.get()
は指定したURLからhtmlを抽出します。timeout=3.0
を指定すると,アクセスしようとするサーバーから3.0秒間応答がない場合に処理を中断します。サーバーから応答が無いとき,処理が次に進まず止まってしまうことがあるので,例外処理を行うべきです。
except Timeout:
はサーバーから応答が無い場合に実行されます。メッセージを表示し,continue
によって繰り返し処理の最初に戻り,次の抽出を行います。
soup = BeautifulSoup(res.text, 'html.parser')
elems = soup.find_all(href=re.compile("/news/"))
print(str(len(elems))+' URLs extracted'+'('+str(i+1)+'/'+str(len(pre_extracted_urls))+')')
抽出したhtmlはres.text
に格納され,それをBeutifulSoup
に渡します。.find_all()
は/news/
を含むURLを抽出し,リストelem
に格納します。
for j in range(len(elems)):
url = elems[j].attrs['href']
if not 'http' in url:
extracted_urls.append('https://www.bbc.com'+url)
else:
extracted_urls.append(url)
リストelem
に格納された文字列はタグを含むため,.attrs['href']
を用いてURLを抽出し文字列url
に格納します。
URLにドメイン名が含まれていない場合は,ドメイン名を加え,リストextracted_urls
に追加します。
extracted_urls = np.unique(extracted_urls).tolist()
抽出された文字列は重複したURLを含みます。np.unique()
は重複したデータを削除します。処理よってnumpy配列が作られるので,tolist()
で通常の配列に戻し,extracted_urls
に格納します。
pre_extracted_urls = extracted_urls
urls.extend(extracted_urls)
urls = np.unique(urls).tolist()
抽出されたURLを再びpre_extracted_urls
に格納します。次の繰り返し処理では,格納された複数のURLに基づいて新たなURLを抽出します。同時に,抽出されたURLをリストurls
に追加します。リストに要素を1つ追加するときには.append()
を使いますが,リストのように複数の要素を追加するときには.extend()
を用います。
さらに,np.unique()
で重複したURLを削除します。
URLのリストから本文を抽出する
for i in range(len(urls)):
try:
res = requests.get(urls[i], timeout=3.0)
except Timeout:
print('Connection timeout')
continue
soup = BeautifulSoup(res.text, "html.parser")
リストurls
の要素数に応じて繰り返し処理を行います。
上と同じように,request.get()
で記事のURLからhtmlを抽出し,BeutifulSoup()
に渡します。
elems = soup.select('#page > div > div.container > div > div.column--primary > div.story-body > div.story-body__inner > p')
Chromeを用いてセレクタを取得できます。ブラウザに表示された本文を右クリックし,検証をクリックします。画面の右側にhtmlが表示されるので,青い線を右クリックします。さらに,Copy → Copy selector をクリックすると,クリップボードにセレクタがコピーされます。コピーされた文字列には:nth-child()
が含まれていますが,これは必要ありません。
soup.select()
は指定されたセレクタに含まれる文字列を抽出し,リストelems
に格納します。
if not len(elems) == 0:
for j in range(len(elems)):
texts.append(str(elems[j]))
もし,htmlが本文を含まない場合,リストelems
の要素数は0
です。要素数が0
でなければ,elems
の要素数に応じて繰り返し処理を行い,文字列をリストtexts
に追加します。
text = ' '.join(texts)
p = re.compile(r"<[^>]*?>")
text = p.sub("", text)
text = re.sub('["“”,—]','', text)
text = text.lower()
text = text.replace('. ','\n')
.join()
はリストtexts
を一つの文字列text
に結合します。
re.compile()
を用いて正規表現の規則を与え,.sub()
で規則に当てはまる文字列を削除します。ここでは,文字列からタグを削除します。さらに,同じ方法でダブルクォーテーションやハイフンを削除します。
.lower()
は文字列を小文字に変換します。また,.replace()
はピリオドを改行文字\n
に変換します。
with io.open('articles_bbc.txt', 'w', encoding='utf-8') as f:
f.write(text)
文字列text
をテキストファイルarticles_bbc.txt
に保存します。
プログラムを実行したところ,2.59MBの記事の本文が抽出されました。
全体のコードを示します。
import numpy as np
import requests
from requests.exceptions import Timeout
from bs4 import BeautifulSoup
import io
import re
urls = []
pre_extracted_urls = []
pre_extracted_urls.append('https://www.bbc.com/news')
for depth in range(2):
print('start depth: '+str(depth)+'.........................')
extracted_urls = []
for i in range(len(pre_extracted_urls)):
try:
res = requests.get(pre_extracted_urls[i], timeout=3.0)
except Timeout:
print('Connection timeout')
continue
soup = BeautifulSoup(res.text, 'html.parser')
elems = soup.find_all(href=re.compile("/news/"))
print(str(len(elems))+' URLs extracted'+'('+str(i+1)+'/'+str(len(pre_extracted_urls))+')')
for j in range(len(elems)):
url = elems[j].attrs['href']
if not 'http' in url:
extracted_urls.append('https://www.bbc.com'+url)
else:
extracted_urls.append(url)
extracted_urls = np.unique(extracted_urls).tolist()
pre_extracted_urls = extracted_urls
urls.extend(extracted_urls)
urls = np.unique(urls).tolist()
print('total: '+str(len(urls))+' URLs')
print('start extracting html....')
texts = []
for i in range(len(urls)):
try:
res = requests.get(urls[i], timeout=3.0)
except Timeout:
print('Connection timeout')
continue
soup = BeautifulSoup(res.text, "html.parser")
elems = soup.select('#page > div > div.container > div > div.column--primary > div.story-body > div.story-body__inner > p')
if not len(elems) == 0:
for j in range(len(elems)):
texts.append(str(elems[j]))
print(str(i+1)+' / '+str(len(urls))+' finished')
text = ' '.join(texts)
p = re.compile(r"<[^>]*?>")
text = p.sub("", text)
text = re.sub('["“”,—]','', text)
text = text.lower()
text = text.replace('. ','\n')
with io.open('articles_bbc2.txt', 'w', encoding='utf-8') as f:
f.write(text)
with io.open('articles_bbc.txt', 'w', encoding='utf-8') as f:
f.write(text)
SNSでシェア