Selenium x Heroku でTwitter自動投稿

2023/07/08に公開

序論

某ロンマスクによるツイッターAPIの有料化により多くの有益botへ被害がでた。
Seleniumを用いればそんな被害を受けずに自動ツイートが可能だ。Seleniumはブラウザを操作するpythonライブラリーである。Seleniumはブラウザを操作するため、通常はPC一台を常時稼働させる必要がある。Seleniumによるツイッター投稿をHerokuなどのリモートサーバーで実行できればとても嬉しい。

この記事は、
Seleniumを用いてツイッターに
1. ローカルで投稿する方法
2. Herokuで投稿する方法
を解説する。


Seleniumを用いてツイッターへ投稿する (ローカル版)

Chrome ドライバーをインストール
https://chromedriver.chromium.org/downloads

selenium_tweet.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
import os
import time
import urllib
import sys
import datetime

#コマンドライン引数でツイート内容を与えると便利
tweet_contents = str(sys.argv[1])

#ドライバーの読み込み
driver = webdriver.Chrome('username/chromedriver_mac64/chromedriver')

#ユーザー情報
USERNAME = "@ユーザーID"
PASSWORD = "パスワード"
email = "メールアドレス"

#ログイン画面へ移動
driver.get('https://twitter.com/i/flow/login')
time.sleep(2)

#ユーザー名入力
username_form = driver.find_element_by_xpath('//input[@name="text"]')
username_form.send_keys(USERNAME)
time.sleep(2)
next_button = driver.find_element_by_xpath('//div[@class="css-18t94o4 css-1dbjc4n r-sdzlij r-1phboty r-rs99b7 r-ywje51 r-usiww2 r-2yi16 r-1qi8awa r-1ny4l3l r-ymttw5 r-o7ynqc r-6416eg r-lrvibr r-13qz1uu"]')
next_button.click()

#パスワード入力
password_form = driver.find_element_by_xpath('//input[@name="password"]')
password_form.send_keys(PASSWORD)
time.sleep(2)
login_button = driver.find_element_by_xpath('//div[@data-testid="LoginForm_Login_Button"]')
login_button.click()

#ログイン情報入力後、メール認証を求められることがある。
try:
	#メール認証求められた場合の処理
	#要素が見つけられない場合はメール認証なし→スキップ
	input_form = driver.find_element_by_xpath('//input[@name="text"]')
	input_form.send_keys(email)
	print("Email authentication")
	next_button = driver.find_element_by_xpath('//div[@data-testid="ocfEnterTextNextButton"]')
	next_button.click()
	time.sleep(5)
	print("current page: {}".format(driver.title))
except:
	print("No Email authentication")


#ツイート画面に移動
url = f"https://twitter.com/intent/tweet?text={tweet_contents}"
driver.get(url)
time.sleep(3)
tweet_button = driver.find_element_by_xpath('//div[@data-testid="tweetButton"]')
tweet_button.click()

time.sleep(5)
driver.close()

コマンドライン引数にツイート内容を与え以下のように実行する。

$ python selenium_tweet.py "テストツイートだってばよ"

これを実行するとブラウザが立ち上がり勝手に画面が操作されるんだってばよ。

Seleniumを用いてツイッターへ投稿する Heroku版

さて、ここでHerokuからツイートする完成コードをバーンとのせて済ますことも可能であるが、おそらくそのままでは動かないのがスクレイピングあるあるである。であるからして、完成版コードを乗せるよりかは、投稿までへの道のりを解説する方が有益であるに違いない。

1. Herokuの設定

この記事を参考にdriverの設定をheroku仕様にする。
https://codelabsjp.net/heroku-selenium/

selenium_tweet.py のdriver読み込み部分をheroku仕様にするとコードの最初の部分は以下のようになる。GOOGLE_CHROME_BIN, CHROMEDRIVER_PATH, は上記記事にて設定している環境変数

selenium_tweet.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
import os
import time
import urllib
import sys
import datetime
​
#コマンドライン引数でツイート内容を与えると便利
tweet_contents = str(sys.argv[1])#ドライバーの読み込み
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN")
driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), options=chrome_options)

print("Selenium is successfully loaded!!")

#ユーザー情報
USERNAME = "@ユーザーID"
PASSWORD = "パスワード"
email = "メールアドレス"#ログイン画面へ移動
driver.get('https://twitter.com/i/flow/login')


これでselenium自体は動くようになる。環境によってselenium起動時に

/app/.heroku/python/lib/python3.10/site-packages/selenium/webdriver/firefox/firefox_profile.py:208: SyntaxWarning: "is" with a literal. Did you mean "=="?
if setting is None or setting is '':

という警告が出るが無視して構わない。

2. 鬼のデバック作業

上記の設定 + ローカルで動かしたのと同じようなコードで、正しく動くと思われるが、そうはいかない。headlessモードだと認証も厳しくなってくるしUIも若干変わるっぽい...?

エラーの原因をサーバー上で確認するには、プログラムが止まる箇所のHTMLをみるしかない。

print(driver.page_source)

