#はじめに
今回が初投稿になります。プログラミングを独学で勉強しはじめて半年強ほどの大学生ですが、学んだことを少しずつアウトプットしてみたいと考え、投稿させていただきます。
コードの書き方等雑であったり汚い点が多々あると思いますので、遠慮なく指摘していただいて構いません。
#目標・手順
タイトルにある通り、AWSのlambdaというツールを使って(Yahooニュースの記事)を定期的(6時間毎)にスクレイピングして、更新された記事について、LINEmessagingAPIを使って自分のLINEアカウントに通知することを目標とします。開発環境はlambdaとの連携を考慮し、cloud9を使います。
手順についてですが、
①PythonのBeautifulSoupモジュールを用いてニュースサイトから記事のタイトルとurlを20件取得、urlはAWSのS3に置いたcsvファイルに書き込む。(書き込む部分はプログラムの最後に行います。)
②前回実行分のurl(S3からcsvを読み込む)と今回取得したurlを比較して、更新分を求める。
③更新分の記事のタイトルとurlをLINEmessagingAPIを用いて通知する。
という順序になります。
LINEmessagingAPIについては、LINE BOT で ウェブページの更新監視する
BeautifulSoupでのスクレイピングは、BeautifulSoupを使ったWebスクレイピング
S3のcsvfileの読み込み、書き込みについては、AWS S3のCSVファイルをシンプルに読み込むPythonコードとWrite a Pandas dataframe to CSV on S3あたりの記事を参考にさせていただきました。
#コード
import urllib.request
from bs4 import BeautifulSoup
import csv
import pandas as pd
import io
import boto3
import s3fs
import itertools
from linebot import LineBotApi
from linebot.models import TextSendMessage
def lambda_handler(event, context):
url = 'https://follow.yahoo.co.jp/themes/051839da5a7baa353480'
html = urllib.request.urlopen(url)
# htmlパース
soup = BeautifulSoup(html, "html.parser")
def news_scraping(soup=soup):
"""
記事のタイトルとurlを取得
"""
title_list = []
titles = soup.select('#wrapper > section.content > ul > li:nth-child(n) > a.detailBody__wrap > div.detailBody__cnt > p.detailBody__ttl')
for title in titles:
title_list.append(title.string)
url_list = []
urls = soup.select('#wrapper > section.content > ul > li:nth-child(n) > a.detailBody__wrap')
for url in urls:
url_list.append(url.get('href'))
return title_list,url_list
def get_s3file(bucket_name, key):
"""
S3からcsvを読み取る
"""
s3 = boto3.resource('s3')
s3obj = s3.Object(bucket_name, key).get()
return io.TextIOWrapper(io.BytesIO(s3obj['Body'].read()))
def write_df_to_s3(csv_list):
"""
S3に書き込む
"""
csv_buffer = io.StringIO()
csv_list.to_csv(csv_buffer,index=False,encoding='utf-8-sig')
s3_resource = boto3.resource('s3')
s3_resource.Object('バケット名','ファイル名').put(Body=csv_buffer.getvalue())
def send_line(content):
access_token = ********
#Channel access token を記入
line_bot_api = LineBotApi(access_token)
line_bot_api.broadcast(TextSendMessage(text=content))
ex_csv =[]
#前回スクレイピング分のurlを入れる
for rec in csv.reader(get_s3file('バケット名', 'ファイル名')):
ex_csv.append(rec)
ex_csv = ex_csv[1:]
#index=Falseとして書き込んだはずだが読み込んだcsvの先頭に0というインデックス(?)が書き込まれていた
ex_csv = list(itertools.chain.from_iterable(ex_csv))
#読み込んだcsvが二次元配列になっていたため一次元に変換
title,url = news_scraping()
#スクレイピング実行
csv_list = url
#ex_csvと比較して更新分を取り出していく
for i in range(20):
if csv_list[i] in ex_csv[0]:
#完全一致してくれなかったためinを使用
num = i
#ex_csvの先頭にある記事がcsv_listの何番目の記事に当たるか調べる
break
else:
num = 'all'
if num == 'all':
send_list = [None]*2*20
send_list[::2] = title
send_list[1::2] = url
send_list = "\n".join(send_list)
#タイトル、urlを交互に挿入し、改行
elif num == 0:
send_list = '新しいニュースはありません'
else:
send_list = [None]*2*num
send_list[::2] = title[:num]
send_list[1::2] = url[:num]
send_list = "\n".join(send_list)
##タイトル、urlを交互に挿入し、改行
send_line(send_list)
csv_list = pd.DataFrame(csv_list)
#リストのままS3に書き込むとエラーが出るためデータ型を変換
write_df_to_s3(csv_list)
#S3にcsv_listを書き込んで終了
コードは以上です。(numからsend_listを作るところは関数を定義した方がよかったですね)
後はremoteにデプロイして、Amazon CloudWatch Eventsで定期実行の設定をしてあげます。特定の時間毎に実行するにはCron式でスケジューリングするのがいいかと思います。
S3へのアクセスはIAMでアクセス権限を付与する必要があるので注意する必要がありますね。
#おわりに
S3のcsvの読み取り書き込みはなかなか思い通りにいかなかったです。特に書き込む際に二次元配列になってしまったところなど改善する余地がありそうです。
実は以前にselniumやheadless-chromeとlambdaでのスクレイピングを実践したことがあった(lambdaを初めて使用したことに加え、chromebinary関連で山ほどエラーが出て苦労しました。)
ため、今回は比較的短時間でコードを書くことが出来ました。
とはいえ、ローカル内のスクレイピングと比べるとかなり作業量が多くなりますね。lambdaの使い方はここでは省いていますが、かなりややこしいので他の記事を参照してみてください。
最近はdjangoやTwitterAPIなどに手を出しているので、このあたりで何か気づいた点があればまた投稿しようと考えています。
ありがとうございました。