Pythonを使用したRedditスクレイピングガイド

このステップガイドでは、Pythonを使ってRedditをスクレイピングし、Reddit API手数料を回避する方法を説明します。
7 min read
How to scrape Reddit in Python

このステップガイドでは、Pythonを使用してRedditをスクレイピングする方法を説明します。

このチュートリアルでは、次の内容を取り上げます。

  • 新しいReddit APIポリシー
  • Reddit APIとRedditスクレイピングの違い
  • Seleniumを使用したRedditスクレイピング

新しいReddit APIポリシー

2023年4月、RedditはデータAPIの新しい料金を発表し、基本的に小規模企業はその料金を支払うことができなくなりました。本稿執筆時点で、API手数料は1,000コールあたり0.24ドルに設定されています。ご想像のとおり、控え目に使用してもこの数字はすぐに膨れ上がります。Redditで利用可能な大量のユーザー生成コンテンツと、それを取得するために必要な膨大な量のコールを考えれば、そのことに疑いの余地はありません。Reddit API上に構築された最も利用されているサードパーティアプリの1つであるApolloは、そのために閉鎖を余儀なくされました。

これは、センチメント分析、ユーザーフィードバック、トレンドデータのソースとしてのRedditの終わりを意味するのでしょうか?そんなことはありません!より効果的で、より安価で、企業の一晩の決定に左右されないソリューションがあります。そのソリューションがウェブスクレイピングと呼ばれるものです。その理由を探ってみましょう!

Reddit APIとRedditスクレイピングの違い

RedditのAPIは、サイトからデータを取得するための公式の方法です。最近のポリシー変更とプラットフォームの方向性を考慮すると、Redditのスクレイピングがより良いソリューションである理由は次のとおりです。

  • コストパフォーマンス:Redditの新しいAPIコストを考慮すると、Redditのスクレイピングははるかに手頃な代替手段となり得ます。Python Redditスクレイパーを構築することで、API使用に関連する追加費用を負担することなくデータを収集できます。
  • コストパフォーマンス:Redditの新しいAPIコストを考慮すると、Redditのスクレイピングははるかに手頃な代替手段となり得ます。Python Redditスクレイパーを構築することで、API使用に関連する追加費用を負担することなくデータを収集できます。
  • 非公式データへのアクセス:RedditのAPIは厳選された情報へのアクセスのみを提供しますが、スクレイピングはサイト上の一般にアクセス可能なデータへのアクセスを提供します。

APIを呼び出すよりもスクレイピングの方が効果的な理由がわかったところで、PythonでRedditスクレイパーを作成する方法を見てみましょう。次の章に進む前に、Pythonを使用したウェブスクレイピングに関する詳細なガイドをご覧ください。

Seleniumを使用したRedditスクレイピング

このステップバイステップのチュートリアルでは、RedditウェブスクレイピングPythonスクリプトを作成する方法を説明します。

ステップ1:プロジェクトのセットアップ

まず、次の前提条件を満たしていることを確認します。

以下のコマンドを使用して、仮想環境でPythonプロジェクトを初期化します。


mkdir reddit-scraper
cd reddit-scraper
python -m venv env

ここで作成したreddit-scraperフォルダーは、Pythonスクリプトのプロジェクトフォルダーです。

IDEでこのディレクトリを開き、scraper.pyファイルを作成し、以下のように初期化します。

print('Hello, World!')

現時点では、このスクリプトは単に「Hello, World!」を出力するだけですが、すぐにスクレイピングロジックが含まれるようになります。

IDEの実行ボタンを押すか、以下を起動して、プログラムが動作することを確認します。

python scraper.py

ターミナルには次のように表示されているはずです。

Hello, World!

素晴らしい!これでRedditスクレイパー用のPythonプロジェクトができました。

ステップ2:スクレイピングライブラリを選択し、インストールする

ご存知かもしれませんが、Redditは非常にインタラクティブなプラットフォームです。このサイトは、ユーザーがクリックとスクロール操作を通じてページとどのようにやり取りするかに基づいて、新しいデータを動的に読み込んでレンダリングします。技術的な観点からは、これはRedditがJavaScriptに大きく依存していることを意味します。

