Как собрать данные Scratch Wiki при помощи MediaWiki API

Материал из Поле цифровой дидактики
Версия от 08:50, 14 апреля 2026; Patarakin (обсуждение | вклад) (get-all-pages)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Описание Знания сообщества 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()