معالجة النصوص

الدرس 19: معالجة النصوص

🎯 تشبيه من الحياة

تخيل أنك أمين مكتبة. كل يوم تتعامل مع كمية كبيرة من العمل النصي:

معالجة النصوص هي "العمل النصي" في البرامج — يحتاج إليها كل برنامج يتعامل مع النصوص.


المفاهيم الأساسية

توفر Go عدة مكتبات قياسية للتعامل مع النصوص:

الحزمة الغرض الدوال الشائعة
strings البحث والاستبدال والتقسيم والدمج في النصوص Contains, Replace, Split, Join, Trim
strconv التحويل بين النصوص والأنواع الأخرى Atoi, Itoa, ParseBool, FormatFloat
unicode/utf8 عمليات الترميز UTF-8 RuneCountInString, ValidString
strings.Builder الربط الفعال للعديد من النصوص WriteString, String

نقاط أساسية:

  1. النصوص في Go غير قابلة للتعديل — أي تعديل ينشئ نصاً جديداً
  2. النصوص في Go هي تسلسلات بايت مرمزة بـ UTF-8 في الأساس
  3. len(str) تعيد عدد البايتات، وليس الأحرف
  4. rune هو نوع Go لتمثيل نقاط Unicode (أساساً int32)

الصياغة الأساسية والاستخدام

1. حزمة strings

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    str := "Hello, Go!"

    // البحث
    fmt.Println(strings.Contains(str, "Go"))       // true
    fmt.Println(strings.HasPrefix(str, "Hello"))    // true
    fmt.Println(strings.HasSuffix(str, "!"))        // true
    fmt.Println(strings.Index(str, "Go"))           // 7

    // الاستبدال
    result := strings.Replace(str, "Go", "Golang", 1)
    fmt.Println(result) // "Hello, Golang!"

    // استبدال الكل
    s := "aabbcc"
    fmt.Println(strings.ReplaceAll(s, "a", "x")) // "xxbbcc"

    // التقسيم والدمج
    csv := "apple,banana,cherry"
    fruits := strings.Split(csv, ",")
    fmt.Println(fruits) // [apple banana cherry]

    joined := strings.Join(fruits, " | ")
    fmt.Println(joined) // "apple | banana | cherry"

    // إزالة الفراغات
    padded := "  Hello World  "
    fmt.Println(strings.TrimSpace(padded))         // "Hello World"
    fmt.Println(strings.Trim("##Hello##", "#"))     // "Hello"
    fmt.Println(strings.TrimLeft("##Hello##", "#"))  // "Hello##"

    // تحويل الحالة
    fmt.Println(strings.ToUpper("hello")) // "HELLO"
    fmt.Println(strings.ToLower("HELLO")) // "hello"

    // التكرار
    fmt.Println(strings.Repeat("Go", 3)) // "GoGoGo"

    // العد
    fmt.Println(strings.Count("banana", "an")) // 2
}
💡 نصيحة: عندما يكون فاصل strings.Split فارغاً، يتم تقسيم النص إلى شريحة من الأحرف الفردية.

2. حزمة strconv

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // نص ← عدد صحيح
    num, err := strconv.Atoi("42")
    if err != nil {
        fmt.Println("فشل التحويل:", err)
    }
    fmt.Println(num) // 42

    // عدد صحيح ← نص
    str := strconv.Itoa(42)
    fmt.Println(str) // "42"

    // نص ← منطقي
    b, err := strconv.ParseBool("true")
    fmt.Println(b, err) // true <nil>

    // نص ← عدد عشري
    f, err := strconv.ParseFloat("3.14", 64)
    fmt.Println(f, err) // 3.14 <nil>

    // عدد عشري ← نص
    // 'f' يعني التنسيق العادي، -1 يعني الحد الأدنى من الأرقام، 64 يعني float64
    s := strconv.FormatFloat(3.14, 'f', -1, 64)
    fmt.Println(s) // "3.14"

    // مخرجات منسقة (مشابهة لـ sprintf في C)
    formatted := strconv.FormatInt(255, 16) // سداسي عشري
    fmt.Println(formatted) // "ff"
}
💡 نصيحة: strconv.Atoi تعادل strconv.ParseInt(s, 10, 0)، وتعيد نوع int التابع للمنصة.

