PythonでTwitterのプロフィールを自動更新

AtCoderCodeforcesのユーザー名を入力するだけでTwitterのプロフィールを更新できるスクリプトの作り方についての記事です。
例えばこんなことが簡単にできるようになります。

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()

このパート、割と難儀でした。
JPEGPNGなどの画像ファイルならBeautiful Soupを使って簡単にダウンロードできるのですが、AtCoderのRatingグラフはJavaScriptで描画されているようなので一筋縄ではいきません。 そこで、SeleniumAtCoderのUserページにアクセスしてスクリーンショットを撮影する方針にしました。

まずheadless mode(ブラウザを陽に表示しないモード)でdriverを開きます。headless modeでないと、スクリーンショットのサイズが使っているディスプレイの解像度等に影響を受けてしまうそうです。
(この際PATHの通っているところに、開くブラウザのdriverをダウンロードしておく必要があります。詳しくはここを参照してください。)

あとはAtCoderのUserページにアクセスして、画面サイズを調節した上でスクリーンショットを撮ります。撮影前にtime.sleep(3)をしているのは、読み込みが完了していない状態で撮影をしてしまい失敗したことがあったためです。劣悪なネット環境のせいかも

ここまでで、このような画像が取得できました。 f:id:Rorent:20200611134153p:plain

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で見るとこのくらいがちょうど良さそうです。

f:id:Rorent:20200611134611p:plain f:id:Rorent:20200611134712p:plain

右に若干寄っているのが気になる?いえ、知らない子ですね。
(数値的には合っていそうなのに何故ずれるのか、私も疑問です…。もし分かったら教えて下さい。)

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)