2021년 10월 8일 금요일

python BeautifulSoup 기초 사용법과 실전 예제 (BeautifulSoup basic usage and practical examples)

 BeautifulSoup 를 사용할때마다 느끼는 거지만 뭔가 꽉막힌듯한 뭐부터 해야하나 고민을 할때가 많습니다. 어떤 한값을 읽어내는건 단순합니다. 그러나 웹크롤링이나 뭔가 복잡하게 표를 읽어내는건 고민을 좀 해야하는 부분도 많고 해서, 그래서 실전 예제를 통해서 제가 사용하는 절차를 정리해 보았습니다.

여기에서 사용할 예제는 https://stackoverflow.com/tags 여기입니다.

그사이 tags 정보가 변경되어 예제가 동작이 되지 않을 수도 있는점 참고 부탁드립니다.


목표

목표는 tags의 값을 모으는 작업입니다.


위와 같은 html 있다면 아래와 같은 dict 형태의 결과를 얻도록 하는것이 이번 목표입니다.
{'javascript':[2280764,808,4505], 'python':[1809544,1089,6298], 'java' ....


1. 정보를 가져오고자 하는 웹 페이지의 HTML 저장

간단하게 코드를 만들었습니다. 원하는 page는 test.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를 찾음

1개의 세트분이라고 한다면 여기에서는 'javascript':[2280764,808,4505] 이 데이터를 얻을 수 있는 내용이 되겠습니다. 이 부분을 찾는 방법은 친절하게 html파일을 텍스트로 열어서 해당되는 글자를 읽어서 찾으면 편합니다. 여기에서는 "For questions"가 고유한 text이니 해당 글씨 앞쪽으로 보면 될듯합니다.

찾았습니다. 몇줄위를 보면 원하는 "javascript" 부분도 있습니다. 그러면 이중에 어디가 한개의 set 분의 데이터인지 알 수 있을까요? 솔직하게 얘기하자면 사실 알기는 힘듭니다.


크롬의 F12를 눌러서 찾는 방법도 있습니다. 위 화면을 보자면 <div class="s-card js-tag-cell d-flex fd-column"> 여기가 원하는 결과의 한개분의 SET 위치가 되겠습니다.

3. 한개분의 셋트 위치 TAG로 find_all 모두 찾기

2번에서 정확하게 찾지 못하더라도 3번과 2번을 반복함으로서 정확한 위치를 찾을 수 있습니다.
여기에서 답을 알고 있을지라도, 모르는척 해보겠습니다.
즉 <div class="d-flex jc-space-between ai-center mb12"> 이런값이 정답인지 아닌지 확인해보도록 하겠습니다.

저장된 test.html 을 읽어와 <div class="d-flex jc-space-between ai-center mb12"> 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&amp;days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a>, <a href="/questions/tagged/javascript?sort=newest&amp;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&amp;days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a> => a href="/questions/tagged/javascript?sort=newest&amp;days=1"

4.4 this week : 결과에서 <a href="/questions/tagged/javascript?sort=newest&amp;days=7" title="4505 questions tagged javascript in the last 7 days">4505 this week</a> => a href="/questions/tagged/javascript?sort=newest&amp;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&amp;days=1" title="810 questions tagged javascript in the last 24 hours">810 asked today</a>
<a href="/questions/tagged/javascript?sort=newest&amp;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&amp;days=1" title="1093 questions tagged python in the last 24 hours">1093 asked today</a>
<a href="/questions/tagged/python?sort=newest&amp;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]


댓글 없음:

댓글 쓰기