を追加すれば現ページのHTMLを取得できる。HTMLをコピー、ローカルサーバーのhtmlファイルにペーストし、ブラウザで開いてみることで、エラーの原因となっているwebページを見れる。HTMLだけ持ってきてるので画面デザインはごっちゃごちゃだが、要素名は変わっていない。開発者ツール https://www.rectus.co.jp/archives/3346 を用いて要素名を調べ、対応させる。

3. 確認コードの入力

UIを確認して要素を正しく認証できて、ID、パスワード、メアド認証まで進んだところで、初回ログイン時は最後に確認コードの入力が求められる。ツイッターに登録しているメールアドレス宛に届くコードをフォームに入力しないといけない...!
この操作はサーバーからの初回ログイン時のみに必要な操作。従って、初回だけは半分人力に頼りコンソールから確認コードを入力する方針をとる (Are you human? の認証に嘘をついていないことになる!!)。

try:
	print("code confirmination")
	confirmation_code_form = driver.find_element_by_xpath('//input[@data-testid="ocfEnterTextTextInput"]')
	time.sleep(1)
	#フォームには初期値が入っているので.clear()で削除してから入力する。
	confirmation_code_form.clear()
	#コンソールから入力待ち
	confirm_key = input("Please enter code:")
	print("key:{}".format(confirm_key))
	confirmation_code_form.send_keys(confirm_key)
	time.sleep(5)
	confirmation_code_form.send_keys(Keys.ENTER)
	print("successfully send")
except:
	print("No code confirmination")

初回のみこのコードはherokuサーバーのコンソールから実行して、メールアドレスに届いた確認コードを入力する。詳しい実行手順は以下

4. Selenium x Heroku でTwitter自動投稿!!!

後はherokuのおっそい処理時間を考慮して、ページ遷移間の待ち時間を調節する。
最終的なコードは以下。なお待ち時間は最適化しきっていないのでもっと速くできるだろう。

selenium_test.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
import os
import time
import urllib
import sys
import datetime

USERNAME = "@ユーザーID"
PASSWORD = "パスワード"
email = "メールアドレス"

body = str(sys.argv[1])

#herokuで動かす用
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")

chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN")
driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), options=chrome_options)

# login process
driver.get('https://twitter.com/i/flow/login')
time.sleep(5)

print("Process 1:")
print("current page: {}".format(driver.title))

#Input username
username_form = driver.find_element_by_xpath('//input[@name="text"]')
username_form.send_keys(USERNAME)
time.sleep(2)
#Subimt username
next_button = driver.find_element_by_xpath('//div[@class="css-18t94o4 css-1dbjc4n r-sdzlij r-1phboty r-rs99b7 r-ywje51 r-usiww2 r-2yi16 r-1qi8awa r-1ny4l3l r-ymttw5 r-o7ynqc r-6416eg r-lrvibr r-13qz1uu"]')
next_button.click()

time.sleep(10)

print("Process 2:")
print("current page: {}".format(driver.title))
print(driver.current_url)

#Input password
password_form = driver.find_element_by_xpath('//input[@name="password"]')
print("password_form: {}".format(username_form))
password_form.send_keys(PASSWORD)
#Select login execution button

login_button = driver.find_element_by_xpath('//div[@data-testid="LoginForm_Login_Button"]')
login_button.click()

time.sleep(10)

try:
	#Email authentication
	input_form = driver.find_element_by_xpath('//input[@name="text"]')
	input_form.send_keys(email)
	print("Email authentication")
	next_button = driver.find_element_by_xpath('//div[@data-testid="ocfEnterTextNextButton"]')
	next_button.click()
	time.sleep(5)
	print("current page: {}".format(driver.title))
except:
	print("No Email authentication")

time.sleep(3)
print("Process 3:")
print("current page: {}".format(driver.title))

try:
	print("code confirmination")
	confirmation_code_form = driver.find_element_by_xpath('//input[@data-testid="ocfEnterTextTextInput"]')
	time.sleep(1)
	confirmation_code_form.clear()
	confirm_key = input("Please enter code:")
	print("key:{}".format(confirm_key))
	confirmation_code_form.send_keys(confirm_key)
	time.sleep(5)
	confirmation_code_form.send_keys(Keys.ENTER)
	print("successfully send")
except:
	print("No code confirmination")

time.sleep(60)
print("Process 4:")
print("current page: {}".format(driver.title))

url = f"https://twitter.com/intent/tweet?text={body}"
driver.get(url)
time.sleep(60)

print("Process 5:")
print("current page: {}".format(driver.title))

tweet_button = driver.find_element_by_xpath('//div[@data-testid="tweetButton"]')
tweet_button.click()

time.sleep(10)
driver.close()

なおデバックがしやすいように要所要所で現在のページタイトルを確認している。

初ログイン時の実行の流れ

heroku run bash

でheroku内のコンソールを開き実行

$ python selenium_test.py "ツイート内容"

すると

$ Please enter code:

とコードの入力が求められる。メールを確認してコードを入力する。

初回のログインに成功すれば

python selenium_test.py "ツイート内容"

をschedularに設定して自動ツイートが可能だ。

Discussion