前回の続き。あまりにもseleniumだと動作が遅いので、スクレイピングプログラムをrequestsとBeautifulSoupを利用した版に書き直してみた。
ということで変更記録と考えたことをつらつら書いていきます。
ライブラリのインストール
まずは必要なライブラリのインストールから。pipを使います。
install requests
HTTPライブラリ
pip install beautifulsoup4
HTMLパーサー
pip install lxml
xpathを指定して、DOM木を走査するために必要
pip freeze | grep -e request -e lxml -e beautiful
パッケージの確認用コマンド
beautifulsoup4==4.11.1
lxml==4.9.1
requests==2.28.1
https://qiita.com/mtskhs/items/edf7dbba9b0b0246ef8f を参考にさせていただきました。
selenium版からの主な変更点
driver.getのところを置き換えていくイメージ。たとえばこのような感じ
前
driver.get('https://toaru-shinbun.jp/editorial/')
article_list = driver.find_element(By.XPATH,"//html/body/article]")
latest_url = article_list.find_element(By.XPATH, 'ul/li/a')
後
res = requests.get(self.editorial_url)
soup = BeautifulSoup(res.content, 'lxml')
dom = html.fromstring(str(soup))
article1_url = dom.xpath(self.article1_url_xpath)
article1_url = article1_url[0].attrib['href']
resに対象URLのweb情報を格納します。
そして、 BeautifulSoup(res.content, 'lxml')
で整形して読み込みます。このとき、res.textよりres.contentのほうがよいようです。理由は文字コードの解釈をBeautifulSoup側に行わせるためです。res.textにするとrequests側で解釈されておかしくなる場合があるようです。
さらに、dom = html.fromstring(str(soup))
でHTML構文としてパースします。ここで読み込みが終わればDDM木としてアクセス可能です。
タグ名で検索することはもちろんXpath検索も対応します。
あとは、指定のXpathで要素を抜き出してます。self.article1_url_xpathに格納してます。この変数は次に説明するメンテナンス性を高めるためにつくったインスタンス変数です。変更前でいうところの//html/body/article/ul/li/aが格納されてます。
順番が前後しましたが、importはこんな感じ。これでrequestsとBeautifulSoupが利用可能になります。
import requests
from lxml import html
from bs4 import BeautifulSoup
メンテナンス性を高める改良
メンテナンス性を上げるためにclassを作成しました。
また記事取得は、classメソッドとして実装しました。
実行時は新聞社ごとにclassからインスタンスを生成します。
新聞社ごとの違いはインスタンス化時にコンストラクタへ渡す引数を変更することで対処します。
引数渡しで対処ができなかったURLの絶対参照と相対参照のみ、classメソッド内で場合分けを行いました。
イメージはこんな感じ。
class Shinbunsya:
def __init__(self, dt_today, target_file, editorial_url, article1_url_xpath,
article2_url_xpath, article_title_xpath, article_xpath, article_date_xpath,
absolute_url):
self.dt_today = dt_today
self.target_file = target_file
self.editorial_url = editorial_url
self.article1_url_xpath = article1_url_xpath
self.article2_url_xpath = article2_url_xpath
self.article_title_xpath = article_title_xpath
self.article_xpath = article_xpath
self.article_date_xpath = article_date_xpath
self.absolute_url = absolute_url
def get_article(self):
res = requests.get(self.editorial_url)
soup = BeautifulSoup(res.content, 'lxml')
dom = html.fromstring(str(soup))
article1_url = dom.xpath(self.article1_url_xpath)
if self.target_file == "toaru":
article1_url = article1_url[0].attrib['href']
else:
article1_url = "https:" + self.absolute_url + article1_url[0].attrib['href']
print(article1_url)
self.get_detail(article1_url, True)
def get_detail(self, detail_url, first_flag):
# first_flag means first article or second article
res_detail = requests.get(detail_url)
soup_detail = BeautifulSoup(res_detail.content, 'lxml')
dom_detail = html.fromstring(str(soup_detail))
print(dom_detail.xpath(self.article_date_xpath)[0].text)
if first_flag:
t_file = dt_today + "_" + self.target_file + ".csv"
else:
t_file = dt_today + "_" + self.target_file + "_2.csv"
with open(t_file, 'w', encoding='UTF-8') as f:
f.write(self.dt_today)
f.write("\n")
f.write(self.target_file)
f.write("\n")
article_title = dom_detail.xpath(self.article_title_xpath)[0].text
print(article_title)
f.write(article_title)
f.write("\n")
article_data = dom_detail.xpath(self.article_xpath)
for p_body in article_data:
print(p_body.text)
f.write(p_body.text)
f.write("\n")
すいません。インデントがおかしくなってます。
コンストラクタで初期化する変数は、新聞社の固有のパラメータです。
実際に使う場合はmain内で、インスタンスを生成した後に、classメソッドを実行すると記事を取得できます。
また今回の改修に伴い、前回に課題としていた2記事の取得を行うようにしました。
日付の自動判定はできてませんが、記事内の日付をprint文で表示するようにしたので、当日の記事かは一発でわかるようになりました。
toaru= Shinbunsya(
dt_today,
"toarui", 新聞社識別子
'https://www.toaru-shinbun.jp/editorial', #社説のweb URL
"/html/body/div[4]/div/div[1]/div/div[1]/div[2]/div[2]/ul/li[1]/a", #その日の社説1Xpath
"/html/body/div[4]/div/div[1]/div/div[1]/div[2]/div[2]/ul/li[2]/a", #その日の社説2Xpath
"/html/body/div[4]/div/main/div[2]/h1", #社説のタイトルのXpath
"/html/body/div[4]/div/main/div[6]/p", #社説の本体部のXpath
"/html/body/div[4]/div/main/div[2]/div/span/time", #その記事の時刻のXpath
"" #URL補完用 絶対参照
)
toaru.get_article()
上記のようにしておくことで、仮に新聞社のXpathが変わったとしても、各インスタンス変数の該当部分を書き換えるだけで修正できます。
各新聞社ごとに記事取得用のメソッドを書く場合に比べ、修正が大幅に楽になりました。
実際これ書いているときも1社Xpathがしれっと変わってました。。。しかし、すぐに修正できるようなりました。
はまったエラー
ValueError: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration
というものが、
dom = html.fromstring(str(soup))
にて発生。lxmlライブラリでHTMLをパースしようとしたら、怒られた。
原因は文字コードが読み込ませようとしているsoupの中のHTMLで指定されているからみたい。
この新聞社だけ、XHTML使っているのだが、なぜだろう。こだわりがあるのだろうか。
soupの中を調べてみたら文字コード指定が<?xml・・・>のタグで記載されていた。
冒頭の<?xml ・・・>タグを飛ばす用に処理を追加したら、一応上記のエラーは解消して、HTMLを読み込めたが、この新聞社はなぜかwebページが動的な生成だったので、これ以上読み込んでも無駄だった。
つまり、seleniumで読み込むしか方法がなかったです。全部requests化して高速化したかったのに。
今後の課題
よって、XHTMLつかっている新聞社だけは、いったんrequest+beautifulsoupによる記事取得はあきらめます。
時間かかりますが、従来通りselenuim経由のプログラムを利用したいと思います。
プログラムが二つになって使い分けが必要なのが地味に面倒。
個別の記事のページはもしかしたら静的な生成かもしれないので、そしたら記事一覧からの記事の生成は、seleniumで行い、個別ページからの記事の取得はrequestで行うという方法も取れるかもしれない。
⇒ダメでした。個別ページも見事に動的生成になっていた。何のこだわりがあるんだろ
あとがき
requestsとBeautifulSoupの構成にしてかなり処理が速くなりました。
これまでは、3社取得するのに、3分程度かかってましたが、2-3秒で終了します。
圧倒的にストレスがなくなりました。
残念ながらseleniumを採用せざるを得ない新聞社がありますが、headlessというオプションをChrome Driverにつければ、ブラウザを起動しなくて済むので速くなるというコメントをいただいたので、変更してみました。
確かに早くなりましたが、requests構成ほどではないので、やはりこちらを採用したいところ。