→コード: v4.1.10 魚拓からツイートを探すモードで、x.comからのリダイレクトが取れていなかったのを修正。_get_tweet_from_archiveが失敗しても未掲載のurl一覧だけは最悪取れるように修正
>Fet-Fe (→コード: v4.1.9 --search-unarchivedモードでツイートの整形も行う。ちゃんとテストはできていない。v4.1.8ではNitterでのページングのためにCookieを追加した。) |
>Fet-Fe (→コード: v4.1.10 魚拓からツイートを探すモードで、x.comからのリダイレクトが取れていなかったのを修正。_get_tweet_from_archiveが失敗しても未掲載のurl一覧だけは最悪取れるように修正) |
||
11行目: | 11行目: | ||
"""Twitter自動収集スクリプト | """Twitter自動収集スクリプト | ||
ver4.1. | ver4.1.10 2023/12/17恒心 | ||
当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | 当コードは恒心停止してしまった https://rentry.co/7298g の降臨ショーツイート自動収集スクリプトの復刻改善版です | ||
66行目: | 66行目: | ||
import subprocess | import subprocess | ||
import sys | import sys | ||
import warnings | |||
from abc import ABCMeta, abstractmethod | from abc import ABCMeta, abstractmethod | ||
from argparse import ArgumentParser, Namespace | from argparse import ArgumentParser, Namespace | ||
74行目: | 75行目: | ||
from re import Match, Pattern | from re import Match, Pattern | ||
from time import sleep | from time import sleep | ||
from traceback import TracebackException | |||
from types import MappingProxyType, TracebackType | from types import MappingProxyType, TracebackType | ||
from typing import (Final, NamedTuple, NoReturn, Self, assert_never, final, | from typing import (Final, NamedTuple, NoReturn, Self, assert_never, final, | ||
84行目: | 86行目: | ||
from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||
from bs4.element import NavigableString, ResultSet, Tag | from bs4.element import NavigableString, ResultSet, Tag | ||
from selenium.common.exceptions import (InvalidSwitchToTargetException, | from selenium.common.exceptions import (InvalidSwitchToTargetException, | ||
WebDriverException) | WebDriverException) | ||
from selenium.webdriver import Firefox as FirefoxDriver | |||
from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||
from selenium.webdriver.firefox.options import Options as FirefoxOptions | from selenium.webdriver.firefox.options import Options as FirefoxOptions | ||
139行目: | 141行目: | ||
"""``--search-unarchived`` オプションを付けたときに使用する設定値。 | """``--search-unarchived`` オプションを付けたときに使用する設定値。 | ||
""" | """ | ||
tweet_url_prefix_default: Final[str] = ' | tweet_url_prefix_default: Final[str] = '173' | ||
"""Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。 | """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。 | ||
153行目: | 155行目: | ||
""" | """ | ||
incremented_num_default: Final[int] = | incremented_num_default: Final[int] = 4 | ||
"""Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。 | """Final[int]: ツイートURLの数字部分うち、インクリメントする桁のデフォルト値。 | ||
241行目: | 243行目: | ||
HEADERS: Final[dict[str, str]] = { | HEADERS: Final[dict[str, str]] = { | ||
'User-Agent': | 'User-Agent': | ||
'Mozilla/5.0 ( | 'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/120.0' | ||
} | } | ||
"""Final[dict[str, str]]: HTTPリクエスト時のヘッダ。 | """Final[dict[str, str]]: HTTPリクエスト時のヘッダ。 | ||
444行目: | 446行目: | ||
try: | try: | ||
logger.debug('ブラウザ起動') | logger.debug('ブラウザ起動') | ||
self._driver: | self._driver: FirefoxDriver = FirefoxDriver( | ||
options=self._options) | options=self._options) | ||
sleep(1) | sleep(1) | ||
web_driver_wait: Final[WebDriverWait] = | web_driver_wait: Final[WebDriverWait[FirefoxDriver]] = ( | ||
self._driver, | WebDriverWait(self._driver, self.WEB_DRIVER_WAIT_TIME)) | ||
web_driver_wait.until( | web_driver_wait.until( | ||
ec.element_to_be_clickable((By.ID, 'connectButton')) | ec.element_to_be_clickable((By.ID, 'connectButton')) | ||
1,887行目: | 1,888行目: | ||
class ArchiveCrawler(TwitterArchiver): | class ArchiveCrawler(TwitterArchiver): | ||
"""archive.todayに記録された尊師のツイートのうち、Wiki未掲載のものを収集する。 | """archive.todayに記録された尊師のツイートのうち、Wiki未掲載のものを収集する。 | ||
Warning: | |||
このモードではURLリストの他にツイート本文も整形して取得するナリが、 | |||
機能のテストが不十分であることと、TwitterのHTML構造はしばしば変更されることから、 | |||
整形後の出力が正しいとは限らないナリ | |||
機能を過信せず、自分の目で確かめてね(笑)、それはできるよね。 | |||
Todo: | Todo: | ||
*ちゃんとテストする。 | *ちゃんとテストする。 | ||
""" | """ | ||
1,943行目: | 1,950行目: | ||
self._twitter_url_pattern: Pattern[str] = re.compile( | self._twitter_url_pattern: Pattern[str] = re.compile( | ||
self.TWITTER_URL + self._name + r'/status/\d+') | '^' + self.TWITTER_URL + self._name + r'/status/\d+') | ||
self._url_list_on_wiki: list[str] = [] | self._url_list_on_wiki: list[str] = [] | ||
2,002行目: | 2,009行目: | ||
'#CONTENT > div > .TEXT-BLOCK') | '#CONTENT > div > .TEXT-BLOCK') | ||
for tweet in tweets: | for tweet in tweets: | ||
# リダイレクトがあればすべてのリンクを、ないなら目的のURLだけが取得できる | |||
urls: Final[list[str]] = list(map( | |||
url_matched: Final[Match[str]] | None = ( | lambda a: a.text, tweet.select('a')[1:])) | ||
self._twitter_url_pattern.match( | # 別のユーザへのリダイレクトがあるものは除く | ||
) | if len(set(map(lambda url: urlparse(url).path, urls))) != 1: | ||
logger.debug('Found an external redirect ' + str(urls)) | |||
continue | |||
url_matched: Final[Match[str]] | None = next( | |||
filter( | |||
lambda x: x is not None, | |||
map(lambda url: self._twitter_url_pattern.match(url), urls) | |||
), None | |||
) # 最初にマッチしたURLを返す | |||
if url_matched is not None: | if url_matched is not None: | ||
a_first_child: Final[Tag | None] = tweet.select_one( | a_first_child: Final[Tag | None] = tweet.select_one( | ||
2,288行目: | 2,304行目: | ||
# 本文 | # 本文 | ||
text_tag: Tag | None = article.select_one( | try: | ||
text_tag: Tag | None = article.select_one( | |||
'div[data-testid="tweetText"]:not(' | |||
'div[role="link"] div[data-testid="tweetText"])') | |||
if text_tag is not None: | |||
text_tag = self._retrieve_emojis(text_tag) | |||
text: str = self._replace_links(text_tag, | |||
accessor).text | |||
else: | |||
text: str = '' | |||
# YouTube等のリンク | |||
card_tag: Final[Tag | None] = article.select_one( | |||
'div[data-testid="card.layoutSmall.media"]:not(' | |||
'div[role="link"] ' | |||
'div[data-testid="card.layoutSmall.media"])') | |||
if card_tag is not None: | |||
text = self._concat_texts( | |||
text, | |||
'{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}') | |||
# 画像に埋め込まれた外部サイトへのリンク | |||
article_tag: Final[Tag | None] = article.select_one( | |||
'article div[data-testid="card.layoutLarge.media"] a') | |||
if article_tag is not None: | |||
text = self._concat_texts( | |||
text, | |||
'{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}') | |||
# 引用の有無のチェック | |||
retweet_tag: Final[Tag | None] = article.select_one( | |||
'article[data-testid="tweet"] div[role="link"]') | |||
if retweet_tag is not None: | |||
account_name_tag: Final[Tag | None] = ( | |||
retweet_tag.select_one( | |||
'div[data-testid="User-Name"] div[tabindex="-1"]')) | |||
assert account_name_tag is not None | |||
text = self._concat_texts( | |||
text, '{{Archive|1=' | |||
+ account_name_tag.text | |||
+ 'のリツイートがあります|2=リツイートがあります}}') | |||
# 画像の取得 | |||
image_list: tuple[str, ...] = self._parse_images( | |||
article, accessor) | |||
image_txt: str = ' '.join(map( | |||
lambda t: f'[[ファイル:{t}|240px]]', image_list)) | |||
text = self._concat_texts(text, image_txt) | |||
# TODO: 投票の処理 | |||
except Exception as e: | |||
# エラーが起きても止めない | |||
logger.exception(e) | |||
text = 'エラーが発生してツイートが取得できませんでした\n' + ''.join( | |||
TracebackException.from_exception(e).format()) | |||
table_builder.append( | table_builder.append( | ||
tweet_callinshow_template, | tweet_callinshow_template, | ||
2,349行目: | 2,374行目: | ||
enable_javascript: bool = True) -> None | NoReturn: | enable_javascript: bool = True) -> None | NoReturn: | ||
logger.info('Wikiに未掲載のツイートのURLを収集しますを') | logger.info('Wikiに未掲載のツイートのURLを収集しますを') | ||
warnings.warn( | |||
'\033[31m' | |||
'このモードではURLリストの他にツイート本文も整形して取得するナリが、' | |||
'機能のテストが不十分であることと、TwitterのHTML構造は' | |||
'しばしば変更されることから、整形後の出力が正しいとは限らないナリ。' | |||
'機能を過信せず、自分の目で確かめてね(笑)、それはできるよね。' | |||
'\033[0m') | |||
# Seleniumドライバーを必ず終了するため、with文を利用する。 | # Seleniumドライバーを必ず終了するため、with文を利用する。 | ||
with AccessorHandler(use_browser, enable_javascript) as accessor: | with AccessorHandler(use_browser, enable_javascript) as accessor: | ||
2,363行目: | 2,395行目: | ||
self.TWEET_URL_PREFIX_DEFAULT, | self.TWEET_URL_PREFIX_DEFAULT, | ||
self.INCREMENTED_NUM_DEFAULT) | self.INCREMENTED_NUM_DEFAULT) | ||
logger.debug(f'{len(self._url_list)} tweets are missed') | |||
# ツイート本文を取得する | # ツイート本文を取得する | ||
filtered_url_tuple: | try: | ||
filtered_url_tuple: tuple[str, ...] = ( | |||
self._get_tweet_from_archive(self._url_list, accessor)) | |||
except Exception: | |||
# エラーが起きたらURLのリストをそのまま返す | |||
logger.exception('異常が起きたので終了するナリ。' | |||
'リツイートを含むURLのリストを返すので、それを元に自分でツイートを整形してほしいナリ') | |||
filtered_url_tuple: tuple[str, ...] = tuple( | |||
map(lambda url_pair: url_pair.url, self._url_list)) | |||
# URL一覧ファイルのダンプ | # URL一覧ファイルのダンプ | ||
with codecs.open(self.URL_LIST_FILENAME, 'w', 'utf-8') as f: | with codecs.open(self.URL_LIST_FILENAME, 'w', 'utf-8') as f: | ||
for url in filtered_url_tuple: | for url in filtered_url_tuple: |