나무위키 덤프 활용하기

1 minute read

참고: 파이썬으로 나무위키 JSON 덤프 데이터 파싱하기

대용량 JSON 읽기

덤프된 나무위키 데이터 JSON은 한 줄에 한 레코드가 저장되는 jsonl 포맷이 아닌 한 줄에 모든 데이터가 다 수록되어 있는 형태이기 때문에 readline()json 패키지 등으로 읽어들이는 방법으로는 처리가 불가능하고 실행 도중 메모리 에러 등을 일으킵니다.
(마찬가지 이유로 vim 등의 편집기로 미리보기도 어렵습니다.)

따라서 line-by-line 이 아닌 방법으로 처리하는 parser 로 처리할 필요가 있으며 ijson (PyPI) 가 여기에 부합합니다.
ijson 은 iterative 한 parser 로서 내부를 정확하게 들여다 보지는 않았지만 character (혹은 byte-sequence) 단위로 처리하는 것으로 생각됩니다.
일반적인 방법으로는 열리지 조차 않는 수 GB 이상의 json 파일 내용을 순차적으로 처리할 수 있습니다.

def parse_namuwiki_json(limit = -1, debug=False):
  i = 0
  doc = {}
  with open(SOURCE_FILE) as f:
    for prefix, event, value in ijson.parse(f):

      if debug:
        print(prefix, event, value)

      if (prefix, event) in capture_values:
        doc[prefix.replace("item.", "")] = value
      if (prefix, event, value) == ("item", "end_map", None):
        yield doc
        doc = {}
        i += 1

        if limit > 0 and i >= limit:
          break

위 예제는 parse() 메서드를 이용해 순차적으로 파싱하는 코드입니다. 이 iterator 는 prefix, event, value 를 값으로 내주게 되며,
name: value 구조의 JSON 을 읽을 때 prefix 는 name, value 는 말 그대로 value에 해당되죠.

다만 iterative 즉 덩어리로 읽는 것이 아닌 순차적으로 읽어나가기 때문에 hierarchical 하게 구성된 JSON 을 읽을 때에는 실제 원하는 leaf node 쪽의 (예를 들어 가장 자식 쪽) 정보를 읽을 때와 같은 상황에서는 주의가 필요합니다.

이 때 필요한 것이 event 부분인데 현재 parser 가 어느 부분을 읽고 있는지를 나타내주게 되죠. map_key 일 경우에 실제 name 에 해당되는 value 를 물고 있는 것이며, start_map 이나 end_map 은 각각 JSON 객체의 시작과 끝에 해당됩니다.
end_map 을 통해 해당 항목을 다 읽었음을 감지할 수 있으며 위 코드에서도 이를 통해 한 document 를 yield 하는데 쓰고 있습니다.

필터링

필터링 부분은 위 참조글에 있는 clean_text() 예제를 그대로 사용했는데, wiki 문법으로 된 데이터의 텍스트만을 끄집어내는 것이라 할 수 있겠습니다.

업데이트: 2022년 6월 현재 namu-wiki-extractorextract_text() 에서 이미 필요한 필터링을 모두 실시합니다.
결과적으로 참조글 코드에 있는 처리가 불필요하며 오히려 미주/각주 주위의 텍스트가 전부 사라지는 등의 오류가 나타날 수 있으니 clean_text() 부분은 따로 구현하지 않고 바로 extract_text() 로 넘기시길 권합니다.

jsonl 로 변환하기

parser 를 통해 얻어진 JSON 객체를 한 줄씩 저장하면 됩니다 🙂

with open(OUT_FILE, "w") as out:
  for i, doc in enumerate(parse_namuwiki_json()):
    doc['text'] = extract_text(doc['text'])

    out.write(json.dumps(doc))
    out.write("\n")

    if (i % 100) == 0:
      print("progress: ", i)

Updated: