Python3を使用してWebページをスクレイプしてコンテンツをTwitterに投稿する方法

提供:Dev Guides
移動先:案内検索

著者は、 Write for DOnations プログラムの一環として、コンピューター歴史博物館を選択して寄付を受け取りました。

序章

Twitter ボットは、ソーシャルメディアを管理するだけでなく、マイクロブログネットワークから情報を抽出するための強力な方法です。 Twitterの用途の広いAPIを活用することで、ボットはツイート、リツイート、「お気に入りのツイート」、特定の関心を持つ人々のフォロー、自動的な返信など、さまざまなことを実行できます。 人々はボットの力を悪用する可能性があり、実際に悪用し、他のユーザーにネガティブな体験をもたらしますが、調査によると、人々はTwitterボットを信頼できる情報源と見なしています。 たとえば、ボットを使用すると、オンラインでない場合でもフォロワーをコンテンツに引き付けることができます。 @EarthquakesSF のように、重要で役立つ情報を提供するボットもあります。 ボットのアプリケーションは無限です。 2019年の時点で、ボットはTwitterのすべてのツイートの約 24% oを占めていると推定されています。

このチュートリアルでは、Python用のこのTwitterAPIライブラリを使用してTwitterボットを構築します。 TwitterアカウントのAPIキーを使用してボットを承認し、2つのWebサイトからコンテンツをスクレイピングできるように構築します。 さらに、ボットをプログラムして、これら2つのWebサイトのコンテンツを設定された時間間隔で交互にツイートします。 このチュートリアルではPython3を使用することに注意してください。

前提条件

このチュートリアルを完了するには、次のものが必要です。

注: Twitterで開発者アカウントを設定します。これには、このボットに必要なAPIキーにアクセスする前に、Twitterによるアプリケーションのレビューが含まれます。 ステップ1では、アプリケーションを完了するための具体的な詳細について説明します。


ステップ1—開発者アカウントを設定してTwitterAPIキーにアクセスする

ボットのコーディングを開始する前に、Twitterがボットのリクエストを認識するためのAPIキーが必要です。 このステップでは、Twitter開発者アカウントを設定し、TwitterボットのAPIキーにアクセスします。

APIキーを取得するには、developer.twitter.com にアクセスし、ページの右上のセクションにあるApplyをクリックしてボットアプリケーションをTwitterに登録します。

次に、開発者アカウントの申請をクリックします。

次に、続行をクリックして、Twitterユーザー名をこのチュートリアルで構築するボットアプリケーションに関連付けます。

次のページでは、このチュートリアルの目的で、私自身の個人的な使用のためのアクセスを要求しているオプションを選択します。これは、あなた自身の個人的な教育用のボットを構築するためです。

アカウント名を選択したら、次のセクションに進みます。 どのユースケースに関心がありますか?ツイートの公開とキュレーションおよび学生プロジェクト/コーディングの学習オプションを選択してください。 これらのカテゴリは、このチュートリアルを完了する理由を最もよく表しています。

次に、構築しようとしているボットの説明を入力します。 Twitterでは、ボットの悪用から保護するためにこれが必要です。 2018年に彼らはそのような審査を導入しました。 このチュートリアルでは、 The NewStackおよびCourseraBlogから技術に焦点を当てたコンテンツをスクレイピングします。

description ボックスに何を入力するかを決定するときは、このチュートリアルの目的のために、次の行で回答をモデル化してください。

thenewstack.io (The New Stack)や blog.coursera.org (Courseraのブログ)などのWebサイトからコンテンツを取得してツイートするTwitterボットを作成するためのチュートリアルに従っています。それらからの引用。 削り取られたコンテンツは集約され、Pythonジェネレーター関数を介してラウンドロビン方式でツイートされます。

最後に、no を選択します。製品、サービス、または分析により、Twitterコンテンツまたは派生情報を政府機関が利用できるようになりますか?

次に、Twitterの利用規約に同意し、 [申し込みを送信]をクリックして、メールアドレスを確認します。 このフォームを送信すると、Twitterから確認メールが送信されます。

メールアドレスを確認すると、審査中の申し込みページに、申し込みプロセスのフィードバックフォームが表示されます。

また、レビューに関してTwitterから別のメールが届きます。

Twitterのアプリケーションレビュープロセスのタイムラインは大幅に異なる可能性がありますが、多くの場合、Twitterは数分以内にこれを確認します。 ただし、アプリケーションのレビューにこれよりも時間がかかる場合は、珍しいことではなく、1〜2日以内に受け取る必要があります。 確認を受け取ると、Twitterはキーの生成を許可します。 developer.twitter.com/appsでアプリの詳細ボタンをクリックすると、キーとトークンタブからこれらにアクセスできます。

最後に、アプリのページのアクセス許可タブに移動し、ツイートコンテンツも書き込みたいので、アクセスアクセス許可オプションを読み取りと書き込みに設定します。 通常、傾向の分析やデータマイニングなどの調査目的で読み取り専用モードを使用します。 最後のオプションでは、チャットボットはダイレクトメッセージにアクセスする必要があるため、ユーザーはチャットボットを既存のアプリに統合できます。