したがって、PythonでRedditをスクレイピングするには、ブラウザでウェブページをレンダリングできるツールが必要です。そこで、Seleniumの出番です!このツールを使用すると、Pythonで動的なウェブサイトをスクレイピングし、ブラウザでのウェブページの操作を自動化できます。

次のコマンドで、SeleniumWebdriver Managerをプロジェクトの依存関係に追加できます。

pip install selenium webdriver-manager

インストールプロセスには時間がかかる場合があります。しばらくお待ちください。

Webdriver-managerパッケージは厳密には必要ではありませんが、強く推奨します。これにより、Seleniumでウェブドライバーを手動でダウンロード、インストール、設定する手間を省くことができます。このライブラリが面倒をすべて見てくれます。

Seleniumをscraper.pyファイルに統合します。


from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options

# enable the headless mode
options = Options()
options.add_argument('--headless=new')

# initialize a web driver to control Chrome
driver = webdriver.Chrome(
    service=ChromeService(ChromeDriverManager().install()),
    options=options
)
# maxime the controlled browser window
driver.fullscreen_window()

# scraping logic...

# close the browser and free up the Selenium resources
driver.quit()

このスクリプトは、Chrome WebDriver オブジェクトをインスタンス化し、Chromeウィンドウをプログラムで制御します。

デフォルトでは、Seleniumは新しいGUIウィンドウでブラウザを開きます。これは、スクリプトがページ上でデバッグのために何をしているかを監視するのに役立ちます。同時に、UIを使用してウェブブラウザを読み込むと、多くのリソースが必要になります。そのため、Chrome をヘッドレスモードで実行するように構成することをお勧めします。具体的には、--headless=newオプションを指定して、UIを何も表示しない状態で開始するようChromeに指示します。  

よくできました!それではターゲットのRedditページにアクセスしましょう!

ステップ3:Redditに接続する

ここでは、r/Technology subredditからデータを抽出する方法を見ていきます。実際のところ、どのsubredditでも構いません。

特に、その週のトップ投稿のあるページをスクレイピングしたいとしましょう。ターゲットページのURLは次のとおりです。

https://www.reddit.com/r/technology/top/?t=week

この文字列をPython変数に格納します。

url = 'https://www.reddit.com/r/technology/top/?t=week'

次に、Seleniumを使ってそのページにアクセスします。

driver.get(url)

get()関数は、パラメータとして渡されたURLで特定されるページに接続するよう、制御対象のブラウザに指示します。

これまでのところ、Redditウェブスクレイパーは次のようになっています。


from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options

# enable the headless mode
options = Options()
options.add_argument('--headless=new')

# initialize a web driver to control Chrome
driver = webdriver.Chrome(
    service=ChromeService(ChromeDriverManager().install()),
    options=options
)
# maxime the controlled browser window
driver.fullscreen_window()

# the URL of the target page to scrape
url = 'https://www.reddit.com/r/technology/top/?t=week'
# connect to the target URL in Selenium
driver.get(url)

# scraping logic...

# close the browser and free up the Selenium resources
driver.quit()

スクリプトをテストします。quit()命令のため、下のブラウザウィンドウが一瞬だけ開いてから閉じます。

Redditスクレイピング

「Chromeは自動テストソフトウェアによって制御されています」というメッセージが表示されます。素晴らしい!これにより、SeleniumがChrome上で正しく動作していることが確認できます。

ステップ4:ターゲットページを検査する

コード作成に入る前に、ターゲットページがどのような情報を提供し、それをどのように取得できるかを調べる必要があります。特に、どのHTML要素に目的のデータが含まれているかを特定し、適切な選択戦略を考案する必要があります。

「vanilla」ブラウザセッションであるSeleniumが動作する条件をシミュレートするには、Redditページをシークレットモードで開きます。ページの任意のセクションを右クリックし、「検査」をクリックしてChrome DevToolsを開きます。

このツールは、ページのDOM構造を理解するのに役立ちます。ご覧のとおり、このサイトはビルド時にランダムに生成されるように見えるCSSクラスに依存しています。言い換えれば、それらに基づいて選択戦略を立てるべきではありません。

