AWS lambdaを使ってニュースをスクレイピングし、更新分を定期的にLINEに通知してみる【python】

#はじめに
今回が初投稿になります。プログラミングを独学で勉強しはじめて半年強ほどの大学生ですが、学んだことを少しずつアウトプットしてみたいと考え、投稿させていただきます。
コードの書き方等雑であったり汚い点が多々あると思いますので、遠慮なく指摘していただいて構いません。
#目標・手順
タイトルにある通り、AWSのlambdaというツールを使って([Yahooニュースの記事](https://follow.yahoo.co.jp/themes/051839da5a7baa353480 "レアルマドリードに関連するニュース"))を定期的（６時間毎）にスクレイピングして、更新された記事について、[LINEmessagingAPI](https://developers.line.biz/ja/docs/messaging-api/getting-started/)を使って自分のLINEアカウントに通知することを目標とします。開発環境はlambdaとの連携を考慮し、cloud9を使います。

手順についてですが、
①PythonのBeautifulSoupモジュールを用いて[ニュースサイト](https://follow.yahoo.co.jp/themes/051839da5a7baa353480 "レアルマドリードに関連するニュース")から記事のタイトルとurlを20件取得、urlはAWSのS3に置いたcsvファイルに書き込む。（書き込む部分はプログラムの最後に行います。）
②前回実行分のurl(S3からcsvを読み込む)と今回取得したurlを比較して、更新分を求める。
③更新分の記事のタイトルとurlをLINEmessagingAPIを用いて通知する。
という順序になります。

LINEmessagingAPIについては、[LINE BOT で ウェブページの更新監視する](https://qiita.com/mmurasawa/items/8d8e6a60c8e2cc443e3f)
BeautifulSoupでのスクレイピングは、[BeautifulSoupを使ったWebスクレイピング](https://hibiki-press.tech/python/scraping_with_bs4/2061)
S3のcsvfileの読み込み、書き込みについては、[AWS S3のCSVファイルをシンプルに読み込むPythonコード](https://qiita.com/shinsaka/items/9b9fbd4d3ba1b5e59bce)と[Write a Pandas dataframe to CSV on S3](https://www.jitsejan.com/write-dataframe-to-csv-on-s3.html)あたりの記事を参考にさせていただきました。

#コード
```python
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](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html)で定期実行の設定をしてあげます。特定の時間毎に実行するにはCron式でスケジューリングするのがいいかと思います。
S3へのアクセスはIAMでアクセス権限を付与する必要があるので注意する必要がありますね。
#おわりに
S3のcsvの読み取り書き込みはなかなか思い通りにいかなかったです。特に書き込む際に二次元配列になってしまったところなど改善する余地がありそうです。

実は以前にselniumやheadless-chromeとlambdaでのスクレイピングを実践したことがあった(lambdaを初めて使用したことに加え、chromebinary関連で山ほどエラーが出て苦労しました。)
ため、今回は比較的短時間でコードを書くことが出来ました。
とはいえ、ローカル内のスクレイピングと比べるとかなり作業量が多くなりますね。lambdaの使い方はここでは省いていますが、かなりややこしいので他の記事を参照してみてください。

最近はdjangoやTwitterAPIなどに手を出しているので、このあたりで何か気づいた点があればまた投稿しようと考えています。

ありがとうございました。