「利用者:夜泣き/スクリプト」の版間の差分
>夜泣き (タイムゾーンの設定忘れ) |
>Fet-Fe 編集の要約なし |
||
6行目: | 6行目: | ||
''' | ''' | ||
ver2.1. | ver2.1.4 2022/9/22恒心 | ||
当コードは恒心停止してしまったhttps://rentry.co/7298gの降臨ショーツイート自動収集スクリプトの復刻改善版です | 当コードは恒心停止してしまったhttps://rentry.co/7298gの降臨ショーツイート自動収集スクリプトの復刻改善版です | ||
42行目: | 42行目: | ||
import re | import re | ||
import json | import json | ||
from datetime import datetime | from datetime import datetime | ||
from zoneinfo import ZoneInfo | |||
from time import sleep | from time import sleep | ||
from typing import Final, NoReturn, TypeAlias | from typing import Final, NoReturn, TypeAlias | ||
from urllib.parse import quote, unquote | from urllib.parse import quote, unquote, urljoin | ||
import warnings | import warnings | ||
import requests | import requests | ||
import bs4 | import bs4 | ||
66行目: | 66行目: | ||
##末尾にスラッシュ必須 | ##末尾にスラッシュ必須 | ||
NITTER_INSTANCE: Final[str] = 'http://nitterqdyumlovt7tjqpdjrluitgmtpa53qq3idlpgoe4kxo7gs3xvad.onion/' | NITTER_INSTANCE: Final[str] = 'http://nitterqdyumlovt7tjqpdjrluitgmtpa53qq3idlpgoe4kxo7gs3xvad.onion/' | ||
##archive.todayの魚拓 | ##archive.todayの魚拓 | ||
79行目: | 78行目: | ||
##twitterのURL | ##twitterのURL | ||
## | ##末尾にスラッシュ必須 | ||
TWITTER_URL: Final[str] = 'https://twitter.com' | TWITTER_URL: Final[str] = 'https://twitter.com/' | ||
##降臨ショーのユーザーネーム | ##降臨ショーのユーザーネーム | ||
157行目: | 156行目: | ||
if krsw: | if krsw: | ||
print('クエリは自動的になしにナリます') | print('クエリは自動的になしにナリます') | ||
self._page: Response | None = self._request(self.NITTER_INSTANCE | self._page: Response | None = self._request(urljoin(self.NITTER_INSTANCE, self.SEARCH_QUERY + name)) | ||
if self._page is None: | if self._page is None: | ||
self._fail() | self._fail() | ||
171行目: | 170行目: | ||
##日付取得 | ##日付取得 | ||
self._date = self._tweet_date(BeautifulSoup(self._page.text, 'html.parser').find(class_='timeline-item')) | self._date: datetime = self._tweet_date(BeautifulSoup(self._page.text, 'html.parser').find(class_='timeline-item')) | ||
self._txt_data.append('') | self._txt_data.append('') | ||
print() | print() | ||
183行目: | 182行目: | ||
##失敗かどうかは呼出側で要判定 | ##失敗かどうかは呼出側で要判定 | ||
def _request_once(self, url: Final[str]) -> Response: | def _request_once(self, url: Final[str]) -> Response: | ||
res: Response = requests.get(url, timeout=self.REQUEST_TIMEOUT, headers=self.HEADERS, allow_redirects=False) | res: Response = requests.get(url, timeout=self.REQUEST_TIMEOUT, headers=self.HEADERS, allow_redirects=False, proxies=self.PROXIES) | ||
sleep(self.WAIT_TIME) ##DoS対策で待つ | sleep(self.WAIT_TIME) ##DoS対策で待つ | ||
return res | return res | ||
208行目: | 207行目: | ||
##URLの最後にスラッシュ付いていなければ付ける | ##URLの最後にスラッシュ付いていなければ付ける | ||
def _check_slash(self) -> None | NoReturn: | def _check_slash(self) -> None | NoReturn: | ||
if self.NITTER_INSTANCE[-1] != '/': | if self.NITTER_INSTANCE[-1] != '/': | ||
216行目: | 214行目: | ||
if self.ARCHIVE_TODAY_STANDARD[-1] != '/': | if self.ARCHIVE_TODAY_STANDARD[-1] != '/': | ||
raise RuntimeError('ARCHIVE_TODAY_STANDARDの末尾には/が必須です') | raise RuntimeError('ARCHIVE_TODAY_STANDARDの末尾には/が必須です') | ||
if self.TWITTER_URL[-1] | if self.TWITTER_URL[-1] != '/': | ||
raise RuntimeError('TWITTER_URLの末尾には/ | raise RuntimeError('TWITTER_URLの末尾には/が必須です') | ||
##Torが使えているかチェック | ##Torが使えているかチェック | ||
258行目: | 256行目: | ||
return self.CALLINSHOW | return self.CALLINSHOW | ||
else: | else: | ||
res: Response | None = self._request(self.NITTER_INSTANCE | res: Response | None = self._request(urljoin(self.NITTER_INSTANCE, account_str)) ##リクエストして結果取得 | ||
if res is None : ##リクエスト失敗判定 | if res is None : ##リクエスト失敗判定 | ||
self._fail() | self._fail() | ||
279行目: | 277行目: | ||
query_str.append(quote(query_input)) | query_str.append(quote(query_input)) | ||
query_input = input() | query_input = input() | ||
res: Response | None = self._request(self.NITTER_INSTANCE | print("検索クエリでリクエストしているナリ……") | ||
res: Response | None = self._request(urljoin(self.NITTER_INSTANCE, self.SEARCH_QUERY + '+'.join(query_str))) ##リクエストして結果取得 | |||
if res is None : ##リクエスト失敗判定 | if res is None : ##リクエスト失敗判定 | ||
self._fail() | self._fail() | ||
300行目: | 299行目: | ||
##self._txt_dataにwikiでテーブル表示にするためのタグをつける | ##self._txt_dataにwikiでテーブル表示にするためのタグをつける | ||
def | def _convert_to_text_table(self, text) -> str: | ||
return '{|class="wikitable" style="text-align: left;"\n' + text + '|}' | |||
##テキスト発行 | ##テキスト発行 | ||
322行目: | 321行目: | ||
def _download_media(self, media_name: Final[str]) -> bool: | def _download_media(self, media_name: Final[str]) -> bool: | ||
os.makedirs(self.MEDIA_DIR, exist_ok=True) | os.makedirs(self.MEDIA_DIR, exist_ok=True) | ||
url: Final[str] = 'https://pbs.twimg.com/media/' | url: Final[str] = urljoin('https://pbs.twimg.com/media/', media_name) | ||
res: Final[Response | None] = self._request(url) | res: Final[Response | None] = self._request(url) | ||
if res is not None: | if res is not None: | ||
if 'image' not in res.headers['content-type']: | if 'image' not in res.headers['content-type']: | ||
return False | return False | ||
with open(self.MEDIA_DIR | with open(os.path.join(self.MEDIA_DIR, media_name), "wb") as f: | ||
f.write(res.content) | f.write(res.content) | ||
return True | return True | ||
335行目: | 334行目: | ||
##ツイートの日付を取得 | ##ツイートの日付を取得 | ||
def _tweet_date(self, tweet: bs4.element.Tag) -> datetime: | def _tweet_date(self, tweet: bs4.element.Tag) -> datetime: | ||
date_str = tweet.find(class_='tweet-date').a['title'] | date_str: str = tweet.find(class_='tweet-date').a['title'] | ||
date = datetime.strptime(date_str, '%b %d, %Y · %I:%M %p | date: datetime = datetime.strptime(date_str, '%b %d, %Y · %I:%M %p %Z').replace(tzinfo=ZoneInfo('UTC')).astimezone(ZoneInfo('Asia/Tokyo')) | ||
return date | return date | ||
#self._dateの日付のツイートがなくなったときの処理 | #self._dateの日付のツイートがなくなったときの処理 | ||
def _next_day(self, date: datetime | None = None) -> None: | def _next_day(self, date: datetime | None = None) -> None: | ||
self. | if self._txt_data[0]: # 空でなければ出力 | ||
self._txt_data[0] = self._convert_to_text_table(self._txt_data[0]) | |||
if os.name == 'nt': # Windows | |||
self._txt_data[0] = self._date.strftime('\n=== %#m月%#d日 ===\n') + self._txt_data[0] | |||
print(self._date.strftime('%#m月%#d日のツイートを取得完了ですを')) | |||
else: # Mac or Linux | |||
self._txt_data[0] = self._date.strftime('\n=== %-m月%-d日 ===\n') + self._txt_data[0] | |||
print(self._date.strftime('%-m月%-d日のツイートを取得完了ですを')) | |||
if date is not None: | if date is not None: | ||
self._txt_data.insert(0,'') | self._txt_data.insert(0, '') | ||
self._date = date | self._date = date | ||
360行目: | 362行目: | ||
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): | ||
print(self.MEDIA_DIR | print(os.path.join(self.MEDIA_DIR, media_name) + ' をアップロードしなければない。') | ||
else: | else: | ||
print('https://pbs.twimg.com/media/' | print(urljoin('https://pbs.twimg.com/media/', media_name) + ' をアップロードしなければない。') | ||
# 動画についてはm3u8で落ちてきて面倒臭いため取得しない | # 動画についてはm3u8で落ちてきて面倒臭いため取得しない | ||
""" | """ | ||
378行目: | 380行目: | ||
link: str = tweet_quote.select_one('.quote-link').get('href') | link: str = tweet_quote.select_one('.quote-link').get('href') | ||
link = re.sub('#.*$', '', link) | link = re.sub('#.*$', '', link) | ||
link = self.TWITTER_URL | link = urljoin(self.TWITTER_URL, link) | ||
quote_txt = '<br>\n' + self._archive_url(link, link) | quote_txt = '<br>\n' + self._archive_url(link, link) | ||
return quote_txt | return quote_txt | ||
def _get_tweet_poll(self, tweet: bs4.element.Tag) -> str: | |||
tweet_poll: Final[bs4.element.Tag | None] = tweet.select_one('.tweet-body > .poll') | |||
poll_txt: str = '' | |||
if tweet_poll is not None: | |||
poll_meters = tweet_poll.select('.poll-meter') | |||
poll_txt += '<br>\n' | |||
for poll_meter in poll_meters: | |||
poll_txt += '<br>\n ' + poll_meter.select_one('.poll-choice-value').text + ' ' + poll_meter.select_one('.poll-choice-option').text | |||
poll_txt += '<br>\n ' + tweet_poll.select_one('.poll-info').text | |||
return poll_txt | |||
#ページからツイート本文をself._txt_dataに収めていく | #ページからツイート本文をself._txt_dataに収めていく | ||
391行目: | 404行目: | ||
if tweet.find(class_='retweet-header') is not None: ##retweet-headerはリツイートを示すので入っていれば処理しない | if tweet.find(class_='retweet-header') is not None: ##retweet-headerはリツイートを示すので入っていれば処理しない | ||
continue | continue | ||
tweet_url: str = self.TWITTER_URL | tweet_url: str = urljoin(self.TWITTER_URL, re.sub('#[^#]*$', '', tweet.find(class_='tweet-link').get('href'))) ##ツイートのURL作成 | ||
date = self._tweet_date(tweet) | date = self._tweet_date(tweet) | ||
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: | ||
398行目: | 411行目: | ||
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) ##ツイートの中身のリンクをテンプレートArchiveに変化 | ||
poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得 | |||
media_txt: str = self._get_tweet_media(tweet) ##ツイートに画像などのメディアを追加 | media_txt: str = self._get_tweet_media(tweet) ##ツイートに画像などのメディアを追加 | ||
quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加 | quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加 | ||
self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \ | ||
+ self._escape_wiki_reserved_words(tweet_content.get_text()) \ | + self._escape_wiki_reserved_words(tweet_content.get_text()) \ | ||
+ quote_txt + media_txt + '\n|-\n' \ | + poll_txt + quote_txt + media_txt + '\n|-\n' \ | ||
+ self._txt_data[0] ##wikiの文法に変化 | + self._txt_data[0] ##wikiの文法に変化 | ||
self._limit_count += 1 ##記録回数をカウント | self._limit_count += 1 ##記録回数をカウント | ||
446行目: | 460行目: | ||
if re.match('https' + self.NITTER_INSTANCE[4:], url.get('href')): | if re.match('https' + self.NITTER_INSTANCE[4:], url.get('href')): | ||
#Nitter上のTwitterへのリンクを直す | #Nitter上のTwitterへのリンクを直す | ||
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, url_link)) ##テンプレートArchiveに変化 | url.replace_with(self._archive_url(url_link, url_link)) ##テンプレートArchiveに変化 | ||
468行目: | 482行目: | ||
##URLから魚拓返す | ##URLから魚拓返す | ||
def _archive(self, url: Final[str]) -> str: | def _archive(self, url: Final[str]) -> str: | ||
archive_url: str = self.ARCHIVE_TODAY_STANDARD | 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(self.ARCHIVE_TODAY | res: Final[Response | None] = self._request(urljoin(self.ARCHIVE_TODAY, quote(unquote(url), safe='&=+?%'))) ##アクセス用URL使って結果を取得 | ||
if res is None : ##魚拓接続失敗時処理 | if res is None : ##魚拓接続失敗時処理 | ||
print(archive_url + 'にアクセス失敗ナリ。出力されるテキストにはそのまま記載されるナリ。') | print(archive_url + 'にアクセス失敗ナリ。出力されるテキストにはそのまま記載されるナリ。') | ||
487行目: | 501行目: | ||
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: str = self.NITTER_INSTANCE | new_url: str = urljoin(self.NITTER_INSTANCE, self.SEARCH + 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: | ||
515行目: | 529行目: | ||
== 実行例 == | == 実行例 == | ||
20件での実行例。 | 20件での実行例。 | ||
=== | |||
=== 9月16日 === | |||
{|class="wikitable" style="text-align: left;" | {|class="wikitable" style="text-align: left;" | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570466085534838785|2=https://archive.ph/vUWjo}} | ||
|- | |- | ||
| | | | ||
ようやく1日が終わる。<br> | |||
<br> | <br> | ||
明日7時起き、やばいな。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570466373406707713|2=https://archive.ph/suecX}} | ||
|- | |- | ||
| | | | ||
仕事に追われ、こんな毎日です。。。<br> | |||
{{Archive|1=https://twitter.com/ | {{Archive|1=https://twitter.com/kokueisecom/status/1570466219643502592|2=https://archive.ph/r6WIp|3=https://twitter.com/kokueisecom/status/1570466219643502592}} | ||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570467186497716224|2=https://archive.ph/iNx2m}} | ||
|- | |- | ||
| | | | ||
{{Archive|1=https:// | ありがとう❗️❗️<br> | ||
{{Archive|1=https://twitter.com/kokueisecom/status/1570467045560713216|2=https://archive.ph/ntkky|3=https://twitter.com/kokueisecom/status/1570467045560713216}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570468936298426370|2=https://archive.ph/yKcg7}} | ||
|- | |- | ||
| | | | ||
今日はブラックIT企業の未払い残業代の話の相談があって、若い人からの搾取やばいよな。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | |} | ||
=== 9月17日 === | |||
{|class="wikitable" style="text-align: left;" | |||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570807392899829760|2=https://archive.ph/50Ntt}} | |||
|- | |- | ||
| | | | ||
弁護士唐澤貴洋への匿名のメッセージを募集中!<br> | |||
<br> | <nowiki>#</nowiki>マシュマロを投げ合おう<br> | ||
{{Archive|1=https://marshmallow-qa.com/apt/d4905b12-9dcf-4f81-af54-b6c3e5237007|2=https://archive.ph/yaEMc|3=https://marshmallow-qa.com/apt/d4905b12-9dcf-4f81-af54-b6c3e5237007}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570878373873995779|2=https://archive.ph/bbED7}} | ||
|- | |- | ||
| | | | ||
土曜日中京5R<br> | |||
<br> | |||
◎マイド<br> | |||
<br> | |||
中山5R<br> | |||
<br> | |||
◎スノードーム | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570983261572329472|2=https://archive.ph/fIArg}} | ||
|- | |- | ||
| | | | ||
今のところ全て的中<br> | |||
<br> | |||
後は中京8レースのみ<br> | |||
[[ファイル:Fc1AvL6aAAALVhw.jpg|240px]] | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1570983442170642434|2=https://archive.ph/gLfrX}} | ||
|- | |- | ||
| | | | ||
早起きおじさんです。<br> | |||
{{Archive|1=https://twitter.com/CallinShow/status/1570983261572329472|2=https://archive.ph/fIArg|3=https://twitter.com/CallinShow/status/1570983261572329472}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | |} | ||
=== 9月19日 === | |||
{|class="wikitable" style="text-align: left;" | |||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571721136668569602|2=https://archive.ph/8l0IJ}} | |||
|- | |- | ||
| | | | ||
{{Archive|1=https://news.yahoo.co.jp/articles/7bb89d4d6c2f92807a86c05c942505592eaec60e|2=https://archive.ph/bPyFE|3=https://news.yahoo.co.jp/articles/7bb89d4d6c2f92807a86c05c942505592eaec60e}}<br> | |||
<br> | |||
{{Archive|1=https:// | 刺青入れたらカタギじゃないってどういう発想なんだ。<br> | ||
<br> | |||
カタギの定義が知りたいよ。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571796244762365952|2=https://archive.ph/6TAu7}} | |||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | |||
|- | |- | ||
| | | | ||
{{Archive|1=https://youtu.be/2trjonQCFTU|2=https://archive.ph/fFdf1|3=https://youtu.be/2trjonQCFTU}}<br> | |||
<br> | <br> | ||
神回。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571796703229145089|2=https://archive.ph/6HzLi}} | ||
|- | |- | ||
| | | | ||
日曜日中山11R<br> | |||
{{Archive|1=https://twitter.com/ | <br> | ||
◎ アスクビクターモア7.25<br> | |||
◯ラーグルフ5.75<br> | |||
▲ローシャムパーク4.75<br> | |||
△オニャンコポン 4.75<br> | |||
△ショウナンマグマ 4.75<br> | |||
△キングズパレス 4.75<br> | |||
△ボーンディスウェイ 4.75<br> | |||
△マテンロウスカイ4.25<br> | |||
△ガイアフォース 4.25<br> | |||
<br> | |||
これを元にして番組で予想しました。<br> | |||
{{Archive|1=https://twitter.com/CallinShow/status/1571796244762365952|2=https://archive.ph/6TAu7|3=https://twitter.com/CallinShow/status/1571796244762365952}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571798678263984129|2=https://archive.ph/VYez0}} | ||
|- | |- | ||
| | | | ||
みんな真剣に予想してるから太組不二雄チャンネル是非みてほしい。<br> | |||
{{Archive|1=https://twitter.com/ | <br> | ||
今日なんか1000人以上が同時接続でいたな。<br> | |||
{{Archive|1=https://twitter.com/CallinShow/status/1571796244762365952|2=https://archive.ph/6TAu7|3=https://twitter.com/CallinShow/status/1571796244762365952}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571799518383067138|2=https://archive.ph/yqeb2}} | ||
|- | |- | ||
| | | | ||
オレの予想は、競馬新聞を100%読む。<br> | |||
<br> | |||
競馬新聞は、情報の宝庫。<br> | |||
<br> | |||
競馬新聞は競馬ブック派。<br> | |||
<br> | |||
地方では、日刊競馬も買う。<br> | |||
<br> | |||
市川俊吾さんの予想を見るために。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1571804494631174144|2=https://archive.ph/APHc1}} | ||
|- | |- | ||
| | | | ||
タトゥーに対してのアンケートを取りたいと思います。<br> | |||
<br> | |||
35% タトゥーは文化だ。<br> | |||
65% タトゥーは文化じゃない。<br> | |||
717 votes • Final results | |||
|- | |- | ||
|} | |} | ||
=== | |||
=== 9月20日 === | |||
{|class="wikitable" style="text-align: left;" | {|class="wikitable" style="text-align: left;" | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572135837009342466|2=https://archive.ph/DnsUI}} | ||
|- | |- | ||
| | | | ||
弁護士唐澤貴洋への匿名のメッセージを募集中!<br> | 弁護士唐澤貴洋への匿名のメッセージを募集中!<br> | ||
<nowiki>#</nowiki>マシュマロを投げ合おう<br> | <nowiki>#</nowiki>マシュマロを投げ合おう<br> | ||
{{Archive|1=https://marshmallow-qa.com/apt/ | {{Archive|1=https://marshmallow-qa.com/apt/1c6628a8-45c6-44f2-9a93-362ce5332b61|2=https://archive.ph/Y66Sy|3=https://marshmallow-qa.com/apt/1c6628a8-45c6-44f2-9a93-362ce5332b61}} | ||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572184661442568193|2=https://archive.ph/K84kE}} | ||
|- | |- | ||
| | | | ||
@rie0985 <br> | |||
<br> | <br> | ||
書類が銀座に届いたのでご確認ください。<br> | |||
<br> | <br> | ||
お返事待っています。 | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572191953613385728|2=https://archive.ph/hJejs}} | ||
|- | |- | ||
| | | | ||
素晴らしい。<br> | |||
{{Archive|1=https://twitter.com/nyan_ika3/status/1572190195583422465|2=https://archive.ph/bQtFW|3=https://twitter.com/nyan_ika3/status/1572190195583422465}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572204964541526021|2=https://archive.ph/Keb1S}} | ||
|- | |- | ||
| | | | ||
35%いれば十分じゃない。<br> | |||
{{Archive|1=https://twitter.com/CallinShow/status/ | <br> | ||
35%がカタギじゃないって国ないだろ。<br> | |||
<br> | |||
35%の人が応援してくれる政党ありじゃない。<br> | |||
{{Archive|1=https://twitter.com/CallinShow/status/1571804494631174144|2=https://archive.ph/APHc1|3=https://twitter.com/CallinShow/status/1571804494631174144}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572208999025741824|2=https://archive.ph/2eoOV}} | ||
|- | |- | ||
| | | | ||
カタギの場所に入れないってことはカタギじゃない扱いだよな。<br> | |||
{{Archive|1=https://twitter.com/ | 定義もしっかりしてないのに、何を意味あるように話してるんだろうな。<br> | ||
{{Archive|1=https://twitter.com/CallinShow/status/1571721136668569602|2=https://archive.ph/8l0IJ|3=https://twitter.com/CallinShow/status/1571721136668569602}} | |||
|- | |- | ||
!{{CallinShowLink|1=https://twitter.com/CallinShow/status/ | !{{CallinShowLink|1=https://twitter.com/CallinShow/status/1572215385348534272|2=https://archive.ph/WsO05}} | ||
|- | |- | ||
| | | | ||
今日も疲れきった。<br> | |||
<br> | <br> | ||
オレも歳だな。 | |||
|- | |- | ||
|} | |} |
2022年9月22日 (木) 00:46時点における版
とりあえず取り急ぎ。バグ報告は利用者・トーク:夜泣き
コード
#!/usr/bin/env python3
'''
ver2.1.4 2022/9/22恒心
当コードは恒心停止してしまったhttps://rentry.co/7298gの降臨ショーツイート自動収集スクリプトの復刻改善版です
前開発者との出会いに感謝
〜〜〜〜〜〜〜〜〜〜〜〜〜【使い方】〜〜〜〜〜〜〜〜〜〜〜〜〜
・terminalに $ python3 (ファイル名) で作動します
・定数類は状況に応じて変えてください
・$ python3 (ファイル名) krsw コマンドライン引数をkrswとつけると自動モードです
・自動モードではユーザーは降臨ショー、クエリはなし、取得上限まで自動です
・つまりユーザー入力が要りません
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
ーーーーーーーーーーーーー【注意点】ーーーーーーーーーーーーー
・Pythonのバージョンは3.10以上
・環境は玉葱前提です。
・Whonix-Workstation, MacOSで動作確認済
・MacOSの場合はbrewでtorコマンドを導入し、実行
・PySocks, bs4はインストールしないと標準で入ってません
・requestsも環境によっては入っていないかもしれない
・$ pip install bs4 requests PySocks
・pipも入っていなければ$ sudo apt install pip
・バグ報告はhttps://krsw-wiki.org/wiki/?curid=15799#利用者:夜泣き/スクリプトについて
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
'''
#インポート類
import sys
import os
import codecs
import re
import json
from datetime import datetime
from zoneinfo import ZoneInfo
from time import sleep
from typing import Final, NoReturn, TypeAlias
from urllib.parse import quote, unquote, urljoin
import warnings
import requests
import bs4
from bs4 import BeautifulSoup
##おそらくはツイートの内容によってMarkupResemblesLocatorWarningが吐き出されることがあるので無効化
warnings.simplefilter('ignore')
##型エイリアス
Response: TypeAlias = requests.models.Response
class TwitterArchiver:
#定数・設定類
##nitterのインスタンス
##生きているのはhttps://github.com/zedeus/nitter/wiki/Instancesで確認
##末尾にスラッシュ必須
NITTER_INSTANCE: Final[str] = 'http://nitterqdyumlovt7tjqpdjrluitgmtpa53qq3idlpgoe4kxo7gs3xvad.onion/'
##archive.todayの魚拓
##実際にアクセスして魚拓があるか調べるのにはonionを使用
##末尾にスラッシュ必須
ARCHIVE_TODAY: Final[str] = 'http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/'
##archive.todayの魚拓
##記事の文章に使うのはクリアネット
##末尾にスラッシュ必須
ARCHIVE_TODAY_STANDARD: Final[str] = 'https://archive.ph/'
##twitterのURL
##末尾にスラッシュ必須
TWITTER_URL: Final[str] = 'https://twitter.com/'
##降臨ショーのユーザーネーム
CALLINSHOW: Final[str] = 'CallinShow'
##HTTPリクエストのタイムアウト秒数
REQUEST_TIMEOUT: Final[int] = 30
##取得するツイート数の上限
LIMIT_N_TWEETS: Final[int] = 100
##記録件数を報告するインターバル
REPORT_INTERVAL: Final[int] = 5
##HTTPリクエスト失敗時の再試行回数
LIMIT_N_REQUESTS: Final[int] = 5
##HTTPリクエスト失敗時にさらに追加する待機時間
WAIT_TIME_FOR_ERROR: Final[int] = 4
##HTTPリクエスト成功失敗関わらず待機時間
##1秒待つだけで行儀がいいクローラーだそうなので既定では1秒
##しかし日本のポリホーモは1秒待っていても捕まえてくるので注意
##https://ja.wikipedia.org/wiki/?curid=2187212
WAIT_TIME: Final[int] = 1
##nitterのURLのドメインとユーザーネーム部分の接続部品
SEARCH_QUERY: Final[str] = 'search?q=from%3A'
##HTTPリクエスト時のユーザーエージェントとヘッダ
HEADERS: Final[dict[str, str]] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0'
}
##HTTP
PROXIES: Final[dict[str, str]] = {
'http': 'socks5h://127.0.0.1:9050',
'https': 'socks5h://127.0.0.1:9050'
}
##nitterでユーザーがいなかったとき返ってくるページのタイトル
##万が一仕様変更で変わったとき用
NITTER_ERROR_TITLE: Final[str] = 'Error|nitter'
##archive.todayで魚拓がなかったときのレスポンス
##万が一仕様変更で変わったとき用
NO_ARCHIVE: Final[str] = 'No results'
##nitterで次ページ遷移時のURLから抜け落ちてる部分
SEARCH: Final[str] = 'search'
##nitterの前ページ読み込み部分の名前
##万が一仕様変更で変わったとき用
NEWEST: Final[str] = 'Load newest'
##画像などのツイートメディアをダウンロードするディレクトリ
MEDIA_DIR: Final[str] = 'tweet_media'
#関数類
def __init__(self, krsw: bool=False):
self._txt_data: list[str] = []
self._limit_count: int = 0 ##記録数
self._check_slash() ##スラッシュが抜けてないかチェック
self._check_tor() ##Torが使えているかチェック
self._check_instance() ##インスタンスが死んでないかチェック
##ユーザー名取得
if krsw:
print('名前は自動的に' + self.CALLINSHOW + 'にナリます')
name: Final[str] = self.CALLINSHOW
else:
name: Final[str] = self._get_name()
##検索クエリとページ取得
if krsw:
print('クエリは自動的になしにナリます')
self._page: Response | None = self._request(urljoin(self.NITTER_INSTANCE, self.SEARCH_QUERY + name))
if self._page is None:
self._fail()
else:
self._page: Response = self._get_query(name)
##終わりにするツイート取得
if krsw:
print('終わりにするツイートは自動的になしにナリます')
self._stop: Final[str] = ''
else:
self._stop: Final[str] = self._stop_word()
##日付取得
self._date: datetime = self._tweet_date(BeautifulSoup(self._page.text, 'html.parser').find(class_='timeline-item'))
self._txt_data.append('')
print()
##坂根輝美に場所を知らせます
def _pickup_counter(self) -> None:
print('ピックアップカウンター付近でふ')
##引数のURLにHTTP接続します
##失敗かどうかは呼出側で要判定
def _request_once(self, url: Final[str]) -> Response:
res: Response = requests.get(url, timeout=self.REQUEST_TIMEOUT, headers=self.HEADERS, allow_redirects=False, proxies=self.PROXIES)
sleep(self.WAIT_TIME) ##DoS対策で待つ
return res
##HTTP接続を再試行回数まで試します
##成功すると結果を返します
##接続失敗が何度も起きるとNoneを返します
##呼出側で要None判定
def _request(self, url: Final[str]) -> Response | None:
counter: int = 1 ##リクエスト挑戦回数を記録
while True:
try:
res: Response = self._request_once(url) ##リクエスト
res.raise_for_status() ##HTTPステータスコードが200番台以外でエラー発生
except requests.exceptions.RequestException as e:
print(url + 'への通信失敗ナリ ' + f"{counter}/{self.LIMIT_N_REQUESTS}回")
if counter < self.LIMIT_N_REQUESTS: ##エラー発生時上限まで再挑戦
counter += 1 ##現在の試行回数1回増やす
sleep(self.WAIT_TIME_FOR_ERROR) ##失敗時は長めに待つ
else:
return None ##失敗したらNone返却し呼出側で対処してもらう
else:
return res ##リクエストの結果返す
##URLの最後にスラッシュ付いていなければ付ける
def _check_slash(self) -> None | NoReturn:
if self.NITTER_INSTANCE[-1] != '/':
raise RuntimeError('NITTER_INSTANCEの末尾には/が必須です')
if self.ARCHIVE_TODAY[-1] != '/':
raise RuntimeError('ARCHIVE_TODAYの末尾には/が必須です')
if self.ARCHIVE_TODAY_STANDARD[-1] != '/':
raise RuntimeError('ARCHIVE_TODAY_STANDARDの末尾には/が必須です')
if self.TWITTER_URL[-1] != '/':
raise RuntimeError('TWITTER_URLの末尾には/が必須です')
##Torが使えているかチェック
def _check_tor(self) -> None | NoReturn:
print('Torのチェック中ですを')
try:
res: Final[Response] = self._request_once('https://check.torproject.org/api/ip') ##リクエスト
is_tor: Final[bool] = json.loads(res.text)['IsTor']
if is_tor:
print('Tor OK')
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)
##nitterのインスタンスが生きているかチェック
##死んでいたらそこで終了
##接続を一回しか試さない_request_onceを使っているのは
##激重インスタンスが指定されたとき試行回数増やして偶然成功してそのまま実行されるのを躱すため
def _check_instance(self) -> None | NoReturn:
print("nitterのインスタンスチェック中ですを")
try:
res: Final[Response] = self._request_once(self.NITTER_INSTANCE) ##リクエスト
res.raise_for_status() ##HTTPステータスコードが200番台以外でエラー発生
except requests.exceptions.RequestException as e: ##エラー発生時は終了
print(e, file=sys.stderr)
print('インスタンスが死んでますを', file=sys.stderr)
exit(1)
##ツイート収集するユーザー名を取得
##何も入力しないと尊師を指定するよう改良
def _get_name(self) -> str | NoReturn:
while True:
print('アカウント名を入れなければない。空白だと自動的に' + self.CALLINSHOW + 'になりますを')
account_str: str = input() ##ユーザー入力受付
##空欄で降臨ショー
if account_str == '':
return self.CALLINSHOW
else:
res: Response | None = self._request(urljoin(self.NITTER_INSTANCE, account_str)) ##リクエストして結果取得
if res is None : ##リクエスト失敗判定
self._fail()
soup: BeautifulSoup = BeautifulSoup(res.text, 'html.parser') ##beautifulsoupでレスポンス解析
if soup.title == self.NITTER_ERROR_TITLE: ##タイトルがエラーでないか判定
print(account_str + "は実在の人物ではありませんでした") ##エラー時ループに戻る
else:
print("最終的に出会ったのが@" + account_str + "だった。")
return account_str ##成功時アカウント名返す
##検索クエリを取得
def _get_query(self, name: Final[str]) -> Response | NoReturn:
while True:
print("検索クエリを入れるナリ。複数ある時は一つの塊ごとに入力するナリ。空欄を入れると検索開始ナリ。")
print("例:「無能」→改行→「脱糞」→改行→「弟殺し」→改行→空欄で改行")
query_str: list[str] = [name] ##検索クエリ文字列をquery_strに収納。ユーザー名を先に含めておく
query_input: str = input() ##ユーザー入力受付
##空欄が押されるまでユーザー入力受付
while query_input != '':
query_str.append(quote(query_input))
query_input = input()
print("検索クエリでリクエストしているナリ……")
res: Response | None = self._request(urljoin(self.NITTER_INSTANCE, self.SEARCH_QUERY + '+'.join(query_str))) ##リクエストして結果取得
if res is None : ##リクエスト失敗判定
self._fail()
soup: BeautifulSoup = BeautifulSoup(res.text, 'html.parser') ##beautifulsoupでレスポンス解析
##1件もないときはHTMLにtimeline-noneがあるから判定
if soup.find(class_='timeline-none') is None:
print("クエリのピースが埋まっていく。")
return res ##成功したリクエストのレスポンスを返す
else:
print("適切なクエリを入力することを切に望む。")
##接続失敗時処理
def _fail(self) -> NoReturn:
print("接続失敗しすぎで強制終了ナリ")
if len(self._txt_data) > 0: ##取得成功したデータがあれば発行
print("取得成功した分だけ発行しますを")
self._make_txt()
else:
exit(1) ##終了
##self._txt_dataにwikiでテーブル表示にするためのタグをつける
def _convert_to_text_table(self, text) -> str:
return '{|class="wikitable" style="text-align: left;"\n' + text + '|}'
##テキスト発行
def _make_txt(self) -> NoReturn:
self._next_day()
result_txt: Final[str] = '\n'.join(self._txt_data) ##リストを合体
##ファイル出力
with codecs.open('tweet.txt', 'w', 'utf-8') as f:
f.write(result_txt)
print("テキストファイル手に入ったやで〜")
exit(0) ##終了
##記録を中断するツイート
def _stop_word(self) -> str:
print(f"ここにツイートの本文を入れる!すると・・・・なんと一致するツイートで記録を中断する!(これは本当の仕様です。)ちなみに、空欄だと{self.LIMIT_N_TWEETS}件まで終了しない。")
end_str: Final[str] = input() ##ユーザー入力受付
return end_str
##Twitterの画像をダウンロード
def _download_media(self, media_name: Final[str]) -> bool:
os.makedirs(self.MEDIA_DIR, exist_ok=True)
url: Final[str] = urljoin('https://pbs.twimg.com/media/', media_name)
res: Final[Response | None] = self._request(url)
if res is not None:
if 'image' not in res.headers['content-type']:
return False
with open(os.path.join(self.MEDIA_DIR, media_name), "wb") as f:
f.write(res.content)
return True
else:
return False
##ツイートの日付を取得
def _tweet_date(self, tweet: bs4.element.Tag) -> datetime:
date_str: str = tweet.find(class_='tweet-date').a['title']
date: datetime = datetime.strptime(date_str, '%b %d, %Y · %I:%M %p %Z').replace(tzinfo=ZoneInfo('UTC')).astimezone(ZoneInfo('Asia/Tokyo'))
return date
#self._dateの日付のツイートがなくなったときの処理
def _next_day(self, date: datetime | None = None) -> None:
if self._txt_data[0]: # 空でなければ出力
self._txt_data[0] = self._convert_to_text_table(self._txt_data[0])
if os.name == 'nt': # Windows
self._txt_data[0] = self._date.strftime('\n=== %#m月%#d日 ===\n') + self._txt_data[0]
print(self._date.strftime('%#m月%#d日のツイートを取得完了ですを'))
else: # Mac or Linux
self._txt_data[0] = self._date.strftime('\n=== %-m月%-d日 ===\n') + self._txt_data[0]
print(self._date.strftime('%-m月%-d日のツイートを取得完了ですを'))
if date is not None:
self._txt_data.insert(0, '')
self._date = date
def _get_tweet_media(self, tweet: bs4.element.Tag) -> str:
tweet_media: bs4.element.Tag | None = tweet.select_one('.tweet-body > .attachments') # 引用リツイート内のメディアを選択しないように.tweet-body直下の.attachmentsを選択
media_txt: str = ''
if tweet_media is not None:
media_list: list[str] = []
# ツイートの画像の取得
for image_a in tweet_media.select('.attachment.image a'):
media_name: str = re.search(r'%2F([^%]*\.jpg)|%2F([^%]*\.jpeg)|%2F([^%]*\.png)|%2F([^%]*\.gif)', image_a.get('href')).group(1)
media_list.append(f"[[ファイル:{media_name}|240px]]")
if self._download_media(media_name):
print(os.path.join(self.MEDIA_DIR, media_name) + ' をアップロードしなければない。')
else:
print(urljoin('https://pbs.twimg.com/media/', media_name) + ' をアップロードしなければない。')
# 動画についてはm3u8で落ちてきて面倒臭いため取得しない
"""
for video in tweet_media.select('.attachment.video-container video'):
media_url: str = unquote(re.search(r'[^\/]+$', video.get('data-url')).group(0))
print(media_url)
"""
media_txt = '<br>\n' + ' '.join(media_list)
return media_txt
def _get_tweet_quote(self, tweet: bs4.element.Tag) -> str:
tweet_quote: Final[bs4.element.Tag | None] = tweet.select_one('.tweet-body > .quote.quote-big') # 引用リツイートを選択
quote_txt: str = ''
if tweet_quote is not None:
link: str = tweet_quote.select_one('.quote-link').get('href')
link = re.sub('#.*$', '', link)
link = urljoin(self.TWITTER_URL, link)
quote_txt = '<br>\n' + self._archive_url(link, link)
return quote_txt
def _get_tweet_poll(self, tweet: bs4.element.Tag) -> str:
tweet_poll: Final[bs4.element.Tag | None] = tweet.select_one('.tweet-body > .poll')
poll_txt: str = ''
if tweet_poll is not None:
poll_meters = tweet_poll.select('.poll-meter')
poll_txt += '<br>\n'
for poll_meter in poll_meters:
poll_txt += '<br>\n ' + poll_meter.select_one('.poll-choice-value').text + ' ' + poll_meter.select_one('.poll-choice-option').text
poll_txt += '<br>\n ' + tweet_poll.select_one('.poll-info').text
return poll_txt
#ページからツイート本文をself._txt_dataに収めていく
def get_tweet(self) -> None | NoReturn:
soup: Final[BeautifulSoup] = BeautifulSoup(self._page.text, 'html.parser') ##beautifulsoupでレスポンス解析
tweets: Final[bs4.element.ResultSet] = soup.find_all(class_='timeline-item') ##一ツイートのブロックごとにリストで取得
for tweet in tweets: ##一ツイート毎に処理
if tweet.a.text == self.NEWEST: ##Load Newestのボタンは処理しない
continue
if tweet.find(class_='retweet-header') is not None: ##retweet-headerはリツイートを示すので入っていれば処理しない
continue
tweet_url: str = urljoin(self.TWITTER_URL, re.sub('#[^#]*$', '', tweet.find(class_='tweet-link').get('href'))) ##ツイートのURL作成
date = self._tweet_date(tweet)
if date.year != self._date.year or date.month != self._date.month or date.day != self._date.day:
self._next_day(date)
archived_tweet_url: str = self._callinshowlink_url(tweet_url) ##ツイートURLをテンプレートCallinShowlinkに変化
tweet_content: bs4.element.Tag = tweet.find(class_='tweet-content media-body') ##ツイートの中身だけ取り出す
self._archive_soup(tweet_content) ##ツイートの中身のリンクをテンプレートArchiveに変化
poll_txt: str = self._get_tweet_poll(tweet) ##投票の取得
media_txt: str = self._get_tweet_media(tweet) ##ツイートに画像などのメディアを追加
quote_txt: str = self._get_tweet_quote(tweet) ##引用リツイートの場合、元ツイートを追加
self._txt_data[0] = '!' + archived_tweet_url + '\n|-\n|\n' \
+ self._escape_wiki_reserved_words(tweet_content.get_text()) \
+ poll_txt + quote_txt + media_txt + '\n|-\n' \
+ self._txt_data[0] ##wikiの文法に変化
self._limit_count += 1 ##記録回数をカウント
if self._limit_count % self.REPORT_INTERVAL == 0:
print(f"ツイートを{self._limit_count}件も記録したンゴwwwwwwwwwww")
if self._stop != '' and self._stop in tweet_content.get_text(): ##目的ツイートか判定
print("目的ツイート発見でもう尾張屋根")
self._make_txt()
if self._limit_count >= self.LIMIT_N_TWEETS: ##上限達成か判定
print(f"{self.LIMIT_N_TWEETS}件も記録している。もうやめにしませんか。")
self._make_txt()
#テンプレート外の#をnowikiで囲む
def _escape_hash(self, text: str) -> str:
archive_begin: Final[str] = '{{Archive|'
callinshowlink_begin: Final[str] = '{{CallinShowLink|'
archive_end: Final[str] = '}}'
current_depth: int = 0
new_text: str = ''
for i in range(len(text)):
if text[i:i+len(archive_begin)] == archive_begin or text[i:i+len(callinshowlink_begin)] == callinshowlink_begin:
current_depth += 1
elif text[i:i+len(archive_end)] == archive_end:
current_depth = max(current_depth-1, 0)
if current_depth == 0 and text[i] == '#':
new_text += '<nowiki>#</nowiki>'
else:
new_text += text[i]
return new_text
#MediaWiki文法と衝突する文字を無効化する
def _escape_wiki_reserved_words(self, text: str) -> str:
text = text.replace('\n', '<br>\n')
text = re.sub(r'^ ', ' ', text, flags=re.MULTILINE)
text = self._escape_hash(text)
return text
#tagをテンプレートArchiveの文字列に変化させる
def _archive_soup(self, tag: bs4.element.Tag) -> None:
urls_in_tweet: Final[bs4.element.ResultSet] = tag.find_all('a')
for url in urls_in_tweet:
if re.match('^https?://', url.get('href')) is not None: ##先頭にhttpが付いていない物はハッシュタグの検索ページへのリンクなので処理しない
if re.match('https' + self.NITTER_INSTANCE[4:], url.get('href')):
#Nitter上のTwitterへのリンクを直す
url_link: str = url.get('href').replace('https' + self.NITTER_INSTANCE[4:], self.TWITTER_URL)
url_link = re.sub('\?.*$', '', url_link)
url.replace_with(self._archive_url(url_link, url_link)) ##テンプレートArchiveに変化
elif re.match('piped.kavin.rocks/', url.text) or re.match('invidio.us/', url.text):
#Nitter上のYouTubeへのリンクを直す
url_link: str = url.get('href')
url_link = url_link.replace('piped.kavin.rocks/', 'youtu.be/')
url_link = url_link.replace('invidio.us/', 'youtu.be/')
url.replace_with(self._archive_url(url_link, url_link)) ##テンプレートArchiveに変化
else:
url.replace_with(self._archive_url(url.get('href'), url.get('href'))) ##テンプレートArchiveに変化
#URLをテンプレートArchiveに変化させる
def _archive_url(self, url: Final[str], text: Final[str]) -> str:
return '{{Archive|1=' + unquote(url) + '|2=' + self._archive(url) + '|3=' + unquote(text) + '}}' ##テンプレートArchiveの文字列返す
#URLをテンプレートCallinShowlinkに変化させる
def _callinshowlink_url(self, url: Final[str]) -> str:
return '{{CallinShowLink|1=' + url + '|2=' + self._archive(url) + '}}' ##テンプレートCallinShowlinkの文字列返す
##URLから魚拓返す
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の形で返される
res: Final[Response | None] = self._request(urljoin(self.ARCHIVE_TODAY, quote(unquote(url), safe='&=+?%'))) ##アクセス用URL使って結果を取得
if res is None : ##魚拓接続失敗時処理
print(archive_url + 'にアクセス失敗ナリ。出力されるテキストにはそのまま記載されるナリ。')
else:
soup: Final[BeautifulSoup] = BeautifulSoup(res.text, 'html.parser') ##beautifulsoupでレスポンス解析
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: ##魚拓があるかないか判定
print(url + "の魚拓がない。これはいけない。")
else:
archive_url = content.find('a').get('href').replace(self.ARCHIVE_TODAY, self.ARCHIVE_TODAY_STANDARD)
return archive_url
##新しいページを取得
def go_to_new_page(self) -> None | NoReturn:
soup: Final[BeautifulSoup] = BeautifulSoup(self._page.text, 'html.parser') ##beautifulsoupでレスポンス解析
show_mores: Final[bs4.element.ResultSet] = soup.find_all(class_="show-more")
for show_more in show_mores: ##show-moreに次ページへのリンクか前ページへのリンクがある
if show_more.text != self.NEWEST: ##前ページへのリンクではないか判定
new_url: str = urljoin(self.NITTER_INSTANCE, self.SEARCH + show_more.a.get('href')) ##直下のaタグのhrefの中身取ってURL頭部分と合体
res: Final[Response | None] = self._request(new_url) ##接続してHTML取ってくる
if res is None:
self._fail()
new_page_soup: Final[BeautifulSoup] = BeautifulSoup(res.text, 'html.parser') ##beautifulsoupでレスポンス解析
if new_page_soup.find(class_="timeline-end") is None: ##ツイートの終端ではtimeline-endだけのページになるので判定
print(res.url + 'に移動しますを')
self._page = res ##まだ残りツイートがあるのでページを返して再度ツイート本文収集
else:
print("急に残りツイートが無くなったな終了するか")
self._make_txt()
if __name__ == '__main__':
if sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 10):
print('Pythonのバージョンを3.10以上に上げて下さい', file=sys.stderr)
exit(1)
krsw: Final[bool] = len(sys.argv) > 1 and sys.argv[1] == 'krsw' ##コマンドライン引数があるかどうかのフラグ
twitter_archiver: TwitterArchiver = TwitterArchiver(krsw)
##ツイートを取得し終えるまでループ
while True:
twitter_archiver.get_tweet() ##self._txt_dataにページ内のリツイート以外の全ツイートの中身突っ込んでいく
twitter_archiver.go_to_new_page() ##新しいページ取得
実行例
20件での実行例。
9月16日
https://twitter.com/CallinShow/status/1570466085534838785(魚拓) |
---|
ようやく1日が終わる。 |
https://twitter.com/CallinShow/status/1570466373406707713(魚拓) |
仕事に追われ、こんな毎日です。。。 |
https://twitter.com/CallinShow/status/1570467186497716224(魚拓) |
ありがとう❗️❗️ |
https://twitter.com/CallinShow/status/1570468936298426370(魚拓) |
今日はブラックIT企業の未払い残業代の話の相談があって、若い人からの搾取やばいよな。 |
9月17日
9月19日
https://twitter.com/CallinShow/status/1571721136668569602(魚拓) |
---|
https://news.yahoo.co.jp/articles/7bb89d4d6c2f92807a86c05c942505592eaec60e(魚拓) |
https://twitter.com/CallinShow/status/1571796244762365952(魚拓) |
https://twitter.com/CallinShow/status/1571796703229145089(魚拓) |
日曜日中山11R |
https://twitter.com/CallinShow/status/1571798678263984129(魚拓) |
みんな真剣に予想してるから太組不二雄チャンネル是非みてほしい。 |
https://twitter.com/CallinShow/status/1571799518383067138(魚拓) |
オレの予想は、競馬新聞を100%読む。 |
https://twitter.com/CallinShow/status/1571804494631174144(魚拓) |
タトゥーに対してのアンケートを取りたいと思います。 |
9月20日
https://twitter.com/CallinShow/status/1572135837009342466(魚拓) |
---|
弁護士唐澤貴洋への匿名のメッセージを募集中! |
https://twitter.com/CallinShow/status/1572184661442568193(魚拓) |
@rie0985 |
https://twitter.com/CallinShow/status/1572191953613385728(魚拓) |
素晴らしい。 |
https://twitter.com/CallinShow/status/1572204964541526021(魚拓) |
35%いれば十分じゃない。 |
https://twitter.com/CallinShow/status/1572208999025741824(魚拓) |
カタギの場所に入れないってことはカタギじゃない扱いだよな。 |
https://twitter.com/CallinShow/status/1572215385348534272(魚拓) |
今日も疲れきった。 |