Spaces:
Configuration error
Configuration error
File size: 8,358 Bytes
7136657 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | import os
import json
import logging
import urllib.parse
from datetime import datetime
import feedparser
from newspaper import Article, Config
from googlenewsdecoder import gnewsdecoder
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Scraper configuration
CONFIG = Config()
CONFIG.browser_user_agent = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/115.0.0.0 Safari/537.36"
)
CONFIG.request_timeout = 15
def get_source_name(entry, fallback_url: str) -> str:
"""
Get the name of the source from feed entry or parse domain as fallback.
"""
if hasattr(entry, 'source') and 'title' in entry.source:
return entry.source.title
try:
parsed_uri = urllib.parse.urlparse(fallback_url)
return parsed_uri.netloc.replace('www.', '')
except Exception:
return "Unknown Source"
def search_articles(query: str, limit: int = 10, lang: str = "id", country: str = "ID", time_range: str = "any") -> list:
"""
Search articles from Google News RSS based on a query, localized to lang/country.
Returns a list of dicts containing metadata (title, link, published, source).
"""
adjusted_query = query
if time_range and time_range != "any":
adjusted_query = f"{query} when:{time_range}"
encoded_query = urllib.parse.quote(adjusted_query)
ceid = f"{country}:{lang}"
rss_url = f"https://news.google.com/rss/search?q={encoded_query}&hl={lang}&gl={country}&ceid={ceid}"
logger.info(f"Searching Google News RSS for query: '{adjusted_query}' (lang: {lang}, country: {country})...")
feed = feedparser.parse(rss_url)
articles = []
for entry in feed.entries[:limit]:
title = getattr(entry, 'title', 'No Title')
link = getattr(entry, 'link', '')
published = getattr(entry, 'published', '')
source = get_source_name(entry, link)
if link:
articles.append({
"title": title,
"url": link,
"publish_date": published,
"source": source,
"query": query
})
logger.info(f"Found {len(articles)} article links for query: '{query}'.")
return articles
def scrape_article(url: str, lang: str = "id") -> str:
"""
Scrapes the full text of an article using Newspaper3k with a specific language setting.
"""
logger.info(f"Scraping content from URL: {url} (lang: {lang})")
try:
# Create a dynamic config to pass the specific language
config = Config()
config.browser_user_agent = CONFIG.browser_user_agent
config.request_timeout = CONFIG.request_timeout
config.language = lang # e.g., 'id' for Indonesian
article = Article(url, config=config)
article.download()
article.parse()
return article.text
except Exception as e:
logger.warning(f"Failed to scrape article from {url}: {e}")
return ""
def run_auto_pipeline(queries: list, limit_per_query: int = 5, output_dir: str = "data", lang: str = "id", country: str = "ID", time_range: str = "any", log_callback = None) -> str:
"""
Runs the automated search and scrape pipeline for a list of queries.
Saves the results to raw_articles.json.
"""
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "raw_articles.json")
if log_callback:
log_callback(f"Memulai pipeline penarikan berita untuk {len(queries)} kata kunci.")
log_callback(f"Rentang waktu terbit berita: {time_range if time_range != 'any' else 'Semua Waktu'}")
# Load existing articles if file exists to prevent overwriting new runs
all_articles = []
if os.path.exists(output_path):
try:
with open(output_path, 'r', encoding='utf-8') as f:
all_articles = json.load(f)
logger.info(f"Loaded {len(all_articles)} existing articles from {output_path}")
if log_callback:
log_callback(f"Ditemukan {len(all_articles)} artikel pra-eksis di arsip lokal.")
except Exception as e:
logger.error(f"Error loading existing raw_articles.json: {e}")
existing_urls = {art["url"] for art in all_articles if "url" in art}
scraped_count = 0
for query in queries:
if log_callback:
log_callback(f"Mencari query: '{query}' di Google News...")
discovered_articles = search_articles(query, limit=limit_per_query, lang=lang, country=country, time_range=time_range)
if log_callback:
log_callback(f"Menemukan {len(discovered_articles)} artikel potensial untuk query: '{query}'")
for art in discovered_articles:
google_url = art["url"]
# Decode the Google News redirect URL to the direct article URL
decoded_url = google_url
try:
if log_callback:
log_callback(f"Mendekode rujukan URL Google News...")
decoded_data = gnewsdecoder(google_url)
if decoded_data.get("status"):
decoded_url = decoded_data["decoded_url"]
logger.info(f"Decoded Google News URL to: {decoded_url}")
else:
logger.warning(f"Could not decode Google News URL, using original: {google_url}")
except Exception as e:
logger.error(f"Error decoding Google News URL: {e}")
art["url"] = decoded_url
if decoded_url in existing_urls:
logger.info(f"Skipping already scraped URL: {decoded_url}")
if log_callback:
log_callback(f"Melewati URL (sudah pernah diunduh): {decoded_url[:60]}...")
continue
if log_callback:
log_callback(f"Mengunduh teks lengkap dari: {art['source']} ({decoded_url[:50]}...)")
raw_text = scrape_article(decoded_url, lang=lang)
# Only save articles that have non-empty content
if raw_text and len(raw_text.strip()) > 150:
art["raw_text"] = raw_text.strip()
art["scraped_at"] = datetime.now().isoformat()
all_articles.append(art)
existing_urls.add(decoded_url)
scraped_count += 1
if log_callback:
log_callback(f"✓ Berhasil mengunduh artikel: \"{art['title'][:45]}...\"")
else:
logger.info(f"Skipped URL due to empty or too short content: {decoded_url}")
if log_callback:
log_callback(f"⚠ Skip artikel (konten kosong/terlalu pendek): {decoded_url[:50]}...")
# Assign sequential IDs to all articles before saving
for idx, art in enumerate(all_articles, 1):
ordered_art = {"id": idx}
ordered_art.update({k: v for k, v in art.items() if k != "id"})
all_articles[idx - 1] = ordered_art
# Save back to JSON
try:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(all_articles, f, indent=4, ensure_ascii=False)
logger.info(f"Pipeline finished. Successfully scraped {scraped_count} new articles. Total saved: {len(all_articles)} articles.")
if log_callback:
log_callback(f"Selesai! {scraped_count} berita baru ditambahkan. Total database proyek memiliki {len(all_articles)} berita.")
except Exception as e:
logger.error(f"Failed to write results to {output_path}: {e}")
if log_callback:
log_callback(f"❌ Gagal menulis hasil ke file lokal: {e}")
return output_path
if __name__ == "__main__":
# Test queries focused on Indonesian industry trends
test_queries = [
"FMCG keberlanjutan Indonesia",
"pemasaran digital ritel Indonesia",
"perubahan perilaku konsumen ritel"
]
print("Starting pipeline test focused on Indonesian news...")
path = run_auto_pipeline(test_queries, limit_per_query=4, lang="id", country="ID")
print(f"Test completed. Output path: {path}")
|