Redditスクレイピングの続き

幸いなことに、サイト上の最も重要な要素には特別なHTML属性があります。たとえば、subreddit descriptionノードには次の属性があります。

data-testid="no-edit-description-block"

これは、効果的なHTML要素選択ロジックを構築する上で有用な情報です。

PythonでRedditをスクレイピングする準備ができるまで、DevToolsでサイトを分析し続け、DOMに慣れ親しんでください。

ステップ5:subredditメイン情報をスクレイピングする

まず、スクレイピングしたデータを格納するPython辞書を作成します。

subreddit = {}

次に、ページ上部の

要素からsubredditの名前を取得できることに注目します。

以下のようにして取得します。


name = driver \
    .find_element(By.TAG_NAME, 'h1') \
    .text

すでにお気づきのように、subredditに関する最も興味深い一般情報の一部は、右側のサイドバーにあります。

Redditサイドバー

次の方法で、テキストの説明、作成日、メンバー数を取得できます。


description = driver \
    .find_element(By.CSS_SELECTOR, '[data-testid="no-edit-description-block"]') \
    .get_attribute('innerText')

creation_date = driver \
    .find_element(By.CSS_SELECTOR, '.icon-cake') \
    .find_element(By.XPATH, "following-sibling::*[1]") \
    .get_attribute('innerText') \
    .replace('Created ', '')

members = driver \
    .find_element(By.CSS_SELECTOR, '[id^="IdCard--Subscribers"]') \
    .find_element(By.XPATH, "preceding-sibling::*[1]") \
    .get_attribute('innerText')

この場合、テキスト文字列はネストしたノードに含まれているため、text属性は使えません。textを使用しても、得られるのは空の文字列です。その代わりに、get_attribute()メソッドを呼び出して、ノードとその子孫のレンダリングされたテキストコンテンツを返すinnerText 属性を読み取る必要があります。

作成日要素を見ると、それを簡単に選択する方法がないことに気づくでしょう。ケーキアイコンに続くノードなので、まずicon-cakeでアイコンを選択し、following-sibling::*[1] XPath式を使って次の兄弟を取得します。Python replace()メソッドを呼び出して、「Created」文字列を削除します。

サブスクライバーメンバーカウンター要素についても、同様のことが起こります。主な違いは、この場合、直前の兄弟にアクセスする必要があることです。

スクレイピングしたデータをsubreddit辞書に忘れずに追加してください。


subreddit['name'] = name
subreddit['description'] = description
subreddit['creation_date'] = creation_date
subreddit['members'] = members

subredditprint(subreddit)で出力すると、以下の表示が得られます。

{'name': '/r/Technology', 'description': 'Subreddit dedicated to the news and discussions about the creation and use of technology and its surrounding issues.', 'creation_date': 'Jan 25, 2008', 'members': '14.4m'}

完璧です!Pythonを使ってウェブスクレイピングを実行できました!

ステップ6:subredditの投稿をスクレイピングする

Subredditには複数の投稿が表示されるため、収集したデータを保存するために配列が必要になります。

posts = []

投稿HTML要素を検査します。

ここで、<code>[data-testid=”post-container”]</code> CSSセレクタですべてを選択できることがわかります。


post_html_elements = driver \
    .find_elements(By.CSS_SELECTOR, '[data-testid="post-container"]')

これらを反復処理します。各要素について、個々の投稿のデータを追跡するための投稿辞書を作成します。


for post_html_element in post_html_elements:
    post = {}

    # scraping logic...

upvote要素を検査します。

賛成票を検査する

その情報は、次のようにしてforループ内で取得できます。


upvotes = post_html_element \
    .find_element(By.CSS_SELECTOR, '[data-click-id="upvote"]') \
    .find_element(By.XPATH, "following-sibling::*[1]") \
    .get_attribute('innerText')

繰り返しになりますが、簡単に選択できるupvoteボタンを取得してから、次の兄弟をポイントしてターゲット情報を取得するのが最善です。

投稿のauthorとtitle要素を検査します。

このデータを取得するのはもう少し簡単です。


