python

범용적인 웹 크롤링 툴 제작 + 파이썬 이해하기

이번 케실주 프로젝트 주제인 "SQL 인젝션 자동화 진단도구" 제작을 위해선

 

그 선행으로 해당 웹 페이지에 대한 크롤링이 선행되어야 한다. 또한 다양한 경우에 대응해야 하므로, 범용성을 가지는 크롤링 툴 제작이 필요하다. 

 

그래서 파이썬의 웹 페이지 크롤링 라이브러리인 BeautifulSoup 와  Requests를 이용한 웹 크롤링을 공부하게 되었다. 

 

기초적인 것들은 다른 블로그나 사이트들에도 있으니 굳이 작성을 하지 않겠고, 

범용성을 가지는 자동화가 목적인 경우 어떤 식으로 크롤링 코드를 작성해야 하는지 정도만 메모하려고 한다. 


 

1. request 모듈 사용시엔 인자로 넘어갈 URL에 반드시 http 혹은 https가 붙어야한다. 

URL을 입력받아 해당 목적지에 요청을 보내는 식의 코드인 경우, 사용자가 http 혹은 https, www 등을 입력 할수도, 하지 않을 수 도 있다. 

따라서 해당 경우들을 모두 상정한 코드 작성이 필요하다. 넣은 경우엔 넣은대로 요청하고, 넣지 않은 경우엔 붙여서 요청할 수 있어야 한다. 

우선적으로 해당 접두어들을 전부 필터링 해준 다음, 경우에 따라 다시 붙이는 식으로 코드를 작성하였다. 

def getsoup(baseurl): #for getting soup object 
    global s
    global cookies
    tmpURL = baseurl
    filter_str = ["https://","http://","www."]
    for f in filter_str:
        tmpURL = tmpURL.replace(f,"")

    s = requests.Session()
    try :
        req = s.post("http://"+tmpURL)
    except : 
        req = s.post("https://"+tmpURL)
    cookies = s.cookies.get_dict()
    soup = bs(req.text,'html.parser')

    return soup,tmpURL

baseurl로 인자가 넘어오면 저러한 접두어들에 대해 필터링을 진행하여 최 상위 서브도메인 까지만 남긴다. 그 다음 요청을 보낼 때 try문으로 요청을 시도하여 http 혹은 https 프로토콜로 연결이 되게끔 코드를 구성했다. 

 


2. soup 객체에서 원하는 태그와 속성 찾기 

크롤링은 기본적으로 request로 요청 - soup 객체로 변환 - 변환된 객체 사용 의 3단계로 이뤄진다.

앞 두단계는 요청하면서 거의 동시에 이뤄져야 하는 작업이니 크게 다룰것이 없지만, 변환된 객체를 사용하는 방법은 무궁 무진하다. 

가져온 웹 페이지에서 특정 태그에 대한 내용을 찾고싶다면 많이 사용하는 것이 findALL 함수다. find류 함수는 많지만 findALL을 사용하는 방식이 자동화 도구에선 가장 범용적이고 직관적인 활용이 가능하다. 

for inputs in soup.findAll("input"):

기본적으로 위와 같은 방식으로 사용하면 input 지역변수 안에 input 태그를 사용한 모든 요소들이 순서대로 담기게 된다. 따라서 for문 내에서 지지고 볶아서 원하는 정보를 찾아내기가 수월해지며, 뒤에 서술한 다른 방식들과 자연스러운 연계가 가능해진다. 

 

지금 이 부분에서 목표로 하는건  input 태그 내에있는 또다른 태그나 해당 태그에서의 특정 속성, 혹은 해당 태그의 상위 태그 등을 찾기 위함이다. 

inputs 에 저장한 input 태그 요소의 특정 속성을 찾고싶으면 

getname = inputs.attrs['name']
getclick = inputs.attrs['onclick'].split("(")[0]

위와같이 쓸 수 있겠다. 이렇게 하면 input 태그에서 지정한 name 속성값을 가져올 수 있다. 

 

