Сравнение 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)