Twitterの強力なAPIにアクセスできます。これは、ボットアプリケーションの重要な部分になります。 次に、環境をセットアップし、ボットの構築を開始します。

ステップ2—Essentialsを構築する

このステップでは、APIキーを使用してTwitterでボットを認証するコードを記述し、Twitterハンドルを介して最初のプログラムによるツイートを作成します。 これは、 The NewStackCourseraBlog からコンテンツを取得し、定期的にツイートするTwitterボットを構築するという目標に向けた良いマイルストーンとして役立ちます。

まず、プロジェクトフォルダとプロジェクト用の特定のプログラミング環境を設定します。

プロジェクトフォルダを作成します。

mkdir bird

プロジェクトフォルダに移動します。

cd bird

次に、プロジェクト用の新しいPython仮想環境を作成します。

python3 -m venv bird-env

次に、次のコマンドを使用して環境をアクティブ化します。

source bird-env/bin/activate

これにより、ターミナルウィンドウのプロンプトに(bird-env)プレフィックスが付加されます。

次に、テキストエディタに移動して、credentials.pyというファイルを作成します。このファイルには、TwitterAPIキーが保存されます。

nano credentials.py

次のコンテンツを追加し、強調表示されたコードをTwitterのキーに置き換えます。

bird / credentials.py

ACCESS_TOKEN='your-access-token'
ACCESS_SECRET='your-access-secret'
CONSUMER_KEY='your-consumer-key'
CONSUMER_SECRET='your-consumer-secret'

次に、Twitterにリクエストを送信するためのメインAPIライブラリをインストールします。 このプロジェクトでは、次のライブラリが必要です:nltkrequeststwitterlxmlrandom、および[X117X ]。 randomtimeはPythonの標準ライブラリの一部であるため、これらのライブラリを個別にインストールする必要はありません。 残りのライブラリをインストールするには、Pythonのパッケージマネージャーであるpipを使用します。

ターミナルを開き、プロジェクトフォルダーにいることを確認して、次のコマンドを実行します。

pip3 install lxml nltk requests twitter
  • lxmlおよびrequests:これらをWebスクレイピングに使用します。
  • twitter:これはTwitterのサーバーへのAPI呼び出しを行うためのライブラリです。
  • nltk :(自然言語ツールキット)ブログの段落を文に分割するために使用します。
  • random:これを使用して、スクレイプされたブログ投稿全体の一部をランダムに選択します。
  • time:特定のアクションの後にボットを定期的にスリープさせるために使用します。

ライブラリをインストールすると、プログラミングを開始する準備が整います。 次に、ボットを実行するメインスクリプトにクレデンシャルをインポートします。 credentials.pyと一緒に、テキストエディタからbirdプロジェクトディレクトリにファイルを作成し、bot.pyという名前を付けます。

nano bot.py

実際には、ボットがますます洗練されるにつれて、ボットの機能を複数のファイルに分散させることになります。 ただし、このチュートリアルでは、デモンストレーションの目的で、すべてのコードを1つのスクリプトbot.pyに配置します。

まず、ボットを承認してAPIキーをテストします。 次のスニペットをbot.pyに追加することから始めます。

bird / bot.py

import random
import time

from lxml.html import fromstring
import nltk
nltk.download('punkt')
import requests
from twitter import OAuth, Twitter

import credentials

ここでは、必要なライブラリをインポートします。 いくつかの例では、ライブラリから必要な関数をインポートします。 コードの後半でfromstring関数を使用して、スクレイプされたWebページの文字列ソースをツリー構造に変換し、ページから関連情報を簡単に抽出できるようにします。 OAuthは、キーから認証オブジェクトを構築するのに役立ちます。Twitterは、Twitterのサーバーとさらに通信するためのメインAPIオブジェクトを構築します。

次に、bot.pyを次の行で拡張します。

bird / bot.py

...
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')

oauth = OAuth(
        credentials.ACCESS_TOKEN,
        credentials.ACCESS_SECRET,
        credentials.CONSUMER_KEY,
        credentials.CONSUMER_SECRET
    )
t = Twitter(auth=oauth)

nltk.download('punkt')は、段落を解析し、それらをより小さなコンポーネントにトークン化(分割)するために必要なデータセットをダウンロードします。 tokenizerは、英語で書かれた段落を分割するためのコードで後で使用するオブジェクトです。

oauthは、インポートされたOAuthクラスにAPIキーをフィードすることによって構築された認証オブジェクトです。 t = Twitter(auth=oauth)の行を介してボットを認証します。 ACCESS_TOKENおよびACCESS_SECRETは、アプリケーションの認識に役立ちます。 最後に、CONSUMER_KEYCONSUMER_SECRETは、アプリケーションがTwitterと対話するためのハンドルを認識するのに役立ちます。 このtオブジェクトを使用して、リクエストをTwitterに伝達します。

次に、このファイルを保存し、次のコマンドを使用してターミナルで実行します。