두번째 줄의 경우는 onclick 속성이 있는 경우 연결되는 함수의 이름을 찾기 위한 코드인데, 마찬가지로 onclick 속성을 문자열 그대로 가져오기 때문에 파이썬의 split 함수를 사용할 수 있고, 리스트 형태로 반환되기 때문에 0번째 인덱스를 선택하면 함수의 이름을 바로 가져올 수 있다. 

파이썬의 강점은 직관성 이라는걸 다시 상기하자. 

 

또, 해당 요소가 들어있는 특정 부모 태그/속성이 필요한 경우도 편하게 찾을 수 있다.

getpar = inputs.find_parent('form')['action'] 
getmethod = inputs.find_parent('form')['method'] 

findAll로 찾아온 요소에 find_parent 함수를 사용하면 해당 요소의 부모객체 중에 특정 태그를 가진 요소를 가져올 수 있다. 딕셔너리 형태로 요소를 가져오기 때문에 바로 ['속성이름'] 같은 형태로도 사용할 수 있다.

저렇게 하면 최종적으로는 문자열 형태 값을 반환한다. 

 

이정도만 해도 soup 객체에서 원하는 태그, 속성값을 가져오는건 자유자재로 가능하다. 


 

3. 로그인 세션 유지 

기본적으로 request 모듈로 연결시에는 로그인 세션등을 유지하지 않는다. 

보통 그래서 많이 사용하는 방식이 

with requests.Session() as s:
	req = s.post("http://"+tmpURL)

위와같은 형태인데, 이런 형태의 장점은 코드가 종료되면 자동으로 접속도 종료한다는 것. 

허나 지금같은 다양한 함수와 클래스를 활용해 작성할 떄는 저런 방식으로는 사용하기 힘드므로, 전역변수 s 를 선언하여 사용해주자. 

    global s
    s = requests.Session()
    try :
        req = s.post("http://"+tmpURL)
    except : 
        req = s.post("https://"+tmpURL)

반드시 requests.session() 을 거친후에 post 요청이 들어가야 로그인 이후에도 세션 유지가 가능하다. 


 

번외. js 스크립트 파일 참조

페이지 외부에서 참조하는 자바 스크립트파일을 가져와서 분석해야할 일도 필요하다. 가져오는것 자체는 어렵지 않다. 

    for s in scripts.findAll("script") : 
        try :
            loadjs = s.attrs['src'] #find from imported javascript files 
            if "http" in loadjs :
                req = requests.get(loadjs) 
            else : 
                req = requests.get(url+loadjs) 

script 태그를 찾아서 그 안의 src 속성에 어떤값이 들어있는지만 텍스트로 가져오면, 그냥 그대로 requests 요청을 하면 되기 때문. 

문제는 그 다음이다. 저렇게 가져온 js 파일은 soup 객체로 해석이 불가능한 그냥 통짜 텍스트이기 때문. 

그래서 다룰때도 텍스트를 사용하듯이 다뤄야 한다.  위 코드에서 이어진다. 

            s = str(s)+req.text
        except:
            s = str(s)
        if funcname in s : 
            for l in s.split("\n") :
                # print("line"+l)
                if "action" in l : #find action target url in javascript function
                    target =  (l.split("\"")[1]) 
                if "method" in l : #find form method in javascript function
                    method =  (l.split("\"")[1]) 
                if "url" in l : #find target url in AJAX function 
                    target = (l.split("\"")[1]) 
                if "type" in l : #find form method in AJAX function 
                    method = (l.split("\"")[1]) 

req 결과로 리턴받은 자바스크립트 파일을 그냥 그대로 원본 script 태그 요소에 문자열 형태 그대로 이어붙였다. 이때 당연히 script 요소는 str 을 통해 문자열로 변환한 상태.  이렇게 하는건 자바스크립트 파일과 script 요소 내에 특정 함수 이름이 있는지 찾기 위해서이다. 

(활용의 간편을 위해 두 요소를 이어붙였을 뿐, 굳이 붙일 필요는 없다. )

 

이렇게 하면 하나의 큰 문자열 형태가 되므로, find split과 같은 문자열 함수를 그냥 사용하면 되고, if 문을 이용해 해당 문자열이 있는지 찾는 형식으로도 코드 작성이 가능하다. 

 

