PythonでTwitterのプロフィールを自動更新
AtCoderとCodeforcesのユーザー名を入力するだけでTwitterのプロフィールを更新できるスクリプトの作り方についての記事です。
例えばこんなことが簡単にできるようになります。
テストでtouristのプロフィールにしてみます
— Lorent (@lorent_kyopro) 2020年6月10日
うん、ちゃんと動いてる pic.twitter.com/OSDUxgBBJU
— Lorent (@lorent_kyopro) 2020年6月10日
1. 現在のRatingを取得
import re import requests from bs4 import BeautifulSoup ac_username = input('AtCoder username : ') ac_url = 'https://atcoder.jp/users/' + ac_username ac_response = requests.get(ac_url) ac_soup = BeautifulSoup(ac_response.text, 'lxml') ac_rating = int(ac_soup.select('table.dl-table')[1].span.get_text()) cf_username = input('Codeforces username : ') cf_url = 'https://codeforces.com/profile/' + cf_username cf_response = requests.get(cf_url) cf_soup = BeautifulSoup(cf_response.text, 'lxml') cf_rating = int(cf_soup.find_all('span', class_=re.compile('^user'))[1].get_text())
RequestsでHTMLを取得、BeautifulSoupでパースして、現在のRatingを抜き出します。
Ratingを抜き出す部分のコードは、ブラウザの開発モード等でHTMLとにらめっこしながら、Pythonのinteractive modeで実験しつつ、いい感じに抜き出せるように頑張ります。
(ちなみに私はスクレイピング超初心者なので、これがいい感じなのかよく分かりませんが、とりあえず取ってこれたのでヨシ!です。)
2. AtCoderのRatingグラフの画像を取得
import time from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument('--headless') driver = webdriver.Chrome(options=options) driver.get(ac_url) driver.set_window_size(1920, 1080) # 気分でFull HD sizeにしました time.sleep(3) img_png = driver.get_screenshot_as_png() driver.quit()
このパート、割と難儀でした。
JPEGやPNGなどの画像ファイルならBeautiful Soupを使って簡単にダウンロードできるのですが、AtCoderのRatingグラフはJavaScriptで描画されているようなので一筋縄ではいきません。 そこで、SeleniumでAtCoderのUserページにアクセスしてスクリーンショットを撮影する方針にしました。
まずheadless mode(ブラウザを陽に表示しないモード)でdriverを開きます。headless modeでないと、スクリーンショットのサイズが使っているディスプレイの解像度等に影響を受けてしまうそうです。
(この際PATHの通っているところに、開くブラウザのdriverをダウンロードしておく必要があります。詳しくはここを参照してください。)
あとはAtCoderのUserページにアクセスして、画面サイズを調節した上でスクリーンショットを撮ります。撮影前にtime.sleep(3)をしているのは、読み込みが完了していない状態で撮影をしてしまい失敗したことがあったためです。劣悪なネット環境のせいかも。
ここまでで、このような画像が取得できました。
3. 画像を加工
import io from PIL import Image dir_name = '----------' # 適当に設定してください gray = (217, 217, 217) brown = (218, 198, 182) green = (190, 217, 185) cyan = (196, 236, 237) blue = (177, 188, 255) yellow = (237, 236, 187) orange = (255, 218, 186) red = (255, 187, 186) color = { 0:gray, 1:brown, 2:green, 3:cyan, 4:blue, 5:yellow, 6:orange, 7:red } color_id = min(ac_rating // 400, 7) img_io = io.BytesIO(img_png) img = Image.open(img_io) cropped_img = img.crop((1400, 600, 2700, 1500)) header_img = Image.new(header_img.mode, (2700, 900), color[color_id]) header_img.paste(cropped_img, (700, 0)) header_img.save(dir_name + 'rating_graph.png')
Twitterのヘッダ画像サイズが1500×500なので、その比に合わせて切り貼りします。
横に引き伸ばしてみたらあまり見た目がよくなかったので、左右の余白部分を今のRatingの色で塗りつぶすことにしました。
Rating色のRGB値はこのサイトで手動で抽出しました。(ここはもっと賢く取得できる気がします。)
既に取得したuserページのスクリーンショットをPillowに渡して、img.crop((left, upper, right, lower))
でうまくRatingグラフの部分を切り取ります。試行錯誤の末、上記の値でうまくいきました。
横が1300px、縦が900pxなので、比を3:1に合わせるため両サイドに幅700pxの余白を取ると考えて、横が700 + 1300 + 700 = 2700px、縦が900pxでRatingの色のベタ塗り画像を作り、その上に先程切り取った画像をペタっと貼り付けます。
完成した画像はこちら。上の画像だと左右の余白がありすぎるように見えますが、TweetDeckで見るとこのくらいがちょうど良さそうです。
右に若干寄っているのが気になる?いえ、知らない子ですね。
(数値的には合っていそうなのに何故ずれるのか、私も疑問です…。もし分かったら教えて下さい。)
4. Twitter APIでプロフィールを更新
import twitter # API keys CK = '----------' CS = '----------' AT = '----------' AS = '----------' api = twitter.Api(CK, CS, AT, AS) api.UpdateBanner(dir_name + 'rating_graph.png') description = 'B5/AtCoder({})/Codeforces({})/第3回PAST70点中級\n{}\n{}'.format(ac_rating, cf_rating, ac_url, cf_url) api.UpdateProfile(description=description)
ここまできたら、あとは更新するだけです。
Twitter API申請がお済みでない方は、解説記事が無数に転がっていますのでそれらを読んで頑張ってください。簡単な英作文課題を解くだけです。
初めはtweepyというライブラリを使ってプロフィールを更新しようと試みたのですが、どうもAPI.update_profile_background_image(filename)
という関数がうまく動きませんでした。色々調べましたが原因不明でしたので、python-twitterという別のライブラリを使うことで、無事その問題を間接的に解決することができました。
ソースコード全体
import io import re import time import requests import twitter from bs4 import BeautifulSoup from PIL import Image from selenium import webdriver from selenium.webdriver.chrome.options import Options # 画像を保存するdirectoryを指定 dir_name = '----------' ac_username = input('AtCoder username : ') ac_url = 'https://atcoder.jp/users/' + ac_username ac_response = requests.get(ac_url) ac_soup = BeautifulSoup(ac_response.text, 'lxml') ac_rating = int(ac_soup.select('table.dl-table')[1].span.get_text()) cf_username = input('Codeforces username : ') cf_url = 'https://codeforces.com/profile/' + cf_username cf_response = requests.get(cf_url) cf_soup = BeautifulSoup(cf_response.text, 'lxml') cf_rating = int(cf_soup.find_all('span', class_=re.compile('^user'))[1].get_text()) options = Options() options.add_argument('--headless') driver = webdriver.Chrome(options=options) driver.get(ac_url) driver.set_window_size(1920, 1080) time.sleep(3) img_png = driver.get_screenshot_as_png() driver.quit() gray = (217, 217, 217) brown = (218, 198, 182) green = (190, 217, 185) cyan = (196, 236, 237) blue = (177, 188, 255) yellow = (237, 236, 187) orange = (255, 218, 186) red = (255, 187, 186) color = { 0:gray, 1:brown, 2:green, 3:cyan, 4:blue, 5:yellow, 6:orange, 7:red } color_id = min(ac_rating // 400, 7) img_io = io.BytesIO(img_png) img = Image.open(img_io) cropped_img = img.crop((1400, 600, 2700, 1500)) header_img = Image.new(cropped_img.mode, (2700, 900), color[color_id]) header_img.paste(cropped_img, (700, 0)) header_img.save(dir_name + 'rating_graph.png') # Twitter API keys CK = '----------' CS = '----------' AT = '----------' AS = '----------' api = twitter.Api(CK, CS, AT, AS) api.UpdateBanner(dir_name + 'rating_graph.png') # 以下は適宜変更してください description = 'B5/AtCoder({})/Codeforces({})/第3回PAST70点中級\n{}\n{}'.format(ac_rating, cf_rating, ac_url, cf_url) api.UpdateProfile(description=description)