[Python] Instagramの自動フォロー解除ツールの作成(前回の続編)

はい、皆さんこんばんは。前回、インスタの自動フォローツールを作成しました。見ていない方は是非[こちら](https://qiita.com/Gretard_25565/items/fce77a2df90bc65249e5 "Instagram-follow-tool")から見ていただけると嬉しいです。んで、その続編として、フォロー解除ツールも作っていこうと思います。
## 1-1 コード
自動フォロー解除のコードはこちらです。
```ruby:Python
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options

def get_user_input():
    try:
        username = input("アンフォロー対象のユーザー名を入力してください: ")
        unfollow_interval = int(input("フォローを外す時間間隔（秒）を指定してください: "))
        headless_input = input("ヘッドレスモードで実行しますか？ (yes/no): ").strip().lower()
        headless = headless_input == "yes"
        unfollow_count = int(input("アンフォローするアカウントの数を指定してください: "))
        return username, unfollow_interval, headless, unfollow_count
    except Exception as e:
        print(f"入力エラー: {e}")
        return "", 0, False, 0

class InstaUnfollower:
    def __init__(self, driver_path, headless):
        try:
            chrome_options = Options()
            if headless:
                chrome_options.add_argument("--headless")
                chrome_options.add_argument("--disable-gpu")
                chrome_options.add_argument("--window-size=1920x1080")
            self.driver = webdriver.Chrome(service=Service(executable_path=driver_path), options=chrome_options)
        except Exception as e:
            print(f"ドライバ初期化エラー: {e}")

    def add_cookies(self, sessionid, csrftoken):
        try:
            cookies = [
                {'name': 'sessionid', 'value': sessionid, 'domain': '.instagram.com'},
                {'name': 'csrftoken', 'value': csrftoken, 'domain': '.instagram.com'}
            ]
            for cookie in cookies:
                self.driver.add_cookie(cookie)
        except Exception as e:
            print(f"クッキー追加エラー: {e}")

    def login(self, url, sessionid, csrftoken):
        try:
            self.driver.get(url)
            sleep(5)
            self.add_cookies(sessionid, csrftoken)
            self.driver.refresh()
            sleep(5)
        except Exception as e:
            print(f"ログインエラー: {e}")

    def get_followers(self, username):
        try:
            self.driver.get(f"https://www.instagram.com/{username}/followers/")
            sleep(25)
            modal_xpath = self.get_valid_xpath(['/html/body/div[7]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[3]', '/html/body/div[6]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[3]'])
            if modal_xpath:
                self.scroll_modal(modal_xpath)
                followers = self.extract_usernames_from_modal()
                return followers
            else:
                print("有効なフォロワーモーダルのXPathが見つかりませんでした。")
                return []
        except Exception as e:
            print(f"フォロワー取得エラー: {e}")
            return []

    def get_following(self, username):
        try:
            self.driver.get(f"https://www.instagram.com/{username}/following/")
            sleep(25)
            modal_xpath = self.get_valid_xpath(['/html/body/div[7]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]', '/html/body/div[6]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]'])
            if modal_xpath:
                self.scroll_modal(modal_xpath)
                following = self.extract_usernames_from_modal()
                return following
            else:
                print("有効なフォロー中モーダルのXPathが見つかりませんでした。")
                return []
        except Exception as e:
            print(f"フォロー中取得エラー: {e}")
            return []

    def get_valid_xpath(self, xpaths):
        for xpath in xpaths:
            for _ in range(3):
                try:
                    self.driver.find_element(By.XPATH, xpath)
                    return xpath
                except:
                    sleep(2)
                    continue
        return None

    def scroll_modal(self, modal_xpath):
        try:
            modal = self.driver.find_element(By.XPATH, modal_xpath)
            last_height = self.driver.execute_script("return arguments[0].scrollHeight", modal)
            while True:
                self.driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", modal)
                sleep(2)
                new_height = self.driver.execute_script("return arguments[0].scrollHeight", modal)
                if new_height == last_height:
                    break
                last_height = new_height
        except Exception as e:
            print(f"スクロールエラー: {e}")

    def extract_usernames_from_modal(self):
        try:
            usernames = []
            elements = self.driver.find_elements(By.CSS_SELECTOR, "a.notranslate._a6hd")
            for element in elements:
                usernames.append(element.text)
            return usernames
        except Exception as e:
            print(f"ユーザー名抽出エラー: {e}")
            return []

    def get_username_from_xpath(self, index):
        xpaths = [
            f'/html/body/div[7]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div[1]/div/div[{index}]/div/div/div/div[2]/div/div/div/div/div/a/div/div/span',
            f'/html/body/div[6]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div[1]/div/div[{index}]/div/div/div/div[2]/div/div/div/div/div/a/div/div/span'
        ]
        for xpath in xpaths:
            for _ in range(3):
                try:
                    element = self.driver.find_element(By.XPATH, xpath)
                    return element.text
                except:
                    sleep(2)
                    continue
        return None

    def get_unfollow_button_xpath(self, index):
        xpaths = [
            f'/html/body/div[7]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div[1]/div/div[{index}]/div/div/div/div[3]/div/button',
            f'/html/body/div[6]/div[1]/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div[1]/div/div[{index}]/div/div/div/div[3]/div/button'
        ]
        for xpath in xpaths:
            for _ in range(3):
                try:
                    self.driver.find_element(By.XPATH, xpath)
                    return xpath
                except:
                    sleep(2)
                    continue
        return None

    def unfollow_users_from_following_page(self, username, followers, unfollow_interval, unfollow_count, exceptions):
        try:
            sleep(3)
            unfollowed = 0
            index = 1
            while unfollowed < unfollow_count:
                user = self.get_username_from_xpath(index)
                if user and user not in followers and user not in exceptions:
                    unfollow_button_xpath = self.get_unfollow_button_xpath(index)
                    if unfollow_button_xpath:
                        unfollow_button = self.driver.find_element(By.XPATH, unfollow_button_xpath)
                        if unfollow_button.text in ["フォロー中", "Following"]:
                            unfollow_button.click()
                            sleep(1)
                            confirm_button_xpath = self.get_valid_xpath(["//button[text()='フォローをやめる']", "//button[text()='Unfollow']"])
                            if confirm_button_xpath:
                                confirm_button = self.driver.find_element(By.XPATH, confirm_button_xpath)
                                confirm_button.click()
                                sleep(unfollow_interval)
                                unfollowed += 1
                index += 1
            self.driver.quit()
        except Exception as e:
            print(f"フォロー中ページからのアンフォロー処理エラー: {e}")

if __name__ == "__main__":
    sessionid = "56212316015%3AcLlyPoM07Y60Lb%3A20%3AAYf_BNXK_lLxdbFAkAY_YMreVtsvhDUSUj9MLmNtzA"
    csrftoken = "V7Ld2XcJL714piRUrvunpole7uH2rFcv"
    chrome_driver_path = r"/usr/local/bin/chromedriver-linux64/chromedriver"
    
    username, unfollow_interval, headless, unfollow_count = get_user_input()
    bot = InstaUnfollower(driver_path=chrome_driver_path, headless=headless)
    bot.login(url="https://www.instagram.com/", sessionid=sessionid, csrftoken=csrftoken)
    
    followers = bot.get_followers(username)
    following = bot.get_following(username)
    
    unfollow_list = list(set(following) - set(followers))
    print("アンフォロー対象:")
    for i, user in enumerate(unfollow_list, start=1):
        print(f"{i}. {user}")
    
    exceptions_input = input("例外に追加するユーザーの番号をカンマ区切りで入力してください（例: 1,3,5）: ")
    exceptions = []
    try:
        exception_indices = [int(index.strip()) - 1 for index in exceptions_input.split(',')]
        for index in exception_indices:
            if 0 <= index < len(unfollow_list):
                exceptions.append(unfollow_list[index])
            else:
                print(f"無効な番号: {index + 1}")
    except ValueError:
        print("無効な入力です。番号をカンマ区切りで入力してください。")

    # フォロー解除を実行
    bot.unfollow_users_from_following_page(username, followers, unfollow_interval, unfollow_count, exceptions)

    print("処理が完了しました。")
```

- ## 1-2 使用方法

前回と同じようにcookie(sessionid,csrftoken)を打ち込んでください。基本的にはここまでしたらもう使えます。ですが尺が足りなさすぎるので機能などを解説します。

- ## 1-3 実装機能
ここではこのコードでは何ができるかについてを解説します。

- ### アンフォローするユーザーの選択
ここはまあ一応自動検出してできるようにできたんですがそこまでするのはちょっと面倒だったのでこの機能が欲しい場合は改造してください。まあ別にそんな不便ではないので問題ないです。どうせcookie情報をそれぞれ分けて書くのでusernameのinputに最初からアンフォロー対象のユーザー名入れておくのもありです。

- ### フォロー解除の時間間隔の設定

フォロー解除が早すぎると自動化を検出されてしまうのでフォロー解除の時間間隔を設定できるようにしました。目安などは一概には言えませんが私は600秒あたりで使用しています。
- ### フォロー解除をするユーザーの数の指定

これも検出対策です。多すぎると検出を食らうので控えめにしてください。目安は150-200くらいです。

- ### ヘッドレスモードの指定

デバッグ用です。何もしないのであれば有効化すれば良いと思います。

- ## 2-1 あとがき
そんな感じです。エラーが出た場合は適宜修正お願いします。インスタの要素は良く変更されるのでxpathのモーダル確認したほうが良いかもです。
ご視聴ありがとうございました。