전체가 스트링 형태로 변했으므로, 개행문자 \n으로 구분하여 포문을 돌며 한줄씩 체크하도록 코드를 만들어놨다. 한줄 한줄 반복문을 돌면서 원하는 내용이 있는지 찾으면 되는것.

단순하게 생각하면 된다. 

 

다시한번 상기하자. 파이썬의 강점은 직관성이다. 

 

 


 

크롤링 자동화에 꼭 필요하다 싶은 내용이 나올 때 마다 항목을 추가할 예정이다. 

나도 파이썬에 대해서 공부하면서 진행하는 중이라 정확하지 않은 내용들이 섞일수도 있을거 같다.

 

기본적으로 파이썬 크롤링툴은 크롤링에 관련된 모듈(requests,bs)에 대한 이해도 보다는, 파이썬이라는 언어 자체에 대한 이해도가 높아야 더 직관적이고 편하게 제작이 가능해 보인다. ( 크롤링 툴에만 국한된게 아니라, 뭘 만들던지 마찬가지이긴 할듯.)

 

파이썬을 공부하다가 느낀 , 타 언어에 비해서 파이썬의 강점이라고 할 만한, 파이썬 이해도를 높이기 위한 활용하기 쉽고 눈에 띄는 특징을 몇가지만 짚어보면

 

1. 객체지향

2. 간편한 함수의 사용

3. 예외문의 사용

4. 자료형

 

정도인것 같다. 

 

파이썬은 객체지향 프로그래밍이 가능한 언어다. 강점을 최대한 활용하여 개발을 진행하는것이 좋다.

class hrefset(): #sub href link dataset object
    def __init__ (self,url,baseurl) :
        self.url = url
        self.baseurl = baseurl
        self.formlist = []
        self.arglist = []
    
    def showdata(self):
        print ("----- href page info -----")
        print ("URL : "+self.url)
        if len(self.formlist) != 0 :
            print ("◇--- forms ---◇")
            for s in self.formlist :
                s.showdata()
        if len(self.arglist) != 0 :
            print ("◆--- args ---◆")
            for s in self.arglist : 
                s.showdata()
        print ("-------------------------")

    def getformset(self):
        self.tmppage = pageset(self.baseurl+self.url)
        for s in self.tmppage.flist:
            self.formlist.append(s)
    
    def addargs(self,args):
        if self.findargs(args) :
            pass
        else : 
            # print ("argname : " + args.name)
            self.arglist.append(args)

    def findargs(self,args): #find same args already in this lists
        for s in self.arglist : 
            if s.name == args.name :
                return 1
        return 0

클래스 없이 생짜로 하나하나 전역변수, 함수 만들어가며 짜는건 거의 불가능에 가까워 보이고, 설령 만든다 하더라도 그렇게 깔끔하게 코드가 나올것 같지는 않다. 클래스를 사용하지 않고 위와 같은 기능을 수행하게끔 만드려면 어떻게 해야하나? 머리가 저절로 복잡해 진다. 

어느정도 이상의 퀄리티를 원한다면 객체에 대한 이해와 활용은 거의 필수에 가까운것 같다. 

 

파이썬의 함수는 여타 언어들에 비해 진짜 과하다 싶을정도로 활용이 간편하다. 인자를 넘길때 자료형도 신경쓸 필요가 없고, 반환할때도 마찬가지.

def getsoup(baseurl): #for getting soup object 
    global s
    global cookies
    tmpURL = baseurl
    filter_str = ["https://","http://","www."]
    for f in filter_str:
        tmpURL = tmpURL.replace(f,"")

    s = requests.Session()
    try :
        req = s.post("http://"+tmpURL)
    except : 
        req = s.post("https://"+tmpURL)
    cookies = s.cookies.get_dict()
    soup = bs(req.text,'html.parser')

    return soup,tmpURL

아까 위에서 만든 soup 함수를 가져왔다. 인자로 넘어오는건 baseurl 이라는 string 자료형 값이지만 굳이 명시해줄 필요가 없고, 사용만 잘한다면 이로 인한 문제도 발생하지 않는다. 

 

