📊 ماکروی تحلیل فراوانی کلمات و عبارات فارسی در Word

اگر با ماکروها آشنا نیستید یا می‌خواهید روش ضبط و اجرای آن‌ها را یاد بگیرید، پیشنهاد می‌کنم پیش از ادامه، تاپیک زیر را مطالعه کنید:

[!tip] برای مطالعه بیشتر
:link: :backhand_index_pointing_left: ماکرو در ورد: خودکارسازی هوشمند کارهای تکراری!


:bullseye: هدف و کارکرد این ماکرو

این ماکرو یک ابزار قدرتمند برای تحلیل فراوانی واژگان و عبارات در اسناد فارسی Word (مایکروسافت ورد) است و امکانات زیر را در اختیار شما قرار می‌دهد:
:sparkles: شمارش کلمات یا گروهی از کلمات (عبارات چندواژه‌ای)
:sparkles: حذف توقف‌واژه‌ها (Stop Words – کلماتی که در متن اهمیت معنایی کمی دارند)
:sparkles: مدیریت نیم‌فاصله‌ها با سه حالت دلخواه
:sparkles: انتخاب نگه‌داشتن یا حذف اعداد
:sparkles: مرتب‌سازی نتایج بر اساس بیشترین فراوانی یا حروف الفبا
:sparkles: تعیین حداقل تعداد تکرار برای نمایش در خروجی
:sparkles: امکان تحلیل فقط متن انتخاب‌شده یا کل سند


:hammer_and_wrench: ویژگی‌های برجسته

  • :high_voltage: سرعت و عملکرد بالا حتی در اسناد حجیم
  • :memo: مرتب‌سازی دقیق و پایدار برای نمایش نتایج
  • :iran: پشتیبانی کامل از زبان فارسی شامل نرمال‌سازی حروف، نیم‌فاصله و علائم
  • :artist_palette: خروجی خوانا و زیبا با فونت فارسی مناسب
  • :puzzle_piece: امکان افزودن توقف‌واژه‌های سفارشی توسط کاربر
  • :chart_increasing: نمایش آمار کلی شامل تعداد عبارات یکتا و پرکاربردترین عبارت

:inbox_tray: دریافت و استفاده

کافیست کد ماکرو را در محیط VBA Editor (محیط برنامه‌نویسی ماکرو در Word) قرار دهید و اجرا کنید.
:pushpin: برای یادگیری روش اضافه‌کردن ماکرو به ورد و تنظیمات اولیه، به آموزشی ماکروها مراجعه کنید.

مشاهده کد نهایی
 Option Explicit

' =========================
' Persian N-gram Frequency Analyzer (Final, optimized, dependency-free)
' =========================

