본문 바로가기
코딩/자동화

파이썬을 이용한 웹스크레이핑(웹크롤링) 예제/requests와 beautifulsoup로 웹페이지 정보 추출하기

by 나홀로코더 2021. 11. 30.
반응형

1. 시작하기 전에

 

앞서 requests와 beautifulsoup의 기본적인 사용법을 소개했다.

 

여기에서는 이를 활용한 실제 웹스크레이핑 예제를 다룬다.

 

참고로 앞선 글과 여기에서 소개하는 방법은 URL 주소를 통해 내가 원하는 웹페이지에 곧바로 접근이 가능할 때에만 사용할 수 있다.

 

무슨 말인지 예시를 한번 보자.

 

아래 URL은 코딩을 공부해본 사람이라면 누구나 한번쯤 들어가봤을 stackoverflow에서 태그가 python이고, 빈도(frequent) 기준으로 정렬한 질문 목록을 보여주는 페이지이다.

 

https://stackoverflow.com/questions/tagged/python?tab=frequent&page=1&pagesize=50

 

URL의 구조를 자세히 보면, "/tagged/"와 "?" 사이에 태그명이 있고, "tab="와 "&" 사이에 정렬 기준이 있고, 그 뒤에 몇 페이지를 볼지, 한번에 몇 개를 볼지가 숫자로 들어가 있다.

 

여기에서 내가 pandas에 관한 글의 목록을 뽑아내고 싶다면, 위의 주소에서 python만 pandas로 바꾸면 될 것이고, 2페이지에 보이는 목록을 뽑고 싶다면 page=1만 page=2로 바꾸면 될 것이다.

 

stackoverflow에서는 특정 검색어로 검색한 결과를 보고 싶을 때도 아래와 같이 "/search?q=[검색어]"만 덧붙여서 간단히 접근할 수 있다.

 

https://stackoverflow.com/search?q=how+to+web+scrape+beautifulsoup

 

비교 대상으로 청와대 국민청원의 검색 페이지로 가보자.

 

https://www1.president.go.kr/search

 

여기에서 원하는 검색어를 입력한 다음에 URL을 확인해보면, 위의 주소에서 바뀌지 않는 것을 볼수 있다.

 

이럴 때는 requests 라이브러리만으로는 원하는 내용을 추출해올 수가 없으며, selenium을 활용해서 직접 브라우저를 조작해 원하는 페이지로 접근해야 한다.

 

이에 대해서는 다른 포스팅에서 소개하려 한다.

 

 

2. 웹스크레이핑 예제(단일 페이지)

 

앞에서 stackoverflow를 보았으니 예제 페이지로도 활용해보자.

 

먼저, 앞선 포스팅에서 본 것처럼, requests와 beautifulsoup를 이용해서 HTML 소스를 파싱하자.

 

import requests 
import bs4 
url = 'https://stackoverflow.com/questions/tagged/python?tab=Frequent'
response = requests.get(url)
soup = bs4.BeautifulSoup(response.text,"html.parser")

 

이제는 내가 원하는 정보가 어느 태그에 있는지를 봐야 하는데, 이 때는 크롬 브라우저를 사용하는 것이 가장 편하다.

 

원하는 페이지에서 F12를 누르면, 아래와 같이 개발자 도구가 열린다.

 

크롬 개발자 도구

 

내가 찾고 싶은 태그의 위치를 정말 간편하게 확인할 수 있는데, 개발자 도구 좌상단에 있는 마우스 포인터 모양의 아이콘을 누른 뒤에 웹페이지 내에서 찾고 싶은 요소를 찾아 클릭하면, 개발자 도구에서 해당 태그가 HTML 소스 내 어디에 위치하는지 표시해준다.

 

 

표시된 위치를 보니 우리가 찾고 싶은 제목은 div, div, div, ... 한참 내려와서 h3 태그 안의 a 태그에 들어 있다.

 

그러니 제목에 빠르게 접근하려면 h3이나 a 태그로 찾으면 될 것인데, 제목은 h태그로 찾는 게 낫다.

 

a 태그는 하이퍼링크를 거는 데 사용되는 태그여서 제목 외에도 너무 많기 때문이다.

 

h3을 다 찾아달라고 해보자.

 

soup.find_all('h3')

 

 

그런데 위에서 돌려준 리스트의 첫번째 항목을 출력해보면, 우리가 원하는 글제목이 아닐 것이다.

 

<h3 class="flex--item"><a href="https://stackoverflow.com">current community</a></h3>

 

문서 내에 글제목 외에도 h3 태그가 더 있기 때문인데, 이럴 때는 리스트 내에서 몇번부터 몇번까지 우리가 원하는 데이터가 들어있는지 체크해봐야 한다.

 