python3 bot.py

出力は次のようになります。これは、認証が成功したことを意味します。

Output[nltk_data] Downloading package punkt to /Users/binaryboy/nltk_data...
[nltk_data]   Package punkt is already up-to-date!

エラーが発生した場合は、保存したAPIキーを Twitter開発者アカウントのキーと確認して、再試行してください。 また、必要なライブラリが正しくインストールされていることを確認してください。 そうでない場合は、pip3を再度使用してインストールしてください。

これで、プログラムで何かをツイートしてみることができます。 -iフラグを指定してターミナルで同じコマンドを入力し、スクリプトの実行後にPythonインタープリターを開きます。

python3 -i bot.py

次に、次のように入力して、アカウントを介してツイートを送信します。

t.statuses.update(status="Just setting up my Twttr bot")

次に、ブラウザでTwitterタイムラインを開くと、投稿したコンテンツを含むツイートがタイムラインの上部に表示されます。

quit()またはCTRL + Dと入力して、インタープリターを閉じます。

これで、ボットにツイートする基本的な機能が追加されました。 有用なコンテンツをツイートするボットを開発するには、次のステップでWebスクレイピングを組み込みます。

ステップ3—ツイートコンテンツのWebサイトをスクレイピングする

タイムラインにさらに興味深いコンテンツを紹介するには、 NewStackCourseraBlog からコンテンツを取得し、このコンテンツをツイートの形でTwitterに投稿します。 一般に、ターゲットWebサイトから適切なデータを取得するには、それらのHTML構造を試す必要があります。 このチュートリアルで作成するボットからの各ツイートには、選択したWebサイトからのブログ投稿へのリンクと、そのブログからのランダムな引用が含まれます。 この手順は、Courseraからコンテンツをスクレイピングするための固有の関数内に実装するため、scrape_coursera()という名前を付けます。

最初に開くbot.py

nano bot.py

scrape_coursera()関数をファイルの最後に追加します。

bird / bot.py

...
t = Twitter(auth=oauth)


def scrape_coursera():

ブログから情報を取得するには、まず、Courseraのサーバーに関連するWebページをリクエストします。 そのためには、requestsライブラリのget()関数を使用します。 get()はURLを受け取り、対応するWebページをフェッチします。 したがって、blog.coursera.orgを引数としてget()に渡します。 ただし、GETリクエストにヘッダーを指定する必要もあります。これにより、Courseraのサーバーがあなたを本物のクライアントとして認識できるようになります。 次の強調表示された行をscrape_coursera()関数に追加して、ヘッダーを提供します。

bird / bot.py

def scrape_coursera():
    HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                      ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
        }

このヘッダーには、特定のオペレーティングシステムで実行されている定義済みのWebブラウザーに関する情報が含まれます。 この情報(通常はUser-Agentと呼ばれます)が実際のWebブラウザーおよびオペレーティングシステムに対応している限り、ヘッダー情報が実際のWebブラウザーおよびコンピューター上のオペレーティングシステムと一致しているかどうかは関係ありません。 したがって、このヘッダーはすべてのシステムで正常に機能します。

ヘッダーを定義したら、次の強調表示された行を追加して、ブログWebページのURLを指定してCourseraにGETリクエストを作成します。

bird / bot.py

...
def scrape_coursera():
    HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                      ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
        }
    r = requests.get('https://blog.coursera.org', headers=HEADERS)
    tree = fromstring(r.content)

これにより、Webページがマシンにフェッチされ、Webページ全体の情報が変数rに保存されます。 rcontent属性を使用して、WebページのHTMLソースコードを評価できます。 したがって、r.contentの値は、ページを右クリックして要素の検査オプションを選択してブラウザでWebページを検査したときに表示される値と同じです。

ここでは、fromstring関数も追加しました。 ウェブページのソースコードをlxmlライブラリからインポートしたfromstring関数に渡して、ウェブページのtree構造を構築することができます。 このツリー構造により、Webページのさまざまな部分に簡単にアクセスできます。 HTMLソースコードは特定のツリーのような構造を持っています。 すべての要素は<html>タグで囲まれ、その後ネストされます。

次に、ブラウザでhttps://blog.coursera.orgを開き、ブラウザの開発者ツールを使用してそのHTMLソースを調べます。 ページを右クリックして、要素の検査オプションを選択します。 ブラウザの下部に、ページのHTMLソースコードの一部を示すウィンドウが表示されます。

次に、表示されているブログ投稿のサムネイルを右クリックして、それを調べます。 HTMLソースは、そのブログのサムネイルが定義されている関連するHTML行を強調表示します。 このページのすべてのブログ投稿は、"recent" クラスを持つ<div>タグ内で定義されていることに気付くでしょう。

したがって、コードでは、このようなすべてのブログ投稿div要素をXPath経由で使用します。これは、Webページの要素をアドレス指定する便利な方法です。

これを行うには、bot.pyで関数を次のように拡張します。

bird / bot.py

