アフェリエイターへの道（３）楽天編

https://qiita.com/EasyCording/items/5910a034b9f78d935c34

# HATENA + Rakute アフェリエイト

本日、さくっと実装終了
意外に時間がかかった。

一番の問題点は、ＡＩが勝手にbeautiful soupを使おうとして、ドツボにはまる。

```python

import gradio as gr
import requests
import json
from bs4 import BeautifulSoup
import xml.etree.ElementTree as ET
from hatena import load_credentials, retrieve_hatena_blog_entries, select_elements_of_tag, return_next_entry_list_uri, is_draft, get_public_link
from rakuten_ import generate_rakuten_affiliate_link
from hatena_post import post_to_hatena
import os
import pdb
from rakuten_ import generate_rakuten_affiliate_link
from dotenv import load_dotenv
import os
import requests
import json

import re
import html


load_dotenv()

def get_blog_entries():
    # はてなブログの設定
    hatena_id = "HATENA-ID"
    blog_domain = "プログID"

    # 認証情報の取得
    user_pass_tuple = load_credentials(hatena_id)

    # root endpointを設定
    root_endpoint = f"https://blog.hatena.ne.jp/{hatena_id}/{blog_domain}/atom"
    blog_entries_uri = f"{root_endpoint}/entry"

    entries = []

    while blog_entries_uri:
        print(f"Requesting entries from: {blog_entries_uri}")  # デバッグ用
        entries_xml = retrieve_hatena_blog_entries(blog_entries_uri, user_pass_tuple)
        root = ET.fromstring(entries_xml)

        links = select_elements_of_tag(root, "{http://www.w3.org/2005/Atom}link")
        blog_entries_uri = return_next_entry_list_uri(links)

        entry_elements = select_elements_of_tag(root, "{http://www.w3.org/2005/Atom}entry")
        for entry in entry_elements:
            if is_draft(entry):
                continue
            title = entry.find("{http://www.w3.org/2005/Atom}title").text
            entry_id = entry.find("{http://www.w3.org/2005/Atom}id").text
            api_link = entry.find("{http://www.w3.org/2005/Atom}link[@rel='edit']").get('href')
            if api_link:
                print(f"Found entry: {title} - {api_link}")  # デバッグ用
                entries.append(f"{title}|{api_link}")

    return entries



def cohere_api_request(payload):
    url = "https://api.cohere.ai/v1/chat"

    headers = {
        "accept": "application/json",
        "content-type": "application/json",
        "Authorization": os.getenv("COHERE_API_KEY")
    }

    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"APIリクエストエラー: {e}")
    except json.JSONDecodeError as e:
        print(f"JSONデコードエラー: {e}")
    except Exception as e:
        print(f"予期せぬエラー: {e}")
    return None

def extract_keywords(content):
    chat_history = [
        {"role": "USER", "message": "あなたは専門的なブログ解析AIです。与えられた記事から重要なキーワードを抽出する任務があります。"},
        {"role": "CHATBOT", "message": "はい、承知しました。記事の内容を詳細に分析し、最も重要で代表的なキーワードを抽出いたします。"},
        {"role": "USER", "message": "キーワードは以下の条件を満たす必要があります：\n1. 記事の主題を適切に表現している\n2. 一般的で検索されやすい単語や短いフレーズである\n3. 製品名や固有名詞がある場合は、それらを優先的に含める\n4. 長すぎる単語や複雑な表現は避ける\n5. カタカナ語と日本語の両方を考慮する"},
        {"role": "CHATBOT", "message": "了解しました。これらの条件に基づいて、最適なキーワードを選択いたします。特に製品名や固有名詞を優先的に抽出します。"},
    ]

    payload = {
        "chat_history": chat_history,
        "message": f"""以下の記事から条件に合致するキーワードを正確に10個抽出してください。特に製品名や商品名を優先的に抽出してください。
        出力形式: キーワード1, キーワード2, キーワード3, キーワード4, キーワード5, キーワード6, キーワード7, キーワード8, キーワード9, キーワード10
        注意事項:
        - キーワードは必ず10個にしてください。
        - カンマと半角スペースで区切ってください。
        - キーワードの前後に余分な空白を入れないでください。
        - 出力は上記の形式のみとし、説明や追加のコメントは不要です。
        - 製品名や商品名を優先的に抽出し、それらが含まれていることを確認してください。

        記事本文:
        {content}
        """,
        "connectors": [],
        "prompt_truncation": "AUTO"
    }

    result = cohere_api_request(payload)
    if result and 'text' in result:
        keywords = result['text'].strip().split(', ')
        if len(keywords) == 10:
            return keywords
        else:
            print(f"警告: 抽出されたキーワードの数が10個ではありません。実際の数: {len(keywords)}")
            return keywords[:10] if len(keywords) > 10 else keywords + [''] * (10 - len(keywords))
    return []

def generate_affiliate_links(keywords):
    affiliate_links = []
    for keyword in keywords:
        if keyword:  # 空の文字列でないことを確認
            link = generate_rakuten_affiliate_link(keyword)
            if link:
                affiliate_links.append((keyword, link))
    return affiliate_links



def preview_content(content):
    # HTMLエスケープ
    escaped_content = html.escape(content)
    
    # 改行を<br>タグに変換
    formatted_content = escaped_content.replace('\n', '<br>')
    
    # プレビュー用のHTMLテンプレート
    preview_html = f"""
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>記事プレビュー</title>
        <style>
            body {{
                font-family: Arial, sans-serif;
                line-height: 1.6;
                color: #333;
                max-width: 800px;
                margin: 0 auto;
                padding: 20px;
            }}
            a {{
                color: #0066cc;
                text-decoration: none;
            }}
            a:hover {{
                text-decoration: underline;
            }}
        </style>
    </head>
    <body>
        <div class="article-content">
            {formatted_content}
        </div>
    </body>
    </html>
    """
    
    return preview_html


from hatena_post import post_to_hatena
import os

def post_updated_content(title, content):
    # はてなブログの設定
    hatena_id = "HATENA-ID"
    blog_domain = "プログID"
    
    # APIキーを環境変数から取得
    api_key = os.getenv("HATENA_BLOG_ATOMPUB_KEY")
    
    if not api_key:
        return "エラー: HATENA_BLOG_ATOMPUB_KEY が設定されていません。"

    try:
        # 記事を投稿
        status_code, response_text = post_to_hatena(hatena_id, blog_domain, api_key, title, content)
        
        # ステータスコードに基づいてメッセージを返す
        if status_code == 201:
            return f"成功: 記事「{title}」が正常に投稿されました。"
        elif status_code == 200:
            return f"成功: 記事「{title}」が正常に更新されました。"
        else:
            return f"エラー: 投稿に失敗しました。ステータスコード: {status_code}, レスポンス: {response_text}"
    
    except Exception as e:
        return f"エラー: 投稿処理中に例外が発生しました。{str(e)}"
    

def process_and_post(selected_article, updated_content):
    title = selected_article.split(',')[0]  # タイトルを取得（カンマで分割された最初の要素）
    result = post_updated_content(title, updated_content)
    return result

def get_article_content(selected_article):
    title, api_url = selected_article.split('|', 1)
    hatena_id = "motochan1969"
    user_pass_tuple = load_credentials(hatena_id)

    print(f"Requesting: {api_url}")  # デバッグ用

    response = requests.get(api_url, auth=user_pass_tuple)

    if response.status_code == 200:
        root = ET.fromstring(response.content)
        content_element = root.find('{http://www.w3.org/2005/Atom}content')
        
        if content_element is not None:
            content_type = content_element.get('type', '')
            if content_type == 'html':
                return content_element.text
            else:
                # HTMLでない場合は、テキストを適切にHTMLに変換
                return f"<p>{html.escape(content_element.text).replace('\n', '<br>')}</p>"
        else:
            return "<p>エラー: 記事のコンテンツが見つかりません。</p>"
    else:
        return f"<p>エラー: 記事の取得に失敗しました。ステータスコード: {response.status_code}</p>"
    
    
import re
from html import escape

import re
from bs4 import BeautifulSoup, NavigableString


import re

def replace_keywords_with_links(html_content, replacements):
    for keyword, link_html in replacements:
        # href属性を抽出
        href_match = re.search(r'href="([^"]*)"', link_html)
        if href_match:
            href = href_match.group(1)
            
            # target属性を抽出（存在する場合）
            target_match = re.search(r'target="([^"]*)"', link_html)
            target_attr = f' target="{target_match.group(1)}"' if target_match else ''
            
            # 新しいリンクを作成
            new_link = f'<a href="{href}"{target_attr}>{keyword}</a>'
            
            # キーワードを新しいリンクで置換
            pattern = re.compile(re.escape(keyword))
            html_content = pattern.sub(new_link, html_content)
    
    return html_content


def process_article(selected_article):
    content_html = get_article_content(selected_article)
    if content_html.startswith("<p>エラー:"):
        return content_html, "", "", "", "", ""
    
    print("Original Content:", content_html[:500])
    soup = BeautifulSoup(content_html, 'html.parser')
    content_text = soup.get_text()
    
    keywords = extract_keywords(content_text)
    print("Extracted Keywords:", keywords)
    
    affiliate_links = generate_affiliate_links(keywords)
    print("Generated Affiliate Links:", affiliate_links)
    
    updated_content = replace_keywords_with_links(content_html, affiliate_links)
    print("Updated Content:", updated_content[:500])
    
    preview_html = preview_content(updated_content)
    return content_html, keywords, affiliate_links, updated_content, preview_html, updated_content


    
with gr.Blocks() as demo:
    gr.Markdown("# はてなブログアフィリエイト追加ツール")
  
    with gr.Row():
        article_list = gr.Dropdown(label="記事一覧", choices=get_blog_entries())
        process_btn = gr.Button("処理開始")
    
    with gr.Row():
        original_content = gr.Textbox(label="元の記事内容")
        preview = gr.HTML(label="プレビュー")
    
    with gr.Row():
        keywords = gr.Textbox(label="抽出されたキーワード")
        affiliate_links = gr.Textbox(label="生成されたアフィリエイトリンク")
   
    with gr.Row():
        updated_content_html = gr.Code(label="更新された記事内容 (HTML)", language="html")
        updated_content_preview = gr.HTML(label="更新された記事内容 (プレビュー)")
    
    post_btn = gr.Button("投稿")
    result = gr.Textbox(label="結果")
    
    process_btn.click(
        process_article, 
        inputs=[article_list], 
        outputs=[original_content, keywords, affiliate_links, updated_content_html, preview, updated_content_preview]
    )

    post_btn.click(post_updated_content, inputs=[article_list, updated_content_html], outputs=[result])
    post_btn.click(process_and_post, inputs=[article_list, updated_content_html], outputs=[result])

demo.launch()
```

