데이터 수집 파트의 둘째날이자 마지막 날.
웹 분야를 완전히 처음 접하는 건 아니지만...
강사님 설명이 무척 빠르셨다.
아예 처음이었으면 멘탈이 많이 갈렸을 듯.
동적 페이지 크롤링 실습
몇가지 사례를 통해 동적 페이지 크롤링을 더 해 보았다.
사전학습 때도 느꼈지만, 크롤링은 변수와 상황이 너무 많아서 힘든 것 같다 ㅠ
공식처럼 쓰는 게 안된달까...
큰 틀은 지난 포스팅에서 본 것과 같다.
# 1. URL
url = 'https://m.stock.naver.com/api/index/KOSPI/price?pageSize=10&page=2'
# 2. request(URL) > response(JSON(str))
response = requests.get(url)
# 3. JSON(str) > list or dict > DataFrame
data = response.json()
df = pd.DataFrame(data)
다만 json 파일의 내부적 구조, 페이지가 get 방식인지 post 방식인지,
post 방식이라면 파라미터 구조는 어떤지... 이런 부분들에서 많은 추가 작업이 발생한다.
이를테면 지난 포스팅의 원-달러 환율 정보는
son파일 안에 있는 result가 진짜 목적이었기 때문에 df를 선언할 때
df = pd.DataFrame(data)['result']
이런 형태를 고려해야 했다.
그럼 오늘 했던 내용들로 그 예들을 보자.
- Daum Exchange 크롤링
이번엔 Daum Exchange에서 데이터를 가져와보자.
지난 포스팅과 똑같이 개발자 도구(F12)의 network 탭에서 트래픽의 url을 가져온다.
import pandas as pd
import requests
# 1. URL 가져오기
url = 'https://finance.daum.net/api/exchanges/summaries'
# 2. request > response
response = requests.get(url)
response # 403 에러 발생
request가 제대로 요청되지 못하고, response 객체는 403 에러를 뱉는다.
여기엔 여러 이유가 있을 수 있지만,
많은 경우 클라이언트가 파이썬 코드를 통해 요청했다는 정보를 바탕으로
서버가 이 요청을 비정상적이라고 판단하기 때문이다.
따라서 request 정보에서 파이썬을 통한 요청이라는 정보를 수정할 필요가 있다.
우리가 보내는 request 정보는 URL이 주인공이지만, 이녀석에 다양한 것들이 덧붙어서 간다.
이번에는 그중에서 header 정보를 수정하여 정상적인 요청으로 만들 필요가 있다.
header에는 다양한 정보가 포함되어 있지만,
오늘은 user-agent와 referer을 중심으로 살펴보자.
개발자 도구의 Headers 탭에는 헤더에 들어갈 여러 정보들이 나와 있다.
구글 브라우저를 통해 정상적으로 데이터를 요청했을 때의 User-Agent와 Referer 정보가 있다.
이 녀석을 사용해서 headers 인자를 채워 주면 된다.
그럼 헤더 정보가 변경된 채 요청을 보내게 된다.
headers = {
'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
'referer' : 'https://finance.daum.net/exchanges'
}
response = requests.get(url, headers=headers)
response # 정상 200 출력
이후로는 똑같다.
올바르게 데이터를 가져 왔으니,
JSON 파일을 DataFrame으로 파싱하면 된다.
# json > DataFrame
cols = ['symbolCode', 'currencyCode', 'currencyName', 'basePrice']
df = pd.DataFrame(response.json()['data'])[cols]
df.head()
필요한 컬럼만 추린 후 response.json() 객체(list or dict)로 만든 DataFrame에서 빼 온다.
- REST API를 활용한 데이터 크롤링
REST API는 Representational State Transfer API의 준말로,
REST는 http를 따르는 데이터 송수신 규칙이고,
API도 일종의 통신 방법론인데, 조금 더 추상적인 개념이라고만 생각하자.
사실 내가 이렇게밖에 모른다.
결국 REST API는 클라이언트와 서버가 REST 형식으로 통신하는 방법을 정의한 것이다.
그래서 이걸로 크롤링을 한다는게 무슨 뜻인고 하니...
웹 서비스 중에는 REST API를 지원하는 것들이 있다.
REST API를 지원하는 웹 서비스에 이 형식으로 데이터를 요청하면,
일반적으로 JSON 형식으로 데이터를 반환한다.
따라서 html을 분석하거나 파싱할 필요가 없어서,
동적 페이지와 비슷하게 적은 단계로 크롤링을 진행할 수 있다.
아직 다루지 않았지만 html을 분석하는 과정이 꽤 복잡하기 때문에,
REST API 크롤링은 좋은 방법으로 권장된다.
자 그럼 네이버의 통합검색어 트렌드 API를 통해 이 과정을 살펴보자.
이 데이터를 크롤링해 오는 방법은 다음과 같다.
import pandas as pd
import requests
# 1. app key 받아오기
CI = 'sdgHFSAF0fj3' # client_id
CS = 'asflfIENF' # client_secret
일반적인 동적 페이지 크롤링과 다른 점은, 웹페이지로부터 app_key를 받아야 한다는 것이다.
네이버의 경우 ID와 Secret 두 개의 키를 사용한다.
해당 키는 네이버 Developers > Application에서 등록하면 부여된다.
위의 내 ID와 Secret은 임의로 바꿨다.
이 뒤로는 기존의 방법과 똑같지만,
네이버의 검색 트렌드는 기본적으로 OPEN API이므로 개발자 도구에서 URL을 따 올 필요가 없다.
이후에는 지난 포스팅과 똑같이
response = request.get(url)
data = response.json
df = pd.DataFrame(data)
하면 되...지 않는다.
post방식은 url이 아니라 헤더에 요청 데이터에 대한 메타데이터가 명세되어 있기 떄문.
페이지에 제시된 파라미터 기준에 따라 인자 'params'를 튜닝해줘야 한다.
# 2. url
url = 'https://openapi.naver.com/v1/datalab/search'
# 3. request(url, params(data) - 포스트 방식은 쿼리가 아니므로 파라미터 추가,
params = {
'startDate' : '2018-01-01', 'endDate' : '2023-12-31', 'timeUnit' : 'month',
'keywordGroups' : [
{'groupName': '트위터', 'keywords': ['트위터', '트윗']},
{'groupName': '페이스북', 'keywords': ['페이스북', '페북']},
{'groupName': '인스타그램', 'keywords': ['인스타그램', '인스타']}
],
}
# 헤더에 정보 명세 후 request.
headers = {
'X-Naver-Client-Id': CI, 'X-Naver-Client-Secret': CS, 'Content-Type' : 'application/json'
}
response = requests.post(url, data=json.dumps(params), headers=headers)
response # join_dumps는 내용 중에 한들이 있어서 인코딩한 것.
# 4. json > list, dict > DataFrame
data = response.json()
df = pd.DataFrame(data)
- Zigbang 원룸 매물 데이터 수집
부동산 정보를 떼 와서 데이터프레임으로 만들어 보자.
부동산 정보 특성상 주소 범위를 명확히 지정해 줘야 하는데,
이 과정에서 geohash를 사용하게 된다.
간단히 말하자면 위도/경도를 바탕으로 저장된 지리 정보.
또 Zigbang의 경우에는 매물 아이디를 바탕으로 매물 정보를 관리한다.
따라서 위도/경도 -> geohash -> 매물 아이디 -> 매물 정보라는 데이터플로우를 거치게 된다.
그렇다는 것은, request를 세 번 해야 한단 거지. 거지같다
# 1. 동이름으로 위도/경도 가져오기
addr = '신사동'
url = f'https://apis.zigbang.com/v2/search?leaseYn=N&q={addr}&serviceType=원룸'
response = requests.get(url) # 부동산 raw 데이터
# 동명의 동들(은평구 신사동, 강남구 신사동) 구분을 위해 인덱싱
data = response.json()['items'][0]
# raw 데이터 중에서 위도/경도 정보 따 오기
lat, lng = data['lat'], data['lng'] # 위도/경도
lat, lng
그런데 저 response를 그대로 쓰면 안 되는 걸까...?
어쨌든 더 진행해 본다.
# 2.위도/경도로 geohash 알아내기
# geohash 계산하는 패키지 필요
!pip install geohash2
import geohash2
geohash = geohash2.encode(lat, lng, precision=5) # precision = 영역의 정도를 나타냄
geohash
이 결과로
'wydm6'
이렇게 geohash가 출력된다.
# 3. geohash로 매물 아이디 가져오기
url = 'https://apis.zigbang.com/v2/items/oneroom?geohash=wydm6&depositMin=0&rentMin=0&salesTypes[0]=전세&salesTypes[1]=월세&domain=zigbang&checkAnyItemWithoutFilter=true'
response = requests.get(url)
ids = [item['itemId'] for item in response.json()['items']]
# 4. 매물 아이디로 매물 정보 가져오기
url = 'https://apis.zigbang.com/v2/items/list' # post방식인 것 확인
params = {'domain': "zigbang", 'item_ids':ids[:15]} # 1회 최대 15개까지만 제공됨
response = requests.post(url, params)
cols = ['item_id', 'deposit', 'rent', 'size_m2', 'address1']
data = response.json()['items']
df = pd.DataFrame(data)[cols]
df.head()
#3에서 geohash가 url에 반영됐고, 그걸 통해 받아 온 id 정보를 for 확장 문법을 통해 ids에 담았다.
이걸 바탕으로 #4에서 다시 requests를 거쳐 15개짜리 데이터프레임을 만들었다.
근데 이 부분 기억이 잘 안 난다...ㅠ 확인.
한 번에 15개 정보밖에 못 가져 오니, 반복문을 통해 정보들을 우루루 받아와서 합친다.
page_block = 15
dfs=[]
for start in range(0, len(ids), 15):
end = start + page_block
print(start, end=' ')
url = 'https://apis.zigbang.com/v2/items/list'
params = {'domain': "zigbang", 'item_ids':ids[:15]}
response = requests.post(url, params)
cols = ['item_id', 'deposit', 'rent', 'size_m2', 'address1']
data = response.json()['items']
df = pd.DataFrame(data)[cols]
dfs.append(df)
result = pd.concat(dfs, ignore_index=True)
result.tail(2)
아래는 이런 과정을 거쳐 얻은 데이터프레임
정적 페이지 크롤링 실습
정적 페이지는 동적 페이지보다 크롤링 과정이 복잡하다.
이는 정적 페이지의 업데이트 방식에 기인하는데,
정적 페이지는 한 번의 업데이트마다 새로운 html을 생성한다.
따라서 우리도 그 사양에 맞추어 url을 편집하여 보내야 한다.
이를 위해서는 html과 CSS selector를 이해해야 하므로, 이를 먼저 확인한다.
- html과 CSS selector 기본 문법
<html>
html은 웹 페이지 작성에 널리 쓰이는 마크업 언어다.
종류 | 명칭 | 의미 |
Document | 한 페이지 | |
Element | 한 레이아웃, 시작태그/끝태그/텍스트로 구성되어 있다. | |
Tag | div | 레이아웃을 정의한다. |
h1 ~ h6 | 제목 문자열 | |
p | 한 문단 문자열. 앞뒤로 줄바꿈이 자동으로 들어간다. | |
span | 한 단락 문자열. 문단 내에서 특정 부분을 묶는 역할. | |
ul | 목록을 정의한다. | |
li | 목록의 항목을 정의한다. | |
a | 링크나 페이지 내 이동을 정의한다. href 속성으로 이동할 장소를 명세한다 |
|
img | 웹 페이지에 표시할 이미지를 정의한다. src 속성으로 이미지 주소를 명세한다 |
|
Attribute | id | 페이지에서 유일한 값 |
class | element를 그룹화할 때 쓰는 요소 | |
attr | id와 class 외의 나머지 속성 | |
Text | 시작태그와 끝태그 사이에 있는 문자열 |
<CSS selector>
역할 | 문법 |
Tag 선택 | div |
id 선택 | #ds1 |
class 선택 | .ds1 |
attr 선택 | [val='data'] |
ds1을 제외한 div 전체 선택 | div:not(.ds1) |
3번째 태그중 div 태그를 가지면 선택 | div:nth-child(3) |
selector1, selector2에 해당하는 모든 엘리먼트 선택 | selector1, selector2 |
selector1과 한단계 하위의 selector2 선택 | selector1 > selector2 |
selector1 모든 하위 엘리먼트중 selector2 선택 | selector1 selector2 |
- 네이버 함께 많이 찾는 주제어 수집
일단 URL을 가져와 requests하는 원리는 동적 페이지 크롤링과 같다.
import pandas as pd
import requests
# html 파싱을 위해 BeautifulSoup 클래스 import
from bs4 import BeautifulSoup
# 개발자 도구 트래픽에서 URL 따 오기
url = 'https://search.naver.com/search.naver?query=삼성전자'
# 서버에 request 요청
response = requests.get(url)
차이는 여기부터, 즉 파싱 단계부터 일어나는데,
동적 페이지 크롤링에서는 response 객체가 JSON이었지만
이번에는 html이기 때문이다.
JSON은 response.json()으로 파싱이 끝난 반면
html은 BeautifulSoup 클래스의 객체가 가진 select() 메서드를 써 줘야 파싱이 된다.
# BS 객체 생성
dom = BeautifulSoup(response.text, 'html.parser')
# BS 객체 > .select or select_one(css-selector) > str(text)
# 목표 element : fds-refine-query-grid 클래스 하의 유일한 a태그 보유
selector = '.fds-refine-query-grid a'
# selector에 해당하는 데이터 크롤링하여 elements에 넣기
elements = dom.select(selector)
# elements는 BS 객체이므로 값만 꺼내서 담기
keywords = [element.text for element in elements]
keywords
JSON이 선녀다
html 파싱에서 가장 중요한 요소는 selector를 이용하여 목표 element를 선택하는 과정이다.
이게 참 어렵지만... 열심히 찾고 쓰다 보면 익숙해지겠지.
이 다음 셀트리온을 크롤링해 보았지만... 솔직히 코드 따라 적기에 급급했던 것 같다.
이후에 확실히 이해하고 리뷰하도록 해야지.