→コード: v4.3.7.post1 コメント修正
>Fet-Fe (→コード: v4.3.7 ツイートの魚拓のhtmlの構造変更に対応) |
>Fet-Fe (→コード: v4.3.7.post1 コメント修正) |
||
11行目: | 11行目: | ||
"""Twitter自動収集スクリプト | """Twitter自動収集スクリプト | ||
ver4.3.7 2024/ | ver4.3.7.post1 2024/7/5恒心 | ||
当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です。 | 当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です。 | ||
105行目: | 105行目: | ||
""" | """ | ||
media_dir: Final[str] = 'tweet_media' | media_dir: Final[str] = 'tweet_media' | ||
"""Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。 | """Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。""" | ||
filename: Final[str] = 'tweet.txt' | filename: Final[str] = 'tweet.txt' | ||
"""Final[str]: ツイートを保存するファイルの名前。 | """Final[str]: ツイートを保存するファイルの名前。""" | ||
log_file: Final[str] = 'twitter_archiver.log' | log_file: Final[str] = 'twitter_archiver.log' | ||
"""Final[str]: ログを保存するファイルの名前。 | """Final[str]: ログを保存するファイルの名前。""" | ||
@dataclass(init=False, eq=False, frozen=True) | @dataclass(init=False, eq=False, frozen=True) | ||
class NitterCrawler: | class NitterCrawler: | ||
"""``--search-unarchived`` オプション有りの時に利用する設定値。 | """``--search-unarchived`` オプション有りの時に利用する設定値。""" | ||
limit_n_tweets: Final[int] = 100 | limit_n_tweets: Final[int] = 100 | ||
"""Final[int]: 取得するツイート数の上限。 | """Final[int]: 取得するツイート数の上限。""" | ||
report_interval: Final[int] = 5 | report_interval: Final[int] = 5 | ||
"""Final[int]: 記録件数を報告するインターバル。 | """Final[int]: 記録件数を報告するインターバル。""" | ||
nitter_instance: Final[str] = 'https://nitter.poast.org/' # noqa: E501 | nitter_instance: Final[str] = 'https://nitter.poast.org/' # noqa: E501 | ||
142行目: | 136行目: | ||
@dataclass(init=False, eq=False, frozen=True) | @dataclass(init=False, eq=False, frozen=True) | ||
class ArchiveCrawler: | class ArchiveCrawler: | ||
"""``--search-unarchived`` オプション無しの時に使用する設定値。 | """``--search-unarchived`` オプション無しの時に使用する設定値。""" | ||
url_list_filename: Final[str] = 'url_list.txt' | url_list_filename: Final[str] = 'url_list.txt' | ||
"""Final[str]: URLのリストをダンプするファイル名。 | """Final[str]: URLのリストをダンプするファイル名。""" | ||
169行目: | 161行目: | ||
class AccessError(Exception): | class AccessError(Exception): | ||
"""RequestsとSeleniumで共通のアクセスエラー。 | """RequestsとSeleniumで共通のアクセスエラー。""" | ||
pass | pass | ||
class ReCaptchaRequiredError(Exception): | class ReCaptchaRequiredError(Exception): | ||
"""JavaScriptがオフの時にreCAPTCHAを要求された場合のエラー。 | """JavaScriptがオフの時にreCAPTCHAを要求された場合のエラー。""" | ||
pass | pass | ||
class AbstractAccessor(metaclass=ABCMeta): | class AbstractAccessor(metaclass=ABCMeta): | ||
"""HTTPリクエストでWebサイトに接続するための基底クラス。 | """HTTPリクエストでWebサイトに接続するための基底クラス。""" | ||
WAIT_TIME: Final[int] = 1 | WAIT_TIME: Final[int] = 1 | ||
197行目: | 186行目: | ||
WAIT_RANGE: Final[int] = 5 | WAIT_RANGE: Final[int] = 5 | ||
"""Final[int]: ランダムな時間待機するときの待機時間の幅(秒)。 | """Final[int]: ランダムな時間待機するときの待機時間の幅(秒)。""" | ||
REQUEST_TIMEOUT: Final[int] = 30 | REQUEST_TIMEOUT: Final[int] = 30 | ||
"""Final[int]: HTTPリクエストのタイムアウト秒数。 | """Final[int]: HTTPリクエストのタイムアウト秒数。""" | ||
@abstractmethod | @abstractmethod | ||
239行目: | 226行目: | ||
class RequestsAccessor(AbstractAccessor): | class RequestsAccessor(AbstractAccessor): | ||
"""RequestsモジュールでWebサイトに接続するためのクラス。 | """RequestsモジュールでWebサイトに接続するためのクラス。""" | ||
HEADERS: Final[dict[str, str]] = { | HEADERS: Final[dict[str, str]] = { | ||
246行目: | 232行目: | ||
'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0' # noqa: E501 | 'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0' # noqa: E501 | ||
} | } | ||
"""Final[dict[str, str]]: HTTPリクエスト時のヘッダ。 | """Final[dict[str, str]]: HTTPリクエスト時のヘッダ。""" | ||
PROXIES_WITH_BROWSER: Final[dict[str, str]] = { | PROXIES_WITH_BROWSER: Final[dict[str, str]] = { | ||
253行目: | 238行目: | ||
'https': 'socks5h://127.0.0.1:9150' | 'https': 'socks5h://127.0.0.1:9150' | ||
} | } | ||
"""Final[dict[str, str]]: Tor Browserを起動しているときのHTTPプロキシの設定。 | """Final[dict[str, str]]: Tor Browserを起動しているときのHTTPプロキシの設定。""" | ||
PROXIES_WITH_COMMAND: Final[dict[str, str]] = { | PROXIES_WITH_COMMAND: Final[dict[str, str]] = { | ||
260行目: | 244行目: | ||
'https': 'socks5h://127.0.0.1:9050' | 'https': 'socks5h://127.0.0.1:9050' | ||
} | } | ||
"""Final[dict[str, str]]: torコマンドを起動しているときのHTTPプロキシの設定。 | """Final[dict[str, str]]: torコマンドを起動しているときのHTTPプロキシの設定。""" | ||
TOR_CHECK_URL: Final[str] = 'https://check.torproject.org/api/ip' | TOR_CHECK_URL: Final[str] = 'https://check.torproject.org/api/ip' | ||
"""Final[str]: Tor経由で通信しているかチェックするサイトのURL。 | """Final[str]: Tor経由で通信しているかチェックするサイトのURL。""" | ||
def __init__(self) -> None: | def __init__(self) -> None: | ||
"""コンストラクタ。 | """コンストラクタ。""" | ||
# Torに必要なプロキシをセット | # Torに必要なプロキシをセット | ||
self._cookies: dict[str, str] = {} | self._cookies: dict[str, str] = {} | ||
390行目: | 371行目: | ||
class SeleniumAccessor(AbstractAccessor): | class SeleniumAccessor(AbstractAccessor): | ||
"""SeleniumでWebサイトに接続するためのクラス。 | """SeleniumでWebサイトに接続するためのクラス。""" | ||
TOR_BROWSER_PATHS: Final[MappingProxyType[str, str]] = MappingProxyType({ | TOR_BROWSER_PATHS: Final[MappingProxyType[str, str]] = MappingProxyType({ | ||
398行目: | 378行目: | ||
'Linux': '/usr/bin/torbrowser' | 'Linux': '/usr/bin/torbrowser' | ||
}) | }) | ||
"""Final[MappingProxyType[str, str]]: OSごとのTor Browserのパス。 | """Final[MappingProxyType[str, str]]: OSごとのTor Browserのパス。""" | ||
WEB_DRIVER_WAIT_TIME: Final[int] = 15 | WEB_DRIVER_WAIT_TIME: Final[int] = 15 | ||
"""Final[int]: 最初のTor接続時の待機時間(秒)。 | """Final[int]: 最初のTor接続時の待機時間(秒)。""" | ||
WAIT_TIME_FOR_RECAPTCHA: Final[int] = 10_000 | WAIT_TIME_FOR_RECAPTCHA: Final[int] = 10_000 | ||
"""Final[int]: reCAPTCHAのための待機時間(秒)。 | """Final[int]: reCAPTCHAのための待機時間(秒)。""" | ||
def __init__(self, enable_javascript: bool) -> None: | def __init__(self, enable_javascript: bool) -> None: | ||
435行目: | 412行目: | ||
def quit(self) -> None: | def quit(self) -> None: | ||
"""Seleniumドライバを終了する。 | """Seleniumドライバを終了する。""" | ||
if hasattr(self, '_driver'): | if hasattr(self, '_driver'): | ||
logger.debug('ブラウザ終了') | logger.debug('ブラウザ終了') | ||
442行目: | 418行目: | ||
def _refresh_browser(self) -> None: | def _refresh_browser(self) -> None: | ||
"""ブラウザを起動する。 | """ブラウザを起動する。""" | ||
try: | try: | ||
logger.debug('ブラウザ起動') | logger.debug('ブラウザ起動') | ||
467行目: | 442行目: | ||
Args: | Args: | ||
url (str): アクセスしようとしているURL。 | url (str): アクセスしようとしているURL。\ | ||
reCAPTCHAが要求されると `current_url` が変わることがあるので必要。 | reCAPTCHAが要求されると `current_url` が変わることがあるので必要。 | ||
549行目: | 524行目: | ||
LIMIT_N_REQUESTS: Final[int] = 5 | LIMIT_N_REQUESTS: Final[int] = 5 | ||
"""Final[int]: HTTPリクエスト失敗時の再試行回数。 | """Final[int]: HTTPリクエスト失敗時の再試行回数。""" | ||
WAIT_TIME_FOR_ERROR: Final[int] = 4 | WAIT_TIME_FOR_ERROR: Final[int] = 4 | ||
"""Final[int]: HTTPリクエスト失敗時にさらに追加する待機時間。 | """Final[int]: HTTPリクエスト失敗時にさらに追加する待機時間。""" | ||
def __init__(self, use_browser: bool, enable_javascript: bool) -> None: | def __init__(self, use_browser: bool, enable_javascript: bool) -> None: | ||
562行目: | 535行目: | ||
Args: | Args: | ||
use_browser (bool): `True` ならSeleniumを利用する。 | use_browser (bool): `True` ならSeleniumを利用する。\ | ||
`False` ならRequestsのみでアクセスする。 | `False` ならRequestsのみでアクセスする。 | ||
enable_javascript (bool): SeleniumでJavaScriptを利用する場合は `True`。 | enable_javascript (bool): SeleniumでJavaScriptを利用する場合は `True`。 | ||
733行目: | 706行目: | ||
class TableBuilder: | class TableBuilder: | ||
"""Wikiの表を組み立てるためのクラス。 | """Wikiの表を組み立てるためのクラス。""" | ||
FILENAME: Final[str] = UserProperties.filename | FILENAME: Final[str] = UserProperties.filename | ||
"""Final[str]: ツイートを保存するファイルの名前。 | """Final[str]: ツイートを保存するファイルの名前。""" | ||
def __init__(self, date: datetime | None = None) -> None: | def __init__(self, date: datetime | None = None) -> None: | ||
774行目: | 745行目: | ||
def dump_file(self) -> None: | def dump_file(self) -> None: | ||
"""Wikiテーブルをファイル出力する。 | """Wikiテーブルをファイル出力する。""" | ||
self._next_day() | self._next_day() | ||
result_txt: Final[str] = '\n'.join(reversed(self._tables)) | result_txt: Final[str] = '\n'.join(reversed(self._tables)) | ||
929行目: | 899行目: | ||
class FfmpegStatus(Enum): | class FfmpegStatus(Enum): | ||
"""ffmpegでの動画保存ステータス。 | """ffmpegでの動画保存ステータス。""" | ||
MP4 = 1 | MP4 = 1 | ||
"""mp4の取得に成功したときのステータス。 | """mp4の取得に成功したときのステータス。""" | ||
TS = 2 | TS = 2 | ||
"""tsの取得までは成功したが、mp4への変換に失敗したときのステータス。 | """tsの取得までは成功したが、mp4への変換に失敗したときのステータス。""" | ||
FAILED = 3 | FAILED = 3 | ||
"""m3u8からtsの取得に失敗したときのステータス。 | """m3u8からtsの取得に失敗したときのステータス。""" | ||
1,000行目: | 966行目: | ||
INVIDIOUS_INSTANCES_URL: Final[str] = \ | INVIDIOUS_INSTANCES_URL: Final[str] = \ | ||
'https://api.invidious.io/instances.json' | 'https://api.invidious.io/instances.json' | ||
"""Final[str]: Invidiousのインスタンスのリストを取得するAPIのURL。 | """Final[str]: Invidiousのインスタンスのリストを取得するAPIのURL。""" | ||
INVIDIOUS_INSTANCES_TUPLE: Final[tuple[str, ...]] = ( | INVIDIOUS_INSTANCES_TUPLE: Final[tuple[str, ...]] = ( | ||
1,015行目: | 980行目: | ||
CALLINSHOW: Final[str] = 'CallinShow' | CALLINSHOW: Final[str] = 'CallinShow' | ||
"""Final[str]: 降臨ショーのユーザーネーム。 | """Final[str]: 降臨ショーのユーザーネーム。""" | ||
LIMIT_N_TWEETS: Final[int] = UserProperties.NitterCrawler.limit_n_tweets | LIMIT_N_TWEETS: Final[int] = UserProperties.NitterCrawler.limit_n_tweets | ||
"""Final[int]: 取得するツイート数の上限。 | """Final[int]: 取得するツイート数の上限。""" | ||
REPORT_INTERVAL: Final[int] = UserProperties.NitterCrawler.report_interval | REPORT_INTERVAL: Final[int] = UserProperties.NitterCrawler.report_interval | ||
"""Final[int]: 記録件数を報告するインターバル。 | """Final[int]: 記録件数を報告するインターバル。""" | ||
TWEETS_OR_REPLIES: Final[str] = 'with_replies' | TWEETS_OR_REPLIES: Final[str] = 'with_replies' | ||
"""Final[str]: NitterのURLのドメインとユーザーネーム部分の接続部品。 | """Final[str]: NitterのURLのドメインとユーザーネーム部分の接続部品。""" | ||
NITTER_ERROR_TITLE: Final[str] = 'Error|nitter' | NITTER_ERROR_TITLE: Final[str] = 'Error|nitter' | ||
1,049行目: | 1,010行目: | ||
MEDIA_DIR: Final[str] = UserProperties.media_dir | MEDIA_DIR: Final[str] = UserProperties.media_dir | ||
"""Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。 | """Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。""" | ||
def __init__(self) -> None: | def __init__(self) -> None: | ||
"""コンストラクタ。 | """コンストラクタ。""" | ||
self._check_constants() # スラッシュが抜けてないかチェック | self._check_constants() # スラッシュが抜けてないかチェック | ||
self._has_ffmpeg: Final[bool] = self._check_ffmpeg() # ffmpegがあるかチェック | self._has_ffmpeg: Final[bool] = self._check_ffmpeg() # ffmpegがあるかチェック | ||
1,072行目: | 1,031行目: | ||
Args: | Args: | ||
accessor (AccessorHandler): アクセスハンドラ | accessor (AccessorHandler): アクセスハンドラ | ||
krsw (bool): Trueの場合、名前が :const:`~CALLINSHOW` になり、 | krsw (bool): Trueの場合、名前が :const:`~CALLINSHOW` になり、\ | ||
クエリと終わりにするツイートが無しになる。 | クエリと終わりにするツイートが無しになる。 | ||
1,264行目: | 1,223行目: | ||
def _input_query(self) -> None: | def _input_query(self) -> None: | ||
"""検索クエリを標準入力から取得する。 | """検索クエリを標準入力から取得する。""" | ||
print('検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。') | print('検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。') | ||
print('例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行') | print('例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行') | ||
1,315行目: | 1,273行目: | ||
Args: | Args: | ||
media_url (str): 画像のURL。 | media_url (str): 画像のURL。 | ||
media_name (str): 画像ファイル名。Nitter上のimgタグのsrc属性では、 | media_name (str): 画像ファイル名。Nitter上のimgタグのsrc属性では、\ | ||
``/pic/media%2F`` に後続する。 | ``/pic/media%2F`` に後続する。 | ||
accessor (AccessorHandler): アクセスハンドラ。 | accessor (AccessorHandler): アクセスハンドラ。 | ||
1,683行目: | 1,641行目: | ||
取得できれば魚拓ページのURLを返す。 | 取得できれば魚拓ページのURLを返す。 | ||
魚拓がなければその旨の警告を表示し、https://archive.ph/https%3A%2F% | 魚拓がなければその旨の警告を表示し、https://archive.ph/https%3A%2F%2Fxxxxxxxx の形で返される。 | ||
アクセスに失敗すればその旨の警告を表示し、https://archive.ph/https%3A%2F% | アクセスに失敗すればその旨の警告を表示し、https://archive.ph/https%3A%2F%2Fxxxxxxxx の形で返される。 | ||
Args: | Args: | ||
1,869行目: | 1,827行目: | ||
Args: | Args: | ||
krsw (bool, optional): `True` の場合、名前が自動で :const:`~CALLINSHOW` になり、 | krsw (bool, optional): `True` の場合、名前が自動で :const:`~CALLINSHOW` になり、\ | ||
クエリと終わりにするツイートが自動で無しになる。 | クエリと終わりにするツイートが自動で無しになる。 | ||
use_browser (bool, optional): `True` ならSeleniumを利用する。 | use_browser (bool, optional): `True` ならSeleniumを利用する。\ | ||
`False` ならRequestsのみでアクセスする。 | `False` ならRequestsのみでアクセスする。 | ||
enable_javascript (bool, optional): SeleniumでJavaScriptを利用する場合は | enable_javascript (bool, optional): SeleniumでJavaScriptを利用する場合は \ | ||
`True`。 | `True`。 | ||
1,912行目: | 1,870行目: | ||
class UrlTuple(NamedTuple): | class UrlTuple(NamedTuple): | ||
"""URLとその魚拓のURLのペア。 | """URLとその魚拓のURLのペア。""" | ||
url: str | url: str | ||
"""URL。 | """URL。""" | ||
archive_url: str | archive_url: str | ||
"""魚拓のURL。 | """魚拓のURL。""" | ||
1,937行目: | 1,892行目: | ||
TEMPLATE_URL: Final[str] = 'https://krsw-wiki.org/wiki/テンプレート:降臨ショー恒心ログ' | TEMPLATE_URL: Final[str] = 'https://krsw-wiki.org/wiki/テンプレート:降臨ショー恒心ログ' | ||
"""Final[str]: テンプレート:降臨ショー恒心ログのURL。 | """Final[str]: テンプレート:降臨ショー恒心ログのURL。""" | ||
URL_LIST_FILENAME: Final[str] = \ | URL_LIST_FILENAME: Final[str] = \ | ||
UserProperties.ArchiveCrawler.url_list_filename | UserProperties.ArchiveCrawler.url_list_filename | ||
"""Final[str]: URLのリストをダンプするファイル名。 | """Final[str]: URLのリストをダンプするファイル名。""" | ||
@override | @override | ||
2,492行目: | 2,445行目: | ||
Args: | Args: | ||
krsw (bool, optional): `True` の場合、名前が自動で :const:`~CALLINSHOW` になり、 | krsw (bool, optional): `True` の場合、名前が自動で :const:`~CALLINSHOW` になり、\ | ||
クエリと終わりにするツイートが自動で無しになる。 | クエリと終わりにするツイートが自動で無しになる。 | ||
use_browser (bool, optional): `True` ならSeleniumを利用する。 | use_browser (bool, optional): `True` ならSeleniumを利用する。\ | ||
`False` ならRequestsのみでアクセスする。 | `False` ならRequestsのみでアクセスする。 | ||
enable_javascript (bool, optional): SeleniumでJavaScriptを利用する場合は | enable_javascript (bool, optional): SeleniumでJavaScriptを利用する場合は \ | ||
`True`。 | `True`。 | ||
""" | """ |