Merhaba,
Python kaynak kodu olarak ücretsiz paylaşıyorum. İsteyen exe derleyebilir. Nasıl kullanırım diye PM atanlara cevap verilmeyecektir.
Sıradaki ücretsiz yazılım için takipte kalın
import re
import requests
from bs4 import BeautifulSoup
import customtkinter as ctk
from tkinter import messagebox
import threading
from datetime import datetime
import json
import concurrent.futures
import pandas as pd
import os
class YouTubeTrackerNoApiApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Wisex Kanal Kontrol")
self.geometry("1400x800")
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# Değişkenler
self.channel_urls = []
self.request_count = 0
self.max_requests = 100
self.is_fetching = False
self.results_data = []
self.successful_channels = 0
# Ana frame
self.main_frame = ctk.CTkFrame(self)
self.main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Sol Frame: Girişler
self.left_frame = ctk.CTkFrame(self.main_frame)
self.left_frame.pack(side="left", fill="y", padx=5, pady=5)
# Çoklu URL Ekleme
ctk.CTkLabel(self.left_frame, text="Kanal Linkleri (her satıra bir link):").pack(pady=5)
self.url_textbox = ctk.CTkTextbox(self.left_frame, height=150, width=400)
self.url_textbox.pack(pady=5)
self.example_button = ctk.CTkButton(self.left_frame, text="Örnek URL Ekle", command=self.add_example_urls)
self.example_button.pack(pady=5)
button_frame = ctk.CTkFrame(self.left_frame)
button_frame.pack(pady=5)
self.add_button = ctk.CTkButton(button_frame, text="Linkleri Ekle", command=self.add_channels)
self.add_button.pack(side="left", padx=5)
self.clear_button = ctk.CTkButton(button_frame, text="Listeyi Temizle", command=self.clear_channels)
self.clear_button.pack(side="left", padx=5)
ctk.CTkLabel(self.left_frame, text="Eklenen Kanallar:").pack(pady=5)
self.channel_listbox = ctk.CTkTextbox(self.left_frame, height=200, width=400)
self.channel_listbox.pack(pady=5, fill="both", expand=True)
# Sağ Frame
self.right_frame = ctk.CTkFrame(self.main_frame)
self.right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
control_frame = ctk.CTkFrame(self.right_frame)
control_frame.pack(pady=10, fill="x")
self.fetch_button = ctk.CTkButton(control_frame, text="Bilgileri Çek", command=self.start_fetch_info)
self.fetch_button.pack(side="left", padx=5)
self.export_button = ctk.CTkButton(control_frame, text="TXT Aktar", command=self.export_results_txt)
self.export_button.pack(side="left", padx=5)
self.export_excel_button = ctk.CTkButton(control_frame, text="Excel Aktar", command=self.export_results_excel)
self.export_excel_button.pack(side="left", padx=5)
self.reset_counter_button = ctk.CTkButton(control_frame, text="Sayacı Sıfırla", command=self.reset_counter)
self.reset_counter_button.pack(side="left", padx=5)
request_frame = ctk.CTkFrame(self.right_frame)
request_frame.pack(pady=5, fill="x")
ctk.CTkLabel(request_frame, text="Maksimum İstek Limiti:").pack(side="left", padx=5)
self.request_limit_entry = ctk.CTkEntry(request_frame, width=80)
self.request_limit_entry.insert(0, str(self.max_requests))
self.request_limit_entry.pack(side="left", padx=5)
self.set_request_limit_button = ctk.CTkButton(request_frame, text="Ayarla", command=self.set_request_limit)
self.set_request_limit_button.pack(side="left", padx=5)
ctk.CTkLabel(request_frame, text="Paralel İstek:").pack(side="left", padx=(20,5))
self.parallel_entry = ctk.CTkEntry(request_frame, width=50)
self.parallel_entry.insert(0, "3")
self.parallel_entry.pack(side="left", padx=5)
ctk.CTkLabel(self.right_frame, text="HTTP İstek Kullanımı:").pack(pady=5)
self.request_progress = ctk.CTkProgressBar(self.right_frame, width=400)
self.request_progress.pack(pady=5)
self.request_label = ctk.CTkLabel(self.right_frame, text=f"Kullanılan: 0 / {self.max_requests}")
self.request_label.pack(pady=5)
self.success_label = ctk.CTkLabel(self.right_frame, text="Başarılı Kanallar: 0")
self.success_label.pack(pady=5)
ctk.CTkLabel(self.right_frame, text="Kanal Bilgileri:").pack(pady=5)
self.result_frame = ctk.CTkFrame(self.right_frame)
self.result_frame.pack(fill="both", expand=True, pady=5)
headers_frame = ctk.CTkFrame(self.result_frame)
headers_frame.pack(fill="x", padx=5, pady=2)
ctk.CTkLabel(headers_frame, text="Kanal Adı", width=180, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Kanal Linki", width=150, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Abone", width=80, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Video", width=60, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Görüntülenme", width=90, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Kuruluş", width=90, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(headers_frame, text="Durum", width=70, anchor="w").pack(side="left", padx=2)
self.results_container = ctk.CTkScrollableFrame(self.result_frame, height=400)
self.results_container.pack(fill="both", expand=True, padx=5, pady=5)
self.status_label = ctk.CTkLabel(self.right_frame, text="Hazır")
self.status_label.pack(pady=5)
def add_example_urls(self):
example_urls = """https://www.youtube.com/@SenAnlatKaradeniz
https://www.youtube.com/@KVPEfsaneSahneler
https://www.youtube.com/c/example2
https://www.youtube.com/channel/UCqfo40MC0FrR90_gpbagHjg"""
self.url_textbox.delete("1.0", "end")
self.url_textbox.insert("1.0", example_urls)
def add_channels(self):
urls_text = self.url_textbox.get("1.0", "end").strip()
if not urls_text:
messagebox.showwarning("Uyarı", "Lütfen kanal linkleri girin.")
return
urls = [url.strip() for url in urls_text.split('\n') if url.strip()]
new_urls = []
for url in urls:
normalized_url = self.normalize_url(url)
if normalized_url and normalized_url not in self.channel_urls:
self.channel_urls.append(normalized_url)
new_urls.append(normalized_url)
if new_urls:
self.update_channel_list()
messagebox.showinfo("Başarılı", f"{len(new_urls)} yeni kanal eklendi!")
else:
messagebox.showinfo("Bilgi", "Tüm kanallar zaten listede mevcut.")
def normalize_url(self, url):
if '/about' in url:
return url.split('?')[0]
if '/channel/' in url or '/@' in url or '/c/' in url:
base_url = url.split('?')[0]
return base_url + '/about' if not base_url.endswith('/about') else base_url
return url
def clear_channels(self):
self.channel_urls.clear()
self.update_channel_list()
self.url_textbox.delete("1.0", "end")
def update_channel_list(self):
self.channel_listbox.delete("1.0", "end")
for i, url in enumerate(self.channel_urls, 1):
self.channel_listbox.insert("end", f"{i}. {url}\n")
def set_request_limit(self):
try:
self.max_requests = int(self.request_limit_entry.get())
self.update_request_display()
messagebox.showinfo("Bilgi", f"Maksimum istek limiti {self.max_requests} olarak ayarlandı.")
except ValueError:
messagebox.showerror("Hata", "Lütfen geçerli bir sayı girin.")
def reset_counter(self):
self.request_count = 0
self.successful_channels = 0
self.update_request_display()
self.update_success_counter()
messagebox.showinfo("Bilgi", "İstek sayacı sıfırlandı.")
def start_fetch_info(self):
if not self.channel_urls:
messagebox.showerror("Hata", "Lütfen en az bir kanal ekleyin.")
return
if self.is_fetching:
messagebox.showwarning("Uyarı", "Zaten veri çekiliyor...")
return
self.is_fetching = True
self.fetch_button.configure(state="disabled")
self.status_label.configure(text="Veriler çekiliyor...")
self.results_data = []
self.successful_channels = 0
self.update_success_counter()
for widget in self.results_container.winfo_children():
widget.destroy()
thread = threading.Thread(target=self.fetch_info_parallel)
thread.daemon = True
thread.start()
def fetch_info_parallel(self):
try:
parallel_workers = min(int(self.parallel_entry.get()), 5)
except:
parallel_workers = 3
with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_workers) as executor:
future_to_url = {
executor.submit(self.fetch_single_channel, url, i): (url, i)
for i, url in enumerate(self.channel_urls)
if self.request_count < self.max_requests
}
for future in concurrent.futures.as_completed(future_to_url):
url, index = future_to_url[future]
try:
channel_info = future.result()
if channel_info and self.is_valid_channel(channel_info):
self.results_data.append(channel_info)
self.add_result_row(channel_info)
self.successful_channels += 1
self.update_success_counter()
except Exception:
continue
self.is_fetching = False
self.fetch_button.configure(state="normal")
self.show_status(f"Tamamlandı! {self.successful_channels} başarılı kanal bulundu.")
def is_valid_channel(self, channel_info):
if channel_info['name'] in ['Bilinmiyor', 'Hata', 'HTTP Hatası', 'Bağlantı Hatası']:
return False
if channel_info['status'] != 'Başarılı':
return False
return True
def fetch_single_channel(self, url, index):
if self.request_count >= self.max_requests:
return None
self.show_status(f"Çekiliyor: {index+1}/{len(self.channel_urls)} - {self.get_short_url(url)}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'tr-TR,tr;q=0.8,en-US;q=0.5,en;q=0.3',
'Connection': 'keep-alive',
}
try:
response = requests.get(url, headers=headers, timeout=8)
self.request_count += 1
self.update_request_display()
if response.status_code == 200:
channel_info = self.parse_channel_info_fast(response.text, url)
if channel_info['name'] == 'Bilinmiyor':
return None
return channel_info
else:
return None
except Exception:
return None
def get_short_url(self, url):
if '/@' in url:
return '@' + url.split('/@')[-1].split('/')[0]
elif '/channel/' in url:
channel_id = url.split('/channel/')[-1].split('/')[0]
return f"channel:{channel_id[:10]}..."
elif '/c/' in url:
return 'c/' + url.split('/c/')[-1].split('/')[0]
else:
return url[-15:]
# -------------------- GÜNCELLENMİŞ PARSE FONKSİYONU (TABLO ODAKLI) --------------------
def parse_channel_info_fast(self, html_content, url):
"""
Kanal bilgilerini doğrudan ytd-about-channel-renderer tablosundan alır.
Bulamazsa regex fallback kullanır.
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 1. Kanal adını al (meta/başlık)
channel_name = self.extract_channel_name_fast(html_content)
if channel_name == 'Bilinmiyor':
return {
'name': 'Bilinmiyor',
'short_url': self.get_short_url(url),
'full_url': url,
'subscribers': 'N/A',
'video_count': 'N/A',
'views': 'N/A',
'join_date': 'N/A',
'status': 'Başarısız',
'timestamp': datetime.now().strftime("%H:%M:%S"),
'url': url
}
# 2. Tabloyu bul
table = soup.find('table', class_='style-scope ytd-about-channel-renderer')
details = {'subscribers': None, 'video_count': None, 'views': None, 'join_date': None}
if table:
# Tablo içindeki her satırı gez (hidden olmayanları)
for tr in table.find_all('tr', class_='description-item'):
# hidden attribute'u kontrol et (true, false veya boş)
if tr.get('hidden') is not None:
continue
tds = tr.find_all('td')
if len(tds) < 2:
continue
icon_td = tds[0]
text_td = tds[1]
yt_icon = icon_td.find('yt-icon')
if not yt_icon:
continue
icon_name = yt_icon.get('icon', '')
# Metni temizle ( vs.)
text = text_td.get_text(strip=True)
if icon_name == 'person_radar':
details['subscribers'] = text
elif icon_name == 'my_videos':
details['video_count'] = text
elif icon_name == 'trending_up':
details['views'] = text
elif icon_name == 'info_outline':
details['join_date'] = text
# Eğer tablodan hiçbir şey gelmediyse regex fallback
if not any(details.values()):
details = {
'subscribers': self.extract_subscribers_fast(html_content),
'video_count': self.extract_video_count_fast(html_content),
'views': self.extract_total_views_corrected(html_content),
'join_date': self.extract_join_date_fast(html_content)
}
return {
'name': channel_name,
'short_url': self.get_short_url(url),
'full_url': url,
'subscribers': details.get('subscribers', 'Bilinmiyor'),
'video_count': details.get('video_count', 'Bilinmiyor'),
'views': details.get('views', 'Bilinmiyor'),
'join_date': details.get('join_date', 'Bilinmiyor'),
'status': 'Başarılı',
'timestamp': datetime.now().strftime("%H:%M:%S"),
'url': url
}
# -------------------- REGEX FALLBACK METOTLARI --------------------
def extract_channel_name_fast(self, html_content):
meta_match = re.search(r'<meta property="og:title" content="([^"]+)"', html_content)
if meta_match:
name = meta_match.group(1).replace(' - YouTube', '').strip()
if name and name != "YouTube" and len(name) > 1:
return name[:30]
title_match = re.search(r'<title>([^<]+)</title>', html_content)
if title_match:
name = title_match.group(1).replace(' - YouTube', '').strip()
if name and name != "YouTube" and len(name) > 1:
return name[:30]
json_ld_match = re.search(r'<script type="application/ld\+json">([^<]+)</script>', html_content)
if json_ld_match:
try:
json_data = json.loads(json_ld_match.group(1))
if isinstance(json_data, list):
json_data = json_data[0]
if json_data.get('name'):
name = json_data['name']
if name and name != "YouTube" and len(name) > 1:
return name[:30]
except:
pass
return "Bilinmiyor"
def extract_subscribers_fast(self, html_content):
patterns = [
r'([\d.,]+\s*(?:[Mm]n|[Mm]ilyon)?)\s*abone',
r'aria-label="([\d,]+)\s+abone"',
r'<span[^>]*>([\d,]+)\s+abone</span>',
r'"subscriberCountText"[^}]*"simpleText":"([\d,]+)"',
r'"subscribers":"([\d,]+)"',
r'(\d+(?:,\d+)*)\s*abone',
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE)
if match:
return match.group(1).strip()
return "Gizli"
def extract_video_count_fast(self, html_content):
patterns = [
r'([\d.,]+)\s*video',
r'aria-label="([\d,]+)\s+video"',
r'<span[^>]*>([\d,]+)\s+video</span>',
r'"videosCountText"[^}]*"runs":[^[]*\[[^]]*"text":"([\d,]+)"',
r'"videoCount":"([\d,]+)"',
r'(\d+(?:,\d+)*)\s*video',
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE)
if match:
return match.group(1).strip()
return "Bilinmiyor"
def extract_total_views_corrected(self, html_content):
patterns = [
r'([\d.,]+)\s*görüntüleme',
r'YtmAboutChannelRendererChannelDetail[^>]*>[\s\S]*?([\d,\.]+)\s*görüntüleme',
r'engagement-panel-section-list[^>]*>[\s\S]*?([\d,\.]+)\s*görüntüleme',
r'"viewCountText"[^}]*"simpleText":"([\d,\.]+)"',
r'totalViews["\s:]*([\d,\.]+)',
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE)
if match:
views = match.group(1).strip()
numeric_views = views.replace(',', '').replace('.', '')
if numeric_views.isdigit() and len(numeric_views) >= 3:
return views
return "Bilinmiyor"
def extract_join_date_fast(self, html_content):
patterns = [
r'(\d{1,2}\s+\w+\s+\d{4})\s*tarihinde katıldı',
r'Joined\s+(\w+\s+\d{1,2},\s+\d{4})',
]
for pattern in patterns:
match = re.search(pattern, html_content)
if match:
return match.group(1)
return "Bilinmiyor"
# -------------------- UI METOTLARI (değişmedi) --------------------
def add_result_row(self, channel_info):
self.after(0, self._add_result_row_ui, channel_info)
def _add_result_row_ui(self, channel_info):
row_frame = ctk.CTkFrame(self.results_container)
row_frame.pack(fill="x", padx=2, pady=1)
ctk.CTkLabel(row_frame, text=channel_info['name'], width=180, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(row_frame, text=channel_info['short_url'], width=150, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(row_frame, text=channel_info['subscribers'], width=80, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(row_frame, text=channel_info['video_count'], width=60, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(row_frame, text=channel_info['views'], width=90, anchor="w").pack(side="left", padx=2)
ctk.CTkLabel(row_frame, text=channel_info['join_date'], width=90, anchor="w").pack(side="left", padx=2)
status_color = "green" if channel_info['status'] == 'Başarılı' else "red"
ctk.CTkLabel(row_frame, text=channel_info['status'], width=70, anchor="w",
text_color=status_color).pack(side="left", padx=2)
def show_status(self, message):
self.after(0, lambda: self.status_label.configure(text=message))
def update_request_display(self):
progress = min(self.request_count / self.max_requests, 1.0)
self.request_progress.set(progress)
self.request_label.configure(text=f"Kullanılan: {self.request_count} / {self.max_requests}")
if progress > 0.8:
self.request_label.configure(text_color="red")
elif progress > 0.6:
self.request_label.configure(text_color="orange")
else:
self.request_label.configure(text_color="white")
def update_success_counter(self):
self.success_label.configure(text=f"Başarılı Kanallar: {self.successful_channels}")
def export_results_txt(self):
if not self.results_data:
messagebox.showwarning("Uyarı", "Dışa aktarılacak veri bulunamadı.\nLütfen önce 'Bilgileri Çek' butonuna tıklayın.")
return
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"youtube_kanal_sonuclari_{timestamp}.txt"
with open(filename, 'w', encoding='utf-8') as f:
f.write("YouTube Kanal Bilgileri\n")
f.write("=" * 150 + "\n")
f.write(f"Oluşturulma: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Toplam Kanal: {len(self.channel_urls)}\n")
f.write(f"Başarılı Kanallar: {self.successful_channels}\n")
f.write(f"Kullanılan İstek: {self.request_count}\n")
f.write("=" * 150 + "\n\n")
f.write(f"{'Kanal Adı':<25} {'Tam Kanal Linki':<50} {'Abone':<10} {'Video':<8} {'Görüntülenme':<12} {'Kuruluş':<12} {'Durum':<10}\n")
f.write("-" * 150 + "\n")
for data in self.results_data:
f.write(f"{data['name']:<25} {data['full_url']:<50} {data['subscribers']:<10} {data['video_count']:<8} {data['views']:<12} {data['join_date']:<12} {data['status']:<10}\n")
messagebox.showinfo("Başarılı", f"Sonuçlar TXT dosyasına kaydedildi!\nDosya: {filename}")
except Exception as e:
messagebox.showerror("Hata", f"TXT dosyası kaydedilemedi: {str(e)}")
def export_results_excel(self):
if not self.results_data:
messagebox.showwarning("Uyarı", "Dışa aktarılacak veri bulunamadı.\nLütfen önce 'Bilgileri Çek' butonuna tıklayın.")
return
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"youtube_kanal_sonuclari_{timestamp}.xlsx"
df_data = []
for data in self.results_data:
df_data.append({
'Kanal Adı': data['name'],
'Tam Kanal Linki': data['full_url'],
'Abone Sayısı': data['subscribers'],
'Video Sayısı': data['video_count'],
'Toplam Görüntülenme': data['views'],
'Katılma Tarihi': data['join_date'],
'Durum': data['status'],
'Son Güncelleme': data['timestamp']
})
df = pd.DataFrame(df_data)
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='YouTube Kanalları', index=False)
workbook = writer.book
worksheet = writer.sheets['YouTube Kanalları']
worksheet.column_dimensions['A'].width = 25
worksheet.column_dimensions['B'].width = 50
worksheet.column_dimensions['C'].width = 15
worksheet.column_dimensions['D'].width = 12
worksheet.column_dimensions['E'].width = 18
worksheet.column_dimensions['F'].width = 15
worksheet.column_dimensions['G'].width = 12
worksheet.column_dimensions['H'].width = 15
for cell in worksheet[1]:
cell.font = cell.font.copy(bold=True)
messagebox.showinfo("Başarılı", f"Sonuçlar Excel dosyasına kaydedildi!\nDosya: {filename}")
except ImportError:
messagebox.showerror("Hata", "Excel için pandas ve openpyxl gerekli.\npip install pandas openpyxl")
except Exception as e:
messagebox.showerror("Hata", f"Excel dosyası kaydedilemedi: {str(e)}")
if __name__ == "__main__":
app = YouTubeTrackerNoApiApp()
app.mainloop()