Сравнение Scratch wikis

Материал из Поле цифровой дидактики
# ============================================
# СРАВНИТЕЛЬНОЕ ИССЛЕДОВАНИЕ: Scratch Wiki на разных языках
# Анализ совместного редактирования (EN, DE, FR, RU)
# ============================================

library(tidyverse)
library(httr)
library(jsonlite)

getwd()
setwd("C:/Users/Evgeny/Documents/R_Text")
# ============================================
# КОНФИГУРАЦИЯ
# ============================================

DEFAULT_DELAY <- 0.5
API_TIMEOUT <- 30
MAX_RETRIES <- 3


# Scratch Wiki platforms на разных языках
SCRATCH_WIKIS <- list(
  "en" = "https://en.scratch-wiki.info/w/api.php",
  "de" = "https://de.scratch-wiki.info/w/api.php",
  "fr" = "https://fr.scratch-wiki.info/w/api.php",
  "ru" = "https://ru.scratch-wiki.info/w/api.php"
)

# ============================================
# УТИЛИТА: Безопасный запрос к API
# ============================================

safe_get <- function(url = NULL, 
                     params, 
                     max_retries = MAX_RETRIES,
                     verbose = FALSE) {
  
  for (attempt in 1:max_retries) {
    tryCatch({
      
      response <- GET(url, 
                     query = params, 
                     timeout(API_TIMEOUT))
      
      if (status_code(response) != 200) {
        if (verbose) {
          warning(paste("HTTP", status_code(response), "попытка", attempt))
        }
        if (attempt < max_retries) {
          Sys.sleep(1)
          next
        }
        return(NULL)
      }
      
      content_txt <- content(response, as = "text", encoding = "UTF-8")
      return(fromJSON(content_txt))
      
    }, error = function(e) {
      if (verbose) {
        cat("Ошибка сети (попытка", attempt, "):", e$message, "\n")
      }
      if (attempt < max_retries) {
        Sys.sleep(1)
      }
      return(NULL)
    })
  }
  
  return(NULL)
}

# ============================================
# МОДУЛЬ 1: ПОЛУЧЕНИЕ ВСЕХ СТРАНИЦ С СОАВТОРСТВОМ
# ============================================