...
def scrape_coursera():
    HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                      ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
                    }
    r = requests.get('https://blog.coursera.org', headers=HEADERS)
    tree = fromstring(r.content)
    links = tree.xpath('//div[@class="recent"]//div[@class="title"]/a/@href')
    print(links)

scrape_coursera()

ここで、 XPathtree.xpath()に渡される文字列)は、クラス"recent"//はウェブページ全体の検索に対応し、divdiv要素のみを抽出するように関数に指示し、[@class="recent"]はそれらのdiv要素のclass属性の値が"recent"である。

ただし、これらの要素自体は必要ありません。必要なのは、それらが指しているリンクだけです。これにより、個々のブログ投稿にアクセスして、コンテンツを取得できます。 したがって、ブログ投稿の以前のdivタグ内にあるhrefアンカータグの値を使用して、すべてのリンクを抽出します。

これまでのプログラムをテストするには、bot.pyの最後にあるscrape_coursera()関数を呼び出します。

bot.pyを保存して終了します。

次に、次のコマンドでbot.pyを実行します。

python3 bot.py

出力には、次のようなURLのリストが表示されます。

Output['https://blog.coursera.org/career-stories-from-inside-coursera/', 'https://blog.coursera.org/unlock-the-power-of-data-with-python-university-of-michigan-offers-new-programming-specializations-on-coursera/', ...]

出力を確認した後、bot.pyスクリプトから最後の2つの強調表示された行を削除できます。

bird / bot.py

...
def scrape_coursera():
    ...
    tree = fromstring(r.content)
    links = tree.xpath('//div[@class="recent"]//div[@class="title"]/a/@href')
    ~~print(links)~~

~~scrape_coursera()~~

次に、bot.pyの関数を次の強調表示された行で拡張して、ブログ投稿からコンテンツを抽出します。

bird / bot.py

...
def scrape_coursera():
    ...
    links = tree.xpath('//div[@class="recent"]//div[@class="title"]/a/@href')
    for link in links:
        r = requests.get(link, headers=HEADERS)
        blog_tree = fromstring(r.content)

各リンクを繰り返し処理し、対応するブログ投稿を取得し、投稿からランダムな文を抽出してから、この文を対応するURLとともに引用符としてツイートします。 ランダムな文の抽出には、次の3つの部分が含まれます。

  1. ブログ投稿のすべての段落をリストとして取得します。
  2. 段落のリストからランダムに段落を選択します。
  3. この段落からランダムに文を選択します。

ブログ投稿ごとにこれらの手順を実行します。 1つをフェッチするには、そのリンクに対してGET要求を行います。

ブログのコンテンツにアクセスできるようになったので、これらの3つのステップを実行して、ブログから必要なコンテンツを抽出するコードを紹介します。 3つのステップを実行するスクレイピング関数に次の拡張機能を追加します。

bird / bot.py

...
def scrape_coursera():
    ...
    for link in links:
        r = requests.get(link, headers=HEADERS)
        blog_tree = fromstring(r.content)
        paras = blog_tree.xpath('//div[@class="entry-content"]/p')
        paras_text = [para.text_content() for para in paras if para.text_content()]
        para = random.choice(paras_text)
        para_tokenized = tokenizer.tokenize(para)
        for _ in range(10):
            text = random.choice(para_tokenized)
            if text and 60 < len(text) < 210:
                break

最初のリンクを開いてブログ投稿を調べると、すべての段落がentry-contentをクラスとして持つdivタグに属していることがわかります。 したがって、paras = blog_tree.xpath('//div[@class="entry-content"]/p')を使用してすべての段落をリストとして抽出します。

リスト要素はリテラル段落ではありません。 それらはElementオブジェクトです。 これらのオブジェクトからテキストを抽出するには、text_content()メソッドを使用します。 この行は、Pythonのリスト内包表記デザインパターンに従います。このパターンは、通常は1行で書き出されるループを使用してコレクションを定義します。 bot.pyでは、各段落要素オブジェクトのテキストを抽出し、テキストが空でない場合はリストに保存します。 この段落リストからランダムに段落を選択するには、randomモジュールを組み込みます。

最後に、変数paraに格納されている、この段落からランダムに文を選択する必要があります。 このタスクでは、最初に段落を文に分割します。 これを実現するための1つのアプローチは、Pythonのsplit()メソッドを使用することです。 ただし、文は複数のブレークポイントで分割される可能性があるため、これは難しい場合があります。 したがって、分割タスクを簡素化するために、nltkライブラリを介した自然言語処理を活用します。 チュートリアルの前半で定義したtokenizerオブジェクトは、この目的に役立ちます。

文のリストができたので、random.choice()を呼び出してランダムな文を抽出します。 この文をツイートの引用にするため、280文字を超えることはできません。 ただし、美的理由から、大きすぎず小さすぎない文を選択します。 ツイート文の長さは60〜210文字にするように指定します。 random.choice()が選択する文は、この基準を満たさない可能性があります。 正しい文を識別するために、スクリプトは10回試行し、毎回基準をチェックします。 ランダムに選択された文が基準を満たしたら、ループから抜け出すことができます。

