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

→‎コード: 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.9 2023/11/27恒心
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 import webdriver
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] = '172'
         tweet_url_prefix_default: Final[str] = '173'
         """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。
         """Final[str]: ツイートURLの数字部分のうち、予め固定しておく部分。


153行目: 155行目:
         """
         """


         incremented_num_default: Final[int] = 6
         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 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.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: webdriver.Firefox = webdriver.Firefox(
             self._driver: FirefoxDriver = FirefoxDriver(
                 options=self._options)
                 options=self._options)
             sleep(1)
             sleep(1)
             web_driver_wait: Final[WebDriverWait] = WebDriverWait(
             web_driver_wait: Final[WebDriverWait[FirefoxDriver]] = (
                 self._driver,
                 WebDriverWait(self._driver, self.WEB_DRIVER_WAIT_TIME))
                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:
         *ちゃんとテストする。
         *ちゃんとテストする。
        *ツイートのテーブル生成でこけてもURL一覧ファイルだけはダンプするようにする。
     """
     """


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:
             a_last_child: Final[Tag | None] = tweet.select_one('a:last-child')
             # リダイレクトがあればすべてのリンクを、ないなら目的のURLだけが取得できる
            assert a_last_child is not None
            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(a_last_child.text)
            # 別のユーザへのリダイレクトがあるものは除く
             )
            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:
                    'div[data-testid="tweetText"]:not('
                    text_tag: Tag | None = article.select_one(
                    'div[role="link"] div[data-testid="tweetText"])')
                        'div[data-testid="tweetText"]:not('
                if text_tag is not None:
                        'div[role="link"] div[data-testid="tweetText"])')
                    text_tag = self._retrieve_emojis(text_tag)
                    if text_tag is not None:
                    text: str = self._replace_links(text_tag, accessor).text
                        text_tag = self._retrieve_emojis(text_tag)
                else:
                        text: str = self._replace_links(text_tag,
                    text: str = ''
                                                        accessor).text
                    else:
                        text: str = ''


                # YouTube等のリンク
                    # YouTube等のリンク
                card_tag: Final[Tag | None] = article.select_one(
                    card_tag: Final[Tag | None] = article.select_one(
                    'div[data-testid="card.layoutSmall.media"]:not('
                        'div[data-testid="card.layoutSmall.media"]:not('
                    'div[role="link"] '
                        'div[role="link"] '
                    'div[data-testid="card.layoutSmall.media"])')
                        'div[data-testid="card.layoutSmall.media"])')
                if card_tag is not None:
                    if card_tag is not None:
                    text = self._concat_texts(
                        text = self._concat_texts(
                        text, '{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}')
                            text,
                            '{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}')


                # 画像に埋め込まれた外部サイトへのリンク
                    # 画像に埋め込まれた外部サイトへのリンク
                article_tag: Final[Tag | None] = article.select_one(
                    article_tag: Final[Tag | None] = article.select_one(
                    'article div[data-testid="card.layoutLarge.media"] a')
                        'article div[data-testid="card.layoutLarge.media"] a')
                if article_tag is not None:
                    if article_tag is not None:
                    text = self._concat_texts(
                        text = self._concat_texts(
                        text, '{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}')
                            text,
                            '{{Archive|1=リンクがあります。重複に注意して下さい|2=リンクがあります}}')


                # 引用の有無のチェック
                    # 引用の有無のチェック
                retweet_tag: Final[Tag | None] = article.select_one(
                    retweet_tag: Final[Tag | None] = article.select_one(
                    'article[data-testid="tweet"] div[role="link"]')
                        'article[data-testid="tweet"] div[role="link"]')
                if retweet_tag is not None:
                    if retweet_tag is not None:
                    account_name_tag: Final[Tag | None] = (
                        account_name_tag: Final[Tag | None] = (
                        retweet_tag.select_one(
                            retweet_tag.select_one(
                            'div[data-testid="User-Name"] div[tabindex="-1"]'))
                                'div[data-testid="User-Name"] div[tabindex="-1"]'))
                    assert account_name_tag is not None
                        assert account_name_tag is not None
                    text = self._concat_texts(
                        text = self._concat_texts(
                        text, '{{Archive|1='
                            text, '{{Archive|1='
                        + account_name_tag.text
                            + account_name_tag.text
                        + 'のリツイートがあります|2=リツイートがあります}}')
                            + 'のリツイートがあります|2=リツイートがあります}}')


                # 画像の取得
                    # 画像の取得
                image_list: tuple[str, ...] = self._parse_images(article,
                    image_list: tuple[str, ...] = self._parse_images(
                                                                accessor)
                        article, accessor)
                image_txt: str = ' '.join(map(
                    image_txt: str = ' '.join(map(
                    lambda t: f'[[ファイル:{t}|240px]]', image_list))
                        lambda t: f'[[ファイル:{t}|240px]]', image_list))
                text = self._concat_texts(text, image_txt)
                    text = self._concat_texts(text, image_txt)


                # TODO: 投票の処理
                    # 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: Final[tuple[str, ...]] = (
             try:
                self._get_tweet_from_archive(self._url_list, accessor))
                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一覧ファイルのダンプ
            # TODO: _get_tweet_from_archiveが失敗しても実行できるようにする。
             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:
匿名利用者