66
回編集
>Fet-Fe (→コード: v4.4.0 -u --krswを指定した時にも上限以内で引っかかった書き込みがあると自動停止する機能を追加) |
(→コード: v4.4.2 WikiのURLをkrsw-wiki.inに変更) |
||
11行目: | 11行目: | ||
"""Twitter自動収集スクリプト | """Twitter自動収集スクリプト | ||
ver4.4. | ver4.4.2 2024/11/9恒心 | ||
当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です。 | 当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です。 | ||
19行目: | 19行目: | ||
Examples: | Examples: | ||
定数類は状況に応じて変えてください。:class:`~UserProperties` で変更できます。 | 定数類は状況に応じて変えてください。:class:`~UserProperties` で変更できます。 | ||
:: | :: | ||
24行目: | 25行目: | ||
オプションに ``--krsw`` とつけると自動モードになります。 | オプションに ``--krsw`` とつけると自動モードになります。 | ||
:: | :: | ||
33行目: | 35行目: | ||
``--no-browser`` オプションでTor Browserを使用しないモードに、 | ``--no-browser`` オプションでTor Browserを使用しないモードに、 | ||
``--disable-script`` オプションでTor BrowserのJavaScriptを使用しないモードになります。 | ``--disable-script`` オプションでTor BrowserのJavaScriptを使用しないモードになります。 | ||
``--search-unarchived`` | ``--search-unarchived`` オプションでは、従来の通りNitterからツイートを収集するモードになります (廃止予定)。 | ||
Note: | Note: | ||
49行目: | 51行目: | ||
* requests, typing_extensionsも環境によっては入っていないかもしれない | * requests, typing_extensionsも環境によっては入っていないかもしれない | ||
.. code-block:: bash | |||
$ python3 -m pip install bs4 requests PySocks selenium typing_extensions | |||
* pipも入っていなければ ``$ sudo apt install pip`` | * pipも入っていなければ ``$ sudo apt install pip`` | ||
* `ffmpeg <https://ffmpeg.org>`_ が入っていると動画も自動取得しますが、無くても動きます | * `ffmpeg <https://ffmpeg.org>`_ が入っていると動画も自動取得しますが、無くても動きます | ||
* バグ報告は `利用者・トーク:夜泣き#利用者:夜泣き/スクリプトについて \ | * バグ報告は `利用者・トーク:夜泣き#利用者:夜泣き/スクリプトについて \ | ||
<https://krsw-wiki. | <https://krsw-wiki.in/wiki/利用者・トーク:夜泣き#利用者:夜泣き/スクリプトについて>`_ | ||
""" | """ | ||
import json | import json | ||
import logging | import logging | ||
76行目: | 79行目: | ||
from datetime import datetime | from datetime import datetime | ||
from enum import Enum | from enum import Enum | ||
from pathlib import Path | |||
from time import sleep | from time import sleep | ||
from traceback import TracebackException | from traceback import TracebackException | ||
354行目: | 358行目: | ||
else: | else: | ||
raise AccessError('サイトにTorのIPでアクセスできていないなりを') | raise AccessError('サイトにTorのIPでアクセスできていないなりを') | ||
except requests.exceptions.ConnectionError | except requests.exceptions.ConnectionError: | ||
logger.critical('通信がTorのSOCKS proxyを経由していないなりを', exc_info=True) | |||
logger.critical('通信がTorのSOCKS proxyを経由していないなりを') | |||
sys.exit(1) | sys.exit(1) | ||
566行目: | 569行目: | ||
def _request_with_callable[T]( | def _request_with_callable[T]( | ||
self, | |||
url: str, | |||
request_callable: Callable[[str], T] | |||
) -> T | None: | |||
"""`request_callable` の実行を :const:`~LIMIT_N_REQUESTS` で定義された回数まで試す。 | """`request_callable` の実行を :const:`~LIMIT_N_REQUESTS` で定義された回数まで試す。 | ||
587行目: | 591行目: | ||
for i in range(self.LIMIT_N_REQUESTS): | for i in range(self.LIMIT_N_REQUESTS): | ||
try: | try: | ||
res: | res: T = request_callable(url) | ||
except AccessError: | except AccessError: | ||
logger.warning( | logger.warning( | ||
url + 'への通信失敗ナリ ' | url + 'への通信失敗ナリ ' | ||
f'{i + 1}/{self.LIMIT_N_REQUESTS}回') | f'{i + 1}/{self.LIMIT_N_REQUESTS}回') | ||
logger.debug('エラーログ', exc_info=True) | |||
if i < self.LIMIT_N_REQUESTS: | if i < self.LIMIT_N_REQUESTS: | ||
sleep(self.WAIT_TIME_FOR_ERROR) # 失敗時は長めに待つ | sleep(self.WAIT_TIME_FOR_ERROR) # 失敗時は長めに待つ | ||
677行目: | 682行目: | ||
Args: | Args: | ||
date (datetime | None, optional): 記録するツイートの最新日付。デフォルトは今日の日付。 | date (datetime | None, optional): 記録するツイートの最新日付。デフォルトは今日の日付。 | ||
Attributes: | |||
_tables (list[str]): ツイートのリストを日毎にまとめたもの。\ | |||
一番最後の要素が `_date` に対応し、最初の要素が最近の日付となる。 | |||
_count (int): 表に追加したツイートの件数。 | |||
_date (datetime): 現在収集中のツイートの日付。 | |||
""" | """ | ||
FILENAME: Final[str] = UserProperties.filename | FILENAME: Final[str] = UserProperties.filename | ||
708行目: | 719行目: | ||
"""Wikiテーブルをファイル出力する。""" | """Wikiテーブルをファイル出力する。""" | ||
self._next_day() | self._next_day() | ||
Path(self.FILENAME).write_text( | |||
'\n'.join(reversed(self._tables)), 'utf-8' | |||
) | |||
logger.info('テキストファイル手に入ったやで〜') | logger.info('テキストファイル手に入ったやで〜') | ||
826行目: | 836行目: | ||
@staticmethod | @staticmethod | ||
def archive_url( | def archive_url( | ||
url: str, | |||
archived_url: str, | |||
text: str | None = None | |||
) -> str: | |||
"""URLをArchiveテンプレートでラップする。 | """URLをArchiveテンプレートでラップする。 | ||
984行目: | 995行目: | ||
def _set_queries( | def _set_queries( | ||
self, accessor: AccessorHandler, krsw: str | None | |||
) -> bool: | |||
"""検索条件を設定する。 | """検索条件を設定する。 | ||
1,102行目: | 1,114行目: | ||
try: | try: | ||
accessor.request_once(self.NITTER_INSTANCE) | accessor.request_once(self.NITTER_INSTANCE) | ||
except AccessError | except AccessError: | ||
logger.critical('インスタンスが死んでますを', exc_info=True) | |||
logger.critical('インスタンスが死んでますを') | |||
sys.exit(1) | sys.exit(1) | ||
logger.info('Nitter OK') | logger.info('Nitter OK') | ||
1,120行目: | 1,131行目: | ||
try: | try: | ||
accessor.request_once(self.ARCHIVE_TODAY) | accessor.request_once(self.ARCHIVE_TODAY) | ||
except AccessError | except AccessError: # エラー発生時は終了 | ||
logger.critical('インスタンスが死んでますを', exc_info=True) | |||
logger.critical('インスタンスが死んでますを') | |||
sys.exit(1) | sys.exit(1) | ||
logger.info('archive.today OK') | logger.info('archive.today OK') | ||
def _invidious_instances( | def _invidious_instances( | ||
self, accessor: AccessorHandler | |||
) -> tuple[str, ...]: | |||
"""Invidiousのインスタンスのタプルを取得する。 | """Invidiousのインスタンスのタプルを取得する。 | ||
1,171行目: | 1,182行目: | ||
+ 'になりますを') | + 'になりますを') | ||
print('> ', end='') | print('> ', end='') | ||
account_str: | account_str: str = input() | ||
# 空欄で降臨ショー | # 空欄で降臨ショー | ||
if account_str == '': | if account_str == '': | ||
return self.CALLINSHOW | return self.CALLINSHOW | ||
else: | else: | ||
res: | res: str | None = accessor.request( | ||
urljoin(self.NITTER_INSTANCE, account_str)) | urljoin(self.NITTER_INSTANCE, account_str)) | ||
if res is None: # リクエスト失敗判定 | if res is None: # リクエスト失敗判定 | ||
self._on_fail(accessor) | self._on_fail(accessor) | ||
return None | return None | ||
soup: | soup: BeautifulSoup = BeautifulSoup(res, 'html.parser') | ||
if soup.title == self.NITTER_ERROR_TITLE: | if soup.title == self.NITTER_ERROR_TITLE: | ||
print(account_str + 'は実在の人物ではありませんでした') | print(account_str + 'は実在の人物ではありませんでした') | ||
1,232行目: | 1,243行目: | ||
def _download_media( | def _download_media( | ||
self, | |||
media_url: str, | |||
media_name: str, | |||
accessor: AccessorHandler | |||
) -> bool: | |||
"""ツイートの画像をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | """ツイートの画像をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | ||
1,250行目: | 1,262行目: | ||
image_bytes: Final[bytes | None] = accessor.request_image(media_url) | image_bytes: Final[bytes | None] = accessor.request_image(media_url) | ||
if image_bytes is not None: | if image_bytes is not None: | ||
Path(self.MEDIA_DIR, media_name).write_bytes(image_bytes) | |||
return True | return True | ||
else: | else: | ||
1,257行目: | 1,268行目: | ||
def _download_m3u8( | def _download_m3u8( | ||
self, | |||
media_url: str, | |||
ts_filename: str | None, | |||
mp4_filename: str, | |||
proxies: dict[str, str] | None | |||
) -> FfmpegStatus: | |||
"""ffmpegで動画をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | """ffmpegで動画をダウンロードし、:const:`~MEDIA_DIR` に保存する。 | ||
1,340行目: | 1,352行目: | ||
def _fetch_tweet_media( | def _fetch_tweet_media( | ||
self, | |||
tweet: Tag, | |||
tweet_url: str, | |||
accessor: AccessorHandler | |||
) -> str: | |||
"""ツイートの画像や動画を取得する。 | """ツイートの画像や動画を取得する。 | ||
1,363行目: | 1,376行目: | ||
for image_a in tweet_media.select('.attachment.image a'): | for image_a in tweet_media.select('.attachment.image a'): | ||
try: | try: | ||
href: | href: str | list[str] | None = image_a.get('href') | ||
assert isinstance(href, str) | assert isinstance(href, str) | ||
img_matched: | img_matched: re.Match[str] | None = ( | ||
self._img_ext_pattern.search(href)) | self._img_ext_pattern.search(href)) | ||
assert img_matched is not None | assert img_matched is not None | ||
media_name: | media_name: str = img_matched.group(1) | ||
media_list.append(f'[[ファイル:{media_name}|240px]]') | media_list.append(f'[[ファイル:{media_name}|240px]]') | ||
if self._download_media( | if self._download_media( | ||
1,394行目: | 1,407行目: | ||
# videoタグがない場合は取得できない | # videoタグがない場合は取得できない | ||
video: | video: Tag | None = video_container.select_one('video') | ||
if video is None: | if video is None: | ||
logger.error(f'{tweet_url}の動画が取得できませんでしたを 当職無能') | logger.error(f'{tweet_url}の動画が取得できませんでしたを 当職無能') | ||
1,401行目: | 1,414行目: | ||
# videoタグのdata-url属性またはvideoタグ直下のsourceタグからURLが取得できる | # videoタグのdata-url属性またはvideoタグ直下のsourceタグからURLが取得できる | ||
data_url: | data_url: str | list[str] | None = video.get('data-url') | ||
source_tag: | source_tag: Tag | None = video.select_one('source') | ||
src_url: | src_url: str | list[str] | None = \ | ||
source_tag.get('src') if source_tag is not None else None | source_tag.get('src') if source_tag is not None else None | ||
video_url: | video_url: str | list[str] | None = data_url or src_url | ||
assert isinstance(video_url, str) | assert isinstance(video_url, str) | ||
tweet_id: | tweet_id: str = tweet_url.split('/')[-1] | ||
if data_url is not None: | if data_url is not None: | ||
# data-url属性からURLを取得した場合 | # data-url属性からURLを取得した場合 | ||
video_matched: | video_matched: re.Match[str] | None = re.search( | ||
r'[^/]+$', video_url) | r'[^/]+$', video_url) | ||
assert video_matched is not None | assert video_matched is not None | ||
media_path: | media_path: str = unquote(video_matched.group()) | ||
media_url: str = urljoin(self.NITTER_INSTANCE, media_path) | media_url: str = urljoin(self.NITTER_INSTANCE, media_path) | ||
ts_filename: str | None = ( | ts_filename: str | None = ( | ||
1,422行目: | 1,435行目: | ||
media_url: str = video_url | media_url: str = video_url | ||
ts_filename: str | None = None | ts_filename: str | None = None | ||
mp4_filename: | mp4_filename: str = ( | ||
f'{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.mp4' | f'{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.mp4' | ||
) | ) | ||
1,491行目: | 1,504行目: | ||
assert poll_info is not None | assert poll_info is not None | ||
for poll_meter in poll_meters: | for poll_meter in poll_meters: | ||
poll_choice_value: | poll_choice_value: Tag | None = poll_meter.select_one( | ||
'.poll-choice-value') | '.poll-choice-value') | ||
assert poll_choice_value is not None | assert poll_choice_value is not None | ||
ratio: | ratio: str = poll_choice_value.text | ||
poll_choice_option: | poll_choice_option: Tag | None = poll_meter.select_one( | ||
'.poll-choice-option') | '.poll-choice-option') | ||
assert poll_choice_option is not None | assert poll_choice_option is not None | ||
1,554行目: | 1,567行目: | ||
urls_in_tweet: Final[ResultSet[Tag]] = tag.find_all('a') | urls_in_tweet: Final[ResultSet[Tag]] = tag.find_all('a') | ||
for url in urls_in_tweet: | for url in urls_in_tweet: | ||
href: | href: str | list[str] | None = url.get('href') | ||
assert isinstance(href, str) | assert isinstance(href, str) | ||
1,574行目: | 1,587行目: | ||
and self._invidious_pattern.search(href)): | and self._invidious_pattern.search(href)): | ||
# Nitter上のYouTubeへのリンクをInvidiousのものから直す | # Nitter上のYouTubeへのリンクをInvidiousのものから直す | ||
invidious_href: | invidious_href: str | list[str] | None = ( | ||
self._invidious_pattern.sub( | self._invidious_pattern.sub( | ||
'youtube.com' if ( | 'youtube.com' if ( | ||
1,594行目: | 1,607行目: | ||
elif url.text.startswith('@'): | elif url.text.startswith('@'): | ||
url_link: str = urljoin(self.TWITTER_URL, href) | url_link: str = urljoin(self.TWITTER_URL, href) | ||
url_text: | url_text: str = url.text | ||
url.replace_with( | url.replace_with( | ||
self._archive_url( | self._archive_url( | ||
1,600行目: | 1,613行目: | ||
def _archive_url( | def _archive_url( | ||
self, | |||
url: str, | |||
accessor: AccessorHandler, | |||
text: str | None = None | |||
) -> str: | |||
"""URLをArchiveテンプレートでラップする。 | """URLをArchiveテンプレートでラップする。 | ||
1,693行目: | 1,707行目: | ||
tweets: Final[list[Tag]] = self._get_timeline_items(soup) | tweets: Final[list[Tag]] = self._get_timeline_items(soup) | ||
for tweet in tweets: | for tweet in tweets: | ||
tweet_a: | tweet_a: Tag | None = tweet.a | ||
assert tweet_a is not None | assert tweet_a is not None | ||
if tweet_a.text == self.NEWEST: | if tweet_a.text == self.NEWEST: | ||
1,714行目: | 1,728行目: | ||
continue | continue | ||
tweet_link: | tweet_link: Tag | NavigableString | None = tweet.find( | ||
class_='tweet-link') | class_='tweet-link') | ||
assert isinstance(tweet_link, Tag) | assert isinstance(tweet_link, Tag) | ||
href: | href: str | list[str] | None = tweet_link.get('href') | ||
assert isinstance(href, str) | assert isinstance(href, str) | ||
tweet_url: | tweet_url: str = urljoin( | ||
self.TWITTER_URL, | self.TWITTER_URL, | ||
self._url_fragment_pattern.sub('', href)) | self._url_fragment_pattern.sub('', href)) | ||
# 日付の更新処理 | # 日付の更新処理 | ||
date: | date: datetime = self._tweet_date(tweet_url) | ||
self._table_builder.next_day_if_necessary(date) | self._table_builder.next_day_if_necessary(date) | ||
tweet_callinshow_template: | tweet_callinshow_template: str = self._callinshowlink_url( | ||
tweet_url, accessor) | tweet_url, accessor) | ||
tweet_content: | tweet_content: Tag | NavigableString | None = tweet.find( | ||
class_='tweet-content media-body') | class_='tweet-content media-body') | ||
assert isinstance(tweet_content, Tag) | assert isinstance(tweet_content, Tag) | ||
self._archive_soup(tweet_content, accessor) | self._archive_soup(tweet_content, accessor) | ||
media_txt: | media_txt: str = self._fetch_tweet_media( | ||
tweet, tweet_url, accessor) | tweet, tweet_url, accessor) | ||
quote_txt: | quote_txt: str = self._get_tweet_quote(tweet, accessor) | ||
poll_txt: | poll_txt: str = self._get_tweet_poll(tweet) | ||
self._table_builder.append( | self._table_builder.append( | ||
tweet_callinshow_template, '<br>\n'.join( | tweet_callinshow_template, '<br>\n'.join( | ||
1,778行目: | 1,792行目: | ||
new_url: str = '' | new_url: str = '' | ||
for show_more in show_mores: # show-moreに次ページへのリンクか前ページへのリンクがある | for show_more in show_mores: # show-moreに次ページへのリンクか前ページへのリンクがある | ||
show_more_a: | show_more_a: Tag | None = show_more.a | ||
assert show_more_a is not None | assert show_more_a is not None | ||
href: | href: str | list[str] | None = show_more_a.get('href') | ||
assert isinstance(href, str) | assert isinstance(href, str) | ||
new_url = urljoin( | new_url = urljoin( | ||
1,805行目: | 1,819行目: | ||
def _signal_handler( | def _signal_handler( | ||
self, signum: int, frame: FrameType | None | |||
) -> NoReturn: | |||
"""ユーザがCtrl + Cでプログラムを止めたときのシグナルハンドラ。 | """ユーザがCtrl + Cでプログラムを止めたときのシグナルハンドラ。 | ||
1,822行目: | 1,837行目: | ||
'See also: https://nitter.cz' | 'See also: https://nitter.cz' | ||
'\033[0m') | '\033[0m') | ||
def execute(self, krsw: str | None = None, use_browser: bool = True, | def execute( | ||
self, | |||
krsw: str | None = None, | |||
use_browser: bool = True, | |||
enable_javascript: bool = True | |||
) -> None: | |||
"""通信が必要な部分のロジック。 | """通信が必要な部分のロジック。 | ||
1,864行目: | 1,883行目: | ||
if not self._go_to_new_page(accessor): | if not self._go_to_new_page(accessor): | ||
break | break | ||
except BaseException | except BaseException: | ||
logger.critical('予想外のエラーナリ。ここまでの成果をダンプして終了するナリ。') | logger.critical('予想外のエラーナリ。ここまでの成果をダンプして終了するナリ。') | ||
self._table_builder.dump_file() | self._table_builder.dump_file() | ||
raise | raise | ||
1,891行目: | 1,910行目: | ||
* ちゃんとテストする。 | * ちゃんとテストする。 | ||
""" | """ | ||
WIKI_URL: Final[str] = 'https://krsw-wiki.in' | |||
"""Final[str]: WikiのURL。""" | |||
TEMPLATE_URL: Final[str] = ' | TEMPLATE_URL: Final[str] = WIKI_URL + '/wiki/テンプレート:降臨ショー恒心ログ' | ||
"""Final[str]: テンプレート:降臨ショー恒心ログのURL。""" | """Final[str]: テンプレート:降臨ショー恒心ログのURL。""" | ||
1,901行目: | 1,922行目: | ||
@override | @override | ||
def _set_queries( | def _set_queries( | ||
self, accessor: AccessorHandler, krsw: str | None | |||
) -> bool: | |||
"""検索条件を設定する。 | """検索条件を設定する。 | ||
1,994行目: | 2,016行目: | ||
urls: Final[list[str]] = list(map( | urls: Final[list[str]] = list(map( | ||
lambda x: | lambda x: self.WIKI_URL + assert_get(x, 'href'), | ||
template_soup.select('.wikitable > tbody > tr > td a'))) | template_soup.select('.wikitable > tbody > tr > td a'))) | ||
for url in urls: | for url in urls: | ||
logger.info(f'{unquote(url)} で収集済みツイートを探索中でふ') | logger.info(f'{unquote(url)} で収集済みツイートを探索中でふ') | ||
page: | page: str | None = accessor.request_with_requests_module(url) | ||
assert page is not None | assert page is not None | ||
soup: | soup: BeautifulSoup = BeautifulSoup(page, 'html.parser') | ||
url_as = soup.select('tr > th a') | url_as = soup.select('tr > th a') | ||
assert len(url_as) > 0 | assert len(url_as) > 0 | ||
2,022行目: | 2,043行目: | ||
for tweet in tweets: | for tweet in tweets: | ||
# リダイレクトがあればすべてのリンクを、ないなら目的のURLだけが取得できる | # リダイレクトがあればすべてのリンクを、ないなら目的のURLだけが取得できる | ||
urls: | urls: list[str] = list(map( | ||
lambda a: a.text, tweet.select('a')[1:])) | lambda a: a.text, tweet.select('a')[1:])) | ||
# 別のユーザへのリダイレクトがあるものは除く | # 別のユーザへのリダイレクトがあるものは除く | ||
2,039行目: | 2,060行目: | ||
logger.debug(url_matched.string + 'は最古の探索対象よりも古いのでポア') | logger.debug(url_matched.string + 'は最古の探索対象よりも古いのでポア') | ||
continue | continue | ||
a_first_child: | a_first_child: Tag | None = tweet.select_one('a:first-child') | ||
assert a_first_child is not None | assert a_first_child is not None | ||
archive_url: | archive_url: str | list[str] | None = a_first_child.get('href') | ||
assert isinstance(archive_url, str) | assert isinstance(archive_url, str) | ||
if url_matched[0] not in self._url_list_on_wiki: | if url_matched[0] not in self._url_list_on_wiki: | ||
2,059行目: | 2,078行目: | ||
def _fetch_next_page( | def _fetch_next_page( | ||
self, soup: Tag, accessor: AccessorHandler | |||
) -> str | None: | |||
"""archive.todayの検索結果のページをpaginateする。 | """archive.todayの検索結果のページをpaginateする。 | ||
2,089行目: | 2,109行目: | ||
while has_next: | while has_next: | ||
self._append_tweet_urls(soup) | self._append_tweet_urls(soup) | ||
next_page: | next_page: str | None = self._fetch_next_page(soup, accessor) | ||
if next_page is not None: | if next_page is not None: | ||
soup = BeautifulSoup(next_page, 'html.parser') | soup = BeautifulSoup(next_page, 'html.parser') | ||
2,097行目: | 2,116行目: | ||
def _next_url( | def _next_url( | ||
self, | |||
accessor: AccessorHandler, | |||
tweet_url_prefix: str, | |||
incremented_num: int, | |||
incremented: bool = False | |||
) -> None: | |||
"""ツイートのURLを、数字部分をインクリメントしながら探索する。 | """ツイートのURLを、数字部分をインクリメントしながら探索する。 | ||
2,179行目: | 2,199行目: | ||
True) | True) | ||
def _parse_images(self, soup: Tag, | def _parse_images( | ||
self, soup: Tag, accessor: AccessorHandler | |||
) -> tuple[str, ...]: | |||
"""ツイートの魚拓から画像をダウンロードし、ファイル名のタプルを返す。 | """ツイートの魚拓から画像をダウンロードし、ファイル名のタプルを返す。 | ||
2,232行目: | 2,253行目: | ||
if len(internal_a_tags) > 0: | if len(internal_a_tags) > 0: | ||
for a_tag in internal_a_tags: | for a_tag in internal_a_tags: | ||
account_name: | account_name: str = a_tag.text | ||
if account_name.startswith('#'): | if account_name.startswith('#'): | ||
a_tag.replace_with(a_tag.text) | a_tag.replace_with(a_tag.text) | ||
else: | else: | ||
url: | url: str = urljoin(self.TWITTER_URL, account_name[1:]) | ||
a_tag.replace_with(TableBuilder.archive_url( | a_tag.replace_with(TableBuilder.archive_url( | ||
url, | url, | ||
2,333行目: | 2,353行目: | ||
def _get_tweet_from_archive( | def _get_tweet_from_archive( | ||
self, | |||
url_pairs: list[UrlTuple], | |||
accessor: AccessorHandler | |||
) -> None: | |||
"""魚拓からツイート本文を取得する。 | """魚拓からツイート本文を取得する。 | ||
2,345行目: | 2,366行目: | ||
for url_pair in url_pairs: | for url_pair in url_pairs: | ||
page: | page: str | None = accessor.request(url_pair.archive_url) | ||
assert page is not None | assert page is not None | ||
article: | article: Tag | None = BeautifulSoup( | ||
page, 'html.parser' | page, 'html.parser' | ||
).select_one('article[tabindex="-1"]') | ).select_one('article[tabindex="-1"]') | ||
2,355行目: | 2,376行目: | ||
logger.debug(url_pair.url + 'を整形しますを') | logger.debug(url_pair.url + 'を整形しますを') | ||
tweet_date: | tweet_date: datetime = self._tweet_date(url_pair.url) | ||
table_builder.next_day_if_necessary(tweet_date) | table_builder.next_day_if_necessary(tweet_date) | ||
2,375行目: | 2,396行目: | ||
# YouTube等のリンク | # YouTube等のリンク | ||
card_tag: | card_tag: Tag | None = article.select_one( | ||
'div[aria-label="Play"]:not(div[role="link"] ' | 'div[aria-label="Play"]:not(div[role="link"] ' | ||
'div[aria-label="Play"])') | 'div[aria-label="Play"])') | ||
2,384行目: | 2,405行目: | ||
# 画像に埋め込まれた外部サイトへのリンク | # 画像に埋め込まれた外部サイトへのリンク | ||
article_tag: | article_tag: Tag | None = article.select_one( | ||
'a[role="link"][aria-label] img:not(div[role="link"] ' | 'a[role="link"][aria-label] img:not(div[role="link"] ' | ||
'a[role="link"][aria-label] img)') | 'a[role="link"][aria-label] img)') | ||
2,393行目: | 2,414行目: | ||
# 引用の有無のチェック | # 引用の有無のチェック | ||
retweet_tag: | retweet_tag: Tag | None = article.select_one( | ||
'div[role="link"]') | 'div[role="link"]') | ||
if retweet_tag is not None: | if retweet_tag is not None: | ||
account_name_tag: | account_name_tag: Tag | None = ( | ||
retweet_tag.select_one( | retweet_tag.select_one( | ||
'div[tabindex="-1"] > div > span:not(:has(> *))')) # noqa: E501 | 'div[tabindex="-1"] > div > span:not(:has(> *))')) # noqa: E501 | ||
2,414行目: | 2,435行目: | ||
# 投票の処理 | # 投票の処理 | ||
poll_txt: | poll_txt: str = self._get_tweet_poll(article) | ||
text = self._concat_texts(text, poll_txt) | text = self._concat_texts(text, poll_txt) | ||
# バージョンの処理 | # バージョンの処理 | ||
possible_version_text_tags: | possible_version_text_tags: ResultSet[Tag] = ( | ||
article.select( | article.select( | ||
'div > span > ' | 'div > span > ' | ||
2,431行目: | 2,452行目: | ||
except Exception as e: | except Exception as e: | ||
# エラーが起きても止めない | # エラーが起きても止めない | ||
logger.exception( | logger.exception('エラーが発生してツイートが取得できませんでしたを', exc_info=True) | ||
text = 'エラーが発生してツイートが取得できませんでした\n' + ''.join( | text = 'エラーが発生してツイートが取得できませんでした\n' + ''.join( | ||
TracebackException.from_exception(e).format()) | TracebackException.from_exception(e).format()) | ||
table_builder.append(tweet_callinshow_template, text) | table_builder.append(tweet_callinshow_template, text) | ||
else: | else: | ||
logger. | logger.warning(url_pair.url + 'はリツイートなので飛ばすナリ。' | ||
'URLリスト収集の時点でフィルタできなかった、これはいけない。') | |||
table_builder.dump_file() | table_builder.dump_file() | ||
@override | @override | ||
def execute(self, krsw: str | None = None, use_browser: bool = True, | def execute( | ||
self, | |||
krsw: str | None = None, | |||
use_browser: bool = True, | |||
enable_javascript: bool = True | |||
) -> None: | |||
"""通信が必要な部分のロジック。 | """通信が必要な部分のロジック。 | ||
2,470行目: | 2,495行目: | ||
sys.exit(1) | sys.exit(1) | ||
# Wikiに既に掲載されているツイートのURLを取得 | # Wikiに既に掲載されているツイートのURLを取得 | ||
self._get_tweet_urls_from_wiki(accessor) | self._get_tweet_urls_from_wiki(accessor) # Wikiに接続できない時はここをコメントアウト | ||
# 未掲載のツイートのURLを取得する | # 未掲載のツイートのURLを取得する | ||
2,477行目: | 2,502行目: | ||
# URL一覧ファイルのダンプ | # URL一覧ファイルのダンプ | ||
with | with Path(self.URL_LIST_FILENAME).open('w', encoding='utf-8') as f: | ||
for url_pair in self._url_list: | for url_pair in self._url_list: | ||
f.write(url_pair.url + '\n') | f.write(url_pair.url + '\n') |
回編集