確率は非常に低いですが、10回の試行でこのサイズの条件を満たす文がない可能性があります。 この場合、対応するブログ投稿を無視して、次のブログ投稿に進みます。

引用する文ができたので、対応するリンクでツイートできます。 これを行うには、ランダムに選択された文と対応するブログリンクを含む文字列を生成します。 このscrape_coursera()関数を呼び出すコードは、生成された文字列をTwitterのAPIを介してTwitterに投稿します。

次のように関数を拡張します。

bird / bot.py

...
def scrape_coursera():
    ...
    for link in links:
        ...
        para_tokenized = tokenizer.tokenize(para)
        for _ in range(10):
            text = random.choice(para)
            if text and 60 < len(text) < 210:
                break
        else:
            yield None
        yield '"%s" %s' % (text, link)

スクリプトは、先行するforループが中断しない場合にのみ、elseステートメントを実行します。 したがって、ループがサイズ条件に一致する文を見つけることができない場合にのみ発生します。 その場合は、Noneを生成するだけで、この関数を呼び出すコードがツイートするものがないと判断できるようになります。 次に、関数を再度呼び出して、次のブログリンクのコンテンツを取得します。 しかし、ループが壊れた場合は、関数が適切な文を見つけたことを意味します。 スクリプトはelseステートメントを実行せず、関数は、単一の空白で区切られた文とブログリンクで構成される文字列を生成します。

scrape_coursera()関数の実装はほぼ完了しています。 別のWebサイトをスクレイピングするために同様の機能を作成する場合は、Courseraのブログをスクレイピングするために作成したコードの一部を繰り返す必要があります。 コードの一部の書き換えや複製を回避し、ボットのスクリプトが DRY の原則(Do n't Repeat Yourself)に従うようにするために、再度使用するコードの一部を特定して抽象化し、後で作成するスクレーパー関数についても同様です。

関数がスクレイピングしているWebサイトに関係なく、段落をランダムに選択してから、この選択した段落からランダムな文を選択する必要があります。これらの機能を別々の関数で抽出できます。 次に、スクレーパー関数からこれらの関数を呼び出すだけで、目的の結果を得ることができます。 scrape_coursera()関数の外部でHEADERSを定義して、すべてのスクレーパー関数で使用できるようにすることもできます。 したがって、次のコードでは、HEADERS定義をスクレーパー関数の定義の前に配置して、最終的に他のスクレーパーで使用できるようにする必要があります。

bird / bot.py

...
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                  ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
    }


def scrape_coursera():
    r = requests.get('https://blog.coursera.org', headers=HEADERS)
    ...

これで、段落オブジェクトのリストからランダムな段落を抽出するためのextract_paratext()関数を定義できます。 ランダムな段落はparas引数として関数に渡され、後で文の抽出に使用する、選択された段落のトークン化された形式を返します。

bird / bot.py

...
HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                      ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
        }

def extract_paratext(paras):
    """Extracts text from <p> elements and returns a clean, tokenized random
    paragraph."""

    paras = [para.text_content() for para in paras if para.text_content()]
    para = random.choice(paras)
    return tokenizer.tokenize(para)


def scrape_coursera():
    r = requests.get('https://blog.coursera.org', headers=HEADERS)
    ...

次に、引数として取得するトークン化された段落から適切な長さ(60〜210文字)のランダムな文を抽出する関数を定義します。これには、paraという名前を付けることができます。 10回試行してもそのような文が検出されない場合、関数は代わりにNoneを返します。 次の強調表示されたコードを追加して、extract_text()関数を定義します。

bird / bot.py

...

def extract_paratext(paras):
    ...
    return tokenizer.tokenize(para)


def extract_text(para):
    """Returns a sufficiently-large random text from a tokenized paragraph,
    if such text exists. Otherwise, returns None."""

    for _ in range(10):
        text = random.choice(para)
        if text and 60 < len(text) < 210:
            return text

    return None


def scrape_coursera():
    r = requests.get('https://blog.coursera.org', headers=HEADERS)
    ...

これらの新しいヘルパー関数を定義したら、scrape_coursera()関数を次のように再定義できます。

bird / bot.py

...
def extract_paratext():
    for _ in range(10):<^>
        text = random.choice(para)
    ...


def scrape_coursera():
    """Scrapes content from the Coursera blog."""

    url = 'https://blog.coursera.org'
    r = requests.get(url, headers=HEADERS)
    tree = fromstring(r.content)
    links = tree.xpath('//div[@class="recent"]//div[@class="title"]/a/@href')

    for link in links:
        r = requests.get(link, headers=HEADERS)
        blog_tree = fromstring(r.content)
        paras = blog_tree.xpath('//div[@class="entry-content"]/p')
        para = extract_paratext(paras)
        text = extract_text(para)
        if not text:
            continue

        yield '"%s" %s' % (text, link)

bot.pyを保存して終了します。