3. حزمة unicode/utf8

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    str := "مرحبا"

    // len() تعيد عدد البايتات
    fmt.Println(len(str)) // 10 (كل حرف عربي يأخذ 2 بايت)

    // utf8.RuneCountInString() تعيد عدد الأحرف
    fmt.Println(utf8.RuneCountInString(str)) // 5

    // التحقق من صحة UTF-8
    fmt.Println(utf8.ValidString(str))  // true
    fmt.Println(utf8.ValidString("abc")) // true

    // التكرار على كل rune في النص
    for i, r := range str {
        fmt.Printf("المؤشر:%d الحرف:%c Unicode:%U\n", i, r, r)
    }
}
💡 نصيحة: عند استخدام range للتكرار على نص، تقوم Go تلقائياً بالتكرار حسب rune (حرف Unicode)، وليس حسب البايت.

4. strings.Builder (ربط فعال)

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ❌ غير فعال: ينشئ نصاً جديداً في كل ربط
    // result := ""
    // for i := 0; i < 1000; i++ {
    //     result += "a"  // يخصص ذاكرة جديدة في كل مرة
    // }

    // ✅ فعال: استخدم strings.Builder
    var builder strings.Builder
    for i := 0; i < 1000; i++ {
        builder.WriteString("a")
    }
    result := builder.String()
    fmt.Println(len(result)) // 1000

    // تخصيص مسبق للسعة لأداء أفضل
    var builder2 strings.Builder
    builder2.Grow(1000) // تخصيص 1000 بايت مسبقاً
    for i := 0; i < 1000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println(builder2.Len()) // 1000
}
💡 نصيحة: strings.Builder يستخدم داخلياً شريحة []byte، مما يتجنب التخصيص المتكرر للذاكرة الناتج عن عدم قابلية تعديل النصوص.


أمثلة الكود

مثال: إحصائيات وتحليل النصوص (الصعوبة ⭐)

GO
package main

import (
    "fmt"
    "strings"
    "unicode"
)

// تحليل عدد أنواع الأحرف المختلفة في نص
func analyzeString(s string) (letters, digits, spaces, others int) {
    for _, r := range s {
        switch {
        case unicode.IsLetter(r):
            letters++
        case unicode.IsDigit(r):
            digits++
        case unicode.IsSpace(r):
            spaces++
        default:
            others++
        }
    }
    return
}

func main() {
    text := "Hello, Go! 2024 Version 2.0"

    letters, digits, spaces, others := analyzeString(text)
    fmt.Printf("النص: %q\n", text)
    fmt.Printf("الأحرف: %d\n", letters)
    fmt.Printf("الأرقام: %d\n", digits)
    fmt.Printf("المسافات: %d\n", spaces)
    fmt.Printf("أخرى: %d\n", others)

    // عدد تكرارات الكلمات
    words := strings.Fields("the go the language the world")
    freq := make(map[string]int)
    for _, w := range words {
        freq[strings.ToLower(w)]++
    }
    fmt.Println("\nتكرار الكلمات:", freq)
}
▶ جرّب الكود

المخرجات:

TEXT
النص: "Hello, Go! 2024 Version 2.0"
الأحرف: 17
الأرقام: 6
المسافات: 5
أخرى: 3

تكرار الكلمات: map[go:1 language:1 the:3 world:1]

مثال: محلل CSV (الصعوبة ⭐⭐)

GO
package main

import (
    "fmt"
    "strings"
)

// محلل سطر CSV بسيط يدعم الحقول المقتبسة
func parseCSVLine(line string) []string {
    var fields []string
    var current strings.Builder
    inQuotes := false

    for _, r := range line {
        switch {
        case r == '"' && !inQuotes:
            // الدخول في منطقة الاقتباس
            inQuotes = true
        case r == '"' && inQuotes:
            // الخروج من منطقة الاقتباس
            inQuotes = false
        case r == ',' && !inQuotes:
            // مصادرة فاصل، حفظ الحقل الحالي
            fields = append(fields, current.String())
            current.Reset()
        default:
            // حرف عادي
            current.WriteRune(r)
        }
    }
    // حفظ الحقل الأخير
    fields = append(fields, current.String())

    return fields
}

// تنظيف وتنسيق الحقول
func cleanFields(fields []string) []string {
    cleaned := make([]string, len(fields))
    for i, f := range fields {
        cleaned[i] = strings.TrimSpace(f)
    }
    return cleaned
}

