新コロナウイルスの猛威の中、ゴールデン・ウィークに突入しました。
政府の発表にて小学校の自宅学習は5月末まで延長されたようです。
サラリーマンも同じような状況ですが、これ以上は業績への影響が大きいので、段階的に緩和される傾向です。
因みにゴールデン・ウィークにどこにも行けない子供連れの家族は公園に向かうようです。
お砂場が三密状態です。
逆効果じゃない・・・?
私のWebサイトではTwitter投稿内容をスクレイピングして、サイトに関わるコメントを表示しています。
ですが2017年頃から正しく記事がスクレイピングできておらず放置していました。
さすがにお粗末なので更新してみました。
Twitter APIの制約を確認する
噂には聞いていましたが、Twitter APIは年々制限が厳しくなっています。
特にUser Streams APIが廃止されるなど、サードパーティのクライアントの開発はなかなか難しい状況です。
例えば、Twitterの検索APIを使用すると、15分ごとに180リクエストしか送信できません。
リクエストあたりの最大ツイート数が100であるため、1時間あたり4 x 180 x 100 = 72.000ツイートをマイニングできるだけです。
更に、過去7日間に書かれたツイートにしかアクセスできません。
Twitterのスクレイピング規約は?
Twitter社的には、Twitterの事前の承諾なしにスクレイピングすることは禁止しています。
ユーザーは、本サービスへアクセスし、またはこれを利用している間、次のいずれをも行ってはならないものとします。
…(省略)
Twitterの事前の承諾なしに本サービスのスクレイピングを行うことは明示的に禁じられています
とは言われても、そもそも私はTwitterのユーザ登録していないからユーザじゃない。
そして、Web公開されているパブリックな自分のサイトのコメントデータを手動で収集する代わりに自動でやってるに過ぎません。
なので、他に方法が無いかと調べたら、OSSにTwitterScraperというPythonライブラリがありました。
TwitterScraperを使ってスクレイピングをしてみる
ライブラリは下記です。
Web上によいサンプルが存在しませんでした。
スクレイピング禁止と書かれているため公開するのを躊躇っているのかもしれません。
数少ないサンプルを見ながら作成したコードは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
from twitterscraper import query_tweets import datetime as dt import pandas as pd import re def check_url(m): if m: return m.group() return "" def multi_replace(m): m = m.replace("\n"," ") m = m.replace("\r"," ") m = m.replace(",",",") return m dt_now = dt.datetime.now() # input begin_date = dt.date(2018,1,1) end_date = dt.date(dt_now.year,dt_now.month,dt_now.day) pool_size = 5 # tweetの収集 tweets = query_tweets("サイト名", begindate=begin_date, enddate=end_date, poolsize=pool_size, lang="ja") tuple_tweet=[(str('{0:02d}'.format(tweet.timestamp.year)) + str('{0:02d}'.format(tweet.timestamp.month)) + str('{0:02d}'.format(tweet.timestamp.day)), str('{0:02d}'.format(tweet.timestamp.year)) + "/" + str('{0:02d}'.format(tweet.timestamp.month)) + "/" + str('{0:02d}'.format(tweet.timestamp.day)), check_url(re.search('(https://[A-Za-z0-9\'~+\-=_.,/%\!;:@#\*&\(\)]+)', tweet.text)), tweet.tweet_url, multi_replace(tweet.username), multi_replace(tweet.text)) for tweet in tweets] df = pd.DataFrame(set(tuple_tweet), columns=['id', 'timestamp', 'url', 'tweet_url', 'username', 'text']) df = df.sort_values('id', ascending=False) df.to_csv('to_csv_out.csv') |
poolsize は、同時実行させるスレッドプールの数です。多くすると高速に処理されますが大量のメモリが必要となります。
さくらインターネットサーバーではpoolsize = 10でもキツかったです。
実行結果
実行すると日付順に
ID、ソート用日付、日付、私のサイトURL、Twitterへのリンク、投稿者名、コメント
が出力されます。
例)
617,20200427,2020/04/27,https://私のサイト名/…,/aaaa/status/125483139,太郎,日本人にとってコロナは深刻な問題 https://私のサイト名/?…
どうやってスクレイピングを実現しているか?
実行ログは次のようなものが出力されていました。
1 2 3 4 5 6 7 8 9 |
$ python sample.py INFO: {'User-Agent': 'Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16'} INFO: queries: ['自分のサイト since:2018-01-01 until:2020-04-29', '自分のサイト -bot since:2018-06-19 until:2018-12-05', '自分のサイト -bot since:2018-12-05 until:2019-05-24', '自分のサイト -bot since:2019-05-24 until:2019-11-09', '自分のサイト -bot since:2019-11-09 until:2020-04-27'] INFO: Querying 自分のサイト -bot since:2018-01-01 until:2018-06-19 INFO: Scraping tweets from https://twitter.com/search?f=tweets&vertical=default&q=自分のサイト%20-bot%20since%3A2018-01-01%20until%3A2018-06-19&l=ja INFO: Using proxy 178.219.37.70:8080 INFO: Querying 自分のサイト -bot since:2018-12-05 until:2019-05-24 INFO: Scraping tweets from https://twitter.com/search?f=tweets&vertical=default&q=自分のサイト%20-bot%20since%3A2018-12-05%20until%3A2019-05-24&l=ja INFO: Using proxy 178.219.37.70:8080 |
TwitterScraperの実装としては、次のような仕組みで同一IPアドレスからのアクセスを回避しているようです。
- プロセスごとに異なるProxyサーバ経由でアクセスする
- ランダムにUser-Agentを書き換えながらツイートをスクレイピングする
これ凄いね・・・・。
この考えは他にも応用できそうです。
ソースコード的には次の部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
HEADERS_LIST = [ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; x64; fr; rv:1.9.2.13) Gecko/20101203 Firebird/3.6.13', 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', 'Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201', 'Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16', 'Mozilla/5.0 (Windows NT 5.2; RW; rv:7.0a1) Gecko/20091211 SeaMonkey/9.23a1pre' ] HEADER = {'User-Agent': random.choice(HEADERS_LIST)} PROXY_URL = 'https://free-proxy-list.net/' def get_proxies(): response = requests.get(PROXY_URL) soup = BeautifulSoup(response.text, 'lxml') table = soup.find('table',id='proxylisttable') list_tr = table.find_all('tr') list_td = [elem.find_all('td') for elem in list_tr] list_td = list(filter(None, list_td)) list_ip = [elem[0].text for elem in list_td] list_ports = [elem[1].text for elem in list_td] list_proxies = [':'.join(elem) for elem in list(zip(list_ip, list_ports))] return list_proxies |
ただし、Proxyサーバーが死んだら使えなくなります。
まとめ
やりたいことは実現できました。
ただし、イタチごっこのような気がします。
感情・流行関係の研究テーマとして、過去のツイート情報は非常に有益だと思いますが、年々厳しくする目的がよく分かりません。
【追記 2020.6.6】
利用できなくなりました。
公式に次のような変更が加わりました。イタチごっこ・・・・。
Indeed, this can be fixed by modifying the header dictionary in query.py from
HEADER = {‘User-Agent’: random.choice(HEADERS_LIST)}
to
HEADER = {‘User-Agent’: random.choice(HEADERS_LIST), ‘X-Requested-With’: ‘XMLHttpRequest’}
【追記 2021.2.11】
新しい方法を調査しました。