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

このステップガイドでは、Pythonを使ってRedditをスクレイピングし、Reddit API手数料を回避する方法を説明します。
2 分読
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プロジェクトを初期化します。

  nmkdir reddit-scraperncd reddit-scrapernpython -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ファイルに統合します。

  nfrom selenium import webdrivernfrom selenium.webdriver.chrome.service import Service as ChromeServicenfrom webdriver_manager.chrome import ChromeDriverManagernfrom selenium.webdriver.chrome.options import Optionsnn# enable the headless modenoptions = Options()noptions.add_argument('u002du002dheadless=new')nn# initialize a web driver to control Chromendriver = webdriver.Chrome(n    service=ChromeService(ChromeDriverManager().install()),n    options=optionsn)n# maxime the controlled browser windowndriver.fullscreen_window()nn# scraping logic...nn# close the browser and free up the Selenium resourcesndriver.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ウェブスクレイパーは次のようになっています。

  nfrom selenium import webdrivernfrom selenium.webdriver.chrome.service import Service as ChromeServicenfrom webdriver_manager.chrome import ChromeDriverManagernfrom selenium.webdriver.chrome.options import Optionsnn# enable the headless modenoptions = Options()noptions.add_argument('u002du002dheadless=new')nn# initialize a web driver to control Chromendriver = webdriver.Chrome(n    service=ChromeService(ChromeDriverManager().install()),n    options=optionsn)n# maxime the controlled browser windowndriver.fullscreen_window()nn# the URL of the target page to scrapenurl = 'https://www.reddit.com/r/technology/top/?t=week'n# connect to the target URL in Seleniumndriver.get(url)nn# scraping logic...nn# close the browser and free up the Selenium resourcesndriver.quit()

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

Redditスクレイピング

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

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

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

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

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

Redditスクレイピングの続き

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

  data-testid=u0022no-edit-description-blocku0022

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

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

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

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

  subreddit = {}

次に、ページ上部の要素からsubredditの名前を取得できることに注目します。

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

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

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