func main() {
    // بيانات CSV محاكاة
    csvData := []string{
        `Alice,28,"Beijing, China"`,
        `Bob,35,"New York, USA"`,
        `Charlie,42,"London, UK"`,
    }

    fmt.Println("=== نتائج تحليل CSV ===")
    for _, line := range csvData {
        fields := parseCSVLine(line)
        fields = cleanFields(fields)
        fmt.Printf("الاسم: %-10s العمر: %-4s الموقع: %s\n",
            fields[0], fields[1], fields[2])
    }

    // العملية العكسية: دمج شريحة في سطر CSV
    record := []string{"David", "30", "Shanghai, China"}
    csvLine := strings.Join(record, ",")
    fmt.Println("\nسطر CSV المُنشأ:", csvLine)
}
▶ جرّب الكود

المخرجات:

TEXT
=== نتائج تحليل CSV ===
الاسم: Alice      العمر: 28   الموقع: Beijing, China
الاسم: Bob        العمر: 35   الموقع: New York, USA
الاسم: Charlie    العمر: 42   الموقع: London, UK

سطر CSV المُنشأ: David,30,Shanghai, China

مثال: محرك القوالب (الصعوبة ⭐⭐⭐)

GO
package main

import (
    "fmt"
    "strconv"
    "strings"
)

// محرك قوالب بسيط: يستبدل {{key}} بالقيم المقابلة
func renderTemplate(template string, data map[string]string) string {
    var result strings.Builder
    result.Grow(len(template) * 2) // تقدير السعة

    i := 0
    for i < len(template) {
        // البحث عن "{{"
        if i+1 < len(template) && template[i] == '{' && template[i+1] == '{' {
            // البحث عن "}}" المقابل
            end := strings.Index(template[i+2:], "}}")
            if end != -1 {
                key := strings.TrimSpace(template[i+2 : i+2+end])
                if value, ok := data[key]; ok {
                    result.WriteString(value)
                } else {
                    // المفتاح غير موجود، الإبقاء كما هو
                    result.WriteString("{{" + key + "}}")
                }
                i += end + 4 // تخطي "}}"
                continue
            }
        }
        result.WriteByte(template[i])
        i++
    }

    return result.String()
}

// تنسيق مخرجات الجدول
func formatTable(headers []string, rows [][]string) string {
    // حساب أقصى عرض لكل عمود
    colWidths := make([]int, len(headers))
    for i, h := range headers {
        colWidths[i] = len(h)
    }
    for _, row := range rows {
        for i, cell := range row {
            if i < len(colWidths) && len(cell) > colWidths[i] {
                colWidths[i] = len(cell)
            }
        }
    }

    var b strings.Builder

    // كتابة الرأس
    for i, h := range headers {
        b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], h))
    }
    b.WriteString("\n")

    // كتابة خط الفصل
    for i := range headers {
        b.WriteString(strings.Repeat("-", colWidths[i]) + "-+-")
    }
    b.WriteString("\n")

    // كتابة صفوف البيانات
    for _, row := range rows {
        for i, cell := range row {
            if i < len(colWidths) {
                b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], cell))
            }
        }
        b.WriteString("\n")
    }

    return b.String()
}

// تحويل سطر رقمي إلى تمثيلات أساس مختلفة
func toBases(numStr string) (map[string]string, error) {
    num, err := strconv.ParseInt(numStr, 10, 64)
    if err != nil {
        return nil, err
    }

    return map[string]string{
        "عشري":       strconv.FormatInt(num, 10),
        "ثنائي":      strconv.FormatInt(num, 2),
        "ثماني":      strconv.FormatInt(num, 8),
        "سداسي عشري": strconv.FormatInt(num, 16),
    }, nil
}

func main() {
    // 1. عرض القوالب
    fmt.Println("=== عرض القوالب ===")
    template := "Hello, {{name}}! Welcome to {{city}}. You have {{count}} new messages."
    data := map[string]string{
        "name":  "Alice",
        "city":  "Beijing",
        "count": "5",
    }
    fmt.Println(renderTemplate(template, data))

    // 2. تنسيق الجدول
    fmt.Println("\n=== تنسيق الجدول ===")
    headers := []string{"الاسم", "العمر", "المدينة"}
    rows := [][]string{
        {"Alice", "28", "Beijing"},
        {"Bob", "35", "New York"},
        {"Charlie", "42", "London"},
    }
    fmt.Print(formatTable(headers, rows))

    // 3. تحويل الأساس
    fmt.Println("\n=== تحويل الأساس ===")
    bases, _ := toBases("255")
    for name, value := range bases {
        fmt.Printf("%-12s: %s\n", name, value)
    }
}
▶ جرّب الكود

