Как собрать данные Scratch Wiki при помощи MediaWiki API
Материал из Поле цифровой дидактики
| Описание | Знания сообщества Scratch хранятся в нескольких вики на разных языках. Мы хотим собрать и сравнить данные о поведении участников |
|---|---|
| Область знаний | Информатика, Робототехника, Моделирование |
| Область использования (ISTE) | |
| Возрастная категория | 14
|
| Поясняющее видео | |
| Близкие рецепту понятия | |
| Среды и средства для приготовления рецепта: | MediaWiki API, Scratch, R |
MediaWiki API — это интерфейс для программного доступа к вики-сайтам, таким как Wikipedia или локальные MediaWiki. Он позволяет запрашивать списки страниц, их содержимое, историю правок и метаданные через HTTP-запросы
Для работы с API в R используйте пакеты httr (или httr2) для HTTP-запросов, jsonlite для парсинга JSON-ответов и tidyverse для обработки данных.
Функции
safe-get
Функция safe_get — это обёртка над GET из httr с обработкой ошибок (например, retry при таймауте), часто реализуемая через purrr::safely.
safe_get <- function(base_url, params) {
library(httr)
library(jsonlite)
resp <- GET(base_url, query = params)
if (http_type(resp) != "application/json") {
warning("API не вернул JSON")
return(NULL)
}
fromJSON(content(resp, "text"))
}
get-all-pages
get_all_pages <- function(base_url, max_pages = 1600, verbose = TRUE) {
all_pages <- list()
apcontinue <- NULL
pages_count <- 0
repeat {
params <- list(
action = "query",
list = "allpages",
apnamespace = "0",
aplimit = 500, # максимум за раз
apdir = "ascending",
format = "json"
)
if (!is.null(apcontinue)) params$apcontinue <- apcontinue
resp <- safe_get(base_url, params)
if (is.null(resp$query$allpages)) break
all_pages <- c(all_pages, resp$query$allpages)
pages_count <- pages_count + length(resp$query$allpages)
if (verbose) cat("Получено страниц:", pages_count, "\n")
# Проверяем условие остановки
if (pages_count >= max_pages || is.null(resp$continue)) break
apcontinue <- resp$continue$apcontinue
}
as_tibble(do.call(rbind, all_pages)) %>%
select(pageid, title) %>%
rename(page_title = title) %>%
mutate(pageid = as.numeric(pageid))
}
Все данные о действиях в MediaWiki
library(tidyverse)
library(httr)
library(jsonlite)
BASE_URL <- "https://wiki.mininuniver.ru/api.php"
DEFAULT_DELAY <- 0.5
API_TIMEOUT <- 30
MAX_RETRIES <- 4
# ---------- Общий помощник для запросов ----------
safe_get <- function(params, max_retries = MAX_RETRIES, verbose = FALSE) {
for (attempt in seq_len(max_retries)) {
ans <- tryCatch({
resp <- GET(BASE_URL,
query = params,
timeout(API_TIMEOUT))
if (status_code(resp) != 200) return(NULL)
txt <- content(resp, as = "text", encoding = "UTF-8")
fromJSON(txt, simplifyVector = TRUE)
}, error = function(e) {
if (verbose) message("Error on attempt ", attempt, ": ", e$message)
NULL
})
if (!is.null(ans)) return(ans)
Sys.sleep(1)
}
NULL
}
# ---------- Надёжный извлекатель страницы из data$query$pages ----------
extract_page_data_digida <- function(pages_obj) {
if (is.null(pages_obj)) return(NULL)
if (is.data.frame(pages_obj)) {
if (nrow(pages_obj) == 0) return(NULL)
return(as.list(pages_obj[1, , drop = FALSE]))
}
if (is.list(pages_obj)) {
if (length(pages_obj) == 0) return(NULL)
first_el <- pages_obj[[1]]
if (is.data.frame(first_el)) {
if (nrow(first_el) == 0) return(NULL)
return(as.list(first_el[1, , drop = FALSE]))
}
if (is.list(first_el)) {
return(first_el)
}
if (!is.null(names(pages_obj)) &&
all(c("pageid", "title") %in% names(pages_obj))) {
return(pages_obj)
}
}
NULL
}
# ---------- 1. Все страницы ns = 0 и 1 ----------
get_all_pages_ns01 <- function(namespaces = c(0, 1),
delay = DEFAULT_DELAY,
verbose = TRUE) {
all_pages <- tibble()
for (ns in namespaces) {
if (verbose) message("Collecting namespace ", ns)
apcontinue <- NULL
repeat {
params <- list(
action = "query",
list = "allpages",
apnamespace = as.character(ns),
aplimit = "max",
apdir = "ascending",
formatversion = "2",
format = "json"
)
if (!is.null(apcontinue)) params$apcontinue <- apcontinue
data <- safe_get(params, verbose = verbose)
if (is.null(data) || is.null(data$query$allpages)) break
batch <- as_tibble(data$query$allpages) %>%
select(any_of(c("pageid", "ns", "title"))) %>%
rename(
pageid = pageid,
source_namespace = ns,
page_title = title
) %>%
mutate(
pageid = as.numeric(pageid),
source_namespace = as.integer(source_namespace)
)
all_pages <- bind_rows(all_pages, batch)
if (verbose) message(" total pages: ", nrow(all_pages))
if (is.null(data$continue$apcontinue)) break
apcontinue <- data$continue$apcontinue
Sys.sleep(delay)
}
}
all_pages %>%
filter(!is.na(pageid)) %>%
distinct(pageid, source_namespace, .keep_all = TRUE) %>%
arrange(source_namespace, pageid)
}
# ---------- 2. Ревизии по одной странице ----------
get_page_revisions <- function(pageid, delay = DEFAULT_DELAY) {
params <- list(
action = "query",
prop = "revisions",
pageids = as.character(pageid),
rvprop = "ids|timestamp|comment|user|userid|size|flags",
rvlimit = "max",
rvdir = "newer",
formatversion = "2",
format = "json"
)
page_rows <- tibble()
repeat {
data <- safe_get(params)
if (is.null(data) || is.null(data$query) || is.null(data$query$pages)) break
page_data <- extract_page_data_digida(data$query$pages)
if (is.null(page_data) || !is.list(page_data)) break
if (is.null(page_data[["revisions"]])) break
revs_raw <- page_data[["revisions"]]
if (is.null(revs_raw)) break
if (is.data.frame(revs_raw)) {
revs_df <- as_tibble(revs_raw)
} else if (is.list(revs_raw) && length(revs_raw) > 0) {
revs_df <- tryCatch(as_tibble(dplyr::bind_rows(revs_raw)),
error = function(e) NULL)
} else {
revs_df <- NULL
}
if (!is.null(revs_df) && nrow(revs_df) > 0) {
parsed <- revs_df %>%
transmute(
author_id = if ("userid" %in% names(.)) suppressWarnings(as.numeric(userid)) else NA_real_,
author_name = if ("user" %in% names(.)) as.character(user) else NA_character_,
revid = if ("revid" %in% names(.)) suppressWarnings(as.numeric(revid)) else NA_real_,
parentid = if ("parentid" %in% names(.)) suppressWarnings(as.numeric(parentid)) else NA_real_,
commit_time = if ("timestamp" %in% names(.)) as.character(timestamp) else NA_character_,
commit_message = if ("comment" %in% names(.)) as.character(comment) else NA_character_,
rev_size = if ("size" %in% names(.)) suppressWarnings(as.integer(size)) else NA_integer_,
minor = if ("minor" %in% names(.)) TRUE else FALSE
)
page_rows <- dplyr::bind_rows(page_rows, parsed)
}
if (!is.null(data$continue) && !is.null(data$continue$rvcontinue)) {
params$rvcontinue <- data$continue$rvcontinue
params$continue <- data$continue$continue
Sys.sleep(delay)
} else {
break
}
}
page_rows
}
# ---------- 3. Contributors по страницам (для wiki_df_team) ----------
get_page_contributors <- function(pages_df,
delay = DEFAULT_DELAY,
verbose = TRUE) {
all_contribs <- tibble()
for (i in seq_len(nrow(pages_df))) {
pid <- pages_df$pageid[i]
ptitle <- pages_df$page_title[i]
pns <- pages_df$source_namespace[i]
if (verbose && i %% 50 == 1) {
message("Contributors: [", i, "/", nrow(pages_df), "] ", ptitle)
}
params <- list(
action = "query",
prop = "contributors",
pageids = as.character(pid),
pclimit = "max",
formatversion = "2",
format = "json"
)
data <- safe_get(params)
if (is.null(data) || is.null(data$query) || is.null(data$query$pages)) {
Sys.sleep(delay)
next
}
pages_obj <- data$query$pages
page_data <- extract_page_data_digida(pages_obj)
if (is.null(page_data) || is.null(page_data[["contributors"]])) {
Sys.sleep(delay)
next
}
contribs_raw <- page_data[["contributors"]]
if (is.data.frame(contribs_raw)) {
contribs_df <- as_tibble(contribs_raw)
} else if (is.list(contribs_raw) && length(contribs_raw) > 0) {
contribs_df <- tryCatch(as_tibble(dplyr::bind_rows(contribs_raw)),
error = function(e) NULL)
} else {
contribs_df <- NULL
}
if (!is.null(contribs_df) && nrow(contribs_df) > 0) {
parsed <- contribs_df %>%
transmute(
author_id = if ("userid" %in% names(.)) suppressWarnings(as.numeric(userid)) else NA_real_,
author_name = if ("name" %in% names(.)) as.character(name) else NA_character_
) %>%
filter(!is.na(author_name)) %>%
mutate(
pageid = pid,
page_title = ptitle,
source_namespace = pns,
category = NA_character_
)
all_contribs <- bind_rows(all_contribs, parsed)
}
Sys.sleep(delay)
}
all_contribs
}
# ---------- 4. Главная функция: собираем events и team ----------
collect_digida_events_and_team <- function(
out_events_csv = "wiki_df_events_digid.csv",
out_team_csv = "wiki_df_team_digid.csv",
delay = DEFAULT_DELAY,
verbose = TRUE
) {
# 4.1. Страницы
pages <- get_all_pages_ns01(delay = delay, verbose = verbose)
# 4.2. Ревизии по всем страницам
all_revs <- tibble()
for (i in seq_len(nrow(pages))) {
pid <- pages$pageid[i]
ptitle <- pages$page_title[i]
pns <- pages$source_namespace[i]
if (verbose) message("[", i, "/", nrow(pages), "] ", ptitle)
revs <- get_page_revisions(pid, delay = delay)
if (nrow(revs) > 0) {
revs <- revs %>%
arrange(commit_time, revid) %>%
mutate(
pageid = pid,
page_title = ptitle,
source_namespace = pns,
total_changes = rev_size - dplyr::lag(rev_size, default = 0L),
commit_type = "wiki_edit",
year = as.integer(substr(commit_time, 1, 4)),
author_anon = ifelse(!is.na(author_id),
paste0("D", author_id),
paste0("DU_", author_name))
)
all_revs <- bind_rows(all_revs, revs)
}
Sys.sleep(delay)
}
# 4.3. wiki_df_events
wiki_df_events <- all_revs %>%
transmute(
author_anon,
object_id = pageid,
object_name = page_title,
commit_time,
year,
commit_type,
total_changes
)
write.csv(wiki_df_events, out_events_csv,
row.names = FALSE, fileEncoding = "UTF-8")
# 4.4. wiki_df_team
team_raw <- get_page_contributors(pages, delay = delay, verbose = verbose)
wiki_df_team <- team_raw %>%
mutate(
author_anon = ifelse(!is.na(author_id),
paste0("D", author_id),
paste0("DU_", author_name)),
object_id = pageid
) %>%
select(
author_anon,
object_id,
pageid,
page_title,
source_namespace,
author_id,
author_name,
category
)
write.csv(wiki_df_team, out_team_csv,
row.names = FALSE, fileEncoding = "UTF-8")
list(
wiki_df_events = wiki_df_events,
wiki_df_team = wiki_df_team
)
}
# ---------- Запуск ----------
res <- collect_digida_events_and_team()