ここでは、returnの代わりにyieldを使用しています。これは、リンクを反復処理するために、スクレーパー機能によってツイート文字列が1つずつ順番に表示されるためです。 つまり、sc = scrape_coursera()として定義されたスクレーパーscを最初に呼び出すと、スクレーパー関数内で計算したリンクのリストの最初のリンクに対応するツイート文字列が取得されます。 インタプリタで次のコードを実行すると、scrape_coursera()内のlinks変数がリストを保持している場合、以下に示すようにstring_1string_2が得られます。 ["https://thenewstack.io/cloud-native-live-twistlocks-virtual-conference/%22, "https://blog.coursera.org/unlock-the-power-of-data-with-python-university-of-michigan-offers-new-programming-specializations-on-coursera/%22, ...]のように見えます。

python3 -i bot.py

スクレーパーをインスタンス化し、scと呼びます。

>>> sc = scrape_coursera()

現在はジェネレーターです。 一度に1つずつ、Courseraから関連コンテンツを生成またはスクレイプします。 scを介してnext()を順番に呼び出すことにより、スクレイプされたコンテンツに1つずつアクセスできます。

>>> string_1 = next(sc)
>>> string_2 = next(sc)

これで、定義した文字列をprintして、スクレイプされたコンテンツを表示できます。

>>> print(string_1)
"Other speakers include Priyanka Sharma, director of cloud native alliances at GitLab and Dan Kohn, executive director of the Cloud Native Computing Foundation." https://thenewstack.io/cloud-native-live-twistlocks-virtual-conference/
>>>
>>> print(string_2)
"You can learn how to use the power of Python for data analysis with a series of courses covering fundamental theory and project-based learning." https://blog.coursera.org/unlock-the-power-of-data-with-python-university-of-michigan-offers-new-programming-specializations-on-coursera/
>>>

代わりにreturnを使用すると、文字列を1つずつ順番に取得することはできません。 scrape_coursera()yieldreturnに置き換えるだけで、最初の呼び出しで最初の文字列を取得するのではなく、常に最初のブログ投稿に対応する文字列を取得します、2番目の呼び出しの2番目、というように続きます。 すべてのリンクに対応するすべての文字列のlistを返すように関数を変更できますが、これはメモリを大量に消費します。 また、この種のプログラムでは、リスト全体をすばやく取得したい場合、Courseraのサーバーに短時間で多くのリクエストを送信する可能性があります。 これにより、ボットがWebサイトへのアクセスを一時的に禁止される可能性があります。 したがって、yieldは、一度に1つずつスクレイピングする情報のみが必要なさまざまなスクレイピングジョブに最適です。

ステップ4—追加コンテンツのスクレイピング

このステップでは、thenewstack.ioのスクレーパーを作成します。 プロセスは前のステップで完了したものと似ているので、これは簡単な概要になります。

ブラウザでWebサイトを開き、ページソースを調べます。 ここでは、すべてのブログセクションがクラスnormalstory-boxdiv要素であることがわかります。

次に、scrape_thenewstack()という名前の新しいスクレーパー関数を作成し、その中からthenewstack.ioに対してGETリクエストを作成します。 次に、これらの要素からブログへのリンクを抽出し、各リンクを繰り返し処理します。 これを実現するには、次のコードを追加します。

bird / bot.py

...
def scrape_coursera():
    ...
    yield '"%s" %s' % (text, link)


def scrape_thenewstack():
    """Scrapes news from thenewstack.io"""

    r = requests.get('https://thenewstack.io', verify=False)

        tree = fromstring(r.content)
        links = tree.xpath('//div[@class="normalstory-box"]/header/h2/a/@href')
        for link in links:

verify=Falseフラグを使用するのは、Webサイトでセキュリティ証明書の有効期限が切れている場合があり、ここでの場合のように、機密データが含まれていない場合はそれらにアクセスできます。 verify=Falseフラグは、requests.getメソッドに、証明書を検証せず、通常どおりデータのフェッチを続行するように指示します。 それ以外の場合、メソッドは期限切れのセキュリティ証明書に関するエラーをスローします。

これで、各リンクに対応するブログの段落を抽出し、前の手順で作成したextract_paratext()関数を使用して、使用可能な段落のリストからランダムな段落を引き出すことができます。 最後に、extract_text()関数を使用してこの段落からランダムな文を抽出し、対応するブログリンクを使用してyieldします。 これらのタスクを実行するには、次の強調表示されたコードをファイルに追加します。

bird / bot.py

...
def scrape_thenewstack():
    ...
    links = tree.xpath('//div[@class="normalstory-box"]/header/h2/a/@href')

    for link in links:
        r = requests.get(link, verify=False)
        tree = fromstring(r.content)
        paras = tree.xpath('//div[@class="post-content"]/p')
        para = extract_paratext(paras)
        text = extract_text(para)  
        if not text:
            continue

        yield '"%s" %s' % (text, link)

これで、スクレイピングプロセスが一般的に何を含むかがわかりました。 たとえば、ランダムな引用符の代わりにブログ投稿の画像をスクレイプできる独自のカスタムスクレーパーを作成できるようになりました。 そのためには、関連する<img>タグを探すことができます。 識別子として機能するタグの正しいパスを取得したら、対応する属性の名前を使用してタグ内の情報にアクセスできます。 たとえば、画像をスクレイピングする場合、src属性を使用して画像のリンクにアクセスできます。

この時点で、2つの異なるWebサイトからコンテンツをスクレイピングするための2つのスクレイパー関数を構築しました。また、2つのスクレイパーに共通する機能を再利用するための2つのヘルパー関数も構築しました。 ボットがツイートする方法とツイートする内容を知ったので、スクレイプされたコンテンツをツイートするコードを記述します。

ステップ5—スクレイプされたコンテンツをツイートする

このステップでは、ボットを拡張して2つのWebサイトからコンテンツを取得し、Twitterアカウントを介してツイートします。 より正確には、2つのWebサイトのコンテンツを交互に、10分間隔で無期限にツイートする必要があります。 したがって、 infinite while loop を使用して、目的の機能を実装します。 これは、main()関数の一部として実行します。この関数は、ボットに実行させたいコアの高レベルプロセスを実装します。

bird / bot.py

...
def scrape_thenewstack():
    ...
    yield '"%s" %s' % (text, link)


def main():
    """Encompasses the main loop of the bot."""
    print('---Bot started---\n')
    news_funcs = ['scrape_coursera', 'scrape_thenewstack']
    news_iterators = []  
    for func in news_funcs:
        news_iterators.append(globals()[func]())
    while True:
        for i, iterator in enumerate(news_iterators):
            try:
                tweet = next(iterator)
                t.statuses.update(status=tweet)
                print(tweet, end='\n\n')
                time.sleep(600)  
            except StopIteration:
                news_iterators[i] = globals()[newsfuncs[i]]()

最初に、前に定義したスクレイピング関数の名前のリストを作成し、それをnews_funcsと呼びます。 次に、実際のスクレーパー機能を保持する空のリストを作成し、そのリストにnews_iteratorsという名前を付けます。 次に、news_funcsリストの各名前を調べ、対応するイテレータをnews_iteratorsリストに追加してデータを入力します。 Pythonの組み込みglobals()関数を使用しています。 これにより、変数名をスクリプト内の実際の変数にマップする辞書が返されます。 イテレータは、スクレーパー関数を呼び出すときに取得するものです。たとえば、coursera_iterator = scrape_coursera()と記述した場合、coursera_iteratorは、next()呼び出しを呼び出すことができるイテレータになります。 next()を呼び出すたびに、scrape_coursera()関数のyieldステートメントで定義されているとおりに、引用符とそれに対応するリンクを含む文字列が返されます。 各next()呼び出しは、scrape_coursera()関数のforループの1回の反復を通過します。 したがって、scrape_coursera()関数のブログリンクと同じ数だけ、next()呼び出しを行うことができます。 その数を超えると、StopIteration例外が発生します。

両方のイテレータがnews_iteratorsリストに入力されると、メインのwhileループが開始されます。 その中には、forループがあり、各イテレーターを通過して、ツイートするコンテンツを取得しようとします。 コンテンツを取得した後、ボットはそれをツイートし、10分間スリープします。 イテレータに提供するコンテンツがこれ以上ない場合、StopIteration例外が発生し、そのイテレータを再インスタンス化して更新し、ソースWebサイトで新しいコンテンツが利用可能かどうかを確認します。 次に、可能な場合は次のイテレータに進みます。 それ以外の場合、実行がイテレータリストの最後に達した場合は、最初から再開して、次に利用可能なコンテンツをツイートします。 これにより、ボットは2つのスクレーパーからコンテンツを交互にツイートします。

現在残っているのは、main()関数を呼び出すことだけです。 これは、スクリプトがPythonインタープリターによって直接と呼ばれるときに行います。

bird / bot.py

...
def main():
    print('---Bot started---\n')<^>
    news_funcs = ['scrape_coursera', 'scrape_thenewstack']
    ...

if __name__ == "__main__":  
    main()

以下は、bot.pyスクリプトの完成版です。 このGitHubリポジトリスクリプトを表示することもできます。

bird / bot.py

"""Main bot script - bot.py
For the DigitalOcean Tutorial.
"""


import random
import time


from lxml.html import fromstring
import nltk  
nltk.download('punkt')
import requests  

from twitter import OAuth, Twitter


import credentials

tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')

oauth = OAuth(
        credentials.ACCESS_TOKEN,
        credentials.ACCESS_SECRET,
        credentials.CONSUMER_KEY,
        credentials.CONSUMER_SECRET
    )
t = Twitter(auth=oauth)

HEADERS = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5)'
                      ' AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'
        }