المخرجات:

TEXT
=== عرض القوالب ===
Hello, Alice! Welcome to Beijing. You have 5 new messages.

=== تنسيق الجدول ===
الاسم    | العمر | المدينة    | 
--------+-----+---------+-
Alice   | 28  | Beijing | 
Bob     | 35  | New York| 
Charlie | 42  | London  | 

=== تحويل الأساس ===
عشري       : 255
ثنائي      : 11111111
ثماني      : 377
سداسي عشري : ff

سيناريوهات التطبيق العملي

السيناريو 1: محلل السجلات

GO
package main

import (
    "fmt"
    "strconv"
    "strings"
    "time"
)

// هيكل إدخال السجل
type LogEntry struct {
    Timestamp string
    Level     string
    Message   string
    Source    string
}

// تحليل سطر سجل
// الصيغة: [2024-01-15 10:30:00] [ERROR] Database connection failed (db-service)
func parseLogLine(line string) (*LogEntry, error) {
    entry := &LogEntry{}

    // استخراج الطابع الزمني
    if start := strings.Index(line, "["); start != -1 {
        if end := strings.Index(line, "]"); end != -1 {
            entry.Timestamp = line[start+1 : end]
            line = line[end+1:]
        }
    }

    // استخراج مستوى السجل
    line = strings.TrimSpace(line)
    if start := strings.Index(line, "["); start != -1 {
        if end := strings.Index(line, "]"); end != -1 {
            entry.Level = line[start+1 : end]
            line = line[end+1:]
        }
    }

    // استخراج الرسالة والمصدر
    line = strings.TrimSpace(line)
    if parenStart := strings.LastIndex(line, "("); parenStart != -1 {
        if parenEnd := strings.LastIndex(line, ")"); parenEnd != -1 {
            entry.Source = line[parenStart+1 : parenEnd]
            entry.Message = strings.TrimSpace(line[:parenStart])
        }
    } else {
        entry.Message = line
    }

    return entry, nil
}

// تحليل مستويات السجلات
func analyzeLogs(entries []LogEntry) map[string]int {
    stats := make(map[string]int)
    for _, e := range entries {
        stats[strings.ToUpper(e.Level)]++
    }
    return stats
}

// تصفية السجلات حسب كلمة مفتاحية
func filterLogs(entries []LogEntry, keyword string) []LogEntry {
    var filtered []LogEntry
    keyword = strings.ToLower(keyword)
    for _, e := range entries {
        if strings.Contains(strings.ToLower(e.Message), keyword) {
            filtered = append(filtered, e)
        }
    }
    return filtered
}

func main() {
    // بيانات سجلات محاكاة
    logLines := []string{
        "[2024-01-15 10:30:00] [INFO] Application started (main-service)",
        "[2024-01-15 10:30:05] [INFO] Connected to database (db-service)",
        "[2024-01-15 10:31:00] [WARN] High memory usage detected (monitor)",
        "[2024-01-15 10:32:00] [ERROR] Database connection timeout (db-service)",
        "[2024-01-15 10:32:01] [ERROR] Retry failed, switching to backup (db-service)",
        "[2024-01-15 10:33:00] [INFO] Backup database connected (db-service)",
        "[2024-01-15 10:35:00] [DEBUG] Cache cleared (cache-service)",
    }

    // تحليل جميع السجلات
    var entries []LogEntry
    for _, line := range logLines {
        entry, err := parseLogLine(line)
        if err == nil {
            entries = append(entries, *entry)
        }
    }

    // إحصائيات مستويات السجلات
    fmt.Println("=== إحصائيات مستويات السجلات ===")
    stats := analyzeLogs(entries)
    for level, count := range stats {
        fmt.Printf("  %s: %d إدخال\n", level, count)
    }

    // تصفية سجلات الأخطاء
    fmt.Println("\n=== سجلات الأخطاء ===")
    for _, e := range entries {
        if strings.ToUpper(e.Level) == "ERROR" {
            fmt.Printf("  %s | %s | %s\n", e.Timestamp, e.Message, e.Source)
        }
    }

    // البحث بكلمة مفتاحية
    fmt.Println("\n=== السجلات التي تحتوي على 'database' ===")
    filtered := filterLogs(entries, "database")
    for _, e := range filtered {
        fmt.Printf("  [%s] %s\n", e.Level, e.Message)
    }
}

