요즘 이직을 위해 코딩을 배우고 있다.
내 직업을 아는 이들에게는 꽤 놀랄만한 소식일 것이다. 그 좋은 꿈의 직장을 팽개치고 이직을 희망한다는 것이.
틀린 말은 아니다. 나도 직장에 대한 불만으로 이직을 희망하는 건 아니니까. 다만 이 직업의 태생적 한계와, 내 성향과 맞지 않는 여러 측면들 때문에 이 같은 결정을 내리게 된 것이다.
잡설은 여기까지 하고.
이 글에서는 이 코딩 신생아의 첫 상용 프로젝트 <다이맥스 어드벤처 포켓몬 총정리 사이트>의 개발과정을 다루고자 한다.
나처럼 파이썬 웹스크래퍼를 활용한 프로젝트를 기획 중인 분들에게 도움이 될 수 있기를 바란다.
소드실드 DLC 왕관의 설원에는 <다이맥스 어드벤처>라는 컨텐츠가 있다.
탐사 초기에 주어지는 랜덤 포켓몬 중 하나를 골라 데려갈 수 있는데 (즉, 자신이 미리 준비해 갈 수 없음) 도중에 포획하거나 획득하는 포켓몬으로 교체할 기회는 주어지지만, 이 또한 랜덤성이 있다.
즉, 상당히 무작위성이 강한 컨텐츠인데, 한 술 더 떠 <엔드리스 모드>라고 하여 이 던전을 무한히 탐색하는 컨텐츠도 있다.
2020년 10월 기준 세계 최고 기록은 33층인 것으로 알고 있다. 참고로 박세준 선수가 꾸린 팀 (박세준 불장난 김용녀 안말이)으로도 11층까지 가는 것이 고작일 정도로 엔드리스 모드는 난이도가 높으며, 11층까지 가는 데 3시간이 소요되었을 정도로 시간 소모도 크다.
나는 치라치노 스피드스타즈 멤버들과 함께 엔드리스에 도전하는 방송 컨텐츠를 기획했고, 이를 원활하게 진행하기 위해 어드벤처에 출현하는 모든 포켓몬들을 검색하고 필터링하는 사이트를 개발하기로 마음 먹었다.
물론 기존에도 세레비넷에 해당 자료는 있었다.
하지만 영어로 구현되어 사용하기 불편한 점도 있었고, 타입별 필터링은 있으나 기술/특성/이름으로 검색하는 기능은 구현되지 않았으며 내용이 표로 구현되어 있지도 않아서 한 눈에 보기 쉽지는 않다.
그래서 세레비넷의 자료를 토대로 한글화된, 그리고 검색 기능이 더 발전된 사이트를 직접 개발하게 되었다.
사용 기술
- Python (+ Requests, Beautiful Soup)
- Repl
- Wix Code
별 거 없다.
참고자료
웹스크래퍼로 내용 긁어오기
노마드코더의 무료강의를 보며 repl에 스크래퍼를 구현했다.
Requests라는 라이브러리를 활용하여 세레비넷의 페이지에서 내용을 추출해온다.
import requests
from bs4 import BeautifulSoup
dynamaxResult = requests.get(
"https://www.serebii.net/swordshield/dynamaxadventurespokemon.shtml")
dynamaxResult라는 변수 안에 그 사이트의 모든 내용이 뭉텅이로 들어간다.
이제 Beautiful Soup를 통해 이 중에서 text만 뽑아낸다.
dynamaxSoup = BeautifulSoup(dynamaxResult.text, "html.parser")
세레비넷의 사이트를 인스펙터로 살펴보면, 내가 찾는 내용은 table 안에, 그리고 trainer라는 클래스에 속해 있는 것을 알 수 있다.
tables라는 이름의 리스트를 만들고, 이 안에 내가 필요로 하는 내용을 넣어준다.
tables = []
table = dynamaxSoup.find_all("table", {"class": "trainer"})
for table in table:
tables.append(table)
왜 table이 아니라 tables일까?
세레비넷에서 어드벤처 출현 포켓몬들을 <일반 포켓몬>과 <전설 포켓몬>으로 나눠 표시했기 때문이다.
즉 tables라는 리스트 안에는 일반 포켓몬들에 대한 내용이 첫번째 item이 되고, 전설 포켓몬들에 대한 내용이 두번째 item으로 들어가게 된다.
내가 만드려는 어드벤처 정보 사이트에서도 일포와 전포를 따로 표시할 필요가 있기 때문에 리스트 안에 서로가 분리되어 들어가도록 한 것.
나중에 isBoss라는 부울리언을 붙여주게 되는데, 일포에게는 false를 전포에게는 true 값을 줄 것이다.
인스펙터로 세레비넷 사이트를 다시 살펴본 결과, 표 안에서 내가 필요로 하는 자료들은 하이퍼링크로도 구현되어 있다는 것을 알게 됐다.
나는 포켓몬의 이름, 타입, 특성, 기술배치를 알아야 하는데, 텍스트로 긁어올 경우 타입에 대한 내용이 빠지게 된다.
왜냐하면 이 놈의 세레비넷에서 포켓몬의 타입은 텍스트가 아니라 이미지로 구현해놨기 때문이지. 그지 같은 놈들!
덕분에 처음에는 텍스트로 긁어오다가 낭패를 봤었다. 도대체 왜 그런 짓을??
componentsNotBoss = tables[0].find_all('a')
componentsBoss = tables[1].find_all('a')
dataNotBoss = []
dataBoss = []
for link in componentsNotBoss:
if 'href' in link.attrs:
dataNotBoss.append(link.attrs['href'])
for link in componentsBoss:
if 'href' in link.attrs:
dataBoss.append(link.attrs['href'])
나는 비전공자이므로 코드가 더러운 것은 양해 바란다.
아무튼 이렇게 해서 dataNotBoss라는 리스트에는 일포들의 내용이, dataBoss 리스트에는 전포들의 내용이 들어갔다.
사실 이런 글은 당시에 사이트를 만들면서 조금씩 기록을 했어야 정확한데, 지금 나는 다 만들고 난 뒤 꽤 시간이 지나서 이 글을 쓰고 있다.
그래서 정리하기가 너무 귀찮다...ㅎㅎ
일단 통째로 뒷부분 코드를 올리면 다음과 같다.
resultNotBoss = []
resultBoss = []
for word in dataNotBoss:
if len(word.split('/')) == 4:
resultNotBoss.append("name: '"+word.split('/')[2]+"'")
elif len(word.split('/abilitydex/')) == 2:
resultNotBoss.append("ability: '"+word.split('/')[-1].replace('.shtml','')+"'")
elif len(word.split('/')) == 3:
resultNotBoss.append("'"+word.split('/')[-1].replace('.shtml','')+"'")
for word in dataBoss:
if len(word.split('/')) == 4:
resultBoss.append("name: '"+word.split('/')[2]+"'")
elif len(word.split('/abilitydex/')) == 2:
resultBoss.append("ability: '"+word.split('/')[-1].replace('.shtml','')+"'")
elif len(word.split('/')) == 3:
resultBoss.append("'"+word.split('/')[-1].replace('.shtml','')+"'")
mergedNotBoss = []
mergedBoss = []
def mergeDataNotBoss(data) :
tempList = [0]
i = 0
data.append('name: ')
while i < len(data):
if 'name:' in data[i]:
tempList.append('isBoss: false')
mergedNotBoss.append(tempList)
tempList = [data[i]]
else :
tempList.append(data[i])
i += 1
def mergeDataBoss(data) :
tempList = [0]
i = 0
data.append('name: ')
while i < len(data):
if 'name:' in data[i]:
tempList.append('isBoss: true')
mergedBoss.append(tempList)
tempList = [data[i]]
else :
tempList.append(data[i])
i += 1
cleanedNotBoss = []
cleanedBoss = []
def cleanData(data, newData) :
j = 0
while j < len(data):
if len(data[j]) == 8:
data[j].insert(2, data[j][1])
newData.append(data[j])
elif len(data[j]) == 9:
newData.append(data[j])
j += 1
doneData = []
tableDex = []
namesKorean = []
refinedKorean = []
def finishData(data1, data2, newData):
newData = data1 + data2
for element in newData:
element[1] = 'type01: '+element[1]
element[2] = 'type02: '+element[2]
element[4] = 'move01: '+element[4]
element[5] = 'move02: '+element[5]
element[6] = 'move03: '+element[6]
element[7] = 'move04: '+element[7]
element.append("image: 'https://img.pokemondb.net/sprites/sword-shield/icon/"+str(element[0]).replace("'",'').replace('name: ','')+ ".png'")
saveFile(newData)
매우 추접게 작성된 코드임에는 분명하다.
아무튼 이 과정을 거치면 세레비넷에 실린 어드벤처 출현 포켓몬들마다 이름/타입/특성/기술배치를 item으로 가지는 리스트를 얻어낼 수 있고, 이들 리스트를 item으로 지니는 더 큰 리스트를 얻게 된다.
마지막 단락에서 type01: 이며 move01: 같은 것을 덕지덕지 붙이는 이유는 나름 그래프큐엘 연습도 같이 해보겠답시고 도전해본 거였다ㅋㅋㅋㅋㅋ
아직 스키마 구조도 제대로 모르는 탓에 그래프큐엘은 좀 더 공부를 해야 한다. 몇 번 도전해보았으나 쉽지 않았고, 그냥 이번에는 csv로 데이터베이스 구현을 하기로 했다. 애초에 동적인 데이터베이스가 전혀 필요 없는 프로젝트이므로, csv로 사이트 내에 구현하는 게 퍼포먼스 상에서도 훨씬 유리할 것이다.
마지막으로 포켓몬마다 스프라이트 이미지가 있어야하므로
포켓몬스터DB의 스프라이트 페이지에서 이미지를 가져올 수 있도록 주소 구문도 넣어준다.
물론 이건 나중에 수동으로 살짝 다듬어 줘야 했다.
결과적으로 이런 내용의 csv 자료를 얻게 된다.
이때 너무 행복했다.
여기까지 하는데 꼬박 하루 걸림.
한글화
자, 이제 영어로 된 데이터는 얻었다. 한글화도 해야겠지? ^^
포켓몬 다국어 데이터베이스가 없는 나로서... 한글화는 수동으로 진행해야 한다. ^ㅗ^
일단 나온 자료를 csv로 저장한 뒤 구글 스프레드시트로 불러왔다.
물론 엑셀로 해도 됨.
이제 이걸 ctrl+f를 통해 모두 바꾸기 해주는 방식으로 한글화 작업을 해주면 된다.
다만 문제가 있다.
포켓몬의 기술이나 특성 타입은 모두바꾸기로 작업과정을 최소화할 수 있는 반면에, 포켓몬의 '이름'은 각각이 고유하므로 진짜 쌩노가다가 된다는 것...
그래서 다시 repl로 돌아왔다.
tableDex = []
namesKorean = []
refinedKorean = []
test = []
with open('result.csv', 'r', encoding='utf-8') as f:
content = csv.reader(f)
for item in content:
test.append(item)
for item in test:
dexResult = requests.get("https://www.serebii.net/pokedex-swsh/"+item[0].replace("name: ",'').replace("'",''))
dexSoup = BeautifulSoup(dexResult.text, "html.parser")
dataScrapped = dexSoup.find_all("td",{"class":"fooinfo"})
for table in dataScrapped:
tableDex.append(str(table.find_all("td")))
i = 0
while i < len(tableDex) :
if '<td><b>Korean</b>: </td>' in tableDex[i]:
namesKorean.append(tableDex[i])
i += 1
for item in namesKorean:
refinedKorean.append(item[item.rfind('<td>')+4:len(item)-6])
이번에는 아까 구한 리스트들의 리스트를 이용해서, 세레비넷의 포켓몬도감에서 해당 포켓몬을 찾고, 그 페이지 내용 중에서 한칭을 찾아오도록 스크래퍼를 새로 만들었다.
이상해풀을 예로 들자면 이렇게 Korean: 이상해풀 이라고 나온 부분이 있기 때문에 이걸 찾아내도록 하는 것.
그런데 이 정신나간 놈의 세레비넷 사이트가 무려 인코딩이 UTF-8으로 되어 있지 않았다!!
돌았나, 진짜!
이상해풀
↑이게 뭔지 앎?
'이상해풀'을 Windows-1252라는 캐릭셋으로 인코딩한 것임ㅋㅋㅋ
실제로 저걸 복사해서 검색창에 붙여넣고 검색하면 이상해풀로 검색되어 나온다ㅋ
오래된 사이트이니만큼 어쩔 수가 없다고 생각하며 파이썬에서 인코딩 변환하는 방법을 찾아내긴 했다ㅠㅠ
근데 다행스럽게도 파이썬은 그런 거 필요없이 그냥 알아서 적당히 인코딩해서 csv로 저장해주는 모양이었다.
사랑한다, 파이썬.
저기서 쉼표로 나눠져 있는 이유는 내가 csv로 변환하는 과정에서 실수를 좀 했기 때문인데, 그냥 이 시점에서 너무도 힘들고 지쳤던 나는 그냥 저걸 몽땅 스프레드시트로 긁어와서 쉼표를 모두 찾아 지우기 해버렸다.
굳이 뭘 또 스크래퍼를 수정하고 앉아 있어..ㅎㅎ...
이렇게 가장 큰 고비라고 할 수 있는 이름 한글화 과정은 끝났고, 나머지 자료들(특성/타입/기술)을 수동으로 한글화 해주었다.
왜냐하면 또 이 그지같은 세레비넷은 포켓몬 이름에 대해서만 다국어로 표기하고, 특성/타입/기술은 영어와 일본어로만 표기해놓았기 때문이다!
양놈의 싀끼들.. 세레비넷 개발자들이 다국어 지원에 대해 얼마나 소극적인지 알 수 있는 대목이다.
진짜 난 2021년 중에 포켓몬 다국어 DB 개발할 거다. 중국어도 지원할 거임.
수동 한글화... 다 하는 데만 30분 이상 걸렸던 것 같다.
두 번 할 짓은 못 된다.
윅스 구현
이제 완벽하게 한글화 된 csv 자료를 구해냈다!
이걸 업로드할 사이트만 만들면 되는 단계.
여기서 나는 그냥 현실과 타협했다ㅋㅋㅋ
윅스로 20분만에 사이트 하나 뚝딱 만들고 퉁치기로 한 것.
미리 알아보니 윅스도 DB를 표로 나타내고 필터링하는 기능이 있었고, 곧바로 작업한 끝에 사이트 프로토타입을 만들었다.
아니 근데
막상 사이트 주소로 들어가보면 표로 나오지 않는 게 아닌가...
알아보니 데이터베이스와 표를 연결할 때, 업로드된 csv는 기본적으로 Sandbox라는 형태의 DB로 저장되는데, 표는 Live DB와 연결되기 때문이라고..
그래서 Sync Sandbox to Live 버튼을 눌러서 Sandbox 데이터와 동일한 Live 데이터를 생성해주면 정상적으로 표시가 된다.
여기서 끝이 아니다.
이제 검색창과 필터를 달아줘야 하기 때문.
2 댓글
좀 짱이시네요...
답글삭제감사합니다~ 아직 갈 길이 머네요ㅎㅎ
삭제