Redditサイドバー

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

  ndescription = driver 
    .find_element(By.CSS_SELECTOR, '[data-testid=u0022no-edit-description-blocku0022]') 
    .get_attribute('innerText')nncreation_date = driver 
    .find_element(By.CSS_SELECTOR, '.icon-cake') 
    .find_element(By.XPATH, u0022following-sibling::*[1]u0022) 
    .get_attribute('innerText') 
    .replace('Created ', '')nnmembers = driver 
    .find_element(By.CSS_SELECTOR, '[id^=u0022IdCardu002du002dSubscribersu0022]') 
    .find_element(By.XPATH, u0022preceding-sibling::*[1]u0022) 
    .get_attribute('innerText')

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

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

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

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

  nsubreddit['name'] = namensubreddit['description'] = descriptionnsubreddit['creation_date'] = creation_datensubreddit['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セレクタですべてを選択できることがわかります。

  npost_html_elements = driver 
    .find_elements(By.CSS_SELECTOR, '[data-testid=u0022post-containeru0022]')

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

  nfor post_html_element in post_html_elements:n    post = {}nn    # scraping logic...

upvote要素を検査します。

賛成票を検査する

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

  nupvotes = post_html_element 
    .find_element(By.CSS_SELECTOR, '[data-click-id=u0022upvoteu0022]') 
    .find_element(By.XPATH, u0022following-sibling::*[1]u0022) 
    .get_attribute('innerText')

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

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

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

  nauthor = post_html_element 
    .find_element(By.CSS_SELECTOR, '[data-testid=u0022post_author_linku0022]') 
    .textnntitle = post_html_element 
    .find_element(By.TAG_NAME, 'h3') 
    .text

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

コメントと外部リンク
  ntry:n    outbound_link = post_html_element 
        .find_element(By.CSS_SELECTOR, '[data-testid=u0022outbound-linku0022]') 
        .get_attribute('href')nexcept NoSuchElementException:n    outbound_link = Nonenncomments = post_html_element 
    .find_element(By.CSS_SELECTOR, '[data-click-id=u0022commentsu0022]') 
    .get_attribute('innerText') 
    .replace(' Comments', '')

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

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

  n# populate the dictionary with the retrieved datanpost['upvotes'] = upvotesnpost['title'] = titlenpost['outbound_link'] = outbound_linknpost['comments'] = commentsnn# to avoid adding ad posts n# to the list of scraped postsnif title:n    posts.append(post)

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

  subreddit['posts'] = posts

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

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

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

  nimport jsonnn# ...nnwith open('subreddit.json', 'w') as file:n    json.dump(video, file)

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

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

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

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

  nfrom selenium import webdrivernfrom selenium.common import NoSuchElementExceptionnfrom selenium.webdriver.chrome.service import Service as ChromeServicenfrom webdriver_manager.chrome import ChromeDriverManagernfrom selenium.webdriver.chrome.options import Optionsnfrom selenium.webdriver.common.by import Bynimport jsonnn# enable the headless modenoptions = Options()noptions.add_argument('u002du002dheadless=new')nn# initialize a web driver to control Chromendriver = webdriver.Chrome(n    service=ChromeService(ChromeDriverManager().install()),n    options=optionsn)n# maxime the controlled browser windowndriver.fullscreen_window()nn# the URL of the target page to scrapenurl = 'https://www.reddit.com/r/technology/top/?t=week'n# connect to the target URL in Seleniumndriver.get(url)nn# initialize the dictionary that will containn# the subreddit scraped datansubreddit = {}nn# subreddit scraping logicnname = driver 
    .find_element(By.TAG_NAME, 'h1') 
    .textnndescription = driver 
    .find_element(By.CSS_SELECTOR, '[data-testid=u0022no-edit-description-blocku0022]') 
    .get_attribute('innerText')nncreation_date = driver 
    .find_element(By.CSS_SELECTOR, '.icon-cake') 
    .find_element(By.XPATH, u0022following-sibling::*[1]u0022) 
    .get_attribute('innerText') 
    .replace('Created ', '')nnmembers = driver 
    .find_element(By.CSS_SELECTOR, '[id^=u0022IdCardu002du002dSubscribersu0022]') 
    .find_element(By.XPATH, u0022preceding-sibling::*[1]u0022) 
    .get_attribute('innerText')nn# add the scraped data to the dictionarynsubreddit['name'] = namensubreddit['description'] = descriptionnsubreddit['creation_date'] = creation_datensubreddit['members'] = membersnn# to store the post scraped datanposts = []nn# retrieve the list of post HTML elementsnpost_html_elements = driver 
    .find_elements(By.CSS_SELECTOR, '[data-testid=u0022post-containeru0022]')nnfor post_html_element in post_html_elements:n    # to store the data scraped from then    # post HTML elementn    post = {}nn    # subreddit post scraping logicn    upvotes = post_html_element 
        .find_element(By.CSS_SELECTOR, '[data-click-id=u0022upvoteu0022]') 
        .find_element(By.XPATH, u0022following-sibling::*[1]u0022) 
        .get_attribute('innerText')nn    author = post_html_element 
        .find_element(By.CSS_SELECTOR, '[data-testid=u0022post_author_linku0022]') 
        .textnn    title = post_html_element 
        .find_element(By.TAG_NAME, 'h3') 
        .textnn    try:n        outbound_link = post_html_element 
            .find_element(By.CSS_SELECTOR, '[data-testid=u0022outbound-linku0022]') 
            .get_attribute('href')n    except NoSuchElementException:n        outbound_link = Nonenn    comments = post_html_element 
        .find_element(By.CSS_SELECTOR, '[data-click-id=u0022commentsu0022]') 
        .get_attribute('innerText') 
        .replace(' Comments', '')nn    # populate the dictionary with the retrieved datan    post['upvotes'] = upvotesn    post['title'] = titlen    post['outbound_link'] = outbound_linkn    post['comments'] = commentsnn    # to avoid adding ad posts n    # to the list of scraped postsn    if title:n        posts.append(post)nnsubreddit['posts'] = postsnn# close the browser and free up the Selenium resourcesndriver.quit()nn# export the scraped data to a JSON filenwith open('subreddit.json', 'w', encoding='utf-8') as file:n    json.dump(subreddit, file, indent=4, ensure_ascii=False)

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

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

  n{n    u0022nameu0022: u0022/r/Technologyu0022,n    u0022descriptionu0022: u0022Subreddit dedicated to the news and discussions about the creation and use of technology and its surrounding issues.u0022,n    u0022creation_dateu0022: u0022Jan 25, 2008u0022,n    u0022membersu0022: u002214.4mu0022,n    u0022postsu0022: [n        {n            u0022upvotesu0022: u002263.2ku0022,n            u0022titleu0022: u0022Mojang exits Reddit, says they '"no longer feel that Reddit is an appropriate place to post official content or refer [its] players to".u0022,n            u0022outbound_linku0022: u0022https://www.pcgamer.com/minecrafts-devs-exit-its-7-million-strong-subreddit-after-reddits-ham-fisted-crackdown-on-protest/u0022,n            u0022commentsu0022: u00222.9ku0022n        },n        {n            u0022upvotesu0022: u002235.7ku0022,n            u0022titleu0022: u0022JP Morgan accidentally deletes evidence in multi-million record retention screwupu0022,n            u0022outbound_linku0022: u0022https://www.theregister.com/2023/06/26/jp_morgan_fined_for_deleting/u0022,n            u0022commentsu0022: u00222.0ku0022n        },n        # omitted for brevity ...        n        {n            u0022upvotesu0022: u00223.6ku0022,n            u0022titleu0022: u0022Facebook content moderators in Kenya call the work 'torture.' Their lawsuit may ripple worldwideu0022,n            u0022outbound_linku0022: u0022https://techxplore.com/news/2023-06-facebook-content-moderators-kenya-torture.htmlu0022,n            u0022commentsu0022: u0022188u0022n        },n        {n            u0022upvotesu0022: u00223.6ku0022,n            u0022titleu0022: u0022Reddit is telling protesting mods their communities ‘will not’ stay privateu0022,n            u0022outbound_linku0022: u0022https://www.theverge.com/2023/6/28/23777195/reddit-protesting-moderators-communities-subreddits-private-reopenu0022,n            u0022commentsu0022: u0022713u0022n        }n    ]n}

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

まとめ

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

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

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

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