WEJUMP INVESTMENT LABS

Systematic Alpha & Quantitative Insights

유튜브 생방송 중 채팅방 입장

[WeJump 무료 툴] 티스토리 맞구독 확인 프로그램

[무료 배포] 티스토리 맞구독 확인 프로그램, 답답해서 직접 만들었습니다!

안녕하세요! 여러분의 투자 파트너이자 'WeJump 투자전략연구소'입니다. 😊

티스토리 블로그를 정성껏 운영하시면서 '맞구독' 관리, 다들 어떻게 하고 계시나요? 구독자는 점점 늘어가는데, "과연 이 중에서 나를 계속 구독해주고 계신 분은 누구일까?", "나는 구독했는데 상대방은 취소한 건 아닐까?" 궁금할 때가 참 많죠.

하나하나 수작업으로 찾아보자니 눈이 빠질 것 같고, 구글에 아무리 검색해 봐도 티스토리용 맞구독 체크 프로그램은 마땅한 게 없더라고요.

"없으면 어떡한다? 내가 직접 만든다!"

그래서 제 블로그에 방문해 주시는 소중한 이웃분들과 구독자님들을 위해, 제가 직접 '티스토리 맞구독 확인 프로그램 (Tistory Follow Checker)'을 개발했습니다. 물론 조건 없이 무료로 배포합니다! 🎉

tistory-follow-checker_V1.2.zip
0.01MB

💡 WeJump표 맞구독 확인 프로그램의 장점

단순히 화면에 보이는 글자만 대충 긁어오는 허술한 프로그램이 아닙니다. 확실하게 만들었습니다.

  1. 소름 돋는 정확성 (데이터 기반)
    • 겉보기에만 의존하는 크롤링 방식이 아닙니다. Playwright라는 강력한 도구를 사용해 티스토리 내부 API 데이터를 직접 비교하므로 결과가 매우 정확합니다.
  2. 절대적인 안전성 (내 PC에서만!)
    • 혹시 내 계정 정보가 어디로 새어나가지 않을까 걱정되시나요? 이 프로그램은 오직 여러분의 로컬 PC 안에서만 돌아갑니다. 비밀번호나 개인정보를 절대 가로채지 않으니 안심하고 사용하세요.

🚀 왕초보도 따라 하는 초간단 사용 방법

파이썬(Python)이나 터미널(CMD) 같은 단어가 낯설어도 겁먹지 마세요! 아래 순서대로 천천히 따라 하시면 됩니다.

1단계: Python 설치하기

  • PC에 Python 3.7 이상 버전이 설치되어 있어야 합니다. (없으시다면 파이썬 공식 홈페이지에서 다운로드해 주세요!)

2단계: 필수 패키지 설치하기

  • 윈도우 검색창에 cmd를 검색해서 '명령 프롬프트(터미널)'를 열어줍니다.
  • 그리고 아래 명령어를 복사해서 붙여넣고 엔터를 쳐주세요.
pip install -r requirements.txt
playwright install chromium

 

3단계: 프로그램 실행하기

  • 패키지 설치가 끝났다면, 터미널 창에 아래 명령어를 입력해서 프로그램을 실행합니다.
python checker.py

 

4단계: 브라우저 로그인 & 자동 분석

  • 프로그램이 실행되면서 인터넷 브라우저 창이 하나 짠! 하고 열릴 겁니다.
  • 평소처럼 티스토리에 로그인을 해주세요.
  • 로그인 후 피드(Feed) 페이지로 넘어가면, 프로그램이 알아서 척척 분석을 시작합니다. (이때는 가만히 지켜보시면 됩니다 😎)

5단계: 결과 확인하기

  • 분석이 끝나면 터미널 창에 '내가 구독했지만 나를 맞구독하지 않은' 블로그 목록이 출력됩니다.
  • 상세한 결과는 폴더 안에 tistory_follow_report.txt 파일로 예쁘게 저장되니 언제든 열어보실 수 있습니다.

보너스 단계: 대시보드 만들기

  • 보기 좋은 웹페이지 형태로 결과를 보고 싶으시다면, 터미널에 아래 명령어를 한 번 더 입력해 주세요.
  • 예쁜 대시보드 파일이 뚝딱 생성됩니다!