# とりあえず画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/fe535f6a-1141-1585-6a7c-ba3e87304fb1.png)

リンク先が設定されたプログ記事

https://arafo40tozan.hatenadiary.jp/entry/20240715/1721027207

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212914/f572f15e-1201-884c-2316-473be6d8afe1.png)


# 課題

HANETAブログの場合、投稿時に使ったエディタの設定が、後で変えられない。
今回はHTMLで処理しているが、マークダウンでも対応できた方がいい。

```python
import re

def replace_keywords_with_links(html_content, replacements):
    for keyword, link_html in replacements:
        # href属性を抽出
        href_match = re.search(r'href="([^"]*)"', link_html)
        if href_match:
            href = href_match.group(1)
            
            # target属性を抽出（存在する場合）
            target_match = re.search(r'target="([^"]*)"', link_html)
            target_attr = f' target="{target_match.group(1)}"' if target_match else ''
            
            # 新しいリンクを作成
            new_link = f'<a href="{href}"{target_attr}>{keyword}</a>'
            
            # キーワードを新しいリンクで置換
            pattern = re.compile(re.escape(keyword))
            html_content = pattern.sub(new_link, html_content)
    
    return html_content
# HTMLコンテンツのサンプル
html_content = '''
<p>太神山でのテント泊体験レポート<br/><br/>梅雨明けの太神山でテント泊を楽しんできました。太神山は大津市にある標高637mの低山で、初心者でも挑戦しやすい山として知られています[1][4]。<br/><br/>準備段階では、軽量化を心がけつつ必要な装備を揃えました。テント、寝袋、マット、ヘッドライト、携帯コンロ、調理器具、食料、水、防寒着、雨具などが主な持ち物です。初めてのテント泊だったため、装備選びには特に気を使いました。<br/><br/>1日目は登山口から約3時間かけて山頂付近のキャンプサイトに到着しました。途中、鹿の鳴き声を聞くなど、自然を身近に感じられる瞬間がありました。キャンプサイトでは、テント設営後に簡単な夕食を作り、満天の星空を眺めながら他の参加者と交流を楽しみました。<br/><br/>2日目は朝日とともに起床し、朝食後にテントを片付けて下山しました。下山中に一時的に道に迷う場面もありましたが、無事に登山口まで戻ることができました。<br/><br/>この体験を通じて、準備の大切さと自然の中で過ごすことの素晴らしさを実感しました。鹿の鳴き声や満天の星空など、印象的な思い出がたくさんできました[4]。<br/><br/>太神山は湖南アルプスの一部で、近くには堂山もあります。この地域は手軽に楽しめる山岳エリアとして人気があり、今回のような短期のテント泊登山に適しています[1][5]。<br/><br/>テント泊登山は準備
'''

# 置換するキーワードとリンクのリスト
replacements = [
    ('テント泊', '<a href="http://hb.afl.rakuten.co.jp/hgc/1feccffa.7c7bccd7.1feccffb.111f7d7e/?pc=https%3A//product.rakuten.co.jp/product/-/9c996843f40d53fa6bbd8300d3f1b9b1/%3Frafcid%3Dwsc_i_ps_1056199525991339251&m=https%3A//product.rakuten.co.jp/m/product/-/9c996843f40d53fa6bbd8300d3f1b9b1/%3Frafcid%3Dwsc_i_ps_1056199525991339251" target="_blank">'),
    ('低山', '<a href="http://hb.afl.rakuten.co.jp/hgc/1feccffa.7c7bccd7.1feccffb.111f7d7e/?pc=https%3A//product.rakuten.co.jp/product/-/579326138af9cec99d3f13fc7d764936/%3Frafcid%3Dwsc_i_ps_1056199525991339251&m=https%3A//product.rakuten.co.jp/m/product/-/579326138af9cec99d3f13fc7d764936/%3Frafcid%3Dwsc_i_ps_1056199525991339251" target="_blank">'),
    ('キャンプサイト', '<a href="http://hb.afl.rakuten.co.jp/hgc/1feccffa.7c7bccd7.1feccffb.111f7d7e/?pc=https%3A//product.rakuten.co.jp/product/-/2b3c3133d9a615d1a7a451e88df7d9c0/%3Frafcid%3Dwsc_i_ps_1056199525991339251&m=https%3A//product.rakuten.co.jp/m/product/-/2b3c3133d9a615d1a7a451e88df7d9c0/%3Frafcid%3Dwsc_i_ps_1056199525991339251" target="_blank">'),
    ('自然', '<a href="http://hb.afl.rakuten.co.jp/hgc/1feccffa.7c7bccd7.1feccffb.111f7d7e/?pc=https%3A//product.rakuten.co.jp/product/-/00a869433b08c4cfb802db4572cfe758/%3Frafcid%3Dwsc_i_ps_1056199525991339251&m=https%3A//product.rakuten.co.jp/m/product/-/00a869433b08c4cfb802db4572cfe758/%3Frafcid%3Dwsc_i_ps_1056199525991339251" target="_blank">')
]

# HTMLコンテンツにリンクを挿入
linked_content = replace_keywords_with_links(html_content, replacements)
print(linked_content)
```