get_all_pages_with_coauthors <- function(base_url,
                                         max_pages = 1600,
                                         min_authors = 2,
                                         delay = DEFAULT_DELAY,
                                         verbose = TRUE) {
  
  if (verbose) {
    cat("\n╔════════════════════════════════════════════════╗\n")
    cat("║  🔍 ПОЛУЧЕНИЕ ВСЕХ СТРАНИЦ С СОАВТОРСТВОМ   ║\n")
    cat("║  (Universal mode: без категорий)             ║\n")
    cat("╚════════════════════════════════════════════════╝\n\n")
    cat("URL:", base_url, "\n")
    cat("Максимум страниц для проверки:", max_pages, "\n")
    cat("Минимум авторов для отбора:", min_authors, "\n\n")
  }
  
  # === ШАГ 1: Получаем список первых N страниц ===
  
  if (verbose) cat("📄 ШАГ 1: Получение списка всех страниц...\n")
  
  params <- list(
    action = "query",
    list = "allpages",
    apnamespace = "0",       # Только основное пространство
    aplimit = min(max_pages, 1500),  # API лимит
    apdir = "ascending",
    format = "json"
  )
  
  resp <- safe_get(url = base_url, params = params, verbose = FALSE)
  
  if (is.null(resp) || is.null(resp$query$allpages)) {
    cat("❌ Ошибка получения списка страниц\n")
    return(tibble())
  }
  
  pages_list <- resp$query$allpages
  pages_df <- as_tibble(pages_list) %>%
    select(pageid, title) %>%
    rename(page_title = title) %>%
    mutate(pageid = as.numeric(pageid))
  
  if (verbose) {
    cat("✓ Получено страниц:", nrow(pages_df), "\n\n")
  }
  
  # === ШАГ 2: Проверяем соавторство для каждой страницы ===
  
  if (verbose) {
    cat("🔗 ШАГ 2: Проверка соавторства (это может занять время)...\n")
    cat("─────────────────────────────────────────────────────\n")
  }
  
  pages_with_coauthors <- tibble()
  checked_count <- 0
  
  for (i in seq_len(nrow(pages_df))) {
    
    page_title <- pages_df$page_title[i]
    pageid <- pages_df$pageid[i]
    
    # Периодический отчет о прогрессе
    if (i %% 50 == 0 && verbose) {
      cat("  [", i, "/", nrow(pages_df), "] ... проверено ", 
          nrow(pages_with_coauthors), " со соавт.\n", sep = "")
    }
    
    # Запрашиваем contributors
    params <- list(
      action = "query",
      prop = "contributors",
      titles = page_title,
      pclimit = "max",
      format = "json"
    )
    
    resp <- safe_get(url = base_url, params = params, verbose = FALSE)
    checked_count <- checked_count + 1
    
    if (!is.null(resp) && !is.null(resp$query$pages)) {
      
      pages_data <- resp$query$pages
      
      # Может быть data.frame (одна страница) или список
      if (is.data.frame(pages_data)) {
        pages_list_inner <- list(pages_data)
      } else {
        pages_list_inner <- pages_data
      }
      
      # Проверяем первую (и единственную) страницу
      if (length(pages_list_inner) > 0) {
        api_page <- pages_list_inner[[1]]
        
        if (!is.null(api_page$contributors)) {
          contribs_data <- api_page$contributors
          
          # Парсим contributors
          if (is.data.frame(contribs_data)) {
            contribs_df <- contribs_data
          } else if (is.list(contribs_data)) {
            tryCatch({
              contribs_df <- bind_rows(contribs_data)
            }, error = function(e) {
              contribs_df <<- as.data.frame(
                do.call(rbind, contribs_data), 
                stringsAsFactors = FALSE
              )
            })
          } else {
            contribs_df <- NULL
          }
          
          # Проверяем количество авторов
          if (!is.null(contribs_df) && nrow(contribs_df) >= min_authors) {
            
            pages_with_coauthors <- bind_rows(
              pages_with_coauthors,
              tibble(
                pageid = pageid,
                page_title = page_title,
                n_authors = nrow(contribs_df)
              )
            )
          }
        }
      }
    }
    
    Sys.sleep(delay)
  }
  
  if (verbose) {
    cat("\n✓ Проверка завершена\n\n")
    cat("🎯 ИТОГИ ОТБОРА:\n")
    cat("─────────────────────────────────────────────────────\n")
    cat("Проверено страниц:", checked_count, "\n")
    cat("Найдено с ≥", min_authors, "авторами:", nrow(pages_with_coauthors), "\n")
    pct <- ifelse(checked_count > 0, 
                   round(100 * nrow(pages_with_coauthors) / checked_count, 1),
                   0)
    cat("Процент совместных страниц:", pct, "%\n\n")
  }
  
  return(pages_with_coauthors)
}

# ============================================
# МОДУЛЬ 2: ПОЛУЧЕНИЕ CONTRIBUTORS ДЛЯ ОТОБРАННЫХ СТРАНИЦ
# ============================================

