はじめの前のおねがい
できれば「いいね♡」をお願いします。励みになります。
はじめに
下記にお気をつけください。本コードの使用を以って、下記の「注意」と「免責」について同意されたとみなします。
注意・免責
注意:YouTube関連機能に関しましては、著作権上の問題もありますので利用には著作権者への同意をご自身でお願いします。
免責:私個人は取得されたデータによるいかなる損害も保証しません。
概要
本コードは「クリップボードに入っている文字列(URL・住所・任意文字列)」を読み取り、その内容に従って下記の操作を実行します。
- GoogleMap住所データを「乗換案内」の検索欄用に整形:郵便記号「〒」ではじまる日本の郵便番号+住所 → 住所部分のみクリップボードへ再コピー/コンソールに整形されたものを表示)
- AmazonのURL短縮:AmazonのURLから商品名などの情報を切り取ってASINのみを含んだURLに整形
- Wikipedia/WiktionaryのページIDのURLを作成:Wikipedia/Wiktionary の /wiki/ 形式URL → pageid(curid)形式URLへ変換
- YouTubeライブチャットのコメント取得(Pythonista非対応):YouTube URL → ライブチャットのJSON取得とテキストファイル化
- 上記のどれにも該当しない文字列 → URLエンコードしてクリップボードへ格納
利用者は基本的に「クリップボードへ期待する入力をコピーしてから本コードを実行する」だけです。
動作確認
作成者は以下の環境で動作確認しています。
Python3.13.5
Pythonista 3.4 (340012)
このコードはYouTubeライブチャット取得以外にはPython 3とPythonista 3に対応しています。
具体的な挙動
住所処理(class AddressProcessor
部分)
工程
- 先頭9文字(例:「〒123-4567」)を郵便番号として切り出し、残部を住所本文として扱います。
- 全角ASCII相当文字(U+FF01〜U+FF5E)を半角に正規化
- マイナス記号 U+2212(−)をハイフン - に置換
- 先頭空白を除去
- クリップボードには「空白区切りの一行住所」を格納
クリップボード
クリップボードへは
- 住所のみ
をコピーします。
入出力例
クリップボード入力:〒100-0001 東京都 千代田区 千代田 1-1
実行後のクリップボード:東京都 千代田区 千代田 1-1
コンソール
クリップボード入力:〒100-0001 東京都 千代田区 千代田 1-1
コンソールへの出力:
100-0001
東京都
千代田区
千代田
1-1
注意
・先頭が「〒」でない場合は本処理には入りません。
・郵便番号の形が不規則でも先頭9文字を機械的に切り出します(入力整形は利用者側でご確認ください)。
Amazon URL正規化(class AmazonURLProcessor
部分)
工程
- ドメインTLD(co.jp, com, de など)と ASIN(/dp/ 直後の10文字)を抽出
- 最終的に
https://www.amazon.[国]/dp/[ASINコード]
の形式を生成し、クリップボードへ格納 - コンソールには
クリップボードにコピー完了(Amazon):上記URL
を表示 - ASIN パターンが見つからない場合でも、/gp/product/ または /gp/aw/d/ を /product/, /aw/d/ に読み替えて10文字切り出しを試行
入出力例
クリップボード入力:https://www.amazon.co.jp/gp/product/B012345678/ref=……
クリップボードとコンソール出力:https://www.amazon.co.jp/dp/B012345678
注意
URL が http(非https)、smile.amazon、www なし等は対象外
Wikipedia/WiktionaryのページIDのURLを作成(class WikiURLProcessor
部分)
工程
-
.m.
の付くモバイル用ドメインは通常に - MediaWiki API(action=query, titles=…)で pageid を取得
- Wikipedia: https://[言語].wikipedia.org/?curid=[ページID]
- Wiktionary: https://[言語].wiktionary.org/w/index.php?curid=[ページID]
- 変換後のものをクリップボードにコピー
- コンソールには下記を表示
元タイトル(/wiki/ 以下)
各種エンコード済みURL(PC用とMobile用)
PageID と ID 付きURL(PC用とMobile用)
エラー/終了条件
・URLに既に?curid=
が含まれる場合:「既に変換済みです」と表示
・URL構造が不正な場合:「無効なURLです」と表示
・pageid を取得できない場合:「変換不可能なリンクです」と表示
・HTTP 通信は 10 秒でタイムアウト(WIKI_TIMEOUT変数で変更可能)
注意
MediaWiki APIは「存在しないページ」に対しpageid=-1
とmissingフラグを返すことがありますが、本コードはpageidの有無のみで判定しており、まれに不整合URLが生成される可能性があります。
YouTubeライブチャットのコメント取得(class YouTubeChatDownloader
部分)
工程
- Pythonistaの場合は「PythonistaではYouTubeチャット履歴は取得できません。」と出力
- yt-dlpを用いてライブチャットJSON([video_id]_.live_chat.json)を取得
- JSONを整形してテキストに書き出し
- タイムスタンプは最初のチャットコメントのtimestampUsecを基準時と見なし、
HH:MM:SS
形式で相対時間表示
注意
- ライブチャットが取得可能な動画に限られます。
- YouTube の権限制限により取得不可となる場合があります。
- Pythonista 3ではyt-dlpが使えず、またローカルにファイルを作成できないために、チャット取得は行いません。
- 動画IDは複数パターンを考慮しますが、カスタムURL等では失敗する可能性があります。
URLエンコードしてクリップボードへ格納(class TextURLEncoder
部分)
上記のどれにも該当しない場合
- urllib.parse.quote により URLエンコード(/ : = @ , . ! ? " ' は safe)を施し、エンコード結果をクリップボードへ格納
ソースコード
import re
import sys
import urllib.parse
import requests
try:
import clipboard as _ios_clipboard # Pythonista 3
except ImportError:
_ios_clipboard = None
try:
import pyperclip as _pyperclip
except ImportError:
_pyperclip = None
def _cb_get():
if _ios_clipboard is not None:
return _ios_clipboard.get()
if _pyperclip is not None:
return _pyperclip.paste()
sys.exit("クリップボードモジュールが利用できません。")
def _cb_set(text):
if _ios_clipboard is not None:
_ios_clipboard.set(text)
return
if _pyperclip is not None:
_pyperclip.copy(text)
return
sys.exit("クリップボードモジュールが利用できません。")
from datetime import timedelta
import json
import os
WIKI_HEADERS = {"User-Agent": "ClipboardProcessor/1.0 (contact: your_email@example.com)"}
WIKI_TIMEOUT = 10
class ClipboardURLFetcher:
def fetch_url(self):
url = _cb_get()
if not url:
sys.exit("URLをクリップボードに入れてください")
return urllib.parse.unquote(url)
class BaseURLProcessor:
def process_url(self, url):
raise NotImplementedError
class AmazonURLProcessor(BaseURLProcessor):
def process_url(self, url):
if 'https://www.amazon' not in url:
return None
domain = self.extract_domain(url)
asin = self.extract_asin(url)
if asin:
short_url = self.generate_short_url(asin, domain)
self.copy_to_clipboard(short_url)
sys.exit(0)
patterns = {
'/gp/product/': '/product/',
'/gp/aw/d/': '/aw/d/'
}
for suffix, pattern in patterns.items():
if suffix in url:
short_url = self.asin_url(url, pattern, domain)
self.copy_to_clipboard(short_url)
sys.exit(0)
return None
def extract_domain(self, url):
match = re.search(r'https://www\.amazon\.([a-z.]+)/', url)
if match:
return match.group(1)
return None
def extract_asin(self, url):
match = re.search(r'/dp/(\w{10})', url)
if match:
return match.group(1)
return None
def asin_url(self, url, pattern, domain):
pos = url.find(pattern) + len(pattern)
asin = url[pos:pos + 10]
return self.generate_short_url(asin, domain)
def generate_short_url(self, asin, domain):
return f'https://www.amazon.{domain}/dp/{asin}'
def copy_to_clipboard(self, url):
_cb_set(url)
print(f'クリップボードにコピー完了(Amazon):{url}')
class WikiURLProcessor:
def __init__(self, site):
self.site = site
def process_url(self, url):
if '?curid=' in url:
sys.exit(f'既に変換済みです:{url}')
if 'https://' not in url or (f'.{self.site}.org/wiki/' not in url and f'.m.{self.site}.org/wiki/' not in url):
return None
if f'.m.{self.site}.org/wiki/' in url:
url = url.replace(f'.m.{self.site}.org/wiki/', f'.{self.site}.org/wiki/')
match = re.search(r'https://([a-z\-]+)\.' + re.escape(self.site) + r'\.org/wiki/', url)
if not match:
sys.exit(f'無効なURLです:{url}')
lang = match.group(1)
text = url.replace(f'https://{lang}.{self.site}.org/wiki/', '')
page_id = self.get_page_id(lang, text)
if not page_id:
sys.exit(f'変換不可能なリンクです:{url}')
new_url = f'https://{lang}.{self.site}.org/?curid={page_id}' if self.site == 'wikipedia' else f'https://{lang}.{self.site}.org/w/index.php?curid={page_id}'
_cb_set(new_url)
self.print_info(url, lang, text, page_id)
return new_url
def get_page_id(self, lang, title):
url = f'https://{lang}.{self.site}.org/w/api.php'
params = {
'action': 'query',
'titles': title,
'format': 'json'
}
response = requests.get(url, params=params, headers=WIKI_HEADERS, timeout=WIKI_TIMEOUT)
data = response.json()
page = next(iter(data['query']['pages'].values()))
return page.get('pageid')
def print_info(self, url, lang, text, page_id):
encoded_url = urllib.parse.quote(url, safe='/:=@,.!?\"\'')
mobile_url = url.replace(f'.{self.site}.org/wiki/', f'.m.{self.site}.org/?curid=')
mobile_encoded_url = urllib.parse.quote(mobile_url, safe='/:=@,.!?\"\'')
print(f'{self.site.capitalize()}タイトル:{text}')
print('URLエンコード')
print(f'タイトル込みURL:{url}')
print(f'PC用エンコード済みURL:{encoded_url}')
print(f'Mobile用エンコード済みURL:{mobile_encoded_url}')
print('')
print('curid情報')
print(f'Page(s)ID:{page_id}')
print(f'PC用ID込みURL:https://{lang}.{self.site}.org/?curid={page_id}')
print(f'Mobile用ID込みURL:https://{lang}.m.{self.site}.org/?curid={page_id}')
class TextURLEncoder:
def process_text(self, text):
encoded_text = urllib.parse.quote(text, safe='/:=@,.!?"\'')
_cb_set(encoded_text)
print(f'URLエンコードされたテキストをクリップボードにコピーしました:{encoded_text}')
class YouTubeChatDownloader:
def __init__(self):
self.video_start_usec = None
def process_message(self, renderer):
if "message" in renderer:
message_items = renderer["message"]["runs"]
message = ''
for element in message_items:
if "text" in element:
message += element["text"]
elif "emoji" in element:
message += element["emoji"]["image"]["accessibility"]["accessibilityData"]["label"]
return message
return ""
def process_username(self, renderer):
if "authorName" in renderer:
name_runs = renderer["authorName"]["simpleText"]
return name_runs
return "Unknown User"
def process_superchat(self, renderer):
amount = renderer.get("purchaseAmountText", {}).get("simpleText", "")
message = self.process_message(renderer)
return message, amount
def process_timestamp(self, renderer):
timestamp_usec = int(renderer["timestampUsec"])
timestamp_delta = timedelta(microseconds=(timestamp_usec - self.video_start_usec))
hours, remainder = divmod(timestamp_delta.total_seconds(), 3600)
minutes, seconds = divmod(remainder, 60)
formatted_timestamp = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
return formatted_timestamp
def download_chat(self, url):
youtube_pattern = r'(https?://)?(www\.)?(youtube\.com|youtu\.?be)/[A-Za-z0-9_/?=&\-]+'
if not re.match(youtube_pattern, url):
print("Please provide a valid YouTube URL.")
return
from urllib.parse import urlparse, parse_qs
parsed = urlparse(url)
qs = parse_qs(parsed.query or "")
if 'v' in qs and qs['v']:
video_id = qs['v'][0]
elif parsed.netloc.endswith('youtu.be'):
video_id = parsed.path.strip('/').split('/')[0]
else:
m = re.search(r'/(shorts|embed)/([A-Za-z0-9_\-]{6,})', parsed.path)
video_id = m.group(2) if m else parsed.path.rstrip('/').split('/')[-1].split('?')[0]
try:
from yt_dlp import YoutubeDL
except Exception:
return
ydl_video_opts = {
'outtmpl': f'{video_id}_.%(ext)s',
'format': 'best',
'writesubtitles': True,
'subtitleslangs': ['live_chat'],
'subtitlesformat': 'json',
'skip_download': True
}
with YoutubeDL(ydl_video_opts) as ydl:
ydl.download([url])
input_file_name = f'{video_id}_.live_chat.json'
output_file_name = os.path.splitext(input_file_name)[0] + '.txt'
with open(input_file_name, 'r', encoding='utf-8') as infile, \
open(output_file_name, 'w', encoding='utf-8') as outfile:
lines = infile.readlines()
for line in lines:
data = json.loads(line)
action = data["replayChatItemAction"]["actions"][0]
if "addChatItemAction" in action:
item = action["addChatItemAction"]["item"]
if self.video_start_usec is None:
if "liveChatTextMessageRenderer" in item:
self.video_start_usec = int(item["liveChatTextMessageRenderer"]["timestampUsec"])
elif "liveChatPaidMessageRenderer" in item:
self.video_start_usec = int(item["liveChatPaidMessageRenderer"]["timestampUsec"])
elif "liveChatPaidStickerRenderer" in item:
self.video_start_usec = int(item["liveChatPaidStickerRenderer"]["timestampUsec"])
if "liveChatTextMessageRenderer" in item:
renderer = item["liveChatTextMessageRenderer"]
username = self.process_username(renderer)
message = self.process_message(renderer)
timestamp = self.process_timestamp(renderer)
outfile.write(f"{timestamp} [{username}]: {message}\n")
elif "liveChatPaidMessageRenderer" in item:
renderer = item["liveChatPaidMessageRenderer"]
username = self.process_username(renderer)
message, amount = self.process_superchat(renderer)
timestamp = self.process_timestamp(renderer)
outfile.write(f"{timestamp} [{username}] (Super Chat {amount}): {message}\n")
elif "liveChatPaidStickerRenderer" in item:
renderer = item["liveChatPaidStickerRenderer"]
username = self.process_username(renderer)
_, amount = self.process_superchat(renderer) # Stickersの場合の処理
timestamp = self.process_timestamp(renderer)
outfile.write(f"{timestamp} [{username}] (Super Sticker {amount})\n")
class YouTubeURLProcessor:
def process_url(self, url):
youtube_pattern = r'(https?://)?(www\.)?(youtube\.com|youtu\.?be)/[A-Za-z0-9_/?=&\-]+'
if re.match(youtube_pattern, url):
if _ios_clipboard is not None:
print("PythonistaではYouTubeチャット履歴は取得できません。")
print(url)
return url
downloader = YouTubeChatDownloader()
downloader.download_chat(url)
else:
print("YouTubeのURLではありません。")
class AddressProcessor:
def process_address(self, in_text):
if '〒' in in_text[:2]:
url = in_text[:9]
text = in_text.replace(url, '')
address = text.translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))
address = address.replace('−','-')
address = address.lstrip()
url = url.replace('〒', '')
_cb_set(address)
address = address.replace(' ', '\n')
print(url)
print(address)
else:
print("住所ではありません。")
def main():
fetcher = ClipboardURLFetcher()
url = fetcher.fetch_url()
if '〒' in url[:2]:
address_processor = AddressProcessor()
address_processor.process_address(url)
elif 'amazon' in url:
AmazonURLProcessor().process_url(url)
elif 'wikipedia.org/wiki/' in url:
WikiURLProcessor('wikipedia').process_url(url)
elif 'wiktionary.org/wiki/' in url:
WikiURLProcessor('wiktionary').process_url(url)
elif re.match(r'(https?://)?(www\.)?(youtube\.com|youtu\.?be)/[A-Za-z0-9_/?=&\-]+', url):
YouTubeURLProcessor().process_url(url)
else:
TextURLEncoder().process_text(url)
if __name__ == "__main__":
main()
以上です。お役に立てれば幸いです。