adnandi's picture
Deploy
7136657
Raw
History Blame Contribute Delete
8.16 kB
import os
import re
import json
import logging
import nltk
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Ensure NLTK data is downloaded
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
logger.info("Downloading NLTK punkt tokenizer...")
nltk.download('punkt', quiet=True)
try:
nltk.data.find('tokenizers/punkt_tab')
except LookupError:
logger.info("Downloading NLTK punkt_tab...")
nltk.download('punkt_tab', quiet=True)
try:
nltk.data.find('corpora/stopwords')
except LookupError:
logger.info("Downloading NLTK stopwords...")
nltk.download('stopwords', quiet=True)
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords as nltk_stopwords
# Initialize Sastrawi Stemmer
logger.info("Initializing Sastrawi Stemmer...")
stemmer_factory = StemmerFactory()
sastrawi_stemmer = stemmer_factory.create_stemmer()
# Caching for Sastrawi Stemmer to speed up processing
CACHE_PATH = "data/stem_cache.json"
STEM_CACHE = {}
def load_stem_cache():
global STEM_CACHE
if os.path.exists(CACHE_PATH):
try:
with open(CACHE_PATH, 'r', encoding='utf-8') as f:
STEM_CACHE = json.load(f)
logger.info(f"Loaded {len(STEM_CACHE)} stemmed words from cache file.")
except Exception as e:
logger.warning(f"Failed to load stem cache: {e}")
def save_stem_cache():
if STEM_CACHE:
try:
os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
with open(CACHE_PATH, 'w', encoding='utf-8') as f:
json.dump(STEM_CACHE, f, ensure_ascii=False, indent=4)
logger.info(f"Saved {len(STEM_CACHE)} stemmed words to cache file.")
except Exception as e:
logger.warning(f"Failed to save stem cache: {e}")
def stem_word(word: str) -> str:
"""
Stems a single word using Sastrawi with a global cache.
"""
if word in STEM_CACHE:
return STEM_CACHE[word]
stemmed = sastrawi_stemmer.stem(word)
STEM_CACHE[word] = stemmed
return stemmed
# Build unified stopwords set
try:
indonesian_stop = set(nltk_stopwords.words('indonesian'))
except Exception:
logger.warning("Failed to load NLTK indonesian stopwords, using empty fallback.")
indonesian_stop = set()
try:
english_stop = set(nltk_stopwords.words('english'))
except Exception:
logger.warning("Failed to load NLTK english stopwords, using empty fallback.")
english_stop = set()
custom_stops = {
"yang", "di", "dan", "ke", "dari", "untuk", "dengan", "ini", "itu", "atau",
"adalah", "pada", "juga", "saya", "kami", "mereka", "dia", "anda", "telah",
"dalam", "oleh", "serta", "karena", "tersebut", "bisa", "ada", "lebih",
"bagi", "akan", "dapat", "ia", "sebagai", "bahwa", "tidak", "hanya", "seperti",
"yaitu", "para", "namun", "terkait", "mengatakan", "menjadi", "juga",
"tentang", "hanya", "dari", "ia", "oleh", "hingga", "dalam", "setelah", "namun",
"sebuah", "salah", "satu", "ia", "telah", "ia", "serta", "atau", "ada", "bisa",
"olehnya", "selain", "ia", "bahwasanya", "jika", "maka", "apabila", "saat",
"saatnya", "begitu", "sehingga", "serta", "berbagai", "secara", "terhadap",
"melalui", "maupun", "lalu", "kemudian", "kepada", "kembali", "banyak", "beberapa",
"hal", "tersebut", "ia", "kita", "kamu", "dia", "mereka", "anda", "kalian", "mereka"
}
ALL_STOPWORDS = indonesian_stop.union(english_stop).union(custom_stops)
def clean_text_full(text: str) -> str:
"""
Cleans raw text by removing HTML tags, URLs, email addresses, digits, special characters, and emojis.
Returns cleaned lowercase text.
"""
if not text:
return ""
# 1. Remove HTML tags
text = re.sub(r'<[^>]+>', '', text)
# 2. Remove URLs
text = re.sub(r'https?://\S+|www\.\S+', '', text)
# 3. Remove email addresses
text = re.sub(r'\S+@\S+', '', text)
# 4. Remove digits and numbers
text = re.sub(r'\d+', '', text)
# 5. Remove special characters/punctuation/emojis (keep letters and spaces)
text = re.sub(r'[^a-zA-Z\s\-]', ' ', text)
# 6. Normalize multiple spaces/newlines to a single space
text = re.sub(r'\s+', ' ', text).strip()
# 7. Convert to lowercase
return text.lower()
def preprocess_article(text: str) -> tuple:
"""
Preprocesses raw article text: cleans, tokenizes, removes stopwords, and stems words.
Returns a tuple of (clean_text, clean_tokens).
"""
# Step 1: Clean text
cleaned_text = clean_text_full(text)
# Step 2: Tokenize
tokens = word_tokenize(cleaned_text)
# Step 3 & 4: Stopword removal and Stemming
clean_tokens = []
for token in tokens:
# Resolve hyphenated words (e.g., 'toko-toko' -> stem both or keep)
# We can split hyphenated words to handle them better in Indonesian
sub_tokens = token.split('-')
for st in sub_tokens:
st = st.strip()
if st and st not in ALL_STOPWORDS and len(st) > 2:
stemmed = stem_word(st)
if stemmed and stemmed not in ALL_STOPWORDS and len(stemmed) > 2:
clean_tokens.append(stemmed)
# Reconstruct clean_text from clean_tokens
reconstructed_text = " ".join(clean_tokens)
return reconstructed_text, clean_tokens
def run_preprocessing_pipeline(input_path: str = "data/raw_articles.json", output_path: str = "data/preprocessed_articles.json") -> str:
"""
Loads raw articles, preprocesses each, and saves the cleaned dataset.
"""
# Load stem cache from disk if it exists
load_stem_cache()
logger.info(f"Loading raw dataset from: {input_path}")
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file {input_path} does not exist.")
with open(input_path, 'r', encoding='utf-8') as f:
articles = json.load(f)
logger.info(f"Preprocessing {len(articles)} articles...")
preprocessed_articles = []
# Track progress and cache performance
total = len(articles)
for idx, art in enumerate(articles, 1):
raw_text = art.get("raw_text", "")
clean_text, clean_tokens = preprocess_article(raw_text)
# Create a new dictionary to preserve original fields and append preprocessed fields
new_art = art.copy()
new_art["clean_text"] = clean_text
new_art["clean_tokens"] = clean_tokens
preprocessed_articles.append(new_art)
if idx % 20 == 0 or idx == total:
logger.info(f"Processed {idx}/{total} articles. Stem cache size: {len(STEM_CACHE)}")
# Save back to JSON
os.makedirs(os.path.dirname(output_path), exist_ok=True)
try:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(preprocessed_articles, f, indent=4, ensure_ascii=False)
logger.info(f"Preprocessing complete. Cleaned dataset saved to: {output_path}")
except Exception as e:
logger.error(f"Failed to save preprocessed dataset: {e}")
raise e
# Save cache back to disk
save_stem_cache()
return output_path
if __name__ == "__main__":
# Test on dummy text
sample_text = (
"Industri FMCG di Indonesia tengah menerapkan teknologi digital untuk meningkatkan efisiensi. "
"Peritel modern versus koperasi tradisional saling bersaing memperebutkan hati para konsumen."
)
print("Testing preprocess_article on sample text:")
clean_txt, tokens = preprocess_article(sample_text)
print("Cleaned text:", clean_txt)
print("Tokens:", tokens)
# Run pipeline on test data if exists
test_input = "data/raw_articles.json"
test_output = "data/preprocessed_articles.json"
if os.path.exists(test_input):
print("\nRunning pipeline on raw_articles.json...")
run_preprocessing_pipeline(test_input, test_output)