BeautifulSoup 를 사용할때마다 느끼는 거지만 뭔가 꽉막힌듯한 뭐부터 해야하나 고민을 할때가 많습니다. 어떤 한값을 읽어내는건 단순합니다. 그러나 웹크롤링이나 뭔가 복잡하게 표를 읽어내는건 고민을 좀 해야하는 부분도 많고 해서, 그래서 실전 예제를 통해서 제가 사용하는 절차를 정리해 보았습니다.
여기에서 사용할 예제는 https://stackoverflow.com/tags 여기입니다.
그사이 tags 정보가 변경되어 예제가 동작이 되지 않을 수도 있는점 참고 부탁드립니다.
목표
목표는 tags의 값을 모으는 작업입니다.
1. 정보를 가져오고자 하는 웹 페이지의 HTML 저장
import requests def write_file(filename,str_data): with open(filename, 'w', encoding='utf-8') as fp: fp.write(str_data) fp.close() resp = requests.get('https://stackoverflow.com/tags') write_file("test.html",resp.text)
2. 반복된는 내용을 구하자고 할때 한개의 set 분의 데이터를 얻을수 있는 최상위 html tag를 찾음
3. 한개분의 셋트 위치 TAG로 find_all 모두 찾기
import requests from bs4 import BeautifulSoup def bs_example(data): soup = BeautifulSoup(data,"html.parser") #<div class="d-flex jc-space-between ai-center mb12"> sresult = soup.find_all("div",attrs={"class":"d-flex jc-space-between ai-center mb12"}) for rone in sresult: print(rone) print("##########################") def write_file(filename,str_data): with open(filename, 'w', encoding='utf-8') as fp: fp.write(str_data) fp.close() if __name__ == "__main__": #resp = requests.get('https://stackoverflow.com/tags') #bs_example(resp.text) #write_file("test.html",resp.text) bs_example(open("test.html",encoding='utf-8'))
##### 이건 구분선입니다. 내용이 많아서 앞쪽에 3개만 출력해봤습니다.
보시면 javascript , python , java 이런것은 포함되어있음을 알 수 있는데 나머지 수치적인 정보는 빠져있음을 알 수 있습니다. 그렇다면 좀 더 앞쪽 tag를 가져오도록 하겠습니다.
<div class="d-flex jc-space-between ai-center mb12"> <div class="flex--item"> <a class="post-tag" href="/questions/tagged/javascript" rel="tag" title="show questions tagged 'javascript'">javascript</a> </div> </div> ########################## <div class="d-flex jc-space-between ai-center mb12"> <div class="flex--item"> <a class="post-tag" href="/questions/tagged/python" rel="tag" title="show questions tagged 'python'">python</a> </div> </div> ########################## <div class="d-flex jc-space-between ai-center mb12"> <div class="flex--item"> <a class="post-tag" href="/questions/tagged/java" rel="tag" title="show questions tagged 'java'">java</a> </div> </div>
이번에 앞쪽에 있는 <div class="s-card js-tag-cell d-flex fd-column"> 이것입니다.
변경된 코드입니다.
import requests from bs4 import BeautifulSoup def bs_example(data): soup = BeautifulSoup(data,"html.parser") #<div class="s-card js-tag-cell d-flex fd-column"> sresult = soup.find_all("div",attrs={"class":"s-card js-tag-cell d-flex fd-column"}) for rone in sresult: print(rone) print("##########################") def write_file(filename,str_data): with open(filename, 'w', encoding='utf-8') as fp: fp.write(str_data) fp.close() if __name__ == "__main__": #resp = requests.get('https://stackoverflow.com/tags') #bs_example(resp.text) #write_file("test.html",resp.text) bs_example(open("test.html",encoding='utf-8'))
내용이 많아서 첫번째 내용 결과만 확인 해보겠습니다. 아래 내용 보면 <div class="flex--item">2280774 questions</div> title="810 questions tagged javascript in the last 24 hours" title="4505 questions tagged javascript in the last 7 days" 이런 부분들 모두 포함 되었음을 알 수 있습니다.
<div class="s-card js-tag-cell d-flex fd-column"> <div class="d-flex jc-space-between ai-center mb12"> <div class="flex--item"> <a class="post-tag" href="/questions/tagged/javascript" rel="tag" title="show questions tagged 'javascript'">javascript</a> </div> </div> <div class="flex--item fc-medium mb12 v-truncate4"> For questions regarding programming in ECMAScript (JavaScript/JS) and its various dialects/implementations (excluding ActionScript). Please include all relevant tags on your question; e.g., [node.js],… </div> <div class="mt-auto d-flex jc-space-between fs-caption fc-black-400"> <div class="flex--item">2280774 questions</div> <div class="flex--item s-anchors s-anchors__inherit"> <a href="/questions/tagged/javascript?sort=newest&days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a>, <a href="/questions/tagged/javascript?sort=newest&days=7" title="4505 questions tagged javascript in the last 7 days">4505 this week</a> </div> </div> </div> ##########################
결국 정답은 <div class="s-card js-tag-cell d-flex fd-column"> 이것 이었습니다.
4. 세부 항목들 찾기
이제 내부적으로는 find를 사용합니다. find_all 한 결과 변수를 for loop를 이용해서 사용하면 됩니다.
위 결과에서 찾아야 하는 부분은 javascript 라는 text인데 <a class="post-tag" href="/questions/tagged/javascript" rel="tag" title="show questions tagged 'javascript'">javascript</a> 여기에 있습니다.
정리해보겠습니다.
4.1 javascript : 결과에서 <a class="post-tag" href="/questions/tagged/javascript" rel="tag" title="show questions tagged 'javascript'">javascript</a> => a class="post-tag"
4.2 전체 questions : 결과에서 <div class="flex--item">2280774 questions</div> => div class="flex--item"
4.3 asked today : 결과에서 <a href="/questions/tagged/javascript?sort=newest&days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a> => a href="/questions/tagged/javascript?sort=newest&days=1"
4.4 this week : 결과에서 <a href="/questions/tagged/javascript?sort=newest&days=7" title="4505 questions tagged javascript in the last 7 days">4505 this week</a> => a href="/questions/tagged/javascript?sort=newest&days=7"
그럼 4.2를 제외한 코드를 만들어 보겠습니다.
막상 해보면 href 쪽에 각각의 내용이 변경됩니다. 따라서 검색하려면 조금 변경이 필요한데 re 패키지를 이용하여 정규식 매칭도 지원하고 있어서 이 부분은 간단하게 해결하였습니다.
이 부분입니다. herf 에서 days=1,days=7 이라는 문자열이 있는곳을 찾게 됩니다.
print(rone.find("a",href=re.compile(r"days=1")))
print(rone.find("a",href=re.compile(r"days=7")))
import requests from bs4 import BeautifulSoup import re def bs_example(data): soup = BeautifulSoup(data,"html.parser") #<div class="s-card js-tag-cell d-flex fd-column"> sresult = soup.find_all("div",attrs={"class":"s-card js-tag-cell d-flex fd-column"}) for rone in sresult: #print(rone) print(rone.find("a",attrs={"class":"post-tag"})) print(rone.find("a",href=re.compile(r"days=1"))) print(rone.find("a",href=re.compile(r"days=7"))) print("##########################") def write_file(filename,str_data): with open(filename, 'w', encoding='utf-8') as fp: fp.write(str_data) fp.close() if __name__ == "__main__": #resp = requests.get('https://stackoverflow.com/tags') #bs_example(resp.text) #write_file("test.html",resp.text) bs_example(open("test.html",encoding='utf-8'))
실행 결과
<a class="post-tag" href="/questions/tagged/javascript" rel="tag" title="show questions tagged 'javascript'">javascript</a> <a href="/questions/tagged/javascript?sort=newest&days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a> <a href="/questions/tagged/javascript?sort=newest&days=7" title="4505 questions tagged javascript in the last 7 days">4505 this week</a> ########################## <a class="post-tag" href="/questions/tagged/python" rel="tag" title="show questions tagged 'python'">python</a> <a href="/questions/tagged/python?sort=newest&days=1" title="1093 questions tagged python in the last 24 hours">1093 asked today</a> <a href="/questions/tagged/python?sort=newest&days=7" title="6304 questions tagged python in the last 7 days">6304 this week</a>
다음으로 미루어 두었던 4.2입니다.
이건 div class="flex--item" tag가 많아서 미루었습니다. 찾는 방법이 어려가지가 존재할 수는 있는데 find_all 로 찾아서 text가 questions가 없으면 버리는 형태로 구현해 보겠습니다.
<div class="flex--item fc-medium mb12 v-truncate4"> 문제는 이러한 경우도 match가 되게 됩니다.
이걸 적당히 만들어보면 이렇습니다. text에는 questions 문자열이 있고 class에는 fc-medium 없는 조건입니다.
이렇게 되면 text가 "숫자 questions" 로 올라오게 되는데 split 로 나누고 첫번째 항목만 가져와서 int 형태로 변환하면 됩니다.
def find_questions(bs_data): bsret = bs_data.find_all("div",attrs={"class":"flex--item"}) for bsone in bsret: if ("questions" in bsone.text) and not ("fc-medium" in bsone.get("class")): #print(bsone.text) return int(bsone.text.strip().split(" ")[0].strip())
이제 구현은 어느정도 마무리 되었습니다.
def bs_example(data): soup = BeautifulSoup(data,"html.parser") #<div class="s-card js-tag-cell d-flex fd-column"> sresult = soup.find_all("div",attrs={"class":"s-card js-tag-cell d-flex fd-column"}) for rone in sresult: #print(rone) print(rone.find("a",attrs={"class":"post-tag"}).text) print(find_questions(rone)) print(int(rone.find("a",href=re.compile(r"days=1")).text.strip().split(" ")[0].strip())) print(int(rone.find("a",href=re.compile(r"days=7")).text.strip().split(" ")[0].strip())) print("##########################")
dict 로 결과를 모으고 출력해보도록 하겠습니다.
전체 소스입니다.
import requests from bs4 import BeautifulSoup import re def find_questions(bs_data): bsret = bs_data.find_all("div",attrs={"class":"flex--item"}) for bsone in bsret: if ("questions" in bsone.text) and not ("fc-medium" in bsone.get("class")): return int(bsone.text.strip().split(" ")[0].strip()) def bs_example(data): ret = {} soup = BeautifulSoup(data,"html.parser") sresult = soup.find_all("div",attrs={"class":"s-card js-tag-cell d-flex fd-column"}) for rone in sresult: #print(rone) #print(rone.find("a",attrs={"class":"post-tag"}).text) #print(find_questions(rone)) #print(int(rone.find("a",href=re.compile(r"days=1")).text.strip().split(" ")[0].strip())) #print(int(rone.find("a",href=re.compile(r"days=7")).text.strip().split(" ")[0].strip())) #print("##########################") tag = rone.find("a",attrs={"class":"post-tag"}).text ret[tag] = [find_questions(rone),int(rone.find("a",href=re.compile(r"days=1")).text.strip().split(" ")[0].strip()),int(rone.find("a",href=re.compile(r"days=7")).text.strip().split(" ")[0].strip())] return ret def write_file(filename,str_data): with open(filename, 'w', encoding='utf-8') as fp: fp.write(str_data) fp.close() def f1(x): return x[1][1] def f2(x): return x[1][2] def print_dict(dict_data): for key,data in dict_data: print(key,data) if __name__ == "__main__": USE_FILE = False if USE_FILE: ret = bs_example(open("test.html",encoding='utf-8')) else: resp = requests.get('https://stackoverflow.com/tags') write_file("test.html",resp.text) ret = bs_example(resp.text) print(ret) print("------------------ sorted days=1") data_ = sorted(ret.items(),key=f1,reverse=True) print_dict(data_) print("------------------ sorted days=7") data_ = sorted(ret.items(),key=f2,reverse=True) print_dict(data_)
결과입니다.
누적은 javascript이지만 1일 최고 순위는 python 입니다. 7일을 봐도 python입니다.
{'javascript': [2280813, 801, 4554], 'python': [1809580, 1096, 6387], 'java': [1803186, 417, 2377], 'c#': [1501073, 319, 1687], 'php': [1416808, 245, 1430], 'android': [1350753, 264, 1526], 'html': [1094188, 267, 1716], 'jquery': [1018332, 71, 467], 'c++': [739969, 238, 1229], 'css': [734575, 169, 1168], 'ios': [661851, 109, 575], 'mysql': [633104, 114, 678], 'sql': [606868, 194, 991], 'r': [421633, 217, 1244], 'node.js': [404352, 223, 1266], 'arrays': [374060, 109, 686], 'c': [365866, 102, 588], 'asp.net': [364865, 33, 196], 'reactjs': [336134, 332, 2032], 'ruby-on-rails': [328553, 24, 156], 'json': [326916, 100, 571], '.net': [311611, 76, 316], 'sql-server': [309938, 74, 416], 'swift': [303335, 103, 548], 'python-3.x': [296404, 165, 929], 'objective-c': [291942, 13, 50], 'django': [276511, 110, 727], 'angular': [263495, 168, 827], 'angularjs': [262258, 12, 71], 'excel': [252456, 86, 454], 'regex': [244622, 67, 363], 'ruby': [221270, 21, 122], 'iphone': [221208, 9, 31], 'pandas': [218023, 168, 1020], 'ajax': [217443, 23, 154], 'linux': [208966, 79, 398]} ------------------ sorted days=1 python [1809580, 1096, 6387] javascript [2280813, 801, 4554] java [1803186, 417, 2377] reactjs [336134, 332, 2032] c# [1501073, 319, 1687] html [1094188, 267, 1716] android [1350753, 264, 1526] php [1416808, 245, 1430] c++ [739969, 238, 1229] node.js [404352, 223, 1266] r [421633, 217, 1244] sql [606868, 194, 991] css [734575, 169, 1168] angular [263495, 168, 827] pandas [218023, 168, 1020] python-3.x [296404, 165, 929] mysql [633104, 114, 678] django [276511, 110, 727] ios [661851, 109, 575] arrays [374060, 109, 686] swift [303335, 103, 548] c [365866, 102, 588] json [326916, 100, 571] excel [252456, 86, 454] linux [208966, 79, 398] .net [311611, 76, 316] sql-server [309938, 74, 416] jquery [1018332, 71, 467] regex [244622, 67, 363] asp.net [364865, 33, 196] ruby-on-rails [328553, 24, 156] ajax [217443, 23, 154] ruby [221270, 21, 122] objective-c [291942, 13, 50] angularjs [262258, 12, 71] iphone [221208, 9, 31] ------------------ sorted days=7 python [1809580, 1096, 6387] javascript [2280813, 801, 4554] java [1803186, 417, 2377] reactjs [336134, 332, 2032] html [1094188, 267, 1716] c# [1501073, 319, 1687] android [1350753, 264, 1526] php [1416808, 245, 1430] node.js [404352, 223, 1266] r [421633, 217, 1244] c++ [739969, 238, 1229] css [734575, 169, 1168] pandas [218023, 168, 1020] sql [606868, 194, 991] python-3.x [296404, 165, 929] angular [263495, 168, 827] django [276511, 110, 727] arrays [374060, 109, 686] mysql [633104, 114, 678] c [365866, 102, 588] ios [661851, 109, 575] json [326916, 100, 571] swift [303335, 103, 548] jquery [1018332, 71, 467] excel [252456, 86, 454] sql-server [309938, 74, 416] linux [208966, 79, 398] regex [244622, 67, 363] .net [311611, 76, 316] asp.net [364865, 33, 196] ruby-on-rails [328553, 24, 156] ajax [217443, 23, 154] ruby [221270, 21, 122] angularjs [262258, 12, 71] objective-c [291942, 13, 50] iphone [221208, 9, 31]
댓글 없음:
댓글 쓰기