python make_html.py

🎁 다운로드 및 마무리


블로그를 키워가는 과정은 혼자만의 외로운 싸움일 때가 많습니다. 하지만 이렇게 서로 소통하고 좋은 도구를 나누다 보면 훨씬 수월하고 즐겁게 운영하실 수 있을 거라 믿습니다.

프로그램 사용해 보시고 유용하셨다면, 혹은 궁금한 점이 있으시다면 언제든 편하게 댓글 남겨주세요! 여러분의 공감과 댓글은 제가 더 좋은 자료를 나누는 데 큰 힘이 됩니다.

그럼 오늘도 성투하시고, 즐거운 블로깅 하세요!

tistory-follow-checker_V1.2.zip
0.01MB

📄 부록: 파이썬 소스 코드 (checky.py, 전체 파일은 위 다운로드 파일에)

import os
import sys
import time
from playwright.sync_api import sync_playwright

def run_persistent_checker():
    with sync_playwright() as p:
        print("="*60)
        print("티스토리 전수 조사 (Playwright 로딩 검증 강화 모드)")
        print("="*60)
        
        profile_dir = os.path.join(os.getcwd(), "tistory_session")
        if not os.path.exists(profile_dir): os.makedirs(profile_dir)

        browser_context = p.chromium.launch_persistent_context(
            user_data_dir=profile_dir,
            headless=False,
            args=["--no-sandbox"]
        )
        page = browser_context.new_page()

        print("1. 티스토리 피드로 이동합니다.")
        page.goto("https://www.tistory.com/feed")

        try:
            page.wait_for_selector(".link_info, .txt_num, a:has-text('로그아웃')", timeout=10000)
            print("✅ 로그인 확인 완료.")
        except:
            print("❗ 로그인이 필요합니다. 로그인을 완료해 주세요.")
            page.wait_for_selector(".link_info, .txt_num, a:has-text('로그아웃')", timeout=300000)
            print("✅ 로그인 확인 완료.")

        def collect_data(base_url, target_name):
            print(f"\n[{target_name}] 수집 시작...")
            
            # 페이지 내 아이템 로드를 대기하는 보조 함수
            def wait_for_page_items(timeout=15):
                deadline = time.time() + timeout
                while time.time() < deadline:
                    has_items = page.evaluate("!!document.querySelector('a.info_g, a.box_desc, a.link_info')")
                    if has_items:
                        return True
                    time.sleep(0.5)
                return False

            # 현재 페이징 컨트롤에서 active 페이지 번호를 읽는 보조 함수
            def get_active_page_num():
                return page.evaluate("""
                    (() => {
                        let em = document.querySelector('.paging_tistory em.link_paging');
                        if (!em) return null;
                        return em.innerText.replace('현재페이지', '').trim();
                    })()
                """)

            # 첫 페이지 로드
            page.goto(base_url)
            
            if not wait_for_page_items(20):
                print("   [WARN] 첫 페이지 아이템 로드가 지연되어 추가 대기합니다...")
                time.sleep(10)
            time.sleep(5)  # 렌더링 안정화
            
            all_urls = set()
            current_page = 1
            
            while True:
                # 1. 하단 콘텐츠까지 로드하기 위해 스크롤
                page.evaluate("window.scrollBy(0, 5000)")
                time.sleep(3)
                
                # 2. 블로그 URL 수집
                urls = page.evaluate("""
                    Array.from(document.querySelectorAll('a.info_g, a.box_desc, a.link_info'))
                        .map(a => a.href)
                        .filter(href => href && href.includes('.tistory.com') && !href.includes('www.tistory.com'))
                """) or []
                
                prev_count = len(all_urls)
                for u in urls:
                    clean_url = u.split('?')[0].split('/manage')[0].rstrip('/')
                    if clean_url:
                        all_urls.add(clean_url)
                
                new_found = len(all_urls) - prev_count
                print(f" > {current_page}페이지: {len(urls)}개 발견, 신규 {new_found}개 (누적: {len(all_urls)}명)")

                # 3. 다음 페이지 버튼 탐색 및 클릭
                next_page = current_page + 1
                next_page_str = str(next_page)

                click_script = f"""
                (() => {{
                    let links = document.querySelectorAll('.paging_tistory a.link_paging');
                    for (let link of links) {{
                        if (link.innerText.trim() === '{next_page_str}') {{
                            link.click();
                            return 'NUM_{next_page_str}';
                        }}
                    }}
                    let nextBtn = document.querySelector('.paging_tistory .ico_next');
                    if (nextBtn) {{
                        nextBtn.click();
                        return 'NEXT_BATCH';
                    }}
                    return 'NOT_FOUND';
                }})()
                """

                click_result = page.evaluate(click_script)

                # 버튼을 찾지 못한 경우, 스크롤 후 재시도 (최대 3회)
                retry_count = 0
                while (not click_result or click_result == "NOT_FOUND") and retry_count < 3:
                    print(f"   [!] {next_page_str}페이지 버튼을 찾지 못해 스크롤 후 재시도 중... ({retry_count + 1}/3)")
                    page.evaluate("window.scrollBy(0, 3000)")
                    time.sleep(3)
                    click_result = page.evaluate(click_script)
                    retry_count += 1

                if not click_result or click_result == "NOT_FOUND":
                    print("   버튼을 찾지 못했습니다. 마지막 페이지입니다.")
                    break

                print(f"   액션: {click_result} 클릭됨. 페이지 전환 대기...")

                # 4. 페이지 전환 감지 로직 (<em class="link_paging"> 변화 추적)
                target_num = next_page_str if click_result.startswith("NUM_") else None
                transitioned = False

                for _ in range(40):  # 최대 20초 대기 (0.5초 간격)
                    active = get_active_page_num()
                    if target_num:
                        # 숫자 버튼 클릭 시: 정확한 번호 일치 확인
                        if active == target_num:
                            transitioned = True
                            break
                    else:
                        # '다음페이지' 클릭 시: 현재 페이지 번호에서 벗어남을 확인
                        if active and active != str(current_page):
                            next_page = int(active)  # 실제 이동한 페이지 번호 반영
                            transitioned = True
                            break
                    time.sleep(0.5)

                if transitioned:
                    print(f"   ✅ {next_page}페이지 전환 확인 완료.")
                    # 전환 후 새로운 아이템들이 렌더링될 때까지 대기
                    wait_for_page_items(10)
                    time.sleep(2)
                else:
                    print(f"   [WARN] 페이지 전환 감지 실패 (active={get_active_page_num()}). 5초 추가 대기 후 계속합니다.")
                    time.sleep(5)
                    wait_for_page_items(10)

                current_page = next_page
            
            print(f" > {target_name} 최종 수집 완료: 총 {len(all_urls)}명")
            return all_urls

        # 실행
        subscribers = collect_data("https://www.tistory.com/feed?type=follower", "나를 구독 중")
        followings = collect_data("https://www.tistory.com/feed?type=following", "내가 구독 중")

        not_mutual = sorted(list(followings - subscribers))

        print("\n" + "="*50)
        print(f"📊 최종 분석 결과")
        print(f"- 내가 구독 중: {len(followings)}명")
        print(f"- 나를 구독 중: {len(subscribers)}명")
        print(f"- 맞구독 안 함: {len(not_mutual)}명")
        print("="*50)

        if not_mutual:
            print(f"\n⚠️ 맞구독하지 않은 블로그 ({len(not_mutual)}명):")
            for url in not_mutual:
                print(f"- {url}")
            
            with open("tistory_final_report.txt", "w", encoding="utf-8") as f:
                f.write(f"티스토리 맞구독 결과 (정밀 분석)\n")
                f.write(f"구독 중({len(followings)}) | 구독자({len(subscribers)})\n\n")
                for url in not_mutual:
                    f.write(f"{url}\n")
        else:
            print("\n✅ 모두 맞구독 중입니다!")

        input("\n모든 분석이 완료되었습니다. 엔터를 눌러 종료하세요...")
        browser_context.close()

if __name__ == "__main__":
    run_persistent_checker()

WeJump investment Labs

 

 


문의하기
연구소 구독