Public Sub RunPersianWordFrequencyAnalyzer()
    Dim doc As Document
    Dim srcText As String
    Dim wordDict As Object, stopWordDict As Object
    
    Dim ans As String
    Dim nGramSize As Long
    Dim sortByFreq As Boolean
    Dim zwnjOption As Long
    Dim exStart As Boolean, exEnd As Boolean, exAny As Boolean
    Dim keepNumbers As Boolean
    Dim minFreq As Long
    Dim processSelection As Boolean
    Dim extraStop As String
    
    ' --- دريافت تنظيمات ---
    ans = MsgBox("فقط متن انتخاب‌شده پردازش شود؟", vbYesNo + vbQuestion, "محدوده پردازش")
    processSelection = (ans = vbYes)
    
    ans = InputBox("تعداد کلمات در هر عبارت (1=تک‌واژه، حداکثر 10):", "اندازه N-gram", "1")
    If StrPtr(ans) = 0 Then Exit Sub
    If IsNumeric(ans) And Val(ans) > 0 Then
        nGramSize = CLng(ans): If nGramSize > 10 Then nGramSize = 10
    Else
        MsgBox "ورودي نامعتبر، مقدار پيش‌فرض 1 انتخاب شد.", vbInformation
        nGramSize = 1
    End If
    
    ans = InputBox("مرتب‌سازي بر اساس 'FREQ' (فراواني) يا 'WORD' (عبارت)؟", "نحوه مرتب‌سازي", "FREQ")
    If StrPtr(ans) = 0 Then Exit Sub
    ans = UCase$(Trim$(ans))
    If ans <> "WORD" And ans <> "FREQ" Then
        MsgBox "ورودي نامعتبر، حالت 'FREQ' انتخاب شد.", vbInformation
        sortByFreq = True
    Else
        sortByFreq = (ans = "FREQ")
    End If
    
    ans = InputBox("نحوه برخورد با نيم‌فاصله:" & vbCrLf & _
                   "0 = جايگزيني با فاصله" & vbCrLf & _
                   "1 = حذف کامل" & vbCrLf & _
                   "2 = حفظ نيم‌فاصله", "تنظيمات نيم‌فاصله", "0")
    If StrPtr(ans) = 0 Then Exit Sub
    If IsNumeric(ans) And CInt(ans) >= 0 And CInt(ans) <= 2 Then
        zwnjOption = CInt(ans)
    Else
        zwnjOption = 0
    End If
    
    ans = MsgBox("اعداد در تحليل حفظ شوند؟" & vbCrLf & "بله = حفظ | خير = حذف", vbYesNo + vbQuestion, "اعداد")
    keepNumbers = (ans = vbYes)
    
    ans = InputBox("حداقل فراواني براي نمايش در خروجي:", "آستانه نمايش", "1")
    If StrPtr(ans) = 0 Then Exit Sub
    If IsNumeric(ans) And CLng(ans) >= 1 Then
        minFreq = CLng(ans)
    Else
        minFreq = 1
    End If
    
    exStart = (MsgBox("عباراتي که با توقف‌واژه شروع مي‌شوند حذف شوند؟", vbYesNo + vbQuestion) = vbYes)
    exEnd = (MsgBox("عباراتي که با توقف‌واژه تمام مي‌شوند حذف شوند؟", vbYesNo + vbQuestion) = vbYes)
    exAny = (MsgBox("عباراتي که شامل توقف‌واژه هستند حذف شوند؟", vbYesNo + vbQuestion) = vbYes)
    
    extraStop = InputBox("توقف‌واژه‌هاي اضافي (جداشده با ويرگول)، اختياري:", "توقف‌واژه‌هاي سفارشي", "")
    If StrPtr(extraStop) = 0 Then Exit Sub
    
    ' --- پردازش ---
    Application.ScreenUpdating = False
    On Error GoTo ErrorHandler
    
    Set doc = ActiveDocument
    If processSelection And Selection Is Nothing Then processSelection = False
    
    If processSelection And Selection.Range.Characters.Count > 0 Then
        srcText = Selection.Range.Text
    Else
        srcText = doc.Content.Text
    End If
    
    Set wordDict = CreateObject("Scripting.Dictionary")
    Set stopWordDict = CreateObject("Scripting.Dictionary")
    
    BuildStopWords stopWordDict, extraStop
    
    Dim cleanedText As String
    cleanedText = CleanText(srcText, zwnjOption, keepNumbers)
    If Len(cleanedText) = 0 Then
        Documents.Add.Content.Text = "هيچ عبارتي يافت نشد."
        GoTo CleanExit
    End If
    
    ExtractNGramsFromText cleanedText, wordDict, stopWordDict, nGramSize, exStart, exEnd, exAny
    
    DisplayResults wordDict, sortByFreq, minFreq
    
    MsgBox "تحليل با موفقيت انجام شد." & vbCrLf & _
           "تعداد عبارات يکتا: " & CStr(wordDict.Count), vbInformation, "پايان کار"
    
CleanExit:
    Application.ScreenUpdating = True
    Set doc = Nothing
    Set wordDict = Nothing
    Set stopWordDict = Nothing
    Exit Sub
    
ErrorHandler:
    MsgBox "يک خطاي پيش‌بيني‌نشده رخ داد:" & vbCrLf & Err.Description, vbCritical, "خطا"
    Resume CleanExit
End Sub