get_contributors_for_pages <- function(base_url,
                                       pages_df,
                                       batch_size = 10,
                                       delay = DEFAULT_DELAY,
                                       verbose = TRUE) {
  
  if (verbose) {
    cat("\n╔════════════════════════════════════════════════╗\n")
    cat("║  📚 ПОЛУЧЕНИЕ ДЕТАЛЕЙ CONTRIBUTORS          ║\n")
    cat("╚════════════════════════════════════════════════╝\n\n")
    cat("Страниц для обработки:", nrow(pages_df), "\n")
    cat("Batch size:", batch_size, "\n")
    cat("Ожидаемых батчей:", ceiling(nrow(pages_df) / batch_size), "\n\n")
  }
  
  all_contributors <- tibble()
  n_batches <- ceiling(nrow(pages_df) / batch_size)
  
  for (batch_num in 1:n_batches) {
    
    start_idx <- (batch_num - 1) * batch_size + 1
    end_idx <- min(batch_num * batch_size, nrow(pages_df))
    
    batch_pages <- pages_df[start_idx:end_idx, ]
    
    if (verbose) {
      cat("[", batch_num, "/", n_batches, "] Страницы ",
          start_idx, "-", end_idx, " ... ", sep = "")
    }
    
    # Подготавливаем titles
    titles_param <- paste(batch_pages$page_title, collapse = "|")
    
    # Запрос к API
    params <- list(
      action = "query",
      prop = "contributors",
      titles = titles_param,
      pcexcludegroup = "widgeteditor",
      formatversion = "2",
      format = "json"
    )
    
    data <- safe_get(url = base_url, params = params, verbose = FALSE)
    batch_contributors <- tibble()
    
    if (!is.null(data) && !is.null(data$query$pages)) {
      
      pages_from_api <- data$query$pages
      
      if (is.data.frame(pages_from_api)) {
        pages_list <- list(pages_from_api)
      } else if (is.list(pages_from_api)) {
        pages_list <- pages_from_api
      } else {
        pages_list <- NULL
      }
      
      # Обрабатываем все страницы
      if (!is.null(pages_list)) {
        
        for (page_idx in seq_along(pages_list)) {
          
          api_page <- pages_list[[page_idx]]
          if (is.null(api_page)) next
          
          api_title <- as.character(api_page$title)
          api_pageid <- as.numeric(api_page$pageid)
          
          # Ищем соответствие в batch_pages
          matching_idx <- which(batch_pages$page_title == api_title)
          
          if (length(matching_idx) == 0) {
            matching_idx <- which(
              trimws(tolower(batch_pages$page_title)) == 
              trimws(tolower(api_title))
            )
          }
          
          if (length(matching_idx) > 0) {
            logical_pageid <- batch_pages$pageid[matching_idx[1]]
            logical_title <- batch_pages$page_title[matching_idx[1]]
          } else {
            logical_pageid <- api_pageid
            logical_title <- api_title
          }
          
          # Извлекаем contributors
          contributors_col <- api_page$contributors
          
          if (!is.null(contributors_col)) {
            
            if (is.data.frame(contributors_col)) {
              contribs_df <- contributors_col
            } else if (is.list(contributors_col) && length(contributors_col) > 0) {
              tryCatch({
                contribs_df <- bind_rows(contributors_col)
              }, error = function(e) {
                contribs_df <<- as.data.frame(
                  do.call(rbind, contributors_col), 
                  stringsAsFactors = FALSE
                )
              })
            } else {
              contribs_df <- NULL
            }
            
            if (!is.null(contribs_df) && nrow(contribs_df) > 0) {
              
              if (!("userid" %in% names(contribs_df)) || 
                  !("name" %in% names(contribs_df))) {
                next
              }
              
              page_contribs <- contribs_df %>%
                as_tibble() %>%
                select(userid, name) %>%
                filter(!is.na(.data$userid), 
                       !is.na(.data$name),
                       .data$name != "") %>%
                mutate(
                  author_id = as.numeric(.data$userid),
                  author_name = as.character(.data$name)
                ) %>%
                select(author_id, author_name) %>%
                mutate(
                  pageid = logical_pageid,
                  page_title = logical_title
                ) %>%
                select(author_id, author_name, pageid, page_title)
              
              batch_contributors <- bind_rows(batch_contributors, page_contribs)
            }
          }
        }
      }
      
      if (verbose) {
        unique_pages <- n_distinct(batch_contributors$pageid)
        unique_authors <- n_distinct(batch_contributors$author_id)
        cat("✓ ", nrow(batch_contributors), " связей", sep = "")
        if (nrow(batch_contributors) > 0) {
          cat(" (", unique_authors, " авторов, ", unique_pages, " стр)\n", sep = "")
        } else {
          cat("\n")
        }
      }
    } else if (verbose) {
      cat("✗ ошибка API\n")
    }
    
    all_contributors <- bind_rows(all_contributors, batch_contributors)
    Sys.sleep(delay)
  }
  
  # Финальная обработка
  if (verbose) {
    cat("\n🔄 ФИНАЛЬНАЯ ОБРАБОТКА:\n")
    cat("─────────────────────────────────────\n")
  }
  
  all_contributors_clean <- all_contributors %>%
    distinct(author_id, pageid, .keep_all = TRUE) %>%
    arrange(pageid, author_id)
  
  if (verbose) {
    cat("✓ Дубли удалены\n")
    cat("  Итого связей:", nrow(all_contributors_clean), "\n")
    cat("  Уникальных авторов:", n_distinct(all_contributors_clean$author_id), "\n")
    cat("  Уникальных страниц:", n_distinct(all_contributors_clean$pageid), "\n\n")
  }
  
  return(all_contributors_clean)
}