def extract_paratext(paras):
    """Extracts text from <p> elements and returns a clean, tokenized random
    paragraph."""

    paras = [para.text_content() for para in paras if para.text_content()]
    para = random.choice(paras)
    return tokenizer.tokenize(para)


def extract_text(para):
    """Returns a sufficiently-large random text from a tokenized paragraph,
    if such text exists. Otherwise, returns None."""

    for _ in range(10):
        text = random.choice(para)
        if text and 60 < len(text) < 210:
            return text

    return None


def scrape_coursera():
    """Scrapes content from the Coursera blog."""
    url = 'https://blog.coursera.org'
    r = requests.get(url, headers=HEADERS)
    tree = fromstring(r.content)
    links = tree.xpath('//div[@class="recent"]//div[@class="title"]/a/@href')

    for link in links:
        r = requests.get(link, headers=HEADERS)
        blog_tree = fromstring(r.content)
        paras = blog_tree.xpath('//div[@class="entry-content"]/p')
        para = extract_paratext(paras)  
        text = extract_text(para)  
        if not text:
            continue

        yield '"%s" %s' % (text, link)  


def scrape_thenewstack():
    """Scrapes news from thenewstack.io"""

    r = requests.get('https://thenewstack.io', verify=False)

    tree = fromstring(r.content)
    links = tree.xpath('//div[@class="normalstory-box"]/header/h2/a/@href')

    for link in links:
        r = requests.get(link, verify=False)
        tree = fromstring(r.content)
        paras = tree.xpath('//div[@class="post-content"]/p')
        para = extract_paratext(paras)
        text = extract_text(para)  
        if not text:
            continue

        yield '"%s" %s' % (text, link)