' --- توقف‌واژه‌ها (پيش‌فرض + سفارشي کاربر) ---
Private Sub BuildStopWords(ByRef stopDict As Object, ByVal extraStopCSV As String)
    Dim baseList As String
    baseList = "و,در,به,از,که,اين,آن,براي,با,است,را,مي,تا,اما,اگر,هم,يا,يک,شود,کرد,شد,نيز,بر,هر,چون"
    
    Dim item As Variant
    For Each item In Split(baseList, ",")
        item = LCase$(Trim$(CStr(item)))
        If Len(item) > 0 Then
            If Not stopDict.Exists(item) Then stopDict.Add item, True
        End If
    Next item
    
    If Len(Trim$(extraStopCSV)) > 0 Then
        Dim ex As Variant
    extraStopCSV = Replace(extraStopCSV, "،", ",")
    extraStopCSV = Replace(extraStopCSV, "*", ",")
    extraStopCSV = Replace(extraStopCSV, "#", ",")
        For Each ex In Split(extraStopCSV, ",")
            ex = LCase$(Trim$(CStr(ex)))
            If Len(ex) > 0 Then
                If Not stopDict.Exists(ex) Then stopDict.Add ex, True
            End If
        Next ex
    End If
End Sub

' --- پاکسازي متن و نرمال‌سازي ---
' --- پاکسازي متن و نرمال‌سازي ---
Private Function CleanText(ByVal txt As String, ByVal zwnjOption As Long, ByVal keepNumbers As Boolean) As String
    Dim re As Object: Set re = CreateObject("VBScript.RegExp")
    re.Global = True
    
    ' حروف کوچک
    txt = LCase$(txt)
    
    ' نرمال‌سازي حروف فارسي/عربي
    txt = Replace(txt, ChrW(1603), ChrW(1705)) ' ك -> ک
    txt = Replace(txt, ChrW(1610), ChrW(1740)) ' ي -> ي
    txt = Replace(txt, ChrW(1570), ChrW(1575)) ' آ -> ا
    txt = Replace(txt, ChrW(1571), ChrW(1575)) ' أ -> ا
    txt = Replace(txt, ChrW(1573), ChrW(1575)) ' إ -> ا
    txt = Replace(txt, ChrW(1577), ChrW(1607)) ' ة -> ه
    
    ' نيم‌فاصله
    Select Case zwnjOption
        Case 0: txt = Replace(txt, ChrW(8204), " ")
        Case 1: txt = Replace(txt, ChrW(8204), "")
        Case 2: ' حفظ
    End Select
    
    ' NBSP به فاصله
    txt = Replace(txt, ChrW(160), " ")
    
    ' حذف علائم اعراب و کنترل
    re.Pattern = "[\u064B-\u065F\u0670\u0640\u200D\u200E\u200F]"
    txt = re.Replace(txt, "")
    
    ' حذف اعداد و نشانه‌گذاري (در صورت نياز)
    If keepNumbers Then
        ' فقط علائم نگارشي حذف شوند
        re.Pattern = "[\[\]{}<>\./\\\|\+\=\*\&\^%$#@~`_\-:;!,\?""'،؛؟«»–—]"
    Else
        ' اعداد و علائم نگارشي حذف شوند
        re.Pattern = "[0-9\[\]{}<>\./\\\|\+\=\*\&\^%$#@~`_\-:;!,\?""'،؛؟«»–—]"
    End If
    
    txt = re.Replace(txt, " ")
    
    ' يکنواخت‌سازي فاصله‌ها
    re.Pattern = "\s+"
    txt = re.Replace(txt, " ")
    
    CleanText = Trim$(txt)
End Function

' --- استخراج N-gram از متن پاکسازي‌شده ---
Private Sub ExtractNGramsFromText(ByVal cleanedText As String, ByRef dict As Object, ByRef stopDict As Object, _
                                  ByVal n As Long, ByVal exStart As Boolean, ByVal exEnd As Boolean, ByVal exAny As Boolean)
    If Len(cleanedText) = 0 Then Exit Sub
    
    Dim wordsArr() As String
    wordsArr = Split(cleanedText, " ")
    If UBound(wordsArr) + 1 < n Then Exit Sub
    
    Dim i As Long, j As Long
    Dim gram As String
    
    For i = 0 To UBound(wordsArr) - (n - 1)
        ' ساختن n-gram
        gram = wordsArr(i)
        For j = 1 To n - 1
            gram = gram & " " & wordsArr(i + j)
        Next j
        
        If Not ShouldSkipGram(gram, stopDict, n, exStart, exEnd, exAny) Then
            If dict.Exists(gram) Then
                dict(gram) = dict(gram) + 1
            Else
                dict.Add gram, 1
            End If
        End If
    Next i
End Sub