author = post_html_element \
    .find_element(By.CSS_SELECTOR, '[data-testid="post_author_link"]') \
    .text

title = post_html_element \
    .find_element(By.TAG_NAME, 'h3') \
    .text

次に、コメント数とアウトバウンドリンクを収集できます。

コメントと外部リンク

try:
    outbound_link = post_html_element \
        .find_element(By.CSS_SELECTOR, '[data-testid="outbound-link"]') \
        .get_attribute('href')
except NoSuchElementException:
    outbound_link = None

comments = post_html_element \
    .find_element(By.CSS_SELECTOR, '[data-click-id="comments"]') \
    .get_attribute('innerText') \
    .replace(' Comments', '')

アウトバウンドリンク要素はオプションであるため、選択ロジックをtryブロックでラップする必要があります。

このデータをpostに追加し、titleが存在する場合のみposts配列に追加します。この追加チェックにより、Redditにより配置された特別な広告投稿がスクレイピングされるのを防ぐことができます。


# populate the dictionary with the retrieved data
post['upvotes'] = upvotes
post['title'] = title
post['outbound_link'] = outbound_link
post['comments'] = comments

# to avoid adding ad posts 
# to the list of scraped posts
if title:
    posts.append(post)

最後に、postssubreddit辞書に追加します。

subreddit['posts'] = posts

よくできました!これで、必要なRedditデータがすべて揃いました!

ステップ7:スクレイピングしたデータをJSONにエクスポートする

現在、収集されたデータはPython辞書の中にあります。これは他のチームと共有するのに最適な形式ではありません。これに対処するには、JSONにエクスポートする必要があります。


import json

# ...

with open('subreddit.json', 'w') as file:
    json.dump(video, file)

Python Standard Libraryからjsonをインポートし、open()を使ってsubreddit.jsonファイルを作成し、json.dump()でそのファイルにデータを入力します。PythonでJSONを解析する方法について、詳しくはガイドを参照してください。

素晴しいです!動的なHTMLページに含まれる生データから始めて、半構造化されたJSONデータを手に入れることができました。これで、Redditスクレイパー全体を見る準備が整いました。

ステップ8:すべてを統合する

これが完全なscraper.pyスクリプトです。


from selenium import webdriver
from selenium.common import NoSuchElementException
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import json

# enable the headless mode
options = Options()
options.add_argument('--headless=new')

# initialize a web driver to control Chrome
driver = webdriver.Chrome(
    service=ChromeService(ChromeDriverManager().install()),
    options=options
)
# maxime the controlled browser window
driver.fullscreen_window()

# the URL of the target page to scrape
url = 'https://www.reddit.com/r/technology/top/?t=week'
# connect to the target URL in Selenium
driver.get(url)

# initialize the dictionary that will contain
# the subreddit scraped data
subreddit = {}

# subreddit scraping logic
name = driver \
    .find_element(By.TAG_NAME, 'h1') \
    .text

description = driver \
    .find_element(By.CSS_SELECTOR, '[data-testid="no-edit-description-block"]') \
    .get_attribute('innerText')

creation_date = driver \
    .find_element(By.CSS_SELECTOR, '.icon-cake') \
    .find_element(By.XPATH, "following-sibling::*[1]") \
    .get_attribute('innerText') \
    .replace('Created ', '')

members = driver \
    .find_element(By.CSS_SELECTOR, '[id^="IdCard--Subscribers"]') \
    .find_element(By.XPATH, "preceding-sibling::*[1]") \
    .get_attribute('innerText')

# add the scraped data to the dictionary
subreddit['name'] = name
subreddit['description'] = description
subreddit['creation_date'] = creation_date
subreddit['members'] = members

# to store the post scraped data
posts = []

# retrieve the list of post HTML elements
post_html_elements = driver \
    .find_elements(By.CSS_SELECTOR, '[data-testid="post-container"]')

