→コード: v4.0.0 最低限動くだけのものをとりとり
>Fet-Fe (→コード: v3.0.3 機能に変化なし。機能の説明コメントをGoogleスタイルのdocstringに変更) |
>Fet-Fe (→コード: v4.0.0 最低限動くだけのものをとりとり) |
||
7行目: | 7行目: | ||
"""Twitter自動収集スクリプト | """Twitter自動収集スクリプト | ||
ver4.0.0 2023/9/23恒心 | |||
当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | 当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | ||
33行目: | 33行目: | ||
* MacOSの場合はbrewでtorコマンドを導入し、実行 | * MacOSの場合はbrewでtorコマンドを導入し、実行 | ||
* PySocks, | * PySocks, bs4, seleniumはインストールしないと標準で入ってません | ||
* requestsも環境によっては入っていないかもしれない | * requestsも環境によっては入っていないかもしれない | ||
* $ pip install bs4 requests PySocks | * $ pip install bs4 requests PySocks selenium | ||
* pipも入っていなければ ``$ sudo apt install pip`` | * pipも入っていなければ ``$ sudo apt install pip`` | ||
49行目: | 49行目: | ||
import re | import re | ||
import json | import json | ||
import random | |||
from datetime import datetime | from datetime import datetime | ||
from zoneinfo import ZoneInfo | from zoneinfo import ZoneInfo | ||
from time import sleep | from time import sleep | ||
from typing import Final, NoReturn, | from types import MappingProxyType | ||
from typing import Final, NoReturn, Any, Self | |||
from collections.abc import Callable | |||
from urllib.parse import quote, unquote, urljoin | from urllib.parse import quote, unquote, urljoin | ||
import warnings | import warnings | ||
import subprocess | import subprocess | ||
import platform | |||
import requests | import requests | ||
import bs4 | import bs4 | ||
from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||
from selenium import webdriver | |||
from selenium.common.exceptions import NoSuchElementException, WebDriverException | |||
from selenium.webdriver.firefox.options import Options | |||
from selenium.webdriver.support.ui import WebDriverWait | |||
from selenium.webdriver.support import expected_conditions as EC | |||
from selenium.webdriver.common.by import By | |||
##おそらくはツイートの内容によってMarkupResemblesLocatorWarningが吐き出されることがあるので無効化 | ##おそらくはツイートの内容によってMarkupResemblesLocatorWarningが吐き出されることがあるので無効化 | ||
warnings.simplefilter('ignore') | warnings.simplefilter('ignore') | ||
Response: | |||
""" | class AccessError(Exception): | ||
""" | """RequestとSeleniumで共通のアクセスエラー。 | ||
""" | |||
pass | |||
class AbstractAccessor: | |||
"""HTTPリクエストでWebサイトに接続するための基底クラス。 | |||
""" | |||
WAIT_TIME: Final[int] = 1 | |||
"""Final[int]: HTTPリクエスト成功失敗関わらず待機時間。 | |||
1秒待つだけで行儀がいいクローラーだそうなので既定では1秒。 | |||
しかし日本のポリホーモは1秒待っていても捕まえてくるので注意。 | |||
https://ja.wikipedia.org/wiki/?curid=2187212 | |||
""" | |||
WAIT_RANGE: Final[int] = 5 | |||
"""Final[int]: ランダムな時間待機するときの待機時間の幅。 | |||
""" | |||
REQUEST_TIMEOUT: Final[int] = 30 | |||
"""Final[int]: HTTPリクエストのタイムアウト秒数。 | |||
""" | |||
def _random_sleep(self) -> None: | |||
"""ランダムな秒数スリープする。 | |||
自動操縦だとWebサイトに見破られないため。 | |||
""" | |||
random.randrange(self.WAIT_TIME, self.WAIT_TIME + self.WAIT_RANGE) | |||
class RequestsAccessor(AbstractAccessor): | |||
"""requestsモジュールでWebサイトに接続するためのクラス。 | |||
""" | |||
HEADERS: Final[dict[str, str]] = { | |||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0' | |||
} | |||
"""Final[dict[str, str]]: HTTPリクエスト時のヘッダ。 | |||
""" | |||
PROXIES_WITH_COMMAND: Final[dict[str, str]] = { | |||
'http': 'socks5h://127.0.0.1:9050', | |||
'https': 'socks5h://127.0.0.1:9050' | |||
} | |||
"""Final[dict[str, str]]: torコマンドを起動しているときのHTTPプロキシの設定。 | |||
""" | |||
PROXIES_WITH_BROWSER: Final[dict[str, str]] = { | |||
'http': 'socks5h://127.0.0.1:9150', | |||
'https': 'socks5h://127.0.0.1:9150' | |||
} | |||
"""Final[dict[str, str]]: Tor Browserを起動しているときのHTTPプロキシの設定。 | |||
""" | |||
TOR_CHECK_URL: Final[str] = 'https://check.torproject.org/api/ip' | |||
"""Final[str]: Tor経由で通信しているかチェックするサイトのURL。 | |||
""" | |||
def __init__(self): | |||
"""コンストラクタ。 | |||
""" | |||
self._proxies: dict[str, str] | None = None | |||
self._proxies = self._get_tor_proxies() ##Torに必要なプロキシをセット | |||
def _execute(self, url: Final[str], proxies: dict[str, str]) -> requests.models.Response: | |||
"""引数のURLにrequestsモジュールでHTTP接続する。 | |||
Args: | |||
url Final[str]: 接続するURL。 | |||
proxies dir[str, str]: 接続に利用するプロキシ。デフォルトでは :func:`~_get_tor_proxies` で設定した値を利用する。 | |||
Returns: | |||
requests.models.Response: レスポンスのオブジェクト。 | |||
Raises: | |||
requests.exceptions.ConnectionError: Torで接続ができないなど、接続のエラー。 | |||
AccessError: ステータスコードが200でない場合のエラー。 | |||
""" | |||
sleep(self.WAIT_TIME) ##DoS対策で待つ | |||
try: | |||
res: requests.models.Response = requests.get(url, timeout=self.REQUEST_TIMEOUT, headers=self.HEADERS, allow_redirects=False, proxies=proxies if proxies is not None else self._proxies) | |||
res.raise_for_status() ##HTTPステータスコードが200番台以外でエラー発生 | |||
except requests.exceptions.ConnectionError: | |||
raise | |||
except requests.exceptions.RequestException as e: | |||
# requestsモジュール固有の例外を共通の例外に変換 | |||
raise AccessError(str(e)) from None | |||
return res | |||
def get(self, url: Final[str], proxies: dict[str, str]=None) -> str: | |||
"""引数のURLにrequestsモジュールでHTTP接続する。 | |||
Args: | |||
url Final[str]: 接続するURL。 | |||
proxies dir[str, str]: 接続に利用するプロキシ。デフォルトでは :func:`~_get_tor_proxies` で設定した値を利用する。 | |||
Returns: | |||
str: レスポンスのHTML。 | |||
Raises: | |||
requests.exceptions.ConnectionError: Torで接続ができないなど、接続のエラー。 | |||
AccessError: ステータスコードが200でない場合のエラー。 | |||
""" | |||
try: | |||
return self._execute(url, proxies).text | |||
except (requests.exceptions.ConnectionError, AccessError): | |||
raise | |||
def get_image(self, url: Final[str]) -> bytes | None: | |||
"""引数のURLから画像のバイナリ列を取得する。 | |||
Args: | |||
url Final[str]: 接続するURL | |||
Returns: | |||
bytes | None: 画像のバイナリ。画像でなければNone。 | |||
Raises: | |||
requests.exceptions.ConnectionError: Torで接続ができないなど、接続のエラー。 | |||
AccessError: ステータスコードが200でない場合のエラー。 | |||
""" | |||
try: | |||
res: requests.models.Response = self._execute(url, self._proxies) | |||
except (requests.exceptions.ConnectionError, AccessError): | |||
raise | |||
if 'image' in res.headers['content-type']: | |||
return res.content | |||
else: | |||
return None | |||
def _get_tor_proxies(self) -> dict[str, str] | None | NoReturn: | |||
"""Torを使うのに必要なプロキシ情報を返す。 | |||
プロキシなしで接続できればNone、Tor Browserのプロキシで接続できるなら :const:`~PROXIES_WITH_BROWSER`、torコマンドのプロキシで接続できるなら :const:`~PROXIES_WITH_COMMAND`。 | |||
いずれでもアクセスできなければ異常終了する。 | |||
Returns: | |||
dict[str, str] | None | NoReturn: プロキシ情報。 | |||
Raises: | |||
RuntimeError: :const:`~TOR_CHECK_URL` にアクセスできない場合のエラー。 | |||
""" | |||
print('Torのチェック中ですを') | |||
# プロキシなしでTorにアクセスできるかどうか | |||
res: str = self.get(self.TOR_CHECK_URL, proxies=None) | |||
is_tor: bool = json.loads(res)['IsTor'] | |||
if is_tor: | |||
print('Tor connection OK') | |||
return None | |||
# Tor BrowserのプロキシでTorにアクセスできるかどうか | |||
res = self.get(self.TOR_CHECK_URL, proxies=self.PROXIES_WITH_BROWSER) | |||
is_tor = json.loads(res)['IsTor'] | |||
if is_tor: | |||
print('Tor connection OK') | |||
return self.PROXIES_WITH_BROWSER | |||
# torコマンドのプロキシでTorにアクセスできるかどうか | |||
try: | |||
res = self.get(self.TOR_CHECK_URL, proxies=self.PROXIES_WITH_COMMAND) | |||
is_tor = json.loads(res)['IsTor'] | |||
if is_tor: | |||
print('Tor proxy OK') | |||
return self.PROXIES_WITH_COMMAND | |||
else: | |||
raise RuntimeError('サイトにTorのIPでアクセスできていないなりを') | |||
except requests.exceptions.ConnectionError as e: | |||
print(e, file=sys.stderr) | |||
print('通信がTorのSOCKS proxyを経由していないなりを', file=sys.stderr) | |||
exit(1) | |||
@property | |||
def proxies(self) -> dict[str, str] | None: | |||
"""オブジェクトのプロキシ設定を返す。 | |||
""" | |||
return self._proxies | |||
class SeleniumAccessor(AbstractAccessor): | |||
"""SeleniumでWebサイトに接続するためのクラス。 | |||
""" | |||
TOR_BROWSER_PATHS: MappingProxyType[str, str] = MappingProxyType({ | |||
"Windows": "", | |||
"Darwin": "/Applications/Tor Browser.app/Contents/MacOS/firefox", | |||
"Linux": "" | |||
}) | |||
"""MappingProxyType[str, str]: OSごとのTor Browserのパス。 | |||
""" | |||
WAIT_TIME_FOR_INIT: Final[int] = 15 | |||
"""Final[int]: 最初のTor接続時の待機時間。 | |||
""" | |||
def __init__(self, enable_javascript: bool): | |||
"""コンストラクタ。 | |||
Tor Browserを自動操縦するためのSeleniumドライバを初期化する。 | |||
Args: | |||
enable_javascript bool: JavaScriptを有効にするかどうか。reCAPTCHA対策に必要。 | |||
""" | |||
options: Options = Options() | |||
options.binary_location = self.TOR_BROWSER_PATHS[platform.system()] | |||
if enable_javascript: | |||
print("reCAPTCHA対策のためJavaScriptをonにしますを") | |||
options.preferences.update({ | |||
"javascript.enabled": enable_javascript, | |||
"intl.accept_languages": "en-US, en", | |||
"intl.locale.requested": "US", | |||
"font.language.group": "x-western", | |||
"dom.webdriver.enabled": False # 自動操縦と見破られないための設定 | |||
}) | |||
self.driver: webdriver.Firefox = webdriver.Firefox(options=options) | |||
sleep(1) | |||
wait_init: WebDriverWait = WebDriverWait(self.driver, self.WAIT_TIME_FOR_INIT) | |||
wait_init.until(EC.element_to_be_clickable((By.ID, "connectButton"))) | |||
self.driver.find_element(By.ID, "connectButton").click() | |||
wait_init.until(EC.url_contains("about:blank")) # Torの接続が完了するまで待つ | |||
self.wait: WebDriverWait = WebDriverWait(self.driver, self.REQUEST_TIMEOUT) | |||
def quit(self) -> None: | |||
"""Seleniumドライバを終了する。 | |||
""" | |||
if self.driver: | |||
self.driver.quit() | |||
def _check_recaptcha(self) -> None: | |||
"""reCAPTCHAが表示されているかどうか判定して、入力を待機する。 | |||
""" | |||
try: | |||
self.driver.find_element(By.CSS_SELECTOR, 'script[src^="https://www.google.com/recaptcha/api.js"]') # 要素がない時に例外を吐く | |||
print("reCAPTCHAを解いてね(笑)、それはできるよね。") | |||
print("botバレしたらNew Tor circuit for this siteを選択するナリよ") | |||
WebDriverWait(self.driver, 10000).until(EC.staleness_of(self.driver.find_element(By.CSS_SELECTOR, 'script[src^="https://www.google.com/recaptcha/api.js"]'))) | |||
sleep(self.WAIT_TIME) ##DoS対策で待つ | |||
except NoSuchElementException: | |||
# reCAPTCHAの要素がなければそのまま | |||
pass | |||
def get(self, url: Final[str]) -> str: | |||
"""引数のURLにSeleniumでHTTP接続する。 | |||
Args: | |||
url Final[str]: 接続するURL。 | |||
Returns: | |||
str: レスポンスのHTML。 | |||
""" | |||
self._random_sleep() ##DoS対策で待つ | |||
try: | |||
self.driver.get(url) | |||
self._check_recaptcha() | |||
except WebDriverException as e: | |||
# Selenium固有の例外を共通の例外に変換 | |||
raise AccessError(str(e)) from None | |||
return self.driver.page_source | |||
class AccessorHandler: | |||
"""WebサイトからHTMLを取得するためのクラス。 | |||
RequestsとSeleniumのどちらかを選択して使用することができ、その違いを隠蔽する。 | |||
""" | |||
LIMIT_N_REQUESTS: Final[int] = 5 | |||
"""Final[int]: HTTPリクエスト失敗時の再試行回数。 | |||
""" | |||
WAIT_TIME_FOR_ERROR: Final[int] = 4 | |||
"""Final[int]: HTTPリクエスト失敗時にさらに追加する待機時間。 | |||
""" | |||
def __init__(self, use_browser: bool=True, enable_javascript=True): | |||
"""コンストラクタ。 | |||
Requestのみを利用するか、Seleniumも利用するか引数で選択して初期化する。 | |||
Args: | |||
use_browser bool: TrueならSeleniumを利用する。FalseならRequestsのみでアクセスする。 | |||
enable_javascript bool: SeleniumでJavaScriptを利用する場合はTrue。 | |||
""" | |||
self.selenium_accessor: SeleniumAccessor | None = SeleniumAccessor(enable_javascript) if use_browser else None | |||
self.requests_accessor: RequestsAccessor = RequestsAccessor() | |||
def __enter__(self) -> Self: | |||
"""withブロックの開始時に実行する。 | |||
""" | |||
return self | |||
def __exit__(self, *args) -> None: | |||
"""withブロックの終了時に実行する。 | |||
""" | |||
if self.selenium_accessor is not None: | |||
self.selenium_accessor.quit() | |||
def request_once(self, url: Final[str]) -> str: | |||
"""引数のURLにHTTP接続する。 | |||
Args: | |||
url Final[str]: 接続するURL。 | |||
Returns: | |||
str: レスポンスのテキスト。 | |||
Raises: | |||
AccessError: アクセスエラー。 | |||
Note: | |||
失敗かどうかは呼出側で要判定。 | |||
""" | |||
try: | |||
if self.selenium_accessor is not None: | |||
return self.selenium_accessor.get(url) | |||
else: | |||
return self.requests_accessor.get(url) | |||
except AccessError: | |||
raise | |||
def _request_with_callable(self, url: Final[str], request_callable: Callable[[str], Any]) -> Any | None: | |||
"""request_callableの実行を :const:`~LIMIT_N_REQUESTS` で定義された回数まで試す。 | |||
成功すると結果を返す。 | |||
接続失敗が何度も起きるとNoneを返す。 | |||
Args: | |||
url Final[str]: 接続するURL | |||
request_callable Callable[[str], Any]: 1回リクエストを行うメソッド。 | |||
Returns: | |||
Any | None: レスポンス。接続失敗が何度も起きるとNoneを返す。 | |||
Note: | |||
失敗かどうかは呼出側で要判定 | |||
""" | |||
for i in range(self.LIMIT_N_REQUESTS): | |||
try: | |||
res: Any = request_callable(url) | |||
except AccessError as e: | |||
print(url + 'への通信失敗ナリ ' + f"{i}/{self.LIMIT_N_REQUESTS}回") | |||
sleep(self.WAIT_TIME_FOR_ERROR) ##失敗時は長めに待つ | |||
else: | |||
return res | |||
return None | |||
def request(self, url: Final[str]) -> str | None: | |||
"""HTTP接続を :const:`~LIMIT_N_REQUESTS` で定義された回数まで試す。 | |||
成功すると結果を返す。 | |||
接続失敗が何度も起きるとNoneを返す。 | |||
Args: | |||
url Final[str]: 接続するURL | |||
Returns: | |||
str | None: レスポンスのテキスト。接続失敗が何度も起きるとNoneを返す。 | |||
Note: | |||
失敗かどうかは呼出側で要判定 | |||
""" | |||
return self._request_with_callable(url, self.request_once) | |||
def request_with_requests_module(self, url: Final[str]) -> str | None: | |||
"""requestsモジュールでのHTTP接続を :const:`~LIMIT_N_REQUESTS` で定義された回数まで試す。 | |||
成功すると結果を返す。 | |||
接続失敗が何度も起きるとNoneを返す。 | |||
Args: | |||
url Final[str]: 接続するURL | |||
Returns: | |||
str | None: レスポンスのテキスト。接続失敗が何度も起きるとNoneを返す。 | |||
Note: | |||
失敗かどうかは呼出側で要判定 | |||
""" | |||
return self._request_with_callable(url, self.requests_accessor.get) | |||
def request_image(self, url: Final[str]) -> bytes | None: | |||
"""requestsモジュールで画像ファイルを取得します。 | |||
成功すると結果を返します。 | |||
接続失敗が何度も起きるとNoneを返します。 | |||
Args: | |||
url Final[str]: 接続するURL | |||
Returns: | |||
bytes | None: レスポンスのバイト列。接続失敗が何度も起きるとNoneを返します。 | |||
""" | |||
return self._request_with_callable(url, self.requests_accessor.get_image) | |||
@property | |||
def proxies(self) -> dict[str, str] | None: | |||
return self.requests_accessor.proxies | |||
class TwitterArchiver: | class TwitterArchiver: | ||
75行目: | 491行目: | ||
""" | """ | ||
NITTER_INSTANCE: Final[str] = 'https://nitter.net/' | |||
NITTER_INSTANCE: Final[str] = ' | |||
"""Final[str]: Nitterのインスタンス。 | """Final[str]: Nitterのインスタンス。 | ||
84行目: | 498行目: | ||
Note: | Note: | ||
末尾にスラッシュ必須。 | 末尾にスラッシュ必須。 | ||
Todo: | |||
Tor専用のインスタンスが使えるようになったら変更する。 | |||
""" | """ | ||
90行目: | 507行目: | ||
ページにアクセスして魚拓があるか調べるのにはonion版のarchive.todayを使用。 | ページにアクセスして魚拓があるか調べるのにはonion版のarchive.todayを使用。 | ||
Note: | Note: | ||
末尾にスラッシュ必須。 | 末尾にスラッシュ必須。 | ||
113行目: | 529行目: | ||
CALLINSHOW: Final[str] = 'CallinShow' | CALLINSHOW: Final[str] = 'CallinShow' | ||
"""Final[str]: 降臨ショーのユーザーネーム。 | """Final[str]: 降臨ショーのユーザーネーム。 | ||
""" | """ | ||
125行目: | 537行目: | ||
REPORT_INTERVAL: Final[int] = 5 | REPORT_INTERVAL: Final[int] = 5 | ||
"""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のドメインとユーザーネーム部分の接続部品。 | ||
""" | """ | ||
184行目: | 567行目: | ||
def __init__(self, krsw: bool=False): | def __init__(self, krsw: bool=False): | ||
"""コンストラクタ | """コンストラクタ | ||
""" | |||
self._txt_data: list[str] = [] | |||
self._limit_count: int = 0 ##記録数 | |||
def _set_queries(self, accessor: AccessorHandler, krsw: bool): | |||
"""検索条件を設定する。 | |||
:class:`~TwitterArchiver` のメンバをセットし、ツイートを収集するアカウントと | :class:`~TwitterArchiver` のメンバをセットし、ツイートを収集するアカウントと | ||
189行目: | 578行目: | ||
Args: | Args: | ||
accessor AccessorHandler: アクセスハンドラ | |||
krsw bool: Trueの場合、名前が自動で :const:`~CALLINSHOW` になり、クエリと終わりにするツイートが自動で無しになる。 | krsw bool: Trueの場合、名前が自動で :const:`~CALLINSHOW` になり、クエリと終わりにするツイートが自動で無しになる。 | ||
""" | """ | ||
##ユーザー名取得 | ##ユーザー名取得 | ||
208行目: | 587行目: | ||
self.name: Final[str] = self.CALLINSHOW | self.name: Final[str] = self.CALLINSHOW | ||
else: | else: | ||
self.name: Final[str] = self._get_name() | self.name: Final[str] = self._get_name(accessor) | ||
##検索クエリとページ取得 | ##検索クエリとページ取得 | ||
216行目: | 595行目: | ||
else: | else: | ||
self._get_query() | self._get_query() | ||
self._page: | self._page: Final[str] | None = accessor.request(urljoin(self.NITTER_INSTANCE, self.name + '/' + self.TWEETS_OR_REPLIES)) | ||
if self._page is None: | if self._page is None: | ||
self._fail() | self._fail() | ||
228行目: | 607行目: | ||
##日付取得 | ##日付取得 | ||
self._date: datetime = self._tweet_date(BeautifulSoup(self._page | self._date: datetime = self._tweet_date(BeautifulSoup(self._page, 'html.parser').find(class_='timeline-item')) | ||
self._txt_data.append('') | self._txt_data.append('') | ||
print() | print() | ||
def _check_slash(self) -> None | NoReturn: | def _check_slash(self) -> None | NoReturn: | ||
299行目: | 629行目: | ||
raise RuntimeError('TWITTER_URLの末尾には/が必須です') | raise RuntimeError('TWITTER_URLの末尾には/が必須です') | ||
def | def _check_nitter_instance(self, accessor: AccessorHandler) -> None | NoReturn: | ||
"""Nitterのインスタンスが生きているかチェックする。 | """Nitterのインスタンスが生きているかチェックする。 | ||
死んでいたらそこで終了。 | 死んでいたらそこで終了。 | ||
接続を一回しか試さない :func:`~_request_once` を使っているのは、激重インスタンスが指定されたとき試行回数増やして偶然成功してそのまま実行されるのを躱すため。 | 接続を一回しか試さない :func:`~_request_once` を使っているのは、激重インスタンスが指定されたとき試行回数増やして偶然成功してそのまま実行されるのを躱すため。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
346行目: | 643行目: | ||
print("Nitterのインスタンスチェック中ですを") | print("Nitterのインスタンスチェック中ですを") | ||
try: | try: | ||
accessor.request_once(self.NITTER_INSTANCE) | |||
except AccessError as e: ##エラー発生時は終了 | |||
except | |||
print(e, file=sys.stderr) | print(e, file=sys.stderr) | ||
print('インスタンスが死んでますを', file=sys.stderr) | print('インスタンスが死んでますを', file=sys.stderr) | ||
354行目: | 650行目: | ||
print("Nitter OK") | print("Nitter OK") | ||
def _check_archive_instance(self) -> None | NoReturn: | def _check_archive_instance(self, accessor: AccessorHandler) -> None | NoReturn: | ||
"""archive.todayのTor用インスタンスが生きているかチェックする。 | """archive.todayのTor用インスタンスが生きているかチェックする。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
362行目: | 661行目: | ||
print("archive.todayのTorインスタンスチェック中ですを") | print("archive.todayのTorインスタンスチェック中ですを") | ||
try: | try: | ||
accessor.request_once(self.ARCHIVE_TODAY) | |||
except AccessError as e: ##エラー発生時は終了 | |||
except | |||
print(e, file=sys.stderr) | print(e, file=sys.stderr) | ||
print('インスタンスが死んでますを', file=sys.stderr) | print('インスタンスが死んでますを', file=sys.stderr) | ||
370行目: | 668行目: | ||
print("archive.today OK") | print("archive.today OK") | ||
def _invidious_instances(self) -> tuple[str] | NoReturn: | def _invidious_instances(self, accessor: AccessorHandler) -> tuple[str] | NoReturn: | ||
"""Invidiousのインスタンスのタプルを取得する。 | """Invidiousのインスタンスのタプルを取得する。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
377行目: | 678行目: | ||
""" | """ | ||
print("Invidiousのインスタンスリストを取得中ですを") | print("Invidiousのインスタンスリストを取得中ですを") | ||
invidious_json: | invidious_json: Final[str] | None = accessor.request_with_requests_module('https://api.invidious.io/instances.json') | ||
if invidious_json is None: | if invidious_json is None: | ||
print("Invidiousが死んでますを") | print("Invidiousが死んでますを") | ||
exit(1) | exit(1) | ||
instance_list: list[str] = [] | instance_list: list[str] = [] | ||
for instance_info in json.loads(invidious_json | for instance_info in json.loads(invidious_json): | ||
instance_list.append(instance_info[0]) | instance_list.append(instance_info[0]) | ||
392行目: | 693行目: | ||
return tuple(instance_list) | return tuple(instance_list) | ||
def _get_name(self, accessor: AccessorHandler) -> str | NoReturn: | |||
def _get_name(self) -> str | NoReturn: | |||
"""ツイート収集するユーザー名を標準入力から取得する。 | """ツイート収集するユーザー名を標準入力から取得する。 | ||
何も入力しないと :const:`~CALLINSHOW` を指定する。 | 何も入力しないと :const:`~CALLINSHOW` を指定する。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
408行目: | 711行目: | ||
return self.CALLINSHOW | return self.CALLINSHOW | ||
else: | else: | ||
res: | res: Final[str]| None = accessor.request(urljoin(self.NITTER_INSTANCE, account_str)) ##リクエストして結果取得 | ||
if res is None : ##リクエスト失敗判定 | if res is None : ##リクエスト失敗判定 | ||
self._fail() | self._fail() | ||
soup: BeautifulSoup = BeautifulSoup(res | soup: BeautifulSoup = BeautifulSoup(res, 'html.parser') ##beautifulsoupでレスポンス解析 | ||
if soup.title == self.NITTER_ERROR_TITLE: ##タイトルがエラーでないか判定 | if soup.title == self.NITTER_ERROR_TITLE: ##タイトルがエラーでないか判定 | ||
print(account_str + "は実在の人物ではありませんでした") ##エラー時ループに戻る | print(account_str + "は実在の人物ではありませんでした") ##エラー時ループに戻る | ||
476行目: | 779行目: | ||
return end_str | return end_str | ||
def _download_media(self, media_name: Final[str]) -> bool: | def _download_media(self, media_name: Final[str], accessor: AccessorHandler) -> bool: | ||
"""ツイートの画像をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | """ツイートの画像をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | ||
Args: | Args: | ||
media_name Final[str]: 画像ファイル名。Nitter上のimgタグのsrc属性では、``/pic/media%2F`` に後続する。 | media_name Final[str]: 画像ファイル名。Nitter上のimgタグのsrc属性では、``/pic/media%2F`` に後続する。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
487行目: | 791行目: | ||
os.makedirs(self.MEDIA_DIR, exist_ok=True) | os.makedirs(self.MEDIA_DIR, exist_ok=True) | ||
url: Final[str] = urljoin('https://pbs.twimg.com/media/', media_name) | url: Final[str] = urljoin('https://pbs.twimg.com/media/', media_name) | ||
image_bytes: Final[bytes | None] = accessor.request_image(url) | |||
if | if image_bytes is not None: | ||
with open(os.path.join(self.MEDIA_DIR, media_name), "wb") as f: | with open(os.path.join(self.MEDIA_DIR, media_name), "wb") as f: | ||
f.write( | f.write(image_bytes) | ||
return True | return True | ||
else: | else: | ||
530行目: | 832行目: | ||
self._date = date | self._date = date | ||
def _get_tweet_media(self, tweet: bs4.element.Tag) -> str: | def _get_tweet_media(self, tweet: bs4.element.Tag, accessor: AccessorHandler) -> str: | ||
"""ツイートの画像や動画を取得する。 | """ツイートの画像や動画を取得する。 | ||
Args: | Args: | ||
tweet bs4.element.Tag: ツイート内容である ``.timeline-item`` タグを表すbeautifulsoup4タグ。 | tweet bs4.element.Tag: ツイート内容である ``.timeline-item`` タグを表すbeautifulsoup4タグ。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
548行目: | 851行目: | ||
media_name: str = [group for group in re.search(r'%2F([^%]*\.jpg)|%2F([^%]*\.jpeg)|%2F([^%]*\.png)|%2F([^%]*\.gif)', image_a.get('href')).groups() if group][0] | media_name: str = [group for group in re.search(r'%2F([^%]*\.jpg)|%2F([^%]*\.jpeg)|%2F([^%]*\.png)|%2F([^%]*\.gif)', image_a.get('href')).groups() if group][0] | ||
media_list.append(f"[[ファイル:{media_name}|240px]]") | media_list.append(f"[[ファイル:{media_name}|240px]]") | ||
if self._download_media(media_name): | if self._download_media(media_name, accessor): | ||
print(os.path.join(self.MEDIA_DIR, media_name) + ' をアップロードしなければない。') | print(os.path.join(self.MEDIA_DIR, media_name) + ' をアップロードしなければない。') | ||
else: | else: | ||
557行目: | 860行目: | ||
media_list.append(f"[[ファイル:(画像の取得ができませんでした)|240px]]") | media_list.append(f"[[ファイル:(画像の取得ができませんでした)|240px]]") | ||
# ツイートの動画の取得。ffmpegが入っていなければ手動で取得すること | # ツイートの動画の取得。ffmpegが入っていなければ手動で取得すること | ||
for i, | for i, video_container in enumerate(tweet_media.select('.attachment.video-container')): | ||
tweet_url: str = urljoin(self.TWITTER_URL, re.sub('#[^#]*$', '', tweet.find(class_='tweet-link').get('href'))) ##ツイートのURL作成 | tweet_url: str = urljoin(self.TWITTER_URL, re.sub('#[^#]*$', '', tweet.find(class_='tweet-link').get('href'))) ##ツイートのURL作成 | ||
video = video_container.select_one('video') | |||
if video is None: | |||
print(f"{tweet_url}の動画が取得できませんでしたを 当職無能") | |||
media_list.append(f"[[ファイル:(動画の取得ができませんでした)|240px]]") | |||
continue | |||
if subprocess.run(['which', 'ffmpeg'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: | if subprocess.run(['which', 'ffmpeg'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: | ||
print(f"ffmpegがないため{tweet_url}の動画が取得できませんでしたを") | print(f"ffmpegがないため{tweet_url}の動画が取得できませんでしたを") | ||
566行目: | 875行目: | ||
tweet_id: str = tweet_url.split('/')[-1] | tweet_id: str = tweet_url.split('/')[-1] | ||
# 動画のダウンロード | # 動画のダウンロード | ||
if | if accessor.proxies is not None: | ||
returncode: int = subprocess.run(["ffmpeg", "-y", "-http_proxy", " | returncode: int = subprocess.run(["ffmpeg", "-y", "-http_proxy", "accessor.proxies['http']", "-i", urljoin(self.NITTER_INSTANCE, media_url), "-c", "copy", f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.ts"], stdout=subprocess.DEVNULL).returncode | ||
else: | else: | ||
returncode: int = subprocess.run(["ffmpeg", "-y", "-i", urljoin(self.NITTER_INSTANCE, media_url), "-c", "copy", f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.ts"], stdout=subprocess.DEVNULL).returncode | returncode: int = subprocess.run(["ffmpeg", "-y", "-i", urljoin(self.NITTER_INSTANCE, media_url), "-c", "copy", f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.ts"], stdout=subprocess.DEVNULL).returncode | ||
584行目: | 893行目: | ||
return media_txt | return media_txt | ||
def _get_tweet_quote(self, tweet: bs4.element.Tag) -> str: | def _get_tweet_quote(self, tweet: bs4.element.Tag, accessor: AccessorHandler) -> str: | ||
"""引用リツイートの引用元へのリンクを取得する。 | """引用リツイートの引用元へのリンクを取得する。 | ||
Args: | Args: | ||
tweet bs4.element.Tag: ツイート内容である ``.timeline-item`` タグを表すbeautifulsoup4タグ。 | tweet bs4.element.Tag: ツイート内容である ``.timeline-item`` タグを表すbeautifulsoup4タグ。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
599行目: | 909行目: | ||
link = re.sub('#.*$', '', link) | link = re.sub('#.*$', '', link) | ||
link = urljoin(self.TWITTER_URL, link) | link = urljoin(self.TWITTER_URL, link) | ||
quote_txt = self._archive_url(link) | quote_txt = self._archive_url(link, accessor) | ||
tweet_quote_unavailable: Final[bs4.element.Tag | None] = tweet.select_one('.tweet-body > .quote.unavailable') # 引用リツイートを選択 | tweet_quote_unavailable: Final[bs4.element.Tag | None] = tweet.select_one('.tweet-body > .quote.unavailable') # 引用リツイートを選択 | ||
if tweet_quote_unavailable is not None: | if tweet_quote_unavailable is not None: | ||
650行目: | 960行目: | ||
return timeline_item_list | return timeline_item_list | ||
def _get_tweet(self, accessor: AccessorHandler) -> None | NoReturn: | |||
def | |||
"""ページからツイート本文を ``self._txt_data`` に収めていく。 | """ページからツイート本文を ``self._txt_data`` に収めていく。 | ||
ツイートの記録を中断するための文を発見するか、記録したツイート数が :const:`~LIMIT_N_TWEETS` 件に達したら終了する。 | ツイートの記録を中断するための文を発見するか、記録したツイート数が :const:`~LIMIT_N_TWEETS` 件に達したら終了する。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
""" | """ | ||
soup: Final[BeautifulSoup] = BeautifulSoup(self._page | soup: Final[BeautifulSoup] = BeautifulSoup(self._page, 'html.parser') ##beautifulsoupでレスポンス解析 | ||
tweets: Final[list[bs4.element.Tag]] = self._get_timeline_items(soup) ##一ツイートのブロックごとにリストで取得 | tweets: Final[list[bs4.element.Tag]] = self._get_timeline_items(soup) ##一ツイートのブロックごとにリストで取得 | ||
for tweet in tweets: ##一ツイート毎に処理 | for tweet in tweets: ##一ツイート毎に処理 | ||
678行目: | 990行目: | ||
if date.year != self._date.year or date.month != self._date.month or date.day != self._date.day: | if date.year != self._date.year or date.month != self._date.month or date.day != self._date.day: | ||
self._next_day(date) | self._next_day(date) | ||
archived_tweet_url: str = self._callinshowlink_url(tweet_url) ##ツイートURLをテンプレートCallinShowlinkに変化 | archived_tweet_url: str = self._callinshowlink_url(tweet_url, accessor) ##ツイートURLをテンプレートCallinShowlinkに変化 | ||
tweet_content: bs4.element.Tag = tweet.find(class_='tweet-content media-body') ##ツイートの中身だけ取り出す | tweet_content: bs4.element.Tag = tweet.find(class_='tweet-content media-body') ##ツイートの中身だけ取り出す | ||
self._archive_soup(tweet_content) ##ツイートの中身のリンクをテンプレートArchiveに変化 | self._archive_soup(tweet_content, accessor) ##ツイートの中身のリンクをテンプレートArchiveに変化 | ||
media_txt: str = self._get_tweet_media(tweet) ##ツイートに画像などのメディアを追加 | media_txt: str = self._get_tweet_media(tweet, accessor) ##ツイートに画像などのメディアを追加 | ||
quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加 | quote_txt: str = self._get_tweet_quote(tweet, accessor) ##引用リツイートの場合、元ツイートを追加 | ||
poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得 | poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得 | ||
self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | ||
748行目: | 1,060行目: | ||
return text | return text | ||
def _archive_soup(self, tag: bs4.element.Tag) -> None: | def _archive_soup(self, tag: bs4.element.Tag, accessor: AccessorHandler) -> None: | ||
"""ツイート内のaタグをテンプレートArchiveの文字列に変化させる。 | """ツイート内のaタグをテンプレートArchiveの文字列に変化させる。 | ||
755行目: | 1,067行目: | ||
Args: | Args: | ||
tag bs4.element.Tag: ツイート内容の本文である ``.media-body`` タグを表すbeautifulsoup4タグ。 | tag bs4.element.Tag: ツイート内容の本文である ``.media-body`` タグを表すbeautifulsoup4タグ。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
""" | """ | ||
urls_in_tweet: Final[bs4.element.ResultSet] = tag.find_all('a') | urls_in_tweet: Final[bs4.element.ResultSet] = tag.find_all('a') | ||
763行目: | 1,076行目: | ||
url_link: str = url.get('href').replace('https' + self.NITTER_INSTANCE[4:], self.TWITTER_URL) | url_link: str = url.get('href').replace('https' + self.NITTER_INSTANCE[4:], self.TWITTER_URL) | ||
url_link = re.sub('\?.*$', '', url_link) | url_link = re.sub('\?.*$', '', url_link) | ||
url.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, accessor)) ##テンプレートArchiveに変化 | ||
elif url.get('href').startswith('https://nitter.kavin.rocks/'): | elif url.get('href').startswith('https://nitter.kavin.rocks/'): | ||
#Nitter上のTwitterへのリンクを直す | #Nitter上のTwitterへのリンクを直す | ||
url_link: str = url.get('href').replace('https://nitter.kavin.rocks/', self.TWITTER_URL) | url_link: str = url.get('href').replace('https://nitter.kavin.rocks/', self.TWITTER_URL) | ||
url_link = re.sub('\?.*$', '', url_link) | url_link = re.sub('\?.*$', '', url_link) | ||
url.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, accessor)) ##テンプレートArchiveに変化 | ||
elif self._invidious_pattern.search(url.get('href')): | elif self._invidious_pattern.search(url.get('href')): | ||
#Nitter上のYouTubeへのリンクをInvidiousのものから直す | #Nitter上のYouTubeへのリンクをInvidiousのものから直す | ||
776行目: | 1,089行目: | ||
else: | else: | ||
url_link = self._invidious_pattern.sub('youtu.be', url_link) | url_link = self._invidious_pattern.sub('youtu.be', url_link) | ||
url.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, accessor)) ##テンプレートArchiveに変化 | ||
elif url.get('href').startswith('https://bibliogram.art/'): | elif url.get('href').startswith('https://bibliogram.art/'): | ||
# Nitter上のInstagramへのリンクをBibliogramのものから直す | # Nitter上のInstagramへのリンクをBibliogramのものから直す | ||
# Bibliogramは中止されたようなのでそのうちリンクが変わるかも | # Bibliogramは中止されたようなのでそのうちリンクが変わるかも | ||
url_link: str = url.get('href').replace('https://bibliogram.art/', 'https://www.instagram.com/') | url_link: str = url.get('href').replace('https://bibliogram.art/', 'https://www.instagram.com/') | ||
url.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, accessor)) ##テンプレートArchiveに変化 | ||
else: | else: | ||
url.replace_with(self._archive_url(url.get('href'))) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url.get('href'), accessor)) ##テンプレートArchiveに変化 | ||
elif url.text.startswith('@'): | elif url.text.startswith('@'): | ||
url_link: str = urljoin(self.TWITTER_URL, url.get('href')) | url_link: str = urljoin(self.TWITTER_URL, url.get('href')) | ||
url_text: str = url.text | url_text: str = url.text | ||
url.replace_with(self._archive_url(url_link, url_text)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, accessor, url_text)) ##テンプレートArchiveに変化 | ||
def _archive_url(self, url: Final[str], text: Final[str|None] = None) -> str: | def _archive_url(self, url: Final[str], accessor: AccessorHandler, text: Final[str|None] = None) -> str: | ||
"""URLをArchiveテンプレートでラップする。 | """URLをArchiveテンプレートでラップする。 | ||
796行目: | 1,109行目: | ||
Args: | Args: | ||
url Final[str]: ラップするURL。 | url Final[str]: ラップするURL。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
text Final[str|None]: ArchiveテンプレートでURLの代わりに表示する文字列。 | text Final[str|None]: ArchiveテンプレートでURLの代わりに表示する文字列。 | ||
804行目: | 1,118行目: | ||
main_url, fragment = url.split('#', maxsplit=1) | main_url, fragment = url.split('#', maxsplit=1) | ||
if text is None: | if text is None: | ||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(main_url) + '#' + fragment + '}}' ##テンプレートArchiveの文字列返す | return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(main_url, accessor) + '#' + fragment + '}}' ##テンプレートArchiveの文字列返す | ||
else: | else: | ||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(main_url) + '#' + fragment + + '|3=' + text + '}}' ##テンプレートArchiveの文字列返す | return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(main_url, accessor) + '#' + fragment + + '|3=' + text + '}}' ##テンプレートArchiveの文字列返す | ||
else: | else: | ||
if text is None: | if text is None: | ||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url) + '}}' ##テンプレートArchiveの文字列返す | return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url, accessor) + '}}' ##テンプレートArchiveの文字列返す | ||
else: | else: | ||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url) + '|3=' + text + '}}' ##テンプレートArchiveの文字列返す | return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url, accessor) + '|3=' + text + '}}' ##テンプレートArchiveの文字列返す | ||
def _callinshowlink_url(self, url: Final[str]) -> str: | def _callinshowlink_url(self, url: Final[str], accessor: AccessorHandler) -> str: | ||
"""URLをCallinShowLinkテンプレートでラップする。 | """URLをCallinShowLinkテンプレートでラップする。 | ||
Args: | Args: | ||
url Final[str]: ラップするURL。 | url Final[str]: ラップするURL。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
str: CallinShowLinkタグでラップしたURL。 | str: CallinShowLinkタグでラップしたURL。 | ||
""" | """ | ||
return '{{CallinShowLink|1=' + url + '|2=' + self._archive(url) + '}}' | return '{{CallinShowLink|1=' + url + '|2=' + self._archive(url, accessor) + '}}' | ||
def _archive(self, url: Final[str]) -> str: | def _archive(self, url: Final[str], accessor: AccessorHandler) -> str: | ||
"""URLから対応するarchive.todayのURLを返す。 | """URLから対応するarchive.todayのURLを返す。 | ||
833行目: | 1,148行目: | ||
Args: | Args: | ||
url Final[str]: 魚拓を取得するURL。 | url Final[str]: 魚拓を取得するURL。 | ||
accessor AccessorHandler: アクセスハンドラ | |||
Returns: | Returns: | ||
838行目: | 1,154行目: | ||
""" | """ | ||
archive_url: str = urljoin(self.ARCHIVE_TODAY_STANDARD, quote(unquote(url), safe='&=+?%')) ##wikiに載せるとき用URLで失敗するとこのままhttps://archive.ph/https%3A%2F%2Fxxxxxxxxの形で返される | archive_url: str = urljoin(self.ARCHIVE_TODAY_STANDARD, quote(unquote(url), safe='&=+?%')) ##wikiに載せるとき用URLで失敗するとこのままhttps://archive.ph/https%3A%2F%2Fxxxxxxxxの形で返される | ||
res: Final[ | res: Final[str | None] = accessor.request(urljoin(self.ARCHIVE_TODAY, quote(unquote(url), safe='&=+?%'))) ##アクセス用URL使って結果を取得 | ||
if res is None : ##魚拓接続失敗時処理 | if res is None : ##魚拓接続失敗時処理 | ||
print(archive_url + 'にアクセス失敗ナリ。出力されるテキストにはそのまま記載されるナリ。') | print(archive_url + 'にアクセス失敗ナリ。出力されるテキストにはそのまま記載されるナリ。') | ||
else: | else: | ||
soup: Final[BeautifulSoup] = BeautifulSoup(res | soup: Final[BeautifulSoup] = BeautifulSoup(res, 'html.parser') ##beautifulsoupでレスポンス解析 | ||
content: bs4.element.Tag = soup.find(id="CONTENT") ##archive.todayの魚拓一覧ページの中身だけ取得 | content: bs4.element.Tag = soup.find(id="CONTENT") ##archive.todayの魚拓一覧ページの中身だけ取得 | ||
if content is None or content.get_text()[:len(self.NO_ARCHIVE)] == self.NO_ARCHIVE: ##魚拓があるかないか判定 | if content is None or content.get_text()[:len(self.NO_ARCHIVE)] == self.NO_ARCHIVE: ##魚拓があるかないか判定 | ||
850行目: | 1,166行目: | ||
return archive_url | return archive_url | ||
def | def _go_to_new_page(self, accessor: AccessorHandler) -> None | NoReturn: | ||
"""Nitterで次のページに移動する。 | """Nitterで次のページに移動する。 | ||
次のページが無ければプログラムを終了する。 | 次のページが無ければプログラムを終了する。 | ||
Args: | |||
accessor AccessorHandler: アクセスハンドラ | |||
""" | """ | ||
soup: Final[BeautifulSoup] = BeautifulSoup(self._page | soup: Final[BeautifulSoup] = BeautifulSoup(self._page, 'html.parser') ##beautifulsoupでレスポンス解析 | ||
show_mores: Final[bs4.element.ResultSet] = soup.find_all(class_="show-more") | show_mores: Final[bs4.element.ResultSet] = soup.find_all(class_="show-more") | ||
new_url: str = '' # ここで定義しないと動かなくなった、FIXME? | new_url: str = '' # ここで定義しないと動かなくなった、FIXME? | ||
861行目: | 1,180行目: | ||
if show_more.text != self.NEWEST: ##前ページへのリンクではないか判定 | if show_more.text != self.NEWEST: ##前ページへのリンクではないか判定 | ||
new_url = urljoin(self.NITTER_INSTANCE, self.CALLINSHOW + '/' + self.TWEETS_OR_REPLIES + show_more.a.get('href')) ##直下のaタグのhrefの中身取ってURL頭部分と合体 | new_url = urljoin(self.NITTER_INSTANCE, self.CALLINSHOW + '/' + self.TWEETS_OR_REPLIES + show_more.a.get('href')) ##直下のaタグのhrefの中身取ってURL頭部分と合体 | ||
res: Final[ | res: Final[str | None] = accessor.request(new_url) ##接続してHTML取ってくる | ||
if res is None: | if res is None: | ||
self._fail() | self._fail() | ||
new_page_soup: Final[BeautifulSoup] = BeautifulSoup(res | new_page_soup: Final[BeautifulSoup] = BeautifulSoup(res, 'html.parser') ##beautifulsoupでレスポンス解析 | ||
if new_page_soup.find(class_="timeline-end") is None: ##ツイートの終端ではtimeline-endだけのページになるので判定 | if new_page_soup.find(class_="timeline-end") is None: ##ツイートの終端ではtimeline-endだけのページになるので判定 | ||
print( | print(new_url + 'に移動しますを') | ||
self._page = res ##まだ残りツイートがあるのでページを返して再度ツイート本文収集 | self._page = res ##まだ残りツイートがあるのでページを返して再度ツイート本文収集 | ||
else: | else: | ||
print("急に残りツイートが無くなったな終了するか") | print("急に残りツイートが無くなったな終了するか") | ||
self._make_txt() | self._make_txt() | ||
def execute(self, krsw: bool=False) -> NoReturn: | |||
"""通信が必要な部分のロジック。 | |||
Args: | |||
krsw bool: Trueの場合、名前が自動で :const:`~CALLINSHOW` になり、クエリと終わりにするツイートが自動で無しになる。 | |||
""" | |||
# Seleniumドライバーを必ず終了するため、with文を利用する。 | |||
with AccessorHandler() as accessor: | |||
# 実行前のチェック | |||
self._check_slash() ##スラッシュが抜けてないかチェック | |||
self._check_nitter_instance(accessor) ##Nitterが死んでないかチェック | |||
self._check_archive_instance(accessor) ##archive.todayが死んでないかチェック | |||
##Invidiousのインスタンスリストの正規表現パターンを取得 | |||
invidious_url_tuple: Final[tuple[str]] = self._invidious_instances(accessor) | |||
self._invidious_pattern: Final[re.Pattern] = re.compile('|'.join(invidious_url_tuple)) | |||
# 検索クエリの設定 | |||
self._set_queries(accessor, krsw) | |||
# ツイートを取得し終えるまでループ | |||
while True: | |||
self._get_tweet(accessor) | |||
self._go_to_new_page(accessor) ##新しいページ取得 | |||
878行目: | 1,221行目: | ||
exit(1) | exit(1) | ||
krsw: Final[bool] = len(sys.argv) > 1 and sys.argv[1] == 'krsw' ##コマンドライン引数があるかどうかのフラグ | krsw: Final[bool] = len(sys.argv) > 1 and sys.argv[1] == 'krsw' ##コマンドライン引数があるかどうかのフラグ | ||
twitter_archiver: TwitterArchiver = TwitterArchiver() | |||
twitter_archiver.execute(krsw) | |||
</syntaxhighlight> | </syntaxhighlight> | ||