본문 바로가기
공부/가짜 뉴스

[Neo4j] 그래프를 다루는 데이터베이스 - 도입

by 죠옹 2021. 3. 5.

P1. 기존 데이터 이식

P2. 데이터베이스 언어 (Cypher)

P3. Graph Apps - Neo4j Bloom (가시화), NeoDash (Feature 트래킹)

P4. graph data science - 알고리즘 (ex: pagerank, community detection)

P5. Gephi와 연동, Large network 가시화

P6. Python 프로그램과 연동, 주기적 DB 업데이트

 

     *굵게 표시한 목표는 이 글에서 다루는 내용

 

 

 이것 저것 가지고 놀다 보니 뉴스 관련 데이터들이 한달의 기간을 넘어가면 점점  pandas나 csv로 다루는데 한계가 있다는 것을 느꼈다.

 해서 그래프 데이터를 다룰 수 있는 데이터베이스 관리 시스템인 Neo4j를 배워보기로 했다.


준비

 

다음 사이트에서 Neo4j 다운로드가 가능하다.

neo4j.com/try-neo4j/

 

Try Neo4j - Neo4j Graph Database Platform

Experience Neo4j in a click with the Sandbox Pick a project and get started in less than 60 seconds. No download required. Network and IT Management Dependency and root cause analysis + more for network and IT management. Launch Crime Investigation Explore

neo4j.com

Sandbox는 다운로드 없이 브라우저에서 바로 이것 저것 해볼 수 있는 것, Desktop은 프로그램을 다운로드 해서 사용하는 것, Aura, Server는 제품 개발을 위한 제품으로 이해햇는데, 일단은 Desktop을 다운로드.

 

추가로, python를 이용한 데이터 크롤링과 접합하기 위해 neo4j 패키지를 설치한다. 

설치는 pip을 통해 가능

pip install neo4j

 


Desktop을 설치 후, 실행

Projects 탭을 보면, 기본으로 "Neo4j Primer project"라는 프로젝트가 있는데, 튜토리얼 같은 거다. 데이터 베이스 사용법 (Cypher)을 어떻게 다루는지 공부하기 좋다. 

 

 

새 프로젝트 만들기

1. Projects 화면에서 "+New"를 클릭

2. 새로 생긴 Project 글씨 옆에서 Project명 변경

3. 우측의 파란색 "+Add" 버튼 클릭 -> Local DBMS

4. Name password 설정 -> Create

5. DBMS start 후, Open을 누르면 Neo4j에서 DBMS가 열림

6. 왼쪽 상단의 3개의 탭 중 Database information탭을 누르면 Connected as 에서 user를 관리할 수 있다.

7. user add를 눌러 python에서 접근할 계정을 하나 만들어준다.

 

 

Python에서 데이터 로딩하기

 기존 naver ranking 뉴스 2월분을 DB에 넣어보는 것이 목표.

 먼저, 크롤링 관련 코드. 코드에 대한 설명은 여기에.

import requests
from bs4 import BeautifulSoup as BS
import pandas as pd
import numpy as np
import re



""" 네이버 랭킹 뉴스 긁어오기 """
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36'}
d_list = []
start_data = 20210201
end_data = 20210228
for date_int in range(start_data, end_data):
    date = str(date_int)
    url = "https://news.naver.com/main/ranking/popularDay.nhn?date=" + date
    html = requests.get(url, headers=headers).text
    soup = BS(html, 'html.parser')
    ranking_total = soup.find_all(class_='rankingnews_box')

    for item in ranking_total:
        media = item.a.strong.text
        news = item.find_all(class_="list_content")
        for new in news:
            d = {}
            d['media'] = media
            d['src'] = "https://news.naver.com/" + new.a['href']
            d['title'] = new.a.text
            d['date'] = date
            d_list.append(d)
df = pd.DataFrame(d_list)