' --- قوانين حذف بر اساس توقف‌واژه ---
Private Function ShouldSkipGram(ByVal gram As String, ByRef stopDict As Object, _
                                ByVal n As Long, ByVal exStart As Boolean, ByVal exEnd As Boolean, ByVal exAny As Boolean) As Boolean
    gram = LCase$(Trim$(gram))
    
    If n = 1 Then
        ShouldSkipGram = stopDict.Exists(gram)
        Exit Function
    End If
    
    Dim parts() As String
    parts = Split(gram, " ")
    
    If exStart And stopDict.Exists(parts(0)) Then
        ShouldSkipGram = True: Exit Function
    End If
    If exEnd And stopDict.Exists(parts(UBound(parts))) Then
        ShouldSkipGram = True: Exit Function
    End If
    
    If exAny Then
        Dim k As Long
        For k = 0 To UBound(parts)
            If stopDict.Exists(parts(k)) Then
                ShouldSkipGram = True: Exit Function
            End If
        Next k
    End If
End Function

' --- نمايش نتايج با مرتب‌سازي پايدار و بدون وابستگي ---
Private Sub DisplayResults(ByRef dict As Object, ByVal sortByFreq As Boolean, ByVal minFreq As Long)
    If dict Is Nothing Or dict.Count = 0 Then
        Documents.Add.Content.Text = "هيچ عبارتي يافت نشد."
        Exit Sub
    End If
    
    ' تبديل به آرايه‌ها
    Dim cnt As Long: cnt = dict.Count
    Dim keys() As String, vals() As Long
    ReDim keys(0 To cnt - 1)
    ReDim vals(0 To cnt - 1)
    
    Dim i As Long
    For i = 0 To cnt - 1
        keys(i) = dict.keys()(i)
        vals(i) = dict.Items()(i)
    Next i
    
    ' محاسبه پرتکرارترين (ساده)
    Dim maxKey As String, maxVal As Long
    maxVal = -1
    maxKey = ""

    For i = 0 To cnt - 1
        If vals(i) > maxVal Then
            maxVal = vals(i)
            maxKey = keys(i)
        End If
    Next i
    
    ' اطمينان از مقداردهي
    If Len(maxKey) = 0 And cnt > 0 Then
        maxKey = keys(0)
        maxVal = vals(0)
    End If
    
    ' مرتب‌سازي
    QuickSortPairs keys, vals, 0, UBound(keys), sortByFreq
    
    ' ساخت خروجي
    Dim header As String
    header = "آمار کلي:" & vbCrLf & _
             "تعداد عبارات يکتا: " & CStr(cnt) & vbCrLf & _
             "پرتکرارترين عبارت: " & maxKey & " (" & CStr(maxVal) & " بار)" & vbCrLf & _
             String$(40, "-") & vbCrLf & "تعداد" & vbTab & "عبارت" & vbCrLf & String$(40, "-")
    
    ' ايجاد خطوط خروجي با فيلتر حداقل فراواني
    Dim lines() As String
    ReDim lines(0 To cnt) ' حداکثر؛ بعداً تعداد واقعي را محاسبه مي‌کنيم
    
    Dim outIndex As Long: outIndex = -1
    For i = 0 To cnt - 1
        If vals(i) >= minFreq Then
            outIndex = outIndex + 1
            lines(outIndex) = ToPersianDigits(CStr(vals(i))) & vbTab & keys(i)
        End If
    Next i
    
    Dim body As String
    If outIndex = -1 Then
        body = vbCrLf & "(هيچ عبارتي با آستانه فراواني تعيين‌شده يافت نشد.)"
    Else
        ReDim Preserve lines(0 To outIndex)
        body = vbCrLf & Join(lines, vbCrLf)
    End If
    
    ' درج در سند جديد
    Dim outputDoc As Document
    Set outputDoc = Documents.Add
    With outputDoc.Content
        .Text = header & body
        On Error Resume Next
        .Font.Name = "B Nazanin"
        .Font.Size = 12
        On Error GoTo 0
    End With