이 예제의 경우 4번째 항목부터가 게시글 제목이다.

 

그리고 soup.find_all('h3)[-1]로 마지막 항목이 뭔지 보니 마지막 항목은 게시글 제목이 맞다.

 

<h3><a class="question-hyperlink" href="/questions/10434599/get-the-data-received-in-a-flask-request">Get the data received in a Flask request</a></h3>

 

따라서 게시글 제목을 뽑아내는 코드는 아래와 같이 된다.

(아래 코드에서 사용한 리스트컴프리헨션에 대해 모르는 경우 관련 포스팅을 참조)

 

[i.text for i in soup.find_all('h3')[3:]]

 

출력 결과 제목이 잘 추출됐다.

 

['How to make good reproducible pandas examples', 'How to test multiple variables against a single value?', 'Asking the user for input until they give a valid response', 'How do I create variable variables?', 'Understanding slice notation', 'List of lists changes reflected across sublists unexpectedly', '"Least Astonishment" and the Mutable Default Argument', 'List changes unexpectedly after assignment. Why is this and how can I prevent it?', 'How can I pivot a dataframe?', 'Pandas Merging 101', 'How to remove items from a list while iterating?', 'How to make a flat list out of a list of lists', 'How can I read inputs as numbers?', 'How do you split a list into evenly sized chunks?', 'How do I pass a variable by reference?', 'What does ** (double star/asterisk) and * (star/asterisk) do for parameters?', 'How to deal with SettingWithCopyWarning in Pandas', 'Using global variables in a function', 'Short description of the scoping rules?', 'What does if __name__ == "__main__": do?', 'Why is the command bound to a Button or event executed when declared?', 'How do I select rows from a DataFrame based on column values?', 'How to execute a program or call a system command?', 'How do I sort a dictionary by value?', 'What does the "yield" keyword do?', 'Why does "a == x or y or z" always evaluate to True?', 'Tkinter: AttributeError: NoneType object has no attribute <attribute name>', 'Flatten an irregular list of lists', "How to filter Pandas dataframe using 'in' and 'not in' like in SQL", "Why is using 'eval' a bad practice?", 'How to iterate through two lists in parallel?', 'How to convert string representation of list to a list?', 'Pandas conditional creation of a series/dataframe column', 'WebDriverWait not working as expected', 'How to iterate over rows in a DataFrame in Pandas', 'Why does Tkinter image not show up if created in a function?', '"is" operator behaves unexpectedly with integers', "What is the purpose of the word 'self'?", 'Does Python have a ternary conditional operator?', 'How do I merge two dictionaries in a single expression (take union of dictionaries)?', 'How to avoid having class data shared among instances?', 'Importing installed package from script raises "AttributeError: module has no attribute" or "ImportError: cannot import name"', 'How do I detect collision in pygame?', 'UnboundLocalError on local variable when reassigned after first use', 'What is the difference between __str__ and __repr__?', 'Selenium - wait until element is present, visible and interactable', 'Converting string into datetime', 'What are metaclasses in Python?', 'How to unnest (explode) a column in a pandas DataFrame', 'Get the data received in a Flask request']

 

이제 원하는 형태로 바꾸기만 하면 되는데, 필자는 보통 판다스를 이용해서 엑셀 파일로 변환하고 있다.

 

import pandas as pd
title = pd.Series([i.text for i in soup.find_all('h3')[3:]])
title.reset_index(drop=True).to_excel('result.xlsx')

 

반응형

 

3. 웹스크레이핑 예제(여러 페이지)

 

만일 여러 페이지에서 데이터를 추출하려 한다면, 위의 예제에다 for문만 더해주면 된다.

 

import requests 
import bs4 
import pandas as pd

result = pd.Series()
# for문을 돌면서 추출한 내용을 저장할 변수를 선언해준다. 마지막에 판다스를 이용해 엑셀로 변환할 것이라서 비어 있는 판다스 시리즈를 만들어줬다.

for page in range(1,5):
	url = f'https://stackoverflow.com/questions/tagged/python?page={page}&sort=Frequent&pageSize=15'
    # 두 번째 페이지로 이동하면 URL이 위와 같은 포맷으로 바뀐다. page수를 넣어 주기 위해서는 위와 같은 포맷을 사용해야 한다.
	response = requests.get(url)
	soup = bs4.BeautifulSoup(response.text,"html.parser")
	title = pd.Series([i.text for i in soup.find_all('h3')[3:]])
	result = result.append(title)
    # 루프를 한번 돌고나서 위에서 만든 판다스 시리즈 변수에다 덧붙이고, 덧붙인 결과를 다시 result 변수로 저장한다.

result.reset_index(drop=True).to_excel('result.xlsx')

 

반응형

댓글