""" 필요 없는 문자 제거 """
def clean_text(row):
    text = row['title']
    pattern = '([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("E-mail제거 : " , text , "\n")
    pattern = '(http|ftp|https)://(?:[-\w.]|(?:%[\da-fA-F]{2}))+'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("URL 제거 : ", text , "\n")
    pattern = '([ㄱ-ㅎㅏ-ㅣ]+)'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("한글 자음 모음 제거 : ", text , "\n")
    pattern = '<[^>]*>'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("태그 제거 : " , text , "\n")
    pattern = r'\([^)]*\)'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("괄호와 괄호안 글자 제거 :  " , text , "\n")
    pattern = '[^\w\s]'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("특수기호 제거 : ", text , "\n" )
    pattern = '[^\w\s]'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("필요없는 정보 제거 : ", text , "\n" )
    pattern = '["단독"]'
    text = re.sub(pattern=pattern, repl='', string=text)
    pattern = '["속보"]'
    text = re.sub(pattern=pattern, repl='', string=text)
    # print("단독 속보 제거 : ", text , "\n" )
    text = text.strip()
    # print("양 끝 공백 제거 : ", text , "\n" )
    text = " ".join(text.split())
    # print("중간에 공백은 1개만 : ", text )
    return text

df['title_c'] = df.apply(clean_text, axis=1)



""" 키워드 추출 from title """
from konlpy.tag import Kkma
from konlpy.tag import Komoran

kkma = Kkma()
komoran = Komoran()
df['keyword'] = ''
for idx_line in range(len(df)):
    nouns_list = komoran.nouns(df['title_c'].loc[idx_line])
    nouns_list_c = [nouns for nouns in nouns_list if len(nouns) > 1]    # 한글자는 이상한게 많아서 2글자 이상
    df.loc[[idx_line], 'keyword'] = set(nouns_list_c)
df = df[df['media'] != '코리아헤럴드']    # 코리아헤럴드는 영어 제목임
df = df[df['media'] != '주간경향']    # 주간경향은 같은 title이 많음

 그리고, neo4j에 탑재하는 코드. Neo4j에서 DB를 Start한 이후에 실행해야 접근 가능하다.

from neo4j import GraphDatabase


""" make node & relationship"""
def add_article(tx, title, date, media, keyword):
    tx.run("MERGE (a:Article {title: $title , date: $date, media: $media, keyword: $keyword})",
           title=title, date=date, media=media, keyword=keyword)


def add_media(tx):
    tx.run("MATCH (a:Article) "
           "MERGE (b:Media {name:a.media}) "
           "MERGE (a)<-[r:Publish]-(b)")


def add_keyword(tx):
    tx.run("MATCH (a:Article) "
           "UNWIND a.keyword as k "
           "MERGE (b:Keyword {name:k}) "
           "MERGE (a)-[r:Include]->(b)")



""" 한자와 공백 제거 """
# Neo4j -> Gephi 에서 parsing error의 원인이 될 수 있음
def clean_text_for_neo4j(row):
    text = row['title_c']
    text = re.sub(pattern='[^a-zA-Z0-9ㄱ-ㅣ가-힣]', repl='', string=text)
    # print("영어, 숫자, 한글만 포함 : ", text )
    return text

df['title_c_neo4j'] = df.apply(clean_text_for_neo4j, axis=1)



""" 연결 """
# Neo4j 브라우저에서 설정한 계정의 ID, PASSWORD를 통해 접속
greeter = GraphDatabase.driver("bolt://localhost:7687", auth=("ID", "PASSWORD"))  



""" 입력 """
# Cyper code를 이용,  크롤링한 Data를 DB에 입력
with greeter.session() as session:
    """ make node """
    for idx in range(len(df)):
        session.write_transaction(add_article, title=df.iloc[idx]['title_c_neo4j'], date=df.iloc[idx]['date'],
                                  media=df.iloc[idx]['media'], keyword=list(df.iloc[idx]['keyword']))
    session.write_transaction(add_media)
    session.write_transaction(add_keyword)