End Sub
' --- QuickSort روي جفت‌هاي (کليد/مقدار) با قاعده مقايسه ---
Private Sub QuickSortPairs(ByRef keys() As String, ByRef vals() As Long, _
                           ByVal first As Long, ByVal last As Long, ByVal byFreq As Boolean)
    Dim i As Long, j As Long
    Dim pivotK As String, pivotV As Long
    Dim mid As Long
    Dim tmpK As String, tmpV As Long
    
    i = first: j = last
    mid = (first + last) \ 2
    pivotK = keys(mid): pivotV = vals(mid)
    
    Do While i <= j
        Do While ComparePair(keys(i), vals(i), pivotK, pivotV, byFreq) < 0
            i = i + 1
        Loop
        Do While ComparePair(keys(j), vals(j), pivotK, pivotV, byFreq) > 0
            j = j - 1
        Loop
        If i <= j Then
            tmpK = keys(i): keys(i) = keys(j): keys(j) = tmpK
            tmpV = vals(i): vals(i) = vals(j): vals(j) = tmpV
            i = i + 1: j = j - 1
        End If
    Loop
    
    If first < j Then QuickSortPairs keys, vals, first, j, byFreq
    If i < last Then QuickSortPairs keys, vals, i, last, byFreq
End Sub

' مقايسه: اگر byFreq=True، مرتب‌سازي بر اساس فراواني نزولي و در تساوي بر اساس عبارت صعودي
' اگر byFreq=False، صرفاً بر اساس عبارت صعودي (در تساوي، فراواني نزولي براي پايداري ظاهري)
Private Function ComparePair(ByVal aK As String, ByVal aV As Long, _
                             ByVal bK As String, ByVal bV As Long, ByVal byFreq As Boolean) As Long
    If byFreq Then
        If aV > bV Then
            ComparePair = -1: Exit Function
        ElseIf aV < bV Then
            ComparePair = 1: Exit Function
        Else
            ComparePair = StrComp(aK, bK, vbTextCompare)
            Exit Function
        End If
    Else
        Dim c As Long
        c = StrComp(aK, bK, vbTextCompare)
        If c <> 0 Then
            ComparePair = c: Exit Function
        ElseIf aV > bV Then
            ComparePair = -1: Exit Function
        ElseIf aV < bV Then
            ComparePair = 1: Exit Function
        Else
            ComparePair = 0: Exit Function
        End If
    End If
End Function

' --- تبديل ارقام لاتين به فارسي براي نمايش ---
Private Function ToPersianDigits(ByVal s As String) As String
    Dim i As Long, ch As Integer
    Dim result As String: result = s
    For i = 0 To 9
        result = Replace(result, CStr(i), ChrW(&H6F0 + i))
    Next i
    ToPersianDigits = result
End Function

' پايان



:pushpin: نکات استفاده

  1. :unlocked: فعال بودن ماکروها – پیش از اجرا، مطمئن شوید تنظیمات امنیتی ورد اجازه اجرای ماکرو را می‌دهد.
  2. :scissors: انتخاب محدوده متن – اگر گزینه‌ی «پردازش متن انتخاب‌شده» را انتخاب کنید، فقط بخشی از سند که هایلایت کرده‌اید بررسی می‌شود، در غیر این صورت کل متن سند پردازش خواهد شد.
  3. :stop_sign: افزودن توقف‌واژه‌های دلخواه – هنگام وارد کردن فهرست توقف‌واژه‌ها، بین هر کلمه و کلمه‌ی بعدی یکی از این چهار علامت را قرار دهید: ویرگول فارسی «،» یا ویرگول انگلیسی «,» یا ستاره «*» یا علامت «#».
  4. :page_facing_up: نمایش نتایج – خروجی ماکرو همیشه در یک سند جدید نمایش داده می‌شود تا متن اصلی شما دست‌نخورده باقی بماند.

:light_bulb: کاربردهای پیشنهادی

  • :books: پژوهش و تحلیل متون علمی
  • :memo: بهینه‌سازی متن برای سئو (SEO – بهینه‌سازی برای موتور جستجو)
  • :card_index_dividers: استخراج کلیدواژه‌ها برای فهرست‌نویسی یا خلاصه‌سازی
  • :writing_hand: بررسی سبک نویسنده و شناسایی الگوهای نوشتاری

:megaphone: جمع‌بندی

این ماکرو حاصل چندین مرحله بازطراحی و بهینه‌سازی است و می‌تواند ابزاری کاربردی برای پژوهشگران، ویراستاران، ناشران و علاقه‌مندان به زبان فارسی باشد.