第37回「スクレイピング(9)」運用しやすいスクリプトに整える

こんにちは、小澤です。

前回は、スクレイピングで取得したデータをpandasで読み込み、整形や集計、簡単な可視化につなげる方法を紹介しました。スクレイピングは、Webページから情報を取得するだけでなく、保存し、確認し、必要に応じてグラフなどにして使える形にすることが大切だという内容でした。

今回は、その処理をもう少し運用しやすくするために、スクリプトの整理について考えていきます。これまでのサンプルでは、説明を分かりやすくするために、ひとつのファイルの中に処理を書いてきました。しかし、実務で何度も使うようになると、URL、保存先、ログ、エラー処理などを整理しておかないと、後から直しにくくなります。

スクレイピングは、最初に動いたときはそれで十分に見えるのですが、あとから対象ページが変わったり、保存先を変えたくなったりします。そのときに、どこを直せばよいか分かるようにしておくことが、運用では重要になります。

長くなったスクリプトで困ること

最初のうちは、requestsでHTMLを取得し、BeautifulSoupで解析し、pandasでCSVに保存する、という処理を上から順に書くだけでも問題ありません。学習の段階では、処理の流れが見えやすいほうが大切です。

ただし、処理が増えてくると、ひとつのファイルの中にいろいろな役割が混ざってきます。たとえば、次のようなものです。

  • 取得先のURL
  • 保存するファイル名
  • HTMLからデータを取り出す処理
  • CSVやExcelへ保存する処理
  • ログ出力やエラー処理

これらがすべて同じ場所に書かれていると、少し変更したいだけでも全体を読み直す必要があります。そこで、まずは役割ごとに整理することを考えます。

設定値を分けておく

最初に分けておくとよいのは、URLやファイル名のような設定値です。プログラムの動きそのものではなく、実行するたびに変わる可能性がある値は、できるだけ上のほうにまとめるか、別ファイルにしておくと扱いやすくなります。

たとえば、config.pyというファイルを用意して、次のように書いておきます。

BASE_URL = "https://example.com/news"
OUTPUT_CSV = "data/articles.csv"
LOG_FILE = "logs/scraping.log"

そして、メインのスクリプト側では、この設定を読み込んで使います。

from config import BASE_URL, OUTPUT_CSV, LOG_FILE

こうしておくと、対象URLや保存先を変えたいときに、処理の中身を探し回らなくて済みます。ログイン情報やAPIキーのような秘密情報を扱う場合も、ソースコードに直接書かないようにします。

フォルダを少しだけ分ける

次に、保存先のフォルダを分けます。たとえば、取得したCSVをdataフォルダに保存し、ログをlogsフォルダに保存するだけでも、かなり見通しがよくなります。

project/
├── main.py
├── config.py
├── data/
└── logs/

このようにしておくと、プログラム本体、設定、出力データ、ログを分けて考えられます。最初から大きな構成にする必要はありません。まずは、dataとlogsを分けるくらいから始めるとよいでしょう。

Pythonでは、フォルダが存在しない場合に作成してから保存する、という処理もよく使います。

from pathlib import Path

Path("data").mkdir(exist_ok=True)
Path("logs").mkdir(exist_ok=True)

pathlibのPathを使うと、ファイルやフォルダの扱いを分かりやすく書くことができます。定期実行では、保存先を明確にしておくことが大切です。

処理を関数に分ける

スクリプトが長くなってきたら、処理を関数に分けます。関数に分ける目的は、かっこよく書くことではありません。どの処理が何をしているのかを分かりやすくするためです。

たとえば、HTMLを取得する処理、データを取り出す処理、CSVに保存する処理を分けると、次のような形になります。

def fetch_html(url):
    response = requests.get(url)
    response.raise_for_status()
    return response.text


def parse_articles(html):
    soup = BeautifulSoup(html, "html.parser")
    articles = []

    for item in soup.select(".article"):
        articles.append({
            "title": item.select_one(".title").get_text(strip=True),
            "url": item.select_one("a")["href"]
        })

    return articles

fetch_html()はHTMLを取得する役割、parse_articles()はHTMLから記事情報を取り出す役割です。このように分けておくと、HTMLの構造が変わったときに見る場所が分かりやすくなります。

また、全体の流れはmain()という関数にまとめておくと分かりやすいです。

def main():
    html = fetch_html(BASE_URL)
    articles = parse_articles(html)
    df = pd.DataFrame(articles)
    df.to_csv(OUTPUT_CSV, index=False)


if __name__ == "__main__":
    main()

if __name__ == "__main__" は、Pythonスクリプトを直接実行したときだけmain()を呼び出すための書き方です。スクリプトを整理していくとよく使う形です。

ログとエラー処理を入れる

定期実行するスクレイピングでは、画面を見ていない状態で処理が動きます。そのため、何が起きたのかを後から確認できるように、ログを残しておくことが重要です。

logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)

logging.info()を使えば、処理を開始したことや、何件取得できたかを記録できます。エラーが起きた場合も、print()だけではなくログに残しておくと、あとから原因を調べやすくなります。

try:
    main()
    logging.info("finished")
except Exception as e:
    logging.exception(e)

logging.exception()を使うと、エラーの内容だけでなく、どこでエラーが起きたのかも記録できます。エラーを完全になくすことはできませんが、気づけるようにしておくことはできます。

実務での注意点

スクリプトを整理するときに大切なのは、最初から完璧な設計を目指しすぎないことです。小さな処理であれば、ひとつのファイルでも十分です。処理が増えてきたところで整理するほうが進めやすいです。

一方で、定期実行する処理や、何度も使う処理では、設定値、保存先、ログ、エラー処理を早めに整えておくと安心です。実在サイトを対象にする場合は、アクセス間隔や利用規約を確認し、必要以上に負荷をかけないようにします。

また、取得したデータを上書きするのか、日付つきで残すのかも決めた方がよいでしょう。目的に合わせて保存方法を選ぶことが大切です。

今回のまとめ

今回は、スクレイピングのスクリプトを運用しやすくするための整理方法を紹介しました。URLや保存先のような設定値を分けること、dataフォルダやlogsフォルダを用意すること、処理を関数に分けること、ログとエラー処理を入れることを確認しました。

スクレイピングは、一度動いたコードをそのまま使い続けるのではなく、後から直しやすい形にしておくことが大切です。最初は小さな整理で十分です。どこを変更すればよいか分かる、エラーが起きたときに原因を追える、取得データの保存場所が分かる、という状態を目指していきましょう。

スクレイピングの話は今回で一区切りとして、次回からは自然言語処理に入ります。Webページやニュース記事、アンケートの自由記述のような文章データを、Pythonでどのように扱うのかを見ていきます。まずは、文章を分ける、数える、特徴をつかむ、といった基本的な考え方から紹介します。次回もお楽しみに。

PAGE TOP