Neo4j에서 설정한 계정 ID와 PASSWORD를 이용, python neo4j 패키지 driver를 통해 연결을 만든다.

세션을 열고, Cypher 언어를 통해 Graph data의 node와 relationship을 추가해 줘야 한다.

 

먼저, 크롤링 한 Data를 이용, 'Article' node를 만든다.

추가로 'Article'의 property를 이용, 'Article'을 발행한 'Media'와, Article에서 포함하는 'Keyword'를 생성한다. 

각각의 관계는 다음과 같다.

(:Media)-[:Publish]->(:Article)->[:Include]->(:Keyword)

Cypher에서 ()는 node를 표현, []는 relationship을 표현한다.

Relationship에서 화살표는 방향성을 나타낸다.

 

node와 relationship 내부에서는 속성을 정의할 수 있는데, 한 예로 다음과 같은 node를 생성할 수 있다.

(:XX {YY: yy, ZZ: zz})

  XX : label. ':XX:AA' 와 같이 여러개의 라벨을 붙일 수 있다.

  YY : Property. 

 

CYPER 언어를 아주 간단히 설명하자면..

 

CREATE : node, relationship을 생성. 대상이 이미 있는 경우, 한 개 더 만듬 (자동으로 생성되는 id로 구분됨)

 

MATCH : 기존 node, relationship을 검색. WHERE와 함께, 조건 부 검색을 가능케 함. RETURN이나 WITH로 매칭되는 대상을 반환할 수 있으며, 해당 대상의 관람 및 편집이 가능해짐.

 

MERGE : CREATE와 MATCH를 합친 함수. 대상이 없는 경우에는 CREATE, 있는 경우 MATCH의 특징을 가짐. 'ON CREATE SET'이나 'ON MATCH SET' 같은 명령을 이용해서 각각의 상황에 맞는 property set이 가능. 

 

자 이제, 모든 준비가 끝났다. DB를 이용해서 놀아볼 시간!


Neo4j Desktop에서 DBMS를 open해서 Neo4j Browers를 실행한다.

왼쪽을 보면 Node와 Relationship이 만들어 진 것을 볼 수 있다. 

클릭을 해보면 생성된 DATA 샘플들을 바로 볼 수 있다.

가시화된 Article node를 클릭해보면 Expand라는 버튼이 있는데, 누르면 관계된 모든 node들이 표시된다.

빨간색 node가 Article을 Publish한 Media이고, 노란색 node는 Article이 포함한 Keyword를 나타낸다.

위의 예에서는 '개미'라는 키워드를 통해 '개미일기...'라는 Article과 연관되는걸 볼 수 있다.

 

구체적으로 대상을 검색할 수도 있다. 

예를 들어 '조선일보'와 '중앙일보'에서 발행하는 기사들의 keyword 연관성을 검색해 보자.

MATCH path=(a:Media)-[:Publish]->(:Article)-[:Include]->(:Keyword)<-[:Include]-(:Article)<-[:Publish]-(b:Media)
WHERE a.name='조선일보' and b.name='중앙일보'
RETURN path

 크롤링한 뉴스가 랭킹뉴스다 보니, 대부분의 keyword들이 서로 공유된 것을 볼 수 있다.

 

 Keyword간의 관계도 검색해 볼 수 있다. 

 예를 들어 '코로나'와 '대통령' 을 포함한 기사들을 검색해보자.

MATCH path=(a:Keyword)<-[:Include]-(:Article)-[:Include]->(b:Keyword)
WHERE a.name='코로나' and b.name='대통령'
RETURN path

Table로 title만을 검색하고 싶다면 다음 처럼 RETURN 해주면 된다.

MATCH path=(a:Keyword)<-[:Include]-(c:Article)-[:Include]->(b:Keyword)
WHERE a.name='코로나' and b.name='대통령'
RETURN c.title


 DB를 다룰 기본적인 준비는 완료!

 다음 번엔 알고리즘, APP, 가시화에 대해 정리해 봐야겠다.

반응형

댓글