위키 레이싱 문서 그래프

한국어 위키백과 2026년 11월 1일 덤프를 전처리한 결과다.

770,114문서 (그래프의 점)
54,085,452링크 (그래프의 화살표)
70.2문서당 평균 나가는 링크
24문서당 나가는 링크 중앙값

문서별 연결 보기

전체 파일은 이 페이지에 담기에 커서 문서 2,003개만 넣었다. 들어오는 링크가 많은 문서 1,500개, 무작위 문서 500개, 예시 문서로 골랐다. 파란 칸을 누르면 그 문서로 이동하고, 회색 칸은 표본에 없는 문서다.

데이터를 불러오는 중이다. 이 문구가 계속 보이면 viewer-data.js가 페이지와 같은 폴더에 있는지 확인해야 한다.

전처리 결과 파일

문서마다 0부터 770,113까지 번호를 매기고, 제목과 링크를 파일 두 개로 나눴다. 자료구조를 바꿔도 모두 이 파일을 읽는다.

파일크기줄 수줄 형식예시
titles.tsv26.7 MB770,114번호, page_id, 제목 (탭으로 구분)179965 710049 리그_오브_레전드
links.txt356.9 MB770,114출발 번호, 대상 번호들 (공백으로 구분)179965 101 146 176 864 ...

links.txt의 n번째 줄(0부터)이 n번 문서다. 대상 번호는 오름차순이고 겹치지 않으며, 나가는 링크가 없는 문서 2,967개는 번호만 있는 줄로 남아 있다. 두 파일을 다시 읽어 문서 수, 링크 수, 번호 범위와 순서를 검사했고 모두 통과했다.

전처리 코드

덤프 4개를 읽어 위 파일 두 개를 만든다. 이 PC에서 약 3분 걸렸고, 표준 라이브러리만 쓴다.

python -I scripts/preprocess.py data/raw data/processed
scripts/preprocess.py
"""덤프 4개를 문서 번호 기반 그래프 파일로 바꾼다.

사용법: python -I scripts/preprocess.py <덤프 폴더> <출력 폴더>

출력
  titles.tsv : 줄마다 "번호<TAB>page_id<TAB>제목". 번호는 0부터 빈틈없이 매긴다.
  links.txt  : 줄마다 "출발번호 대상번호 대상번호 ...". 출발번호 순서이고, 대상번호는 오름차순이며 겹치지 않는다.
               나가는 링크가 없는 문서도 번호만 있는 줄로 남긴다.
"""
import gzip
import re
import sys
import time
from array import array
from pathlib import Path

STR = rb"'((?:[^'\\]|\\.)*)'"
OPT = rb"(NULL|'(?:[^'\\]|\\.)*')"
RX_PAGE = re.compile(rb"\((\d+),(-?\d+)," + STR + rb",(\d),", re.S)
RX_REDIRECT = re.compile(rb"\((\d+),(-?\d+)," + STR + rb"," + OPT + rb",", re.S)
RX_LINKTARGET = re.compile(rb"\((\d+),(-?\d+)," + STR + rb"\)", re.S)
RX_PAGELINKS = re.compile(rb"\((\d+),(-?\d+),(\d+)\)")


def unescape(raw):
    return re.sub(rb"\\(.)", lambda m: m[1], raw, flags=re.S)


def rows(path, rx):
    """INSERT 아래의 행(한 줄에 한 행)을 읽는다. 해석 못 하는 줄이 있으면 바로 멈춘다."""
    in_insert = False
    with gzip.open(path, "rb") as f:
        for line in f:
            if in_insert and line[:1] == b"(":
                m = rx.match(line)
                if m is None:
                    raise ValueError(f"해석 실패 {path}: {line[:200]!r}")
                yield m
                continue
            in_insert = line.startswith(b"INSERT INTO")


def main(raw_dir, out_dir):
    t0 = time.time()
    log = lambda msg: print(f"[{time.time() - t0:7.1f}s] {msg}", flush=True)
    raw, out = Path(raw_dir), Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)

    # 1. page: 일반 문서(공간 0)의 제목과 넘겨주기 여부
    id_of, title_of, redirect_pages = {}, {}, set()
    for m in rows(raw / "kowiki-latest-page.sql.gz", RX_PAGE):
        if m[2] == b"0":
            pid, title = int(m[1]), unescape(m[3])
            id_of[title] = pid
            title_of[pid] = title
            if m[4] == b"1":
                redirect_pages.add(pid)
    log(f"page: 일반 문서 {len(title_of):,}개, 넘겨주기 {len(redirect_pages):,}개")

    # 2. redirect: 넘겨주기 문서 -> 진짜 문서
    rd = {}
    for m in rows(raw / "kowiki-latest-redirect.sql.gz", RX_REDIRECT):
        frm = int(m[1])
        if frm in title_of:
            rd[frm] = (int(m[2]), unescape(m[3]), m[4] not in (b"NULL", b"''"))

    def resolve(pid):
        seen = {pid}
        while True:
            t = rd.get(pid)
            if t is None or t[2] or t[0] != 0:
                return None
            nxt = id_of.get(t[1])
            if nxt is None:
                return None
            if nxt not in redirect_pages:
                return nxt
            if nxt in seen:
                return None
            seen.add(nxt)
            pid = nxt

    final = {p: resolve(p) for p in redirect_pages}
    log(f"redirect: 풀린 넘겨주기 {sum(v is not None for v in final.values()):,}개")

    # 3. 넘겨주기가 아닌 문서에 0부터 새 번호를 매긴다 (page_id 오름차순)
    articles = sorted(p for p in title_of if p not in redirect_pages)
    new_id = {p: i for i, p in enumerate(articles)}
    n = len(articles)
    with open(out / "titles.tsv", "w", encoding="utf-8", newline="\n") as f:
        for i, p in enumerate(articles):
            f.write(f"{i}\t{p}\t{title_of[p].decode('utf-8', 'replace')}\n")
    log(f"titles.tsv: {n:,}줄")

    # 4. linktarget: lt_id -> 새 번호 (문서로 이어지는 대상만)
    lt = {}
    for m in rows(raw / "kowiki-latest-linktarget.sql.gz", RX_LINKTARGET):
        if m[2] != b"0":
            continue
        pid = id_of.get(unescape(m[3]))
        if pid is None:
            continue
        dest = final.get(pid) if pid in redirect_pages else pid
        if dest is not None:
            lt[int(m[1])] = new_id[dest]
    log(f"linktarget: 문서로 이어지는 대상 {len(lt):,}개")
    del id_of, title_of, redirect_pages, rd, final

    # 5. pagelinks: (출발, 대상) 쌍으로 바꾼다. pl_from 순서가 아니므로 모아서 정렬한다.
    packed = array("Q")
    for m in rows(raw / "kowiki-latest-pagelinks.sql.gz", RX_PAGELINKS):
        if m[2] != b"0":
            continue
        src = new_id.get(int(m[1]))
        if src is None:
            continue
        dst = lt.get(int(m[3]))
        if dst is None or dst == src:
            continue
        packed.append(src << 32 | dst)
    log(f"pagelinks: 후보 {len(packed):,}개, 정렬 시작")
    del lt, new_id
    ordered = sorted(packed)
    del packed
    log("정렬 끝, links.txt 쓰는 중")

    # 6. 겹치는 쌍을 지우며 한 줄에 한 문서씩 쓴다
    edges = 0
    with open(out / "links.txt", "w", encoding="ascii", newline="\n", buffering=1 << 20) as f:
        cur, targets, last = 0, [], None
        for v in ordered:
            if v == last:
                continue
            last = v
            src = v >> 32
            while cur < src:
                f.write(" ".join([str(cur), *targets]) + "\n")
                cur, targets = cur + 1, []
            targets.append(str(v & 0xFFFFFFFF))
            edges += 1
        while cur < n:
            f.write(" ".join([str(cur), *targets]) + "\n")
            cur, targets = cur + 1, []
    log(f"links.txt: {n:,}줄, 간선 {edges:,}개")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])
노잼 얘기들 (덤프 구조, 연결 방법, 전처리 순서)

파일 4개

압축을 풀어도 합쳐서 약 2.4GB다. 가장 큰 파일은 pagelinks이고, 제목은 page와 linktarget과 redirect에 나뉘어 있다.

파일담긴 것그래프에서 맡는 일행 수압축압축 해제
page문서 번호, 제목, 넘겨주기 여부점3,632,898119.1 MB426.4 MB
pagelinks어느 문서가 어느 대상 번호로 링크하는지화살표85,418,621303.3 MB1,680.9 MB
linktarget대상 번호와 제목의 대응표번호를 제목으로 바꾸기6,615,70379.9 MB262.2 MB
redirect넘겨주기 문서가 가리키는 제목별명 문서를 진짜 문서로 합치기945,10510.2 MB39.8 MB

연결 방법

링크 행에는 대상 문서의 번호가 없다. pl_target_id는 linktarget의 번호라서, 먼저 제목으로 바꾼 다음 page에서 문서를 찾아야 한다.

pagelinks pl_from pl_from_namespace pl_target_id linktarget lt_id lt_namespace lt_title page page_id page_namespace page_title page_is_redirect redirect rd_from rd_namespace rd_title rd_interwiki ① 출발 문서 찾기 ② 대상 번호를 제목으로 ③ 공간 + 제목으로 문서 찾기 ④ 어떤 문서가 넘겨주기인지 ⑤ 공간 + 제목으로 진짜 문서 찾기
파란 실선은 링크 한 줄을 문서로 바꾸는 길이고, 회색 점선은 넘겨주기를 푸는 길이다.
순서이 값으로여기를 찾는다얻는 것
①pagelinks.pl_frompage.page_id링크를 건 문서
②pagelinks.pl_target_idlinktarget.lt_id대상의 공간 번호와 제목
③lt_namespace, lt_titlepage_namespace, page_title대상 문서. 없으면 아직 안 쓰인 문서
④redirect.rd_frompage.page_id넘겨주기 문서
⑤rd_namespace, rd_titlepage_namespace, page_title넘겨주기가 가리키는 진짜 문서

테이블 구조

파란색 열만 쓰면 된다. 예시 행은 덤프에 있는 실제 값이고, 일반 문서만 골랐다.

page

문서 하나당 한 행
열형식뜻
page_idint문서 번호. 중간에 빈 번호가 많다
page_namespaceint공간 번호. 일반 문서는 0
page_titlevarbinary제목. 띄어쓰기는 밑줄
page_is_redirecttinyint넘겨주기 문서면 1
page_idnamespacepage_titleis_redirect
50지미_카터0
90수학0
3890스타크래프트0
13688660The_New_York_Times1

그 외 8개 열은 안 쓴다: page_is_new, page_random, page_touched, page_links_updated, page_latest, page_len, page_content_model, page_lang

pagelinks

링크 하나당 한 행
열형식뜻
pl_fromint링크를 건 문서의 page_id
pl_from_namespaceint링크를 건 문서의 공간 번호
pl_target_idbigint대상의 lt_id. 문서 번호가 아니다
pl_fromfrom_namespacepl_target_id
71004908031
7100490297729
7100490421263
71004904757119

linktarget

링크 대상 하나당 한 행
열형식뜻
lt_idbigint대상 번호
lt_namespaceint대상의 공간 번호
lt_titlevarbinary대상 제목. 없는 문서의 제목도 들어 있다
lt_idnamespacelt_title
80310스타크래프트
2977290The_New_York_Times
4212630미드_시즌_인비테이셔널_2024
475711910리그_오브_레전드

redirect

넘겨주기 문서 하나당 한 행
열형식뜻
rd_fromint넘겨주기 문서의 page_id
rd_namespaceint가리키는 문서의 공간 번호
rd_titlevarbinary가리키는 문서의 제목
rd_interwikivarbinary값이 있으면 다른 위키로 가는 넘겨주기
rd_fragmentvarbinary문서 안의 위치. 안 쓴다
rd_fromnamespacerd_title
600민주당_(2000년_대한민국)
720알베르트_아인슈타인
1020음악가
13688660뉴욕_타임스

원본 파일의 생김새

SQL 파일이지만 INSERT 줄 아래에 행이 한 줄씩 있어서, 줄 단위로 읽고 정규식으로 나눌 수 있다. 제목에 쉼표와 괄호가 들어가니 쉼표로만 자르면 안 된다.

INSERT INTO `page` VALUES
(3,4,'대문',0,0,0.02613266955,'20261101202310','20261101202453',42458456,14960,'wikitext',NULL),
(5,0,'지미_카터',0,0,0.932405166265001,'20261031012503','20261030071633',42412419,64090,'wikitext',NULL),

링크 하나 따라가기

리그_오브_레전드 문서(page_id 710049)에서 나가는 실제 행들이다. 같은 모양의 행이 대상에 따라 다섯 가지로 갈린다.

pagelinks 행linktarget에서 찾은 값page에서 찾은 결과처리
710049, 0, 80310, 스타크래프트일반 문서 389번화살표로 저장
710049, 0, 2977290, The_New_York_Times넘겨주기 문서 1368866번, 진짜 문서는 뉴욕_타임스 47206번47206으로 바꿔 저장
710049, 0, 3431160, 뉴욕_타임스일반 문서 47206번. 바로 위 행과 같은 화살표겹치므로 하나만
710049, 0, 4212630, 미드_시즌_인비테이셔널_2024page에 없음 (빨간 링크)버림
710049, 0, 475711910, 리그_오브_레전드공간 10번(틀), 일반 문서가 아님버림

남는 행 수

pagelinks 85,418,621행 중 63.3%가 화살표로 남는다. 막대 길이는 전체 행에서 차지하는 비율이다.

분류처리행 수비율비율 막대
출발지가 일반 문서가 아님 (토론, 사용자, 틀 등)버림14,405,61216.86%
대상이 없는 문서 (빨간 링크)버림11,917,36913.95%
대상이 일반 문서가 아님버림3,277,4013.84%
출발지가 넘겨주기 문서버림911,2501.07%
넘겨주기를 풀었더니 겹침하나만792,5160.93%
자기 자신을 가리킴버림28,9460.03%
끊긴 넘겨주기로 향함버림750.00%
최종 화살표저장54,085,45263.32%

후보 54,877,968행 중 3,728,111행은 넘겨주기 문서를 가리켜서 진짜 문서로 바꿨다. linktarget에 번호가 없는 행과 page에 출발 문서가 없는 행은 0개였다.

page: 점으로 남는 문서

전체 행3,632,898
공간 0번 (일반 문서)1,680,906
그중 넘겨주기910,792
점으로 남는 문서770,114

redirect: 넘겨주기 910,792개를 풀면

한 번에 도착910,735
두 번 거쳐 도착2
다른 공간으로 향함53
대상 없음1
서로 가리키며 순환1

전처리 순서

앞 단계에서 만든 표를 다음 단계가 쓰기 때문에 이 순서대로 해야 한다.

  1. page를 읽어 공간 0번만 남기고, 제목 → page_id 표와 넘겨주기 목록을 만든다.
  2. redirect를 읽어 넘겨주기 문서 → 진짜 문서 표를 만든다. 두 번 거치는 경우까지 따라가고, 순환이나 대상 없음은 끊긴 넘겨주기로 표시한다.
  3. 넘겨주기가 아닌 문서 770,114개에 0부터 770,113까지 새 번호를 매긴다. page_id는 최댓값이 4,296,017이라 그대로 배열 위치로 쓰면 빈칸이 많다.
  4. linktarget을 읽어 lt_id → 최종 문서 표를 만든다. 공간 0번 대상 4,501,007개 중 문서로 이어지는 것은 1,119,370개(바로 757,483개, 넘겨주기 361,887개)이고, 3,381,616개는 없는 문서, 21개는 끊긴 넘겨주기다.
  5. pagelinks를 읽어 출발지와 대상을 새 번호로 바꾼다. 자기 자신을 가리키는 행은 버린다.
  6. (출발, 대상) 쌍을 정렬해서 겹치는 행을 지우면 그래프 파일이 된다. 제목 목록도 번호 순서대로 따로 저장한다.

주의할 점

확인한 사실어떻게 대응할지
pagelinks는 pl_from 순서가 아니다. 순서가 뒤집힌 곳이 3,130,616곳이고, 대체로 pl_target_id 순서다(뒤집힌 곳 25곳).문서별로 이미 묶여 있다고 가정하면 안 된다. 전체를 정렬한 뒤 인접 리스트를 만든다.
나가는 링크가 없는 문서가 2,967개, 들어오는 링크가 없는 문서가 60,601개다.시연에서 경로 없음이 나올 수 있으니 그 경우의 출력을 만들어 둔다.
인접 행렬은 칸마다 1비트만 써도 770,114² 칸, 약 74.1GB가 필요하다. 채워지는 칸은 0.0091%다.이 PC의 메모리는 약 24GB라 전체 그래프로는 올릴 수 없다. 계산으로 보여준다.
pagelinks에는 틀에서 온 링크가 섞여 있고 본문 링크와 구분되지 않는다.실제로 누를 수 있는 링크라 그대로 쓴다. 발표 때 "틀 링크 포함"이라고 밝힌다.
파일별 덤프 완료 시각은 11월 1일 23시 18분부터 21분 사이다.한 번 받은 파일을 묶음으로 유지하고, 측정 중에 새로 받지 않는다.

들어오는 링크가 많은 문서

대한민국140,194
영어91,366
위키미디어_공용84,962
일본83,449
미국78,096

나가는 링크가 많은 문서

정치적_이데올로기_목록4,218
경기도수원교육지원청2,914
중국의_연호_목록2,866
국제공항_목록2,173
2026년2,018