for post_html_element in post_html_elements:
    # to store the data scraped from the
    # post HTML element
    post = {}

    # subreddit post scraping logic
    upvotes = post_html_element \
        .find_element(By.CSS_SELECTOR, '[data-click-id="upvote"]') \
        .find_element(By.XPATH, "following-sibling::*[1]") \
        .get_attribute('innerText')

    author = post_html_element \
        .find_element(By.CSS_SELECTOR, '[data-testid="post_author_link"]') \
        .text

    title = post_html_element \
        .find_element(By.TAG_NAME, 'h3') \
        .text

    try:
        outbound_link = post_html_element \
            .find_element(By.CSS_SELECTOR, '[data-testid="outbound-link"]') \
            .get_attribute('href')
    except NoSuchElementException:
        outbound_link = None

    comments = post_html_element \
        .find_element(By.CSS_SELECTOR, '[data-click-id="comments"]') \
        .get_attribute('innerText') \
        .replace(' Comments', '')

    # populate the dictionary with the retrieved data
    post['upvotes'] = upvotes
    post['title'] = title
    post['outbound_link'] = outbound_link
    post['comments'] = comments

    # to avoid adding ad posts 
    # to the list of scraped posts
    if title:
        posts.append(post)

subreddit['posts'] = posts

# close the browser and free up the Selenium resources
driver.quit()

# export the scraped data to a JSON file
with open('subreddit.json', 'w', encoding='utf-8') as file:
    json.dump(subreddit, file, indent=4, ensure_ascii=False)

すばらしい!Python Redditウェブスクレイパーを、100行ちょっとのコードで作ることができるのです!

スクリプトを起動すると、以下のsubreddit.jsonファイルがプロジェクトのルートフォルダに表示されます。


{
    "name": "/r/Technology",
    "description": "Subreddit dedicated to the news and discussions about the creation and use of technology and its surrounding issues.",
    "creation_date": "Jan 25, 2008",
    "members": "14.4m",
    "posts": [
        {
            "upvotes": "63.2k",
            "title": "Mojang exits Reddit, says they '\"no longer feel that Reddit is an appropriate place to post official content or refer [its] players to\".",
            "outbound_link": "https://www.pcgamer.com/minecrafts-devs-exit-its-7-million-strong-subreddit-after-reddits-ham-fisted-crackdown-on-protest/",
            "comments": "2.9k"
        },
        {
            "upvotes": "35.7k",
            "title": "JP Morgan accidentally deletes evidence in multi-million record retention screwup",
            "outbound_link": "https://www.theregister.com/2023/06/26/jp_morgan_fined_for_deleting/",
            "comments": "2.0k"
        },
        # omitted for brevity ...        
        {
            "upvotes": "3.6k",
            "title": "Facebook content moderators in Kenya call the work 'torture.' Their lawsuit may ripple worldwide",
            "outbound_link": "https://techxplore.com/news/2023-06-facebook-content-moderators-kenya-torture.html",
            "comments": "188"
        },
        {
            "upvotes": "3.6k",
            "title": "Reddit is telling protesting mods their communities ‘will not’ stay private",
            "outbound_link": "https://www.theverge.com/2023/6/28/23777195/reddit-protesting-moderators-communities-subreddits-private-reopen",
            "comments": "713"
        }
    ]
}

おめでとうございます!PythonでRedditをスクレイピングする方法を学ぶことができました!

まとめ

Redditのスクレイピングは、特に新たなポリシー以降、APIを使用してデータを取得するよりも勝った方法です。このステップバイステップのチュートリアルでは、subredditデータを取得するスクレイパーをPythonで作成する方法を説明しました。ここに示すように、必要なコードはわずか数行です。

とはいえ、APIポリシーを一夜にして変更したように、Redditは近いうちに厳しいアンチスクレイピング策を導入するかもしれません。そこからデータを抽出するのは大変ですが、解決策はあります!Bright DataのScraping Browserは、SeleniumのようにJavaScriptをレンダリングして、フィンガープリンティング、CAPTCHA、アンチスクレイピングを自動的に処理するツールです。

この方法が好みでない方のニーズを満たすため、当社はReddit Scraperも用意しています。この信頼性が高く、使いやすいソリューションのおかげで、必要なRedditデータすべてを安心して手に入れることができます。

Redditのウェブスクレイピングには全く関心がなくても、subredditのデータには興味がありませんか?Redditデータセットをご購入いただけます。