def main():
    """Encompasses the main loop of the bot."""
    print('Bot started.')
    news_funcs = ['scrape_coursera', 'scrape_thenewstack']
    news_iterators = []  
    for func in news_funcs:
        news_iterators.append(globals()[func]())
    while True:
        for i, iterator in enumerate(news_iterators):
            try:
                tweet = next(iterator)
                t.statuses.update(status=tweet)
                print(tweet, end='\n')
                time.sleep(600)
            except StopIteration:
                news_iterators[i] = globals()[newsfuncs[i]]()


if __name__ == "__main__":  
    main()

bot.pyを保存して終了します。

以下は、bot.pyの実行例です。

python3 bot.py

ボットがスクレイプしたコンテンツを示す出力が、次のような形式で表示されます。

Output[nltk_data] Downloading package punkt to /Users/binaryboy/nltk_data...
[nltk_data]   Package punkt is already up-to-date!
---Bot started---

"Take the first step toward your career goals by building new skills." https://blog.coursera.org/career-stories-from-inside-coursera/

"Other speakers include Priyanka Sharma, director of cloud native alliances at GitLab and Dan Kohn, executive director of the Cloud Native Computing Foundation." https://thenewstack.io/cloud-native-live-twistlocks-virtual-conference/

"You can learn how to use the power of Python for data analysis with a series of courses covering fundamental theory and project-based learning." https://blog.coursera.org/unlock-the-power-of-data-with-python-university-of-michigan-offers-new-programming-specializations-on-coursera/

"“Real-user monitoring is really about trying to understand the underlying reasons, so you know, ‘who do I actually want to fly with?" https://thenewstack.io/how-raygun-co-founder-and-ceo-spun-gold-out-of-monitoring-agony/

ボットのサンプル実行後、ボットがTwitterページに投稿したプログラムによるツイートの完全なタイムラインが表示されます。 次のようになります。

ご覧のとおり、ボットは、各ブログからのランダムな引用をハイライトとして、スクレイプされたブログリンクをツイートしています。 このフィードは、Courseraからのブログの引用とthenewstack.ioの間でツイートが交互に表示される情報フィードになりました。 Webからコンテンツを集約し、Twitterに投稿するボットを作成しました。 これで、さまざまなWebサイトにスクレーパーを追加することで、このボットの範囲を拡大できます。ボットは、すべてのスクレーパーからのコンテンツをラウンドロビン方式で希望の時間間隔でツイートします。

結論

このチュートリアルでは、Pythonを使用して基本的なTwitterボットを構築し、ボットがツイートできるようにWebからコンテンツを取得しました。 試すべきボットのアイデアはたくさんあります。 ボットのユーティリティについて独自のアイデアを実装することもできます。 TwitterのAPIが提供する多彩な機能を組み合わせて、より複雑なものを作成できます。 より洗練されたTwitterボットのバージョンについては、 chirps をチェックしてください。これは、マルチスレッドなどの高度な概念を使用してボットに複数のことを同時に実行させるTwitterボットフレームワークです。 misheardlyのような楽しいアイデアのボットもいくつかあります。 Twitterボットを構築する際に使用できる創造性に制限はありません。 ボットの実装にヒットする適切なAPIエンドポイントを見つけることが不可欠です。

最後に、ボットのエチケットまたは(「ボットケット」)は、次のボットを構築するときに覚えておくことが重要です。 たとえば、ボットにリツイートが組み込まれている場合は、すべてのツイートのテキストをフィルターに通して、乱暴な言葉を検出してからリツイートします。 このような機能は、正規表現と自然言語処理を使用して実装できます。 また、こすり取る情報源を探すときは、あなたの判断に従い、誤った情報を広めるものを避けてください。 ボティケットの詳細については、このトピックに関するJoeMayoによるこのブログ投稿にアクセスしてください。