0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリップボードのURLの整形補助:AmazonのASIN以外の部分を削ったり、GoogleMapの住所から郵便番号を取ったり、WikipediaやWiktionaryからIDを抽出したり、YouTubeライブチャットコメントを抽出したりするコード

Last updated at Posted at 2025-08-30

はじめの前のおねがい

できれば「いいね♡」をお願いします。励みになります。

はじめに

下記にお気をつけください。本コードの使用を以って、下記の「注意」と「免責」について同意されたとみなします。

注意・免責

注意:YouTube関連機能に関しましては、著作権上の問題もありますので利用には著作権者への同意をご自身でお願いします。

免責:私個人は取得されたデータによるいかなる損害も保証しません

概要

本コードは「クリップボードに入っている文字列(URL・住所・任意文字列)」を読み取り、その内容に従って下記の操作を実行します。

  1. GoogleMap住所データを「乗換案内」の検索欄用に整形:郵便記号「〒」ではじまる日本の郵便番号+住所 → 住所部分のみクリップボードへ再コピー/コンソールに整形されたものを表示)
  2. AmazonのURL短縮:AmazonのURLから商品名などの情報を切り取ってASINのみを含んだURLに整形
  3. Wikipedia/WiktionaryのページIDのURLを作成:Wikipedia/Wiktionary の /wiki/ 形式URL → pageid(curid)形式URLへ変換
  4. YouTubeライブチャットのコメント取得(Pythonista非対応):YouTube URL → ライブチャットのJSON取得とテキストファイル化
  5. 上記のどれにも該当しない文字列 → URLエンコードしてクリップボードへ格納

利用者は基本的に「クリップボードへ期待する入力をコピーしてから本コードを実行する」だけです。

動作確認

作成者は以下の環境で動作確認しています。
Python3.13.5
Pythonista 3.4 (340012)
このコードはYouTubeライブチャット取得以外にはPython 3Pythonista 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)を施し、エンコード結果をクリップボードへ格納

ソースコード

clipboard_converter.py
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()

以上です。お役に立てれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?