또 서로 다른 자료형의 두 변수를 한꺼번에 반환한다. 반환하는건 보통의 자료형이 아닌, soup 객체를 반환하고 있다. 그러나 함수 선언시에 별다른 정의가 단 하나도 필요가 없다.  그냥 def 이름(인자): 끝. 

모듈에서 선언된 soup 객체와 문자열을 저렇게 간단하게 동시에 반환 하는건 C언어에서는 상상도 못할 일. 당장 나한테 C로 짜보라고 하면 머리를 한참 싸매야 할것같다.

(C++에서는 비교적 쉽게 가능하겠다만, 이것도 함수를 정의할 때 부터 자료형 명시나 튜플 정의등이 반드시 필요하다. )

        self.soup,self.url = getsoup(url)

받아서 쓸때는 그냥 저렇게 콤마로 구분만 해서 받아주면 된다. 

파이썬의 간편한 함수 활용 방식은 코틀린이나 Go, swift 등등의 비교적 신세대 언어들에선 어떨지 모르지만, 적어도 C, C++ , 자바와 같은 전통적인 프로그래밍 언어들에 비하면 명확하게 강점이라고 말할 수 있는 특징이다. 

* 나중에 찾아보니 go나 swift에서는 간편하게 가능하다. 역시 신세대 언어.. 이 또한 파이썬에 영향 받았다라고 말하기도 한다. 

 

 

예외처리가 간편한것도 공부하다가 느낀 파이썬의 강점이라고 생각한다.

try : # if it has form-action structure
            getpar = inputs.find_parent('form')['action'] 
            getname = inputs.attrs['name']
            getmethod = inputs.find_parent('form')['method'] 
        except : # if it has javascript action structure
            try : 
                getclick = inputs.attrs['onclick'].split("(")[0]
                #find a javascript's target url and method by function's name 
                getpar,getmethod = parsescript(soup,getclick)                 
            except: 
                pass 
            else : 
                for i in inputs.find_parent('form').findAll("input") :
                    try : #filter form args
                        getname = i.attrs['name'] 
                    except :
                        pass
                    else :     
                        try : 
                            gettype = i.attrs['type']
                        except : 
                            gettype = "none"
                        isin = 0
                        for s in lists : 
                            if getpar == s.url :
                                s.addform(getname,gettype)
                                isin = 1
                                break
                        if isin == 0 :
                            lists.append(formset(getpar,getname,gettype,getmethod))                           
        else :
            try : 
                gettype = inputs.attrs['type']
            except :
                gettype = "none"
            isin = 0
            for s in lists : 
                if getpar == s.url :
                    s.addform(getname,gettype)
                    isin = 1
                    break
            if isin == 0 :
                lists.append(formset(getpar,getname,gettype,getmethod))            

try except else 세 단어로 예외처리가 간단하게 가능한건 확실하게 활용하기 좋은 강점인듯 하다. 

다만 이는 역으로 코드가 지저분해지기 쉬운 문제점도 내포하고 있으니, 조심해서 사용해야 할것같다. 

 

 

마지막으로 자료형인데, 흔히 파이썬엔 자료형이 없다라고 하는데 파이썬엔 자료형이 명확하게 있다.

그것도 비교 대상인 C 언어보다 사용하기에 더 빡빡하다.

type()을 이용해 변수의 자료형을 알아올 수 있는데, 여기서 알아오는 자료형은 단순 int, str, float 구분 뿐만 아니라 해당 변수가 어떤 객체 class 속성을 가지는지 또한 알아온다. 

그러니 이는 단순히 자료형 보다는 객체로 바꿔서 생각해야 할듯하고, 그 성질에 대해서만 제대로 이해한다면 비교 대상이 되는 C언어에 비해 자료형간의 변환이나 활용이 거의 없다시피 할 정도로 훨씬 간편해지는건 맞다. 

 

개발을 이정도로 딥하게 해보려고 한적은 없었던 것 같은데, 목표 하나 정해서 쭉 파보니까 생각보다 재미있다.

최근들어 파이썬을 쓸일이 굉장히 많아 (django, 크롤링, pandas) 파이썬 다루는 능력은 쭉쭉 올라가는것 같으니, 관련 글이나 많이 정리해볼까 한다.