# ============================================
# МОДУЛЬ 3: СРАВНИТЕЛЬНЫЙ АНАЛИЗ
# ============================================

compare_scratch_wikis <- function(wikis_list = SCRATCH_WIKIS,
                                  max_pages = 300,
                                  verbose = TRUE) {
  
  if (verbose) {
    cat("\n\n")
    cat(strrep("═", 60), "\n")
    cat("🌍 СРАВНИТЕЛЬНЫЙ АНАЛИЗ SCRATCH WIKI НА РАЗНЫХ ЯЗЫКАХ\n")
    cat(strrep("═", 60), "\n\n")
  }
  
  results <- list()
  
  for (lang in names(wikis_list)) {
    
    base_url <- wikis_list[[lang]]
    
    if (verbose) {
      cat("\n", strrep("─", 60), "\n")
      cat("🌐 ЯЗЫК:", toupper(lang), "\n")
      cat(strrep("─", 60), "\n")
    }
    
    # 1. Получаем страницы с соавторством
    pages_coauth <- get_all_pages_with_coauthors(
      base_url = base_url,
      max_pages = max_pages,
      delay = 1,
      verbose = verbose
    )
    
    if (nrow(pages_coauth) == 0) {
      if (verbose) cat("⚠️ Нет данных для языка", lang, "\n")
      results[[lang]] <- NULL
      next
    }
    
    # 2. Получаем детали contributors
    contributors <- get_contributors_for_pages(
      base_url = base_url,
      pages_df = pages_coauth,
      batch_size = 8,
      delay = 1,
      verbose = verbose
    )
    
    # 3. Вычисляем статистику
    stats <- contributors %>%
      group_by(pageid, page_title) %>%
      summarise(
        n_authors = n_distinct(author_id),
        n_edits = n(),
        .groups = "drop"
      )
    
    results[[lang]] <- list(
      pages = pages_coauth,
      contributors = contributors,
      stats = stats
    )
    
    Sys.sleep(2)
  }
  
  return(results)
}

# ============================================
# МОДУЛЬ 4: ВИЗУАЛИЗАЦИЯ СРАВНЕНИЯ
# ============================================

print_comparison_summary <- function(results) {
  
  cat("\n\n")
  cat(strrep("═", 80), "\n")
  cat("📊 СВОДНАЯ ТАБЛИЦА: СРАВНЕНИЕ SCRATCH WIKI ПО ЯЗЫКАМ\n")
  cat(strrep("═", 80), "\n\n")
  
  summary_table <- tibble()
  
  for (lang in names(results)) {
    
    if (is.null(results[[lang]])) next
    
    pages <- results[[lang]]$pages
    contrib <- results[[lang]]$contributors
    stats <- results[[lang]]$stats
    
    summary_table <- bind_rows(
      summary_table,
      tibble(
        Language = toupper(lang),
        "Pages (all)" = nrow(pages),
        "Pages (≥2 authors)" = nrow(pages),
        "Total authors" = n_distinct(contrib$author_id),
        "Total links" = nrow(contrib),
        "Avg authors/page" = round(mean(stats$n_authors), 2),
        "Max authors/page" = max(stats$n_authors)
      )
    )
  }
  
  print(summary_table)
  
  cat("\n")
  cat("Пояснения:\n")
  cat("- Pages (all): Проверено страниц\n")
  cat("- Pages (≥2 authors): Найдено со совместным редактированием\n")
  cat("- Total authors: Всего уникальных авторов\n")
  cat("- Total links: Связей (автор-страница)\n")
  cat("- Avg authors/page: Средний размер группы авторов\n")
  cat("- Max authors/page: Максимальное количество авторов на странице\n\n")
}

# ============================================
# БЫСТРЫЙ СТАРТ: 
# ============================================



pages_en <- get_all_pages_with_coauthors(SCRATCH_WIKIS$en, max_pages = 200)
contrib_en <- get_contributors_for_pages(SCRATCH_WIKIS$en, pages_en)