# 今後の予定

明日中にPOSTしてブログ投稿を終了
とりあえず、プロンプトは画像つきにするか？
全ブログ記事を自動化するモードは、それから検討する
WORDPRESSにしようかと思ったが、デザインが整っているHATENAの方がやりやすい
AMEBAブログは、残念ながらAPI自動化は困難、かつアフェリエイト広告も貼りこめない　→　捨てるか？


# はい、ブログ記事を2000字程度で書き、指示内容に沿って作成いたします。

タイトル: 
「HATENA + Rakuten アフィリエイト: 簡単実装で収益化への第一歩」

ImageCreator用プロンプト:
A computer screen displaying the Hatena Blog interface with Rakuten affiliate links integrated, showing a mix of Japanese and English text. The image should have a professional and clean look, with a soft blue background. miniature

本文:

こんにちは、皆さん。今日は、HATENAブログとRakutenアフィリエイトを組み合わせた、簡単かつ効果的な収益化方法についてお話しします。最近、この方法を実装してみたのですが、予想以上に時間がかかってしまいました。しかし、その過程で学んだことや注意点を皆さんと共有できればと思います。

まず、HATENAブログとRakutenアフィリエイトの連携の魅力について触れておきましょう。HATENAは使いやすさと豊富な機能で人気のブログプラットフォームです。一方、Rakutenアフィリエイトは日本最大級のECサイトと提携しているため、幅広い商品を紹介できるのが強みです。この2つを組み合わせることで、ブログの内容に関連した商品を自然に紹介し、収益化の可能性を広げることができるのです。

さて、実装にあたって最も苦労したのは、Beautiful Soupの使用でした。当初、このPythonライブラリを使ってHTML解析をしようとしたのですが、思わぬところでつまずいてしまいました。Beautiful Soupは強力なツールですが、HATENAブログの構造に合わせて適切に使用するのは予想以上に難しかったのです。

そこで、別のアプローチを取ることにしました。HATENAブログのAPI（Application Programming Interface）を直接利用する方法です。これにより、ブログの記事データを直接取得し、必要な情報を抽出することができました。

以下に、実装の核となるコードの一部を紹介します：

```python
import requests
import xml.etree.ElementTree as ET
from hatena import load_credentials, retrieve_hatena_blog_entries
from rakuten_ import generate_rakuten_affiliate_link

# HATENAブログの設定
hatena_id = "YOUR_HATENA_ID"
blog_domain = "YOUR_BLOG_DOMAIN"

# 認証情報の取得
user_pass_tuple = load_credentials(hatena_id)

# ブログエントリーの取得
blog_entries_uri = f"https://blog.hatena.ne.jp/{hatena_id}/{blog_domain}/atom/entry"
entries_xml = retrieve_hatena_blog_entries(blog_entries_uri, user_pass_tuple)

# XMLの解析
root = ET.fromstring(entries_xml)
for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):
    title = entry.find('{http://www.w3.org/2005/Atom}title').text
    content = entry.find('{http://www.w3.org/2005/Atom}content').text
    
    # Rakutenアフィリエイトリンクの生成
    affiliate_link = generate_rakuten_affiliate_link(title)
    
    # コンテンツにアフィリエイトリンクを追加
    updated_content = content + f'\n\n関連商品: {affiliate_link}'
    
    # 更新されたコンテンツをHATENAブログに投稿
    # (この部分は省略しています)
```

