「利用者:夜泣き/スクリプト」の版間の差分

→‎コード: v4.3.7.post1 コメント修正
>Fet-Fe
(→‎コード: v4.3.7 ツイートの魚拓のhtmlの構造変更に対応)
>Fet-Fe
(→‎コード: v4.3.7.post1 コメント修正)
11行目: 11行目:
"""Twitter自動収集スクリプト
"""Twitter自動収集スクリプト


ver4.3.7 2024/6/8恒心
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%2Fxxxxxxxxの形で返される。
         魚拓がなければその旨の警告を表示し、https://archive.ph/https%3A%2F%2Fxxxxxxxx の形で返される。
         アクセスに失敗すればその旨の警告を表示し、https://archive.ph/https%3A%2F%2Fxxxxxxxxの形で返される。
         アクセスに失敗すればその旨の警告を表示し、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`。
         """
         """
匿名利用者