المخرجات:

TEXT
=== إحصائيات مستويات السجلات ===
  INFO: 3 إدخال
  WARN: 1 إدخال
  ERROR: 2 إدخال
  DEBUG: 1 إدخال

=== سجلات الأخطاء ===
  2024-01-15 10:32:00 | Database connection timeout | db-service
  2024-01-15 10:32:01 | Retry failed, switching to backup | db-service

=== السجلات التي تحتوي على 'database' ===
  [INFO] Connected to database
  [ERROR] Database connection timeout
  [INFO] Backup database connected

السيناريو 2: التحقق من مدخلات المستخدم وتنظيفها

GO
package main

import (
    "fmt"
    "regexp"
    "strconv"
    "strings"
    "unicode"
)

// تنظيف مدخلات المستخدم لاسم المستخدم
func sanitizeUsername(name string) (string, error) {
    // إزالة المسافات من البداية والنهاية
    name = strings.TrimSpace(name)

    // التحقق من الطول
    if len(name) < 3 {
        return "", fmt.Errorf("اسم المستخدم قصير جداً (الحد الأدنى 3 أحرف)")
    }
    if len(name) > 20 {
        return "", fmt.Errorf("اسم المستخدم طويل جداً (الحد الأقصى 20 حرفاً)")
    }

    // السماح فقط بالأحرف والأرقام والشرطة السفلية
    var cleaned strings.Builder
    for _, r := range name {
        if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
            cleaned.WriteRune(r)
        }
    }

    result := cleaned.String()
    if len(result) < 3 {
        return "", fmt.Errorf("أحرف صالحة قليلة جداً")
    }

    return strings.ToLower(result), nil
}

// التحقق من رقم الهاتف وتحليله
func parsePhone(phone string) (string, error) {
    // إزالة جميع المسافات والشرطات
    phone = strings.ReplaceAll(phone, " ", "")
    phone = strings.ReplaceAll(phone, "-", "")

    // التحقق من البدء بـ +86
    if strings.HasPrefix(phone, "+86") {
        phone = phone[3:]
    } else if strings.HasPrefix(phone, "86") {
        phone = phone[2:]
    }

    // التحقق من الطول
    if len(phone) != 11 {
        return "", fmt.Errorf("طول رقم الهاتف غير صحيح: %d أرقام", len(phone))
    }

    // التحقق من أن جميع الأحرف أرقام
    for _, r := range phone {
        if !unicode.IsDigit(r) {
            return "", fmt.Errorf("رقم الهاتف يحتوي على حرف غير رقمي: %c", r)
        }
    }

    // التحقق من البدء بـ 1
    if !strings.HasPrefix(phone, "1") {
        return "", fmt.Errorf("رقم الهاتف يجب أن يبدأ بـ 1")
    }

    return phone, nil
}

// تحليل سلسلة الحجم بالوحدات
func parseSize(sizeStr string) (int64, error) {
    sizeStr = strings.TrimSpace(strings.ToUpper(sizeStr))

    // استخراج الأجزاء الرقمية والوحدات
    var numPart strings.Builder
    var unitPart strings.Builder

    for _, r := range sizeStr {
        if unicode.IsDigit(r) || r == '.' {
            numPart.WriteRune(r)
        } else if unicode.IsLetter(r) {
            unitPart.WriteRune(r)
        }
    }

    num, err := strconv.ParseFloat(numPart.String(), 64)
    if err != nil {
        return 0, fmt.Errorf("رقم غير صالح: %s", numPart.String())
    }

    // التحويل إلى بايتات بناءً على الوحدة
    unit := unitPart.String()
    multipliers := map[string]int64{
        "B":  1,
        "KB": 1024,
        "MB": 1024 * 1024,
        "GB": 1024 * 1024 * 1024,
        "TB": 1024 * 1024 * 1024 * 1024,
    }

    multiplier, ok := multipliers[unit]
    if !ok {
        return 0, fmt.Errorf("وحدة غير معروفة: %s", unit)
    }

    return int64(num * float64(multiplier)), nil
}