このコードでは、HATENAブログのエントリーを取得し、各エントリーのタイトルを使ってRakutenアフィリエイトリンクを生成しています。そして、生成されたリンクを記事の末尾に追加しています。

実装する際に注意すべき点がいくつかあります：

1. API利用の制限: HATENAブログのAPIには利用制限があるので、大量のリクエストを短時間で送らないよう注意しましょう。

2. コンテンツの自然さ: アフィリエイトリンクは記事の内容に自然に溶け込むよう心がけましょう。読者にとって価値のある情報提供を心がけることが大切です。

3. SEO対策: アフィリエイトリンクの過剰な使用は検索エンジンのペナルティの対象になる可能性があります。適切な量を保つよう注意しましょう。

4. 更新頻度: 定期的にリンクを更新し、最新の商品情報を提供することで、読者の興味を維持しましょう。

この実装により、ブログの各記事に関連する商品を自動的に紹介することができるようになりました。例えば、料理のレシピを紹介する記事であれば、【調理器具セット】などの関連商品のリンクを自然に挿入することができます。

最後に、この方法はあくまでも収益化の一つの手段に過ぎません。最も大切なのは、読者にとって価値のあるコンテンツを提供し続けることです。アフィリエイトは読者のニーズに応える形で活用しましょう。

今回の実装を通じて、技術的なスキルアップだけでなく、コンテンツマーケティングの重要性も再認識しました。これからも、読者の皆さんに喜んでいただけるような記事作りを心がけていきたいと思います。

皆さんも、ぜひHATENAブログとRakutenアフィリエイトの組み合わせにチャレンジしてみてください。きっと新しい可能性が見えてくるはずです。

最後になりましたが、いつも当ブログをご覧いただき、ありがとうございます。これからも有益な情報をお届けしていきますので、ぜひ【RSS登録】をしてください。また、更新情報やお得な情報をお届けしていますので、YouTubeチャンネルへの登録もお願いします。新しいイベント情報などは、Facebookページから随時お知らせしていますので、そちらもチェックしてくださいね。皆さんのご支援が、より良いコンテンツ作りの励みになります。これからもどうぞよろしくお願いいたします！