→コード: Twitterの相次ぐ仕様変更のせいでnitterのsearchが使えなくなっていたので、tweets or repliesから取得するようにとりとり変更。またURLの仕様も変わり、もはや前のスクリプトでは取得ができなくなったので、メジャーバージョンアップ。
>Fet-Fe (→コード: v2.2.8 文字なし投稿で最初に改行が入るのを修正、archiveで冗長な3つ目の引数を除く) |
>Fet-Fe (→コード: Twitterの相次ぐ仕様変更のせいでnitterのsearchが使えなくなっていたので、tweets or repliesから取得するようにとりとり変更。またURLの仕様も変わり、もはや前のスクリプトでは取得ができなくなったので、メジャーバージョンアップ。) |
||
6行目: | 6行目: | ||
''' | ''' | ||
ver3.0.0 2023/4/29恒心 | |||
当コードは恒心停止してしまったhttps://rentry.co/7298gの降臨ショーツイート自動収集スクリプトの復刻改善版です | 当コードは恒心停止してしまったhttps://rentry.co/7298gの降臨ショーツイート自動収集スクリプトの復刻改善版です | ||
108行目: | 108行目: | ||
##nitterのURLのドメインとユーザーネーム部分の接続部品 | ##nitterのURLのドメインとユーザーネーム部分の接続部品 | ||
TWEETS_OR_REPLIES: Final[str] = 'with_replies' | |||
##HTTPリクエスト時のユーザーエージェントとヘッダ | ##HTTPリクエスト時のユーザーエージェントとヘッダ | ||
128行目: | 128行目: | ||
##万が一仕様変更で変わったとき用 | ##万が一仕様変更で変わったとき用 | ||
NO_ARCHIVE: Final[str] = 'No results' | NO_ARCHIVE: Final[str] = 'No results' | ||
##nitterの前ページ読み込み部分の名前 | ##nitterの前ページ読み込み部分の名前 | ||
156行目: | 153行目: | ||
if krsw: | if krsw: | ||
print('名前は自動的に' + self.CALLINSHOW + 'にナリます') | print('名前は自動的に' + self.CALLINSHOW + 'にナリます') | ||
name: Final[str] = self.CALLINSHOW | self.name: Final[str] = self.CALLINSHOW | ||
else: | else: | ||
name: Final[str] = self._get_name() | self.name: Final[str] = self._get_name() | ||
##検索クエリとページ取得 | ##検索クエリとページ取得 | ||
self.query_strs: list[str] = [] | |||
if krsw: | if krsw: | ||
print('クエリは自動的になしにナリます') | print('クエリは自動的になしにナリます') | ||
else: | else: | ||
self._page: Response = self. | self._get_query() | ||
self._page: Response | None = self._request(urljoin(self.NITTER_INSTANCE, self.name + '/' + self.TWEETS_OR_REPLIES)) | |||
if self._page is None: | |||
self._fail() | |||
##終わりにするツイート取得 | ##終わりにするツイート取得 | ||
323行目: | 321行目: | ||
##検索クエリを取得 | ##検索クエリを取得 | ||
def _get_query(self | def _get_query(self) -> None: | ||
print("検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。") | |||
print("例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行") | |||
query_input: str = input() ##ユーザー入力受付 | |||
##空欄が押されるまでユーザー入力受付 | |||
while query_input != '': | |||
self.query_strs.append(query_input) | |||
query_input = input() | |||
print("クエリのピースが埋まっていく。") | |||
##接続失敗時処理 | ##接続失敗時処理 | ||
437行目: | 423行目: | ||
# 動画のダウンロード | # 動画のダウンロード | ||
if self._proxy_is_needed: | if self._proxy_is_needed: | ||
returncode: int = subprocess.run( | returncode: int = subprocess.run(["ffmpeg", "-y", "-http_proxy", "self.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( | 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 | ||
# 取得成功したらtsをmp4に変換 | # 取得成功したらtsをmp4に変換 | ||
if returncode == 0: | if returncode == 0: | ||
ts2mp4_returncode: int = subprocess.run( | ts2mp4_returncode: int = subprocess.run(["ffmpeg", "-y", "-i", f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.ts", "-acodec", "copy", "-vcodec", "copy", f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.mp4"], stdout=subprocess.DEVNULL).returncode | ||
if ts2mp4_returncode == 0: | if ts2mp4_returncode == 0: | ||
print(f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.mp4をアップロードしなければない。") | print(f"{os.path.join(self.MEDIA_DIR, tweet_id)}_{i}.mp4をアップロードしなければない。") | ||
461行目: | 447行目: | ||
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( | quote_txt = self._archive_url(link) | ||
return quote_txt | return quote_txt | ||
503行目: | 489行目: | ||
if tweet.find(class_='retweet-header') is not None: ##retweet-headerはリツイートを示すので入っていれば処理しない | if tweet.find(class_='retweet-header') is not None: ##retweet-headerはリツイートを示すので入っていれば処理しない | ||
continue | continue | ||
if tweet.find(class_='pinned') is not None: ##pinnedは固定ツイートを示すので入っていれば処理しない、未テスト | |||
continue | |||
if len(self.query_strs) > 0: # クエリが指定されている場合、一つでも含まないツイートは処理しない | |||
not_match: bool = False | |||
for query_str in query_strs: | |||
if query_str not in tweet.text: | |||
not_match = True | |||
break | |||
if not_match: | |||
continue | |||
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作成 | ||
date: datetime = self._tweet_date(tweet) | date: datetime = self._tweet_date(tweet) | ||
513行目: | 510行目: | ||
quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加 | quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加 | ||
poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得 | poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得 | ||
self._check_additional_info( | self._check_additional_info(tweet_content, tweet_url) ##編集や長いツイートの省略をチェック | ||
self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | ||
+ '<br>\n'.join(filter(None, [ | + '<br>\n'.join(filter(None, [ | ||
535行目: | 532行目: | ||
#MediaWiki文法と衝突する文字を無効化する | #MediaWiki文法と衝突する文字を無効化する | ||
def _escape_wiki_reserved_words(self, text: str) -> str: | def _escape_wiki_reserved_words(self, text: str) -> str: | ||
def escape_nolink_urls(text: str) -> str: | |||
is_in_archive_template: bool = False | |||
i: int = 0 | |||
while i < len(text): | |||
if is_in_archive_template: | |||
if text[i:i+2] == '}}': | |||
is_in_archive_template = False | |||
i += 2 | |||
else: | |||
if text[i:i+10] == '{{Archive|' or text[i:i+10] == '{{archive|': | |||
is_in_archive_template = True | |||
i += 10 | |||
elif text[i:i+8] == 'https://': | |||
text = text[:i] + '<nowiki>https://</nowiki>' + text[i+8:] | |||
i += 25 | |||
elif text[i:i+7] == 'http://': | |||
text = text[:i] + '<nowiki>http://</nowiki>' + text[i+7:] | |||
i += 24 | |||
i += 1 | |||
return text | |||
text = text.replace('\n', '<br>\n') | text = text.replace('\n', '<br>\n') | ||
text = re.sub(r'^ ', ' ', text, flags=re.MULTILINE) | text = re.sub(r'^ ', ' ', text, flags=re.MULTILINE) | ||
text = re.sub(r'^([\*#:;])', r'<nowiki>\1</nowiki>', text, flags=re.MULTILINE) | text = re.sub(r'^([\*#:;])', r'<nowiki>\1</nowiki>', text, flags=re.MULTILINE) | ||
text = re.sub(r'^----', '<nowiki>----</nowiki>', text, flags=re.MULTILINE) | text = re.sub(r'^----', '<nowiki>----</nowiki>', text, flags=re.MULTILINE) | ||
text = escape_nolink_urls(text) | |||
return text | return text | ||
# | #aタグをテンプレートArchiveの文字列に変化させる | ||
def _archive_soup(self, tag: bs4.element.Tag) -> None: | def _archive_soup(self, tag: bs4.element.Tag) -> None: | ||
urls_in_tweet: Final[bs4.element.ResultSet] = tag.find_all('a') | urls_in_tweet: Final[bs4.element.ResultSet] = tag.find_all('a') | ||
550行目: | 569行目: | ||
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.replace_with(self._archive_url(url_link)) ##テンプレート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.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | ||
elif self._invidious_pattern.search(url.get('href')): | elif self._invidious_pattern.search(url.get('href')): | ||
#Nitter上のYouTubeへのリンクをInvidiousのものから直す | #Nitter上のYouTubeへのリンクをInvidiousのものから直す | ||
url_link: str = url.get('href') | url_link: str = url.get('href') | ||
if re.match('https://[^/]+/[^/]+/', url_link): | if re.match('https://[^/]+/[^/]+/', url_link) or re.search('/@[^/]*$', url_link): | ||
url_link = self._invidious_pattern.sub('youtube.com', url_link) | url_link = self._invidious_pattern.sub('youtube.com', url_link) | ||
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.replace_with(self._archive_url(url_link)) ##テンプレート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.replace_with(self._archive_url(url_link)) ##テンプレートArchiveに変化 | ||
else: | else: | ||
url.replace_with(self._archive_url( | url.replace_with(self._archive_url(url.get('href'))) ##テンプレートArchiveに変化 | ||
# | #URLをテンプレートArchiveに変化させる。#がURLに含まれていたら別途処理 | ||
def _archive_url(self, url | def _archive_url(self, url: Final[str]) -> str: | ||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url) + '}}' ##テンプレートArchiveの文字列返す | if '#' in url: | ||
main_url, fragment = url.split('#', maxsplit=1) | |||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(main_url) + '#' + fragment + '}}' ##テンプレートArchiveの文字列返す | |||
else: | |||
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url) + '}}' ##テンプレートArchiveの文字列返す | |||
#URLをテンプレートCallinShowlinkに変化させる | #URLをテンプレートCallinShowlinkに変化させる | ||
582行目: | 605行目: | ||
##URLから魚拓返す | ##URLから魚拓返す | ||
def _archive(self, url: Final[str]) -> str: | def _archive(self, url: Final[str]) -> str: | ||
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[Response | None] = self._request(urljoin(self.ARCHIVE_TODAY, quote(unquote(url), safe='&=+?%'))) ##アクセス用URL使って結果を取得 | res: Final[Response | None] = self._request(urljoin(self.ARCHIVE_TODAY, quote(unquote(url), safe='&=+?%'))) ##アクセス用URL使って結果を取得 | ||
616行目: | 622行目: | ||
soup: Final[BeautifulSoup] = BeautifulSoup(self._page.text, 'html.parser') ##beautifulsoupでレスポンス解析 | soup: Final[BeautifulSoup] = BeautifulSoup(self._page.text, '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? | |||
for show_more in show_mores: ##show-moreに次ページへのリンクか前ページへのリンクがある | for show_more in show_mores: ##show-moreに次ページへのリンクか前ページへのリンクがある | ||
if show_more.text != self.NEWEST: ##前ページへのリンクではないか判定 | if show_more.text != self.NEWEST: ##前ページへのリンクではないか判定 | ||
new_url | new_url = urljoin(self.NITTER_INSTANCE, self.CALLINSHOW + '/' + self.TWEETS_OR_REPLIES + show_more.a.get('href')) ##直下のaタグのhrefの中身取ってURL頭部分と合体 | ||
res: Final[Response | None] = self._request(new_url) ##接続してHTML取ってくる | res: Final[Response | None] = self._request(new_url) ##接続してHTML取ってくる | ||
if res is None: | if res is None: |