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

→‎コード: v4.1.7 ユーザがよく変更する定数をUserPropertiesに移動。ページの読み込みが完了するまで待つよう幾つかの箇所を修正
>Fet-Fe
(→‎コード: v4.1.6)
>Fet-Fe
(→‎コード: v4.1.7 ユーザがよく変更する定数をUserPropertiesに移動。ページの読み込みが完了するまで待つよう幾つかの箇所を修正)
11行目: 11行目:
"""Twitter自動収集スクリプト
"""Twitter自動収集スクリプト


ver4.1.6 2023/11/6恒心
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の場合はbrewでtorコマンドを導入し、実行
         * 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]: HTTPリクエスト成功失敗関わらず待機時間。
     """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=getattr(self, '_proxies', None))
             proxies=self._proxies)
         res.raise_for_status()  # HTTPステータスコードが200番台以外でエラー発生
         res.raise_for_status()  # HTTPステータスコードが200番台以外でエラー発生
         return res
         return res
312行目: 383行目:
     """
     """


     WAIT_TIME_FOR_INIT: 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のための待機時間(秒)。
     """
     """


360行目: 431行目:
                 options=self._options)
                 options=self._options)
             sleep(1)
             sleep(1)
             wait_init: Final[WebDriverWait] = WebDriverWait(
             web_driver_wait: Final[WebDriverWait] = WebDriverWait(
                 self._driver,
                 self._driver,
                 self.WAIT_TIME_FOR_INIT)
                 self.WEB_DRIVER_WAIT_TIME)
             wait_init.until(
             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の接続が完了するまで待つ
             wait_init.until(ec.url_contains('about:blank'))
             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)
                        By.CSS_SELECTOR,
                        'iframe[title="recaptcha challenge expires in two minutes"]'
                    )
                 )
                 )
                 try:
                 try:
                     WebDriverWait(
                     WebDriverWait(
                         self._driver,
                         self._driver, self.WAIT_TIME_FOR_RECAPTCHA
                        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(1, self.LIMIT_N_REQUESTS + 1):
        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}/{self.LIMIT_N_REQUESTS}回')
                     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] = 'tweet.txt'
     FILENAME: Final[str] = UserProperties.NitterCrawler.filename
     """Final[str]: ツイートを保存するファイルの名前。
     """Final[str]: ツイートを保存するファイルの名前。
     """
     """
815行目: 895行目:
     """
     """


     NITTER_INSTANCE: Final[str] = 'http://nitter.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/'
     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] = 100
     LIMIT_N_TWEETS: Final[int] = UserProperties.NitterCrawler.limit_n_tweets
     """Final[int]: 取得するツイート数の上限。
     """Final[int]: 取得するツイート数の上限。
     """
     """


     REPORT_INTERVAL: Final[int] = 5
     REPORT_INTERVAL: Final[int] = UserProperties.NitterCrawler.report_interval
     """Final[int]: 記録件数を報告するインターバル。
     """Final[int]: 記録件数を報告するインターバル。
     """
     """
904行目: 985行目:
     """
     """


     MEDIA_DIR: Final[str] = 'tweet_media'
     MEDIA_DIR: Final[str] = UserProperties.media_dir
     """Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。
     """Final[str]: 画像などのツイートメディアをダウンロードするディレクトリ。
     """
     """
1,005行目: 1,086行目:
             bool: ffmpegがインストールされているか。
             bool: ffmpegがインストールされているか。
         """
         """
         return subprocess.run(
         return shutil.which('ffmpeg') is not None
            ['which', 'ffmpeg'],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL
        ).returncode == 0


     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.exception(f'{tweet_url}の画像が取得できませんでしたを 当職無能')
                     logger.error(f'{tweet_url}の画像が取得できませんでしたを 当職無能')
                     media_list.append('[[ファイル:(画像の取得ができませんでした)|240px]]')
                     media_list.append('[[ファイル:(画像の取得ができませんでした)|240px]]')


1,747行目: 1,828行目:
     """
     """


     TWEET_URL_PREFIX_DEFAULT: Final[str] = '17207'
     TWEET_URL_PREFIX_DEFAULT: Final[str] = \
        UserProperties.ArchiveCrawler.tweet_url_prefix_default
     """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。
     """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。


    ツイッターURLの数字部分がこの数字で始まるもののみをクロールする。
     :func:`~_next_url` の `tweet_url_prefix` のデフォルト値。
     :func:`~_next_url` の `tweet_url_prefix` のデフォルト値。
     """
     """


     INCREMENTED_NUM_DEFAULT: Final[int] = 4
     INCREMENTED_NUM_DEFAULT: Final[int] = \
        UserProperties.ArchiveCrawler.incremented_num_default
     """Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。
     """Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。


1,765行目: 1,847行目:
     """
     """


     FILENAME: Final[str] = 'url_list.txt'
     FILENAME: Final[str] = UserProperties.ArchiveCrawler.filename
     """Final[str]: URLのリストをダンプするファイル名。
     """Final[str]: URLのリストをダンプするファイル名。
     """
     """
匿名利用者