File size: 4,794 Bytes
3d9ba5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import re
from typing import List, Set

class ArabicPreprocessor:
    """
    Arabic NLP Preprocessor with diacritics removal, letter normalization,
    kashida removal, light stemming, and stop-word filtering.
    """

    TASHKEEL_REGEX = re.compile(r'[\u0617-\u061A\u064B-\u0652]')
    TATWEEL_REGEX = re.compile(r'\u0640')
    PUNCTUATION_REGEX = re.compile(r'[ุŒุ›ุŸยซยป""\'\(\)\[\]\{\}\.,:!ุŸ\-_/\\|*~`@#$%^&+=<>]')
    NON_ARABIC_REGEX = re.compile(r'[^\u0600-\u06FF\s]')

    # Affixes for Light Stemmer
    PREFIXES = ['ูƒุงู„', 'ูุงู„', 'ุจุงู„', 'ูˆุงู„', 'ุงู„', 'ู„ู„', 'ูุจ', 'ูˆุจ', 'ูˆุณ', 'ูุณ', 'ู', 'ูˆ', 'ุจ', 'ูƒ', 'ู„', 'ุณ', 'ูŠ', 'ุช', 'ู†', 'ุง']
    SUFFIXES = ['ูƒู…ุง', 'ู‡ู…ุง', 'ู‡ู…', 'ู‡ู†', 'ูƒู…', 'ูƒู†', 'ู†ุง', 'ู‡ุง', 'ุงู†', 'ูŠู†', 'ูˆู†', 'ุงุช', 'ุชู…', 'ุชู†', 'ูˆุง', 'ุชุง', 'ุชูŠ', 'ู†ูŠ', 'ู‡', 'ูŠ', 'ูƒ', 'ุช', 'ุฉ']

    def __init__(self, stopwords_file: str = None):
        self.stopwords: Set[str] = set()
        if stopwords_file and os.path.exists(stopwords_file):
            self.load_stopwords(stopwords_file)
        else:
            default_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'arabic_stopwords.txt')
            if os.path.exists(default_path):
                self.load_stopwords(default_path)

    def load_stopwords(self, filepath: str):
        with open(filepath, 'r', encoding='utf-8') as f:
            for line in f:
                word = line.strip()
                if word:
                    self.stopwords.add(self.normalize_letters(self.remove_tashkeel(word)))

    def remove_tashkeel(self, text: str) -> str:
        """Removes Arabic vowel marks (Harakat/Tashkeel)."""
        return self.TASHKEEL_REGEX.sub('', text)

    def remove_tatweel(self, text: str) -> str:
        """Removes elongation character (Kashida/Tatweel)."""
        return self.TATWEEL_REGEX.sub('', text)

    def normalize_letters(self, text: str) -> str:
        """Standardizes varied Arabic letter forms."""
        # Normalize Alefs
        text = re.sub(r'[ุฅุฃุขูฑ]', 'ุง', text)
        # Normalize Yaa / Alef Maksura
        text = re.sub(r'ู‰', 'ูŠ', text)
        # Normalize Taa Marbuta
        text = re.sub(r'ุฉ', 'ู‡', text)
        # Normalize Hamza variants
        text = re.sub(r'ุค', 'ุก', text)
        text = re.sub(r'ุฆ', 'ุก', text)
        return text

    def clean_punctuation(self, text: str) -> str:
        """Removes punctuation symbols while keeping spaces."""
        return self.PUNCTUATION_REGEX.sub(' ', text)

    def light_stem(self, word: str) -> str:
        """
        Light stemming for Arabic words by systematically stripping
        common prefixes and suffixes while protecting root lengths.
        """
        if len(word) <= 3:
            return word

        # Strip prefixes
        for prefix in sorted(self.PREFIXES, key=len, reverse=True):
            if word.startswith(prefix) and len(word) - len(prefix) >= 3:
                word = word[len(prefix):]
                break

        # Strip suffixes
        for suffix in sorted(self.SUFFIXES, key=len, reverse=True):
            if word.endswith(suffix) and len(word) - len(suffix) >= 3:
                word = word[:-len(suffix)]
                break

        return word

    def preprocess_word(self, word: str, stem: bool = False) -> str:
        """Cleans and standardizes a single Arabic word."""
        word = self.remove_tashkeel(word)
        word = self.remove_tatweel(word)
        word = self.normalize_letters(word)
        word = self.clean_punctuation(word).strip()
        if stem:
            word = self.light_stem(word)
        return word

    def preprocess_text(self, text: str, remove_stopwords: bool = True, stem: bool = False) -> str:
        """Full pipeline for cleaning and standardizing raw Arabic text."""
        if not text:
            return ""
            
        text = self.remove_tashkeel(text)
        text = self.remove_tatweel(text)
        text = self.normalize_letters(text)
        text = self.clean_punctuation(text)
        
        words = text.split()
        processed_words = []
        for w in words:
            w_norm = self.normalize_letters(w)
            if remove_stopwords and (w in self.stopwords or w_norm in self.stopwords):
                continue
            if stem:
                w = self.light_stem(w)
            if len(w) > 0:
                processed_words.append(w)
                
        return ' '.join(processed_words)

    def tokenize_words(self, text: str, remove_stopwords: bool = False, stem: bool = False) -> List[str]:
        """Tokenizes text into a list of cleaned words."""
        cleaned = self.preprocess_text(text, remove_stopwords=remove_stopwords, stem=stem)
        return cleaned.split()