func main() {
    // اختبارات تنظيف اسم المستخدم
    fmt.Println("=== التحقق من اسم المستخدم ===")
    usernames := []string{"  Alice_123  ", "ab", "A!@#B", "GoDeveloper2024"}
    for _, u := range usernames {
        result, err := sanitizeUsername(u)
        if err != nil {
            fmt.Printf("  %q ← خطأ: %v\n", u, err)
        } else {
            fmt.Printf("  %q ← %q\n", u, result)
        }
    }

    // اختبارات تحليل رقم الهاتف
    fmt.Println("\n=== تحليل رقم الهاتف ===")
    phones := []string{"138 0013 8000", "+86-138-0013-8000", "12345", "23800138000"}
    for _, p := range phones {
        result, err := parsePhone(p)
        if err != nil {
            fmt.Printf("  %q ← خطأ: %v\n", p, err)
        } else {
            fmt.Printf("  %q ← %s\n", p, result)
        }
    }

    // اختبارات تحليل حجم الملف
    fmt.Println("\n=== تحليل حجم الملف ===")
    sizes := []string{"1.5GB", "512MB", "1024KB", "100B", "2TB"}
    for _, s := range sizes {
        bytes, err := parseSize(s)
        if err != nil {
            fmt.Printf("  %s ← خطأ: %v\n", s, err)
        } else {
            fmt.Printf("  %s ← %d بايت\n", s, bytes)
        }
    }
}

المخرجات:

TEXT
=== التحقق من اسم المستخدم ===
"  Alice_123  " ← "alice_123"
"ab" ← خطأ: اسم المستخدم قصير جداً (الحد الأدنى 3 أحرف)
"A!@#B" ← خطأ: أحرف صالحة قليلة جداً
"GoDeveloper2024" ← "godeveloper2024"

=== تحليل رقم الهاتف ===
"138 0013 8000" ← 13800138000
"+86-138-0013-8000" ← 13800138000
"12345" ← خطأ: طول رقم الهاتف غير صحيح: 5 أرقام
"23800138000" ← خطأ: رقم الهاتف يجب أن يبدأ بـ 1

=== تحليل حجم الملف ===
1.5GB ← 1610612736 بايت
512MB ← 536870912 بايت
1024KB ← 1048576 بايت
100B ← 100 بايت
2TB ← 2199023255552 بايت

❓ أسئلة شائعة

س1: لماذا len("مرحبا") تعيد 10 بدلاً من 5؟

GO
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    s := "مرحبا"

    // len() تعيد البايتات، وليس الأحرف
    fmt.Println("len():", len(s)) // 10

    // الأحرف العربية تأخذ 2 بايت لكل حرف في UTF-8
    // م(2) + ر(2) + ح(2) + ب(2) + ا(2) = 10

    // الطريقة الصحيحة للحصول على عدد الأحرف
    fmt.Println("RuneCountInString():", utf8.RuneCountInString(s)) // 5

    // أو استخدام range للعد
    count := 0
    for range s {
        count++
    }
    fmt.Println("range count:", count) // 4
}

نقطة أساسية: عند التعامل مع أحرف متعددة البايتات مثل الصينية، استخدم دائماً utf8.RuneCountInString() أو range للحصول على العدد الحقيقي للأحرف.

س2: ربط النصوص — استخدام + أم strings.Builder؟

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // ربط بسيط: استخدم + فقط (المحسن يحسنه)
    s := "Hello" + " " + "World"
    fmt.Println(s)

    // ربط كثير: يجب استخدام strings.Builder
    var builder strings.Builder
    for i := 0; i < 10000; i++ {
        builder.WriteString("a")
    }
    fmt.Println("الطول:", builder.Len())

    // التخصيص المسبق يحسّن الأداء أكثر
    var builder2 strings.Builder
    builder2.Grow(10000) // تخصيص 10000 بايت مسبقاً
    for i := 0; i < 10000; i++ {
        builder2.WriteString("b")
    }
    fmt.Println("الطول:", builder2.Len())
}

قواعد عامة:

السيناريو النهج الموصى به
ربط 2-3 نصوص + أو fmt.Sprintf
ربط في حلقة (عدد معروف) strings.Builder + Grow()
ربط في حلقة (عدد غير معروف) strings.Builder

