02-1 API 사용하기
API란
: 두 프로그램이 서로 대화하기 위한 방법을 정의한 것
웹 페이지를 전송하기 위한 통신 규약:HTTP
HTTP: 인터넷에서 웹 페이지를 전송하는 기본 통신 방법
웹 페이지 문서: HTML
HTML: 웹 브라우저가 화면에 표시할 수 있는 문서의 한 종류이자 웹 페이지를 위한 표준 언어
→ 이와 같은 언어를 마크업이라고 부른다.
- 웹 기반 API는 HTTP 프로토콜을 사용하지만 HTML을 주고받는 것이 아니라 일반적으로 CSV, JSON, XML 같은 파일을 사용
프로그램 A 프로그램 B
← 데이터 요청(HTTP)
→ 데이터 전송(CSV, JSON, XML)
*CSV, JSON, XML을 선호하는 이유: HTML 소스는 구조가 복잡하기 때문
파이썬에서 JSON 데이터 다루기
- JSON(JavaScript Object Notation)
원래는 자바스크립트 언어를 위해 만들어졌다.
대부분의 프로그래밍 언어는 JSON 형태의 텍스트를 읽고 쓸 수 있다.
- 파이썬의 딕셔너리와 리스트를 중첩해놓은 것과 비슷: 키와 값을 콜론(:)으로 연결
ex) <혼자 공부하는 데이터 분석>이라는 도서명 → {"name": "혼자 공부하는 데이터 분석"}
*키와 값에 문자열을 쓰려면 항상 큰따옴표(")로 감싸 주어야 한다.
d = {"name": "혼자 공부하는 데이터 분석"}
print(d['name'])
- 파이썬 객체를 JSON 문자열로 변환하기: json.dumps() 함수
import json
d_str = json.dumps(d, ensure_ascii=False)
print(d_str)
*ensure_ascii 매개변수를 False로 지정한 이유: 딕셔너리 d에 한글이 포함되어 있기 때문
* json.dumps() 함수는 아스키 문자 외의 다른 문자를 16진수로 출력
- type()함수: 데이터 타입 확인
print(type(d_str))
- JSON 문자열을 파이썬 객체로 변환하기: json.loads() 함수
d2 = json.loads(d_str)
print(d2['name'])
print(type(d2))
- 여러 개의 항목이 들어 있는 딕셔너리나, 딕셔너리 안에 리스트를 포함하기
JSON 문자열을 json.loads() 함수에 직접 전달
d3 = json.loads('{"name": "혼자 공부하는 데이터 분석", "author": "박해선", "year": 2022}')
print(d3['name'])
print(d3['author'])
print(d3['year'])
- JSON은 대괄호 안에 여러 항목을 나열하여 배열을 표현할 수 있다.
*파이썬의 리스트와 비슷
*딕셔너리 안에 리스트가 중첩
d3 = json.loads('{"name": "혼자 공부하는 데이터 분석", "author": ["박해선","홍길동"], "year": 2022}')
print(d3['author'][1])
- 여러 개의 도서를 하나의 JSON 배열로 나타내기
*세겹따옴표를 사용하면 긴 문자열을 줄바꿈하여 입력할 수 있다.
d4_str = """
[
{"name": "혼자 공부하는 데이터 분석", "author": "박해선", "year": 2022},
{"name": "혼자 공부하는 머신러닝+딥러닝", "author": "박해선", "year": 2020}
]
"""
d4 = json.loads(d4_str)
print(d4[0]['name'])
- JSON 문자열을 데이터프레임으로 변환하기: read_json() 함수
import pandas as pd
pd.read_json(d4_str)
- JSON 문자열을 파이썬 객체로 만든 다음 DataFrame 클래스를 사용
pd.DataFrame(d4)
파이썬에서 XML 데이터 다루기
- XML(eXtensible Markup Language): 컴퓨터와 사람 모두 읽고 쓰기 편한 문서 포맷을 위해 고안되었다.
*HTML은 웹 페이지를 표현하는 데는 뛰어나지만, 구조적이지 못하기 때문에 프로그램 간의 약속대로 전송하는 API에는 적잘하지 않다.
ex)
<book>
<name>혼자 공부하는 데이터 분석</name>
<author>박해선</author>
<year>2022</year>
</book>
<book>: 부모 엘리먼트(루트 엘리먼트) 혹은 부모 노드
<name>, <author>, <year>: <book>의 자식 엘리먼트
- 엘리먼트들이 계층 구조를 이루면서 정보 표현
- 엘리먼트: 시작 태그와 종료 태그로 감싼다.
- 태그: <기호로 시작해서 >기호로 끝나며 태그 이름은 영문자와 숫자를 사용
*시작 태그와 종료 태그의 이름은 같아야 한다.
*태그 이름은 특수 문자와 공백문자를 포함할 수 없고, '-', '.'와 숫자로 시작할 수 없다.
- XML 문자열을 파이썬 객체로 변환하기: fromstring() 함수
x_str = """
<book>
<name>혼자 공부하는 데이터 분석</name>
<author>박해선</author>
<year>2022</year>
</book>
"""
import xml.etree.ElementTree as et
book = et.fromstring(x_str)
*fromstring() 함수가 반환하는 객체는 ElementTree 모듈 아래에 정의된 Element 클래스의 객체
print(type(book))
book 객체의 태그 속성 출력: 엘리먼트 이름 확인
print(book.tag)
- 자식 엘리먼트 확인하기: findtext() 메서드
book 객체를 리스트로 변환하여 자식 엘리먼트 구하기
book_childs = list(book)
print(book_childs)
*리스트로 변환할 때는 list() 함수 사용
book_child 리스트 각 항목을 name, suthor, year 변수에 할당하고 text 속성으로 엘리먼트에 있는 텍스트 출력
name, author, year = book_childs
print(name.text)
print(author.text)
print(year.text)
*XML은 자식 엘리먼트 순서가 항상 일정하다는 것을 보장하지 않는다.
findtext() 메서드를 사용하면 해당하는 자식 엘리먼트를 탐색하여 자동으로 텍스트를 반환할 수 있다.
name = book.findtext('name')
author = book.findtext('author')
year = book.findtext('year')
print(name)
print(author)
print(year)
두 개의 <book> 엘리먼트가 있을 때
: XML은 배열같은 구조가 없기 때문에 두 개의 <book> 엘리먼트를 감싸는 부모 엘리먼트를 만든다
x2_str = """
<books>
<book>
<name>혼자 공부하는 데이터 분석</name>
<author>박해선</author>
<year>2022</year>
</book>
<book>
<name>혼자 공부하는 머신러닝+딥러닝</name>
<author>박해선</author>
<year>2020</year>
</book>
</books>
"""
fromstring() 사용해 부모 엘리먼트 확인
: x2_str의 부모 엘리먼트는 <books>
books = et.fromstring(x2_str)
print(books.tag)
- 여러 개의 자식 엘리먼트 확인하기: findall() 메서드와 for문
findall() 메서드가 반환하는 자식 엘리먼트 <book>에서 'name', 'author', 'year'를 찾아 출력
for book in books.findall('book'):
name = book.findtext('name')
author = book.findtext('author')
year = book.findtext('year')
print(name)
print(author)
print(year)
print()
XML을 바로 판다스로 바꾸는 방법: read_xml() 함수 사용
pd.read_xml(x2_str)
API로 20대가 가장 좋아하는 도서 찾기
- API를 호출하는 URL 작성하기
- 호출 URL: http://data4library.kr/api/loanItemSrch
- 파라미터
- format: 지정하지 않으면 XML 문서로 반환된다. (여기서는 json으로 지정)
- startDt: 검색 시작 일자. (2021년 4월 1일)
- endDt: 검색 종료 일자. (2021년 4월 30일)
- age: 연령대. (20으로 지정)
- authkey: 인증키. (정부나루 사이트에서 신청)
*조회할 값은 호출 URL 뒤에 파라미터를 뒤에 연결한다.
*파라미터와 값은 = 문자로 연결하고, 파라미터 사이는 & 문자로 연결
*호출 URL과 파라미터는 ? 문자로 연결
→ HTTP GET 방식
http://data4library.kr/api/loanItemSrch?format=json&startDt=2021-04-01&endDt=2021-04-30&age=20&authKey=인증키
- 파이썬으로 API 호출하기: requests 패키지
import requests
http://data4library.kr/api/loanItemSrch?format=json&startDt=2021-04-01&endDt=2021-04-30&age=20&authKey=인증키
URL을 requests.get() 함수에 전달
r = requests.get(url)
*get() 함수가 반환하는 값: API 호출 결과를 담고 있는 requests 패키지의 Response 클래스 객체
json() 메서드: 웹 서버로부터 받은 JSON 문자열을 파이썬 객체로 변환하여 반환
data = r.json()
print(data)
JSON 데이터를 판다스 데이터프레임으로 손쉽게 변환하기 위해 data 딕셔너리의 구조를 바꾼다.
for 문을 사용하여 data['response']['docs']에 매핑된 리스트를 순회하면서 doc 키에 매핑된 딕셔너리를 추출한 후 빈 리스트에 추가
books = []
for d in data['response']['docs']:
books.append(d['doc'])
판다스 DataFrame 클래스에 리스트 넘기기
books_df = pd.DataFrame(books)
books_df
to_json() 메서드: 판다스 데이터프레임을 JSON 파일로 저장
books_df.to_json('20s_best_book.json')
정리
함수/메서드 | 기능 |
json.dumps() | 파이썬 객체를 JSON 문자열로 변환 |
json.loads() | JSON 문자열을 파이썬 객체로 변환 |
pandas.read_json() | JSON 문자열을 판다스 시리즈나 데이터프레임으로 변환 |
xml.etree.ElementTree.fromstring() | XML 문자열을 분석하여 xml.etree.ElementTree.Element 클래스 객체를 반환 |
xml.etree.ElementTree.Element.findtext() | 지정한 태그 이름과 맞는 첫 번째 자식 엘리먼트의 텍스트를 반환 |
xml.etree.ElementTree.Element.findall() | 지정한 태그 이름과 맞는 모든 자식 엘리먼트 반환 |
request.get() | HTTP GET 방식으로 URL 을 호출하고 request.Reaponse 객체 반환 |
request.Reaponse.json() | 응답받은 JSON 문자열을 파이썬 객체로 변환하여 반환 |
02-2 웹 스크래핑 사용하기
웹 스크래핑(웹 크롤링): 프로그램으로 웹사이트의 페이지를 옮겨 가면서 데이터를 추출하는 작업
검색 결과 페이지 가져오기
판다스 데이터프레임으로 불러온 후 head() 메서드로 처음 5개 행 출력
import pandas as pd
books_df = pd.read_json('20s_best_book.json')
books_df.head()
데이터프레임에서 특정 열 선택
: 원하는 열 이름을 리스트로 만들어 데이터프레임의 인덱스처럼 사용
books = books_df[['no','ranking','bookname','authors','publisher','publication_year','isbn13']]
books.head()
- 데이터프레임 행과 열 선택하기: loc 메서드
books_df.loc[[0,1], ['bookname','authors']]
*loc는 대괄호를 사용하여 행의 목록과 열의 목록을 받는다.
books_df.loc[0:1, 'bookname':'authors']
*리스트 대신 슬라이스 연산자(:) 사용 가능
*시작과 끝을 지정하지 않고 슬라이스 연산자를 사용하면 전체를 의미
전체 행과 'no'열에서 'isbn13'열까지 선택하는 코드
books = books_df.loc[:, 'no':'isbn13']
books.head()
- 검색 결과 페이지 HTML 가져오기: requests.get() 함수
requests.get() 함수를 호출할 때 파이썬 문자열의 format() 메서드를 사용해 isbn 변수에 저장된 값을 url 변수에 저장
import requests
isbn = 9791190090018 # '우리가 빛의 속도로 갈 수 없다면'의 ISBN
url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
r = requests.get(url.format(isbn))
requests.get() 함수가 반환한 응답 객체를 사용해 도서 검색 결과 페이지 HTML 출력
print(r.text)
HTML 에서 데이터 추출하기: 뷰티플수프
- 크롬 개발자 도구로 HTML 태그 찾기
: Yes24 웹사이트에서 첫 번째 도서의 ISBN인 [9791190090018] 검색 → 개발자 도구 → HTML에서 상세 페이지 링크 위치 찾기
BeautifulSoup 클래스 임포트
from bs4 import BeautifulSoup
클래스 객체 생성
: 첫 번째 매개변수는 파싱할 HTML 문서이고 두 번째는 파싱에 사용할 파서
soup = BeautifulSoup(r.text, 'html.parser')
*파서: 입력 데이터를 받아 데이터 구조를 만드는 소프트웨어 라이브러리
*링크는 <a> 태그 안에 포함되어 있다.
*<a> 태그는 마우스로 클릭할 수 있는 하이퍼링크를 만드는 태그
- 태그 위치 찾기: find() 메서드
첫 번째 매개변수에는 찾을 태그 이름을 지정하고, attrs 매개변수에는 찾으려는 태그의 속성을 딕셔너리로 지정
ex) soup.find('div', attrs={'id':'search'})는 id 속성이 'search'인 <div> 태그를 찾으라는 의미
prd_link = soup.find('a', attrs={'class':'gd_name'})
*prd_link는 뷰티플수프의 Tag 클래스 객체
print(prd_link)
*태그 안에 포함된 HTML 출력
prd_link를 딕셔너리처럼 사용해 태그 안의 속성 참조 가능
: 링크 주소인 href 속성의 값 알 수 있다.
print(prd_link['href'])
- 도서 상세 페이지 HTML 가져오기
# '우리가 빛의 속도로 갈 수 없다면'의 상세 페이지 가져오기
url = 'http://www.yes24.com'+prd_link['href']
r = requests.get(url)
응답 객체 r을 사용해 가져온 HTML 출력
print(r.text)
개발자 도구를 열어 쪽수가 담긴 HTML 위치 찾기
: 쪽수는 <div id="infoset_specific" class="gd_infoSet infoSet_noLine"> 태그 안에 있다.
→ find() 메서드로 id 속성이 "infoset_specific"인 div 태그 찾기
soup = BeautifulSoup(r.text, 'html.parser')
prd_detail = soup.find('div', attrs={'id':'infoset_specific'})
print(prd_detail)
- 테이블 태그를 리스트로 가져오기: find_all() 메서드
앞서 찾은 <div> 태그 안에 들어 있는 품목정보 테이블에서 '쪽수, 무게, 크기'에 해당하는 행인 <tr> 태그를 찾아 <td> 안에 있는 텍스트 가져오기
prd_tr_list = prd_detail.find_all('tr')
print(prd_tr_list)
*find_all() 메서드를 사용하면 특정 HTML 태그를 모두 찾아서 리스트로 반환해준다.
- 태그 안의 텍스트 가져오기: get_text() 메서드
for 문으로 pre_tr_list를 순회하면서 <th> 태그 안의 텍스트가 '쪽수, 무게, 크기'에 해당하는지 검사
→ 원하는 행을 찾으면 <td> 태그 안에 담긴 텍스트를 page_td 변수에 저장
for tr in prd_tr_list:
if tr.find('th').get_text() == '쪽수, 무게, 크기':
page_td = tr.find('td').get_text()
break
*get_text() 메서드는 태그 안의 텍스트를 반환
ex) tag가 <a href='...'>클릭하세요</a> 태그일 때 tag.get_text()는 '클릭하세요' 반환
print(page_td)
split() 메서드를 호출하여 공백을 기준으로 문자열을 나누어 리스트로 반환
→ 이 리스트에서 첫 번째 원소(쪽수)만 선택해 출력
print(page_td.split()[0])
전체 도서의 쪽수 구하기
: 데이터프레임을 각 행 또는 각 열에 원하는 함수를 자동으로 적용해주는 방법 사용
- 온라인 서점의 검색 결과 페이지 URL을 만든다.
- requests.get() 함수로 검색 결과 페이지의 HTML을 가져온다.
- 뷰티플수프로 HTML을 파싱한다.
- 뷰티플수프의 find() 메서드로 <a>태그를 찾아 상세 페이지 URL을 추출한다.
- requests.get() 함수로 다시 도서 상세 페이지의 HTML을 가져온다.
- 뷰티플수프로 HTML을 파싱한다.
- 뷰티플수프의 find() 메서드로 '품목정보' <div> 태그를 찾는다.
- 뷰티플수프의 find_all() 메서드로 '쪽수'가 들어있는 <tr> 태그를 찾는다.
- 앞에서 찾은 테이블의 행에서 get_text() 메서드로 <td> 태그에 들어 있는 '쪽수'를 가져온다.
→ get_page_cnt() 함수: ISBN 정수 값을 받아 쪽수로 반환
def get_page_cnt(isbn):
# Yes24 도서 검색 페이지 URL
url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
# URL에 ISBN을 넣어 HTML 가져옵니다.
r = requests.get(url.format(isbn))
soup = BeautifulSoup(r.text, 'html.parser') # HTML 파싱
# 검색 결과에서 해당 도서를 선택합니다.
prd_info = soup.find('a', attrs={'class':'gd_name'})
if prd_info == None:
return ''
# 도서 상세 페이지를 가져옵니다.
url = 'http://www.yes24.com'+prd_info['href']
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
# 상품 상세정보 div를 선택합니다.
prd_detail = soup.find('div', attrs={'id':'infoset_specific'})
# 테이블에 있는 tr 태그를 가져옵니다.
prd_tr_list = prd_detail.find_all('tr')
# 쪽수가 들어 있는 th를 찾아 td에 담긴 값을 반환합니다.
for tr in prd_tr_list:
if tr.find('th').get_text() == '쪽수, 무게, 크기':
return tr.find('td').get_text().split()[0]
return ''
get_page_cnt(9791190090018)
- 데이터프레임 행 혹은 열에 함수 적용하기: apply() 메서드
head() 함수로 10개의 행만 가져와 데이터프레임을 만든다.
top10_books = books.head(10)
top10_books의 각 행에 get_page_cnt() 함수를 적용하여 10개 도서의 쪽수를 한 번에 구한다.
def get_page_cnt2(row):
isbn = row['isbn13']
return get_page_cnt(isbn)
*이 함수는 'isbn13'열의 값을 get_page_cnt() 함수로 전달하는 역할
새로만든 get_page_cnt2() 함수를 apply() 메서드에 사용한다.
page_count = top10_books.apply(get_page_cnt2, axis=1)
print(page_count)
*각 행에 함수를 적용해야 하므로 axis 매개변수를 1로 지정
*기본값인 0을 지정하면 각 열에 대해 함수 적용
*각 행에 적용한 get_page_cnt2() 함수의 결과값은 page_count 변수에 판다스 시리즈 객체로 저장
- 함수를 두 번 만들지 않고 더 간결하게 작성하는 방법
: 람다(lamda) 함수 사용
page_count = top10_books.apply(lambda row: get_page_cnt(row['isbn13']), axis=1)
- 데이터프레임과 시리즈 합치기: merge() 함수
name 속성을 사용하여 이름을 간단하게 지정
page_count.name = 'page_count'
print(page_count)
top10_books 데이터프레임과 page_count 시리즈 합치기
top10_with_page_count = pd.merge(top10_books, page_count, left_index=True, right_index=True)
top10_with_page_count
*판다스에서 두 데이터프레임을 합치거나 데이터프레임과 시리즈를 합칠 때 merge() 함수 사용
*첫 번째와 두 번째 매개변수는 합칠 데이터프레임이나 시리즈 객체
*두 객체의 인덱스를 기준으로 합칠 경우 left_index와 right_index 매개변수를 True로 지정
웹 스크래핑할 때 주의할 점
- 웹사이트에서 스크래핑을 허락하였는지 확인: robots.txt 파일
- HTML 태그를 특정할 수 있는지 확인
정리
함수/메서드 | 기능 |
loc | 레이블(이름) 또는 불리언 배열로 데이터프레임의 행과 열을 선택한다. 정수로 지정하면 인덱스의 레이블로 간주한다. 불리언 배열로 지정할 경우 배열의 길이는 행 또는 열의 전체 길이와 같아야 한다. |
BeautifulSoup.find() | 현재 태그 아래의 자식 태그 중에서 지정된 이름에 맞는 첫 번째 태그를 찾는다. 찾은 태그가 없을 경우 None이 반환된다. |
BeautifulSoup.find_all() | 현대 태그 아래의 자식 태그 중에서 지정된 이름에 맞는 모든 태그를 찾는다.뷰티플수프 객체를 함수처럼 호출할 경우 자동으로 find_all() 메서드가 호출된다. 찾은 태그가 없을 경우 빈 리스트가 반환된다. |
BeautifulSoup.get_text() | 태그 안의 텍스트를 반환한다. |
DataFrame.apply() | 데이터프레임의 행 또는 열에 지정한 함수를 적용한다. |
panadas.merge() | 데이터프레임이나 시리즈 객체를 합친다. |
'혼자 공부하는 데이터 분석' 카테고리의 다른 글
6장 복잡한 데이터 표현하기 (0) | 2023.06.08 |
---|---|
5장 데이터 시각화하기 (1) | 2023.05.29 |
4장 데이터 요약하기 (0) | 2023.05.29 |
3장 데이터 정제하기 (0) | 2023.05.22 |
1장 데이터 분석을 시작하며 (0) | 2023.05.12 |
댓글