→コード: v4.1.7 ユーザがよく変更する定数をUserPropertiesに移動。ページの読み込みが完了するまで待つよう幾つかの箇所を修正
>Fet-Fe (→コード: v4.1.6) |
>Fet-Fe (→コード: v4.1.7 ユーザがよく変更する定数をUserPropertiesに移動。ページの読み込みが完了するまで待つよう幾つかの箇所を修正) |
||
11行目: | 11行目: | ||
"""Twitter自動収集スクリプト | """Twitter自動収集スクリプト | ||
ver4.1. | ver4.1.7 2023/11/18恒心 | ||
当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | 当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | ||
17行目: | 17行目: | ||
Examples: | Examples: | ||
定数類は状況に応じて変えてください。 | 定数類は状況に応じて変えてください。:class:`~UserProperties` で変更できます。 | ||
:: | :: | ||
43行目: | 43行目: | ||
* Whonix-Workstation, MacOSで動作確認済 | * Whonix-Workstation, MacOSで動作確認済 | ||
* | * MacOSの場合はTor Browserをダウンロードするかbrewでtorコマンドを導入してから実行する | ||
* PySocks, bs4, seleniumはインストールしないと標準で入ってません | * PySocks, bs4, seleniumはインストールしないと標準で入ってません | ||
63行目: | 63行目: | ||
import random | import random | ||
import re | import re | ||
import shutil | |||
import subprocess | import subprocess | ||
import sys | import sys | ||
68行目: | 69行目: | ||
from argparse import ArgumentParser, Namespace | from argparse import ArgumentParser, Namespace | ||
from collections.abc import Callable | from collections.abc import Callable | ||
from dataclasses import dataclass | |||
from datetime import datetime | from datetime import datetime | ||
from enum import Enum | from enum import Enum | ||
92行目: | 94行目: | ||
logger: Final[logging.Logger] = logging.getLogger(__name__) | logger: Final[logging.Logger] = logging.getLogger(__name__) | ||
logger.setLevel(logging.INFO) # basicConfigで設定するとモジュールのDEBUGログなども出力される | logger.setLevel(logging.INFO) # basicConfigで設定するとモジュールのDEBUGログなども出力される | ||
@dataclass(init=False, eq=False, frozen=True) | |||
class UserProperties: | |||
"""ユーザ設定。 | |||
実行時に変更する可能性のある定数はここで定義する。 | |||
""" | |||
media_dir: Final[str] = 'tweet_media' | |||
"""Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。 | |||
""" | |||
@dataclass(init=False, eq=False, frozen=True) | |||
class NitterCrawler: | |||
"""``--search-unarchived`` オプション無しの時に利用する設定値。 | |||
""" | |||
limit_n_tweets: Final[int] = 100 | |||
"""Final[int]: 取得するツイート数の上限。 | |||
""" | |||
report_interval: Final[int] = 5 | |||
"""Final[int]: 記録件数を報告するインターバル。 | |||
""" | |||
filename: Final[str] = 'tweet.txt' | |||
"""Final[str]: ツイートを保存するファイルの名前。 | |||
""" | |||
nitter_instance: Final[str] = 'http://nitter.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/' | |||
"""Final[str]: Nitterのインスタンス。 | |||
生きているのは https://github.com/zedeus/nitter/wiki/Instances で確認。 | |||
Note: | |||
末尾にスラッシュ必須。 | |||
インスタンスによっては画像の取得ができない。 | |||
Tor用のインスタンスでないと動画の取得ができない。 | |||
""" | |||
@dataclass(init=False, eq=False, frozen=True) | |||
class ArchiveCrawler: | |||
"""``--search-unarchived`` オプションを付けたときに使用する設定値。 | |||
""" | |||
tweet_url_prefix_default: Final[str] = '172' | |||
"""Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。 | |||
URLの数字部分がこの数字で始まるもののみをクロールする。 | |||
Example: | |||
`tweet_url_prefix_default = 172`、`incremented_num_default = 3` の場合、 | |||
`https://twitter.com/CallinShow/status/1723*` から | |||
`https://twitter.com/CallinShow/status/1729*` までを魚拓から検索する。 | |||
See Also: | |||
:const:`~incremented_num_default` | |||
""" | |||
incremented_num_default: Final[int] = 0 | |||
"""Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。 | |||
See Also: | |||
:const:`~tweet_url_prefix_default` | |||
""" | |||
filename: Final[str] = 'url_list.txt' | |||
"""Final[str]: URLのリストをダンプするファイル名。 | |||
""" | |||
111行目: | 182行目: | ||
WAIT_TIME: Final[int] = 1 | WAIT_TIME: Final[int] = 1 | ||
"""Final[int]: | """Final[int]: HTTPリクエスト成功失敗関わらず待機時間(秒)。 | ||
Note: | Note: | ||
123行目: | 194行目: | ||
WAIT_RANGE: Final[int] = 5 | WAIT_RANGE: Final[int] = 5 | ||
"""Final[int]: | """Final[int]: ランダムな時間待機するときの待機時間の幅(秒)。 | ||
""" | """ | ||
208行目: | 279行目: | ||
headers=self.HEADERS, | headers=self.HEADERS, | ||
allow_redirects=False, | allow_redirects=False, | ||
proxies= | proxies=self._proxies) | ||
res.raise_for_status() # HTTPステータスコードが200番台以外でエラー発生 | res.raise_for_status() # HTTPステータスコードが200番台以外でエラー発生 | ||
return res | return res | ||
312行目: | 383行目: | ||
""" | """ | ||
WEB_DRIVER_WAIT_TIME: Final[int] = 15 | |||
"""Final[int]: | """Final[int]: 最初のTor接続時の待機時間(秒)。 | ||
""" | """ | ||
WAIT_TIME_FOR_RECAPTCHA: Final[int] = 10_000 | WAIT_TIME_FOR_RECAPTCHA: Final[int] = 10_000 | ||
"""Final[int]: | """Final[int]: reCAPTCHAのための待機時間(秒)。 | ||
""" | """ | ||
360行目: | 431行目: | ||
options=self._options) | options=self._options) | ||
sleep(1) | sleep(1) | ||
web_driver_wait: Final[WebDriverWait] = WebDriverWait( | |||
self._driver, | self._driver, | ||
self. | self.WEB_DRIVER_WAIT_TIME) | ||
web_driver_wait.until( | |||
ec.element_to_be_clickable((By.ID, 'connectButton')) | ec.element_to_be_clickable((By.ID, 'connectButton')) | ||
) | ) | ||
self._driver.find_element(By.ID, 'connectButton').click() | self._driver.find_element(By.ID, 'connectButton').click() | ||
# Torの接続が完了するまで待つ | # Torの接続が完了するまで待つ | ||
web_driver_wait.until(ec.url_contains('about:blank')) | |||
except BaseException: | except BaseException: | ||
self.quit() | self.quit() | ||
385行目: | 456行目: | ||
ReCaptchaRequiredError: JavaScriptがオフの状態でreCAPTCHAが要求された場合のエラー。 | ReCaptchaRequiredError: JavaScriptがオフの状態でreCAPTCHAが要求された場合のエラー。 | ||
""" | """ | ||
iframe_selector: Final[str] = \ | |||
'iframe[title="recaptcha challenge expires in two minutes"]' | |||
if len(self._driver.find_elements(By.ID, 'g-recaptcha')) > 0: | if len(self._driver.find_elements(By.ID, 'g-recaptcha')) > 0: | ||
if self._options.preferences.get('javascript.enabled'): # type: ignore | if self._options.preferences.get('javascript.enabled'): # type: ignore | ||
390行目: | 464行目: | ||
print('reCAPTCHAを解いてね(笑)、それはできるよね。') | print('reCAPTCHAを解いてね(笑)、それはできるよね。') | ||
print('botバレしたら自動でブラウザが再起動するナリよ') | print('botバレしたら自動でブラウザが再起動するナリよ') | ||
WebDriverWait(self._driver, self.WEB_DRIVER_WAIT_TIME).until( | |||
ec.presence_of_element_located( | |||
(By.CSS_SELECTOR, iframe_selector) | |||
) | |||
) | |||
self._driver.switch_to.frame( # reCAPTCHAのフレームに遷移する | self._driver.switch_to.frame( # reCAPTCHAのフレームに遷移する | ||
self._driver.find_element( | self._driver.find_element(By.CSS_SELECTOR, iframe_selector) | ||
) | ) | ||
try: | try: | ||
WebDriverWait( | WebDriverWait( | ||
self._driver, | self._driver, self.WAIT_TIME_FOR_RECAPTCHA | ||
).until( | ).until( | ||
ec.visibility_of_element_located( | ec.visibility_of_element_located( | ||
427行目: | 503行目: | ||
try: | try: | ||
self._driver.get(url) | self._driver.get(url) | ||
WebDriverWait(self._driver, self.WEB_DRIVER_WAIT_TIME).until_not( | |||
ec.title_is('Redirecting')) # Nitterのリダイレクトを検知する | |||
self._check_recaptcha(url) | self._check_recaptcha(url) | ||
except WebDriverException as e: | except WebDriverException as e: | ||
525行目: | 603行目: | ||
""" | """ | ||
logger.debug('Requesting ' + unquote(url)) | logger.debug('Requesting ' + unquote(url)) | ||
for i in range( | assert url is not None | ||
for i in range(self.LIMIT_N_REQUESTS): | |||
try: | try: | ||
res: Final[T] = request_callable(url) | res: Final[T] = request_callable(url) | ||
531行目: | 611行目: | ||
logger.warning( | logger.warning( | ||
url + 'への通信失敗ナリ ' | url + 'への通信失敗ナリ ' | ||
f'{i + 1}/{self.LIMIT_N_REQUESTS}回') | |||
if i < self.LIMIT_N_REQUESTS: | if i < self.LIMIT_N_REQUESTS: | ||
sleep(self.WAIT_TIME_FOR_ERROR) # 失敗時は長めに待つ | sleep(self.WAIT_TIME_FOR_ERROR) # 失敗時は長めに待つ | ||
605行目: | 685行目: | ||
""" | """ | ||
FILENAME: Final[str] = | FILENAME: Final[str] = UserProperties.NitterCrawler.filename | ||
"""Final[str]: ツイートを保存するファイルの名前。 | """Final[str]: ツイートを保存するファイルの名前。 | ||
""" | """ | ||
815行目: | 895行目: | ||
""" | """ | ||
NITTER_INSTANCE: Final[str] = | NITTER_INSTANCE: Final[str] = UserProperties.NitterCrawler.nitter_instance | ||
"""Final[str]: Nitterのインスタンス。 | """Final[str]: Nitterのインスタンス。 | ||
856行目: | 936行目: | ||
""" | """ | ||
INVIDIOUS_INSTANCES_URL: Final[str] = 'https://api.invidious.io/instances.json' | INVIDIOUS_INSTANCES_URL: Final[str] = \ | ||
'https://api.invidious.io/instances.json' | |||
"""Final[str]: Invidiousのインスタンスのリストを取得するAPIのURL。 | """Final[str]: Invidiousのインスタンスのリストを取得するAPIのURL。 | ||
""" | """ | ||
874行目: | 955行目: | ||
""" | """ | ||
LIMIT_N_TWEETS: Final[int] = | LIMIT_N_TWEETS: Final[int] = UserProperties.NitterCrawler.limit_n_tweets | ||
"""Final[int]: 取得するツイート数の上限。 | """Final[int]: 取得するツイート数の上限。 | ||
""" | """ | ||
REPORT_INTERVAL: Final[int] = | REPORT_INTERVAL: Final[int] = UserProperties.NitterCrawler.report_interval | ||
"""Final[int]: 記録件数を報告するインターバル。 | """Final[int]: 記録件数を報告するインターバル。 | ||
""" | """ | ||
904行目: | 985行目: | ||
""" | """ | ||
MEDIA_DIR: Final[str] = | MEDIA_DIR: Final[str] = UserProperties.media_dir | ||
"""Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。 | """Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。 | ||
""" | """ | ||
1,005行目: | 1,086行目: | ||
bool: ffmpegがインストールされているか。 | bool: ffmpegがインストールされているか。 | ||
""" | """ | ||
return | return shutil.which('ffmpeg') is not None | ||
def _check_nitter_instance( | def _check_nitter_instance( | ||
1,098行目: | 1,175行目: | ||
+ self.CALLINSHOW | + self.CALLINSHOW | ||
+ 'になりますを') | + 'になりますを') | ||
print('> ', end='') | |||
account_str: Final[str] = input() | account_str: Final[str] = input() | ||
# 空欄で降臨ショー | # 空欄で降臨ショー | ||
1,121行目: | 1,199行目: | ||
print('検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。') | print('検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。') | ||
print('例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行') | print('例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行') | ||
print('> ', end='') | |||
query_input: str = input() | query_input: str = input() | ||
# 空欄が押されるまでユーザー入力受付 | # 空欄が押されるまでユーザー入力受付 | ||
while query_input != '': | while query_input != '': | ||
self._query_strs.append(query_input) | self._query_strs.append(query_input) | ||
print('> ', end='') | |||
query_input = input() | query_input = input() | ||
print('クエリのピースが埋まっていく。') | print('クエリのピースが埋まっていく。') | ||
1,148行目: | 1,228行目: | ||
'ここにツイートの本文を入れる!すると・・・・なんとそれを含むツイートで記録を中断する!(これは本当の仕様です。)' | 'ここにツイートの本文を入れる!すると・・・・なんとそれを含むツイートで記録を中断する!(これは本当の仕様です。)' | ||
f'ちなみに、空欄だと{self.LIMIT_N_TWEETS}件まで終了しない。') | f'ちなみに、空欄だと{self.LIMIT_N_TWEETS}件まで終了しない。') | ||
print('> ', end='') | |||
end_str: Final[str] = input() | end_str: Final[str] = input() | ||
return end_str | return end_str | ||
1,287行目: | 1,368行目: | ||
+ ' をアップロードしなければない。') | + ' をアップロードしなければない。') | ||
except AttributeError: | except AttributeError: | ||
logger. | logger.error(f'{tweet_url}の画像が取得できませんでしたを 当職無能') | ||
media_list.append('[[ファイル:(画像の取得ができませんでした)|240px]]') | media_list.append('[[ファイル:(画像の取得ができませんでした)|240px]]') | ||
1,747行目: | 1,828行目: | ||
""" | """ | ||
TWEET_URL_PREFIX_DEFAULT: Final[str] = | TWEET_URL_PREFIX_DEFAULT: Final[str] = \ | ||
UserProperties.ArchiveCrawler.tweet_url_prefix_default | |||
"""Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。 | """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。 | ||
:func:`~_next_url` の `tweet_url_prefix` のデフォルト値。 | :func:`~_next_url` の `tweet_url_prefix` のデフォルト値。 | ||
""" | """ | ||
INCREMENTED_NUM_DEFAULT: Final[int] = | INCREMENTED_NUM_DEFAULT: Final[int] = \ | ||
UserProperties.ArchiveCrawler.incremented_num_default | |||
"""Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。 | """Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。 | ||
1,765行目: | 1,847行目: | ||
""" | """ | ||
FILENAME: Final[str] = | FILENAME: Final[str] = UserProperties.ArchiveCrawler.filename | ||
"""Final[str]: URLのリストをダンプするファイル名。 | """Final[str]: URLのリストをダンプするファイル名。 | ||
""" | """ |