س3: كيف أتحقق مما إذا كان نص يحتوي فقط على أحرف محددة؟

GO
package main

import (
    "fmt"
    "strings"
    "unicode"
)

func main() {
    s := "Hello123"

    // التحقق من أنه يحتوي فقط على أحرف وأرقام
    isAlphanumeric := true
    for _, r := range s {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
            isAlphanumeric = false
            break
        }
    }
    fmt.Println("أحرف وأرقام فقط:", isAlphanumeric)

    // التحقق من أنه يحتوي فقط على أحرف ASCII
    isASCII := true
    for _, r := range s {
        if r > 127 {
            isASCII = false
            break
        }
    }
    fmt.Println("ASCII فقط:", isASCII)

    // التحقق من أنه يحتوي فقط على مجموعة أحرف محددة
    allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    containsOnlyAllowed := true
    for _, r := range s {
        if !strings.ContainsRune(allowed, r) {
            containsOnlyAllowed = false
            break
        }
    }
    fmt.Println("ضمن النطاق المسموح:", containsOnlyAllowed)
}

س4: متى أستخدم strings.Contains مقابل التعبيرات النمطية؟

GO
package main

import (
    "fmt"
    "regexp"
    "strings"
)

func main() {
    text := "My email is test@example.com, phone is 13800138000"

    // بحث بسيط → استخدم حزمة strings (أسرع)
    fmt.Println(strings.Contains(text, "example.com")) // true

    // مطابقة أنماط → استخدم التعبيرات النمطية
    emailRegex := regexp.MustCompile(`[\w.]+@[\w.]+\.\w+`)
    fmt.Println("البريد:", emailRegex.FindString(text))

    phoneRegex := regexp.MustCompile(`1[3-9]\d{9}`)
    fmt.Println("الهاتف:", phoneRegex.FindString(text))
}

إرشادات:


📖 ملخص

الموضوع المحتوى الأساسي
حزمة strings Contains, HasPrefix, HasSuffix, Index, Replace, Split, Join, Trim, ToUpper, ToLower, Count, Repeat, Fields
حزمة strconv Atoi, Itoa, ParseBool, ParseFloat, FormatInt, FormatFloat
unicode/utf8 RuneCountInString, ValidString, unicode.IsLetter/IsDigit/IsSpace
strings.Builder WriteString, WriteRune, WriteByte, Grow, String, Len
المبادئ الأساسية النصوص غير قابلة للتعديل، len تعيد البايتات، range يكرر حسب rune، استخدم Builder للربط الكثير

📝 تمارين

التمرين 1: أساسي — عكس النص

اكتب دالة reverseString(s string) string تعكس نصاً. يجب أن تتعامل بشكل صحيح مع الأحرف الصينية.

تلميح: لا يمكنك ببساطة تحويل النص إلى []byte وعكسه، لأن الأحرف الصينية تشغل بايتات متعددة.

GO
// النتائج المتوقعة
reverseString("Hello")   // "olleH"
reverseString("مرحبا")  // "ابحرم"

التمرين 2: متوسط — تحويل CamelCase و Snake_case

اكتب دالتين:

تلميح: استخدم unicode.IsUpper لتحديد مواضع الأحرف الكبيرة.

GO
// النتائج المتوقعة
camelToSnake("helloWorld")     // "hello_world"
camelToSnake("HTTPResponse")   // "http_response"
snakeToCamel("hello_world")    // "helloWorld"
snakeToCamel("http_response")  // "httpResponse"

التمرين 3: تحدي — مستخرج عناوين Markdown بسيط

اكتب دالة extractHeadings(md string) []string تستخرج جميع العناوين من مستند Markdown.

تلميح: العناوين تبدأ بـ #، وعدد رموز # يشير إلى مستوى العنوان.

GO
// المدخلات
md := `# Heading 1
This is body text
## Heading 2
### Heading 3
## Another Heading 2`

// المخرجات المتوقعة
// ["# Heading 1", "## Heading 2", "### Heading 3", "## Another Heading 2"]

الدرس التالي

تهانينا على إكمال معالجة النصوص! في الدرس التالي، سنتعلم عمليات الإدخال/الإخراج للملفات — كيفية قراءة وكتابة الملفات، والتعامل مع المجلدات، واستخدام الإدخال/الإخراج المؤقت للأداء الأفضل.

👉 الدرس 20: إدخال/إخراج الملفات

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%