파이썬/라이브러리(API)

파이썬 자바스크립트 json 웹페이지 크롤링하기

코데방 2024. 3. 21.
728x90

JSON (JavaScript Object Notation)

 

json은 데이터를 속성과 값으로 나열해서 표현하는 경량의 데이터 교환 형식입니다. 자바스크립트 구문을 기반으로 웹 브라우저와 서버 간 데이터 교환에 많이 사용됩니다. 

 

이 교환방식을 사용하는 페이지의 경우 HTML 형태가 아니기 때문에 기존에 사용하던 BeautifulSoup을 이용한 HTML 파싱이 불가능합니다. 따라서 다른 방법을 이용해야합니다. 

 

 

 

웹페이지 자바스크립트 여부 확인하기

 

HTML 크롤링을 하던데로 해보면 내용은 없고 이상한 문구들만 긁어와지는 경우들이 있습니다. 이 경우 자바스크립트와 JSON 형태의 웹페이지라고 볼 수 있습니다.

 

가장 간단한 방법은 브라우저에서 확인하는 방법입니다. 크롬 기준으로 F12를 눌러 개발자 모드를 활성화 한 뒤, 톱니바퀴 설정버튼을 누른 뒤, "Debugger" 항목에서 "Disable JavaScript"를 체크하고 페이지 새로고침을 해보면 됩니다. 

 

 

 

 

 

 

 

 

 

네이버 웹툰에서 실행해보면 잘 뜨던 페이지가 체크 후 아예 안뜨는것을 확인할 수 있습니다. 네이버 웹툰의 경우 거의 대부분이 자바스크립트 JSON 형태로 되어 있기 때문에 기존 BeautifulSoup을 이용한 HTML 파싱으로 크롤링할 수 없습니다. 

 

 

 

 

 

자바스크립트 웹페이지 JSON 실제 주소 찾기

 

개인적으로 JSON 파일을 찾기만 하면 크롤링은 HTML 형태보다 훨씬 쉬운 것 같습니다.

 

먼저 JSON을 담은 URL 주소를 찾아야 합니다. 먼저 F12를 누른 개발자모드에서 "Fetch/XHR" 또는 "JS" 탭에 들어간 뒤 원하는 페이지 또는 버튼을 눌러줍니다. 

 

 

 

 

 

만약 너무 많이 떠있다면 리셋해준 뒤 다시 클릭해주는게 좋습니다. 

 

 

 

 

 

먼저 네이버웹툰 메인 페이지에서 웹툰 이름들과 링크주소를 위한 ID값을 찾아보겠습니다. 

JSON 파일의 경우 단번에 찾을 수는 없고 "Name" 탭에 나오는 항목들을 하나하나 눌러서 "Preview" 탭에서 해당 내용을 확인해줘야합니다.

 

 

 

 

안에 내용이 숨어있는 경우들이 많기 때문에 하나하나 찾아줍니다. 웹툰 아이디와 이름의 경우 아래 항목에서 찾을 수 있습니다.

 

 

 

 

파일을 찾았으면 "Headers" 항목에 가서 해당 JSON의 실제 주소를 가져옵니다. 

 

 

 

 

 

나중에 필요하니 "Referer"과 "User-Agent" 또한 가져와줍니다.

 

 

 

 

 

 

파이썬으로 JSON 데이터 크롤링하기

 

이제 실제 JSON 주소를 찾았으니 파이썬 코드에서 가져와줍니다. HTML을 BeautifulSoup으로 파싱했다면 JSON은 jason 라이브러리를 이용해 파싱할 수 있습니다.

import requests
import json

 

 

 

먼저 requests를 이용해 해당 JSON 주소를 get해줍니다.

 

만약 웹페이지에서 거부돼 에러가 나거나 또는 가져온 내용이 이상할 경우 Headers 옵션에서 위에서 가져온 "Referer"과 "User-agent"값을 넣어줍니다. 딕셔너리 형태로 만들어서 아래와 같이 넣어주면 됩니다. 

headers = {"referer" : "https://comic.naver.com/webtoon",\
           "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"}

url = requests.get("https://comic.naver.com/api/webtoon/titlelist/weekday?order=user", headers = headers)

 

 

 

 

 

딱 봐도 딕셔너리 형태입니다. json 라이브러리를 통해 파이썬의 딕셔너리 자료형으로 변환시켜줍니다. loads() 메소드를 사용하면 됩니다.  

 

파라미터로는 "url.content"를 통해 문자열로 변환된 내용을 넣어주면 되는데, 아래에서 추가로 나오겠지만 만약 내용에 편집이 필요하다면 문자열로 변환해 replace() 등을 통해 작업해준 뒤 해당 문자열 객체를 넣어주면 됩니다. 

dic_webtoon_name = json.loads(url.content)

 

 

 

 

 

 

이제 원하는 값을 꺼내와주면 됩니다. 딕셔너리 안에 리스트가 들어있고 다시 그 안에 딕셔너리가 들어있기 때문에 계층구조를 잘 파악해야합니다.

 

익숙해지면 그냥 보면 되는데 혹시 헷갈리면 크롬의 확장기능 중 "json viewer"를 설치하고 json 주소를 입력해 들어가면 아래와 같이 잘 정리된 json 파일 내용을 볼 수 있습니다.

 

 

 

 

 

이제 자기만의 스타일로 계층 구조를 뽑아줍니다.

일단 가장 상위 KEY로 "titleListMap"이 있고 해당 VALUE 값이 다시 딕셔너리와 리스트로 이루어져 있는 구조입니다.

 

 

 

 

 

 

원하는 웹툰 이름을 입력하면 해당 "titleID" 값을 뽑아오는 코드를 만들어보겠습니다.

먼저 계층 구조를 그려봅니다. 저는 대충 이런식으로 정리합니다.

# titleListMap
    # 요일 -> []
        # titleId
        # titleName

 

 

 

 

구조만 파악되면 간단하게 뽑아올 수 있습니다. 제 최애 웹툰인 나이트런을 찾아보겠습니다.

# 찾을 웹툰이름, titleID 저장
name = "나이트런"
titleId = ""

# 요일별 딕셔너리
days_toon = dic_webtoon_name["titleListMap"]

# 모든 요일 순회하며 name 검색해서 일치하는 값 찾기
week = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]

# 요일 순회
for w in week:
    # 해당 요일 안 리스트 순회 (days_toon[w]가 리스트 객체)
    for i in days_toon[w]:
        if i["titleName"] == name:
            titleId = i["titleId"]
            break

print(name, ":", titleId)

 

 

 

 

 

 

 

파이썬 json.loads() 에러날 경우 대처법

 

위와 같은 방법으로 나이트런 웹툰의 최신화 베스트 댓글을 한 번 가져와보겠습니다. 

위와 동일하게 BEST댓글 버튼 누르고 네트워크 탭의 JS 항목에 뜨는걸 찾아보면 아래와 같은 JSON을 찾을 수 있습니다.

 

 

 

 

 

 

이제 똑같은 방식으로 긁어와봅니다. 물음표 뒤에 오는 쿼리 스트링이 길어서 주소가 매우 길어졌습니다.

headers = {"referer" : "https://comic.naver.com/webtoon/detail?titleId=64997&no=706&week=sat",\
           "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"}


url = requests.get("https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=comic&templateId=webtoon&pool=cbox3&_cv=20240319154009&_callback=jQuery363006303414543543773_1711030196580&lang=ko&country=KR&objectId=64997_706&categoryId=&pageSize=15&indexSize=10&groupId=64997&listType=OBJECT&pageType=more&page=1&currentPage=1&refresh=true&sort=best&_=1711030196584", \
                   headers = headers)

best_review = json.loads(url.content)

 

 

 

 

너무 길어서 나중에 변수 통제가 잘 안될 경우 params 파라미터를 사용해 아래와 같이 사용해주면 가독성이 좋아집니다. 물음표 뒤의 쿼리스트링을 딕셔너리 형태로 바꿔서 params 파라미터로 제공해주는 것입니다. 

headers = {"referer" : "https://comic.naver.com/webtoon/detail?titleId=64997&no=706&week=sat",\
           "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"}

params = {"ticket":"comic",
          "templateId":"webtoon",
          "pool":"cbox3",
          "_cv":"20240319154009",
          "_callback":"jQuery363006303414543543773_1711030196580",
          "lang":"ko",
          "country":"KR",
          "objectId":"64997_706",
          "pageSize":"15",
          "indexSize":"10",
          "groupId":"64997",
          "listType":"OBJECT",
          "pageType":"more",
          "page":"1",
          "currentPage":"1",
          "refresh":"true",
          "sort":"best",
          "_":"1711030196584"}

url = requests.get("https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json", params = params, headers = headers)

 

 

 

 

하지만 json에서 loads()를 하자 "JSONDecodeError : Expecting value: line 1 column 1 (char 0)" 에러가 발생합니다. 

이 에러는 대충 뭔가 내용의 형태가 잘못돼서 디코드를 못했다는 뜻입니다.

 

해당 내용을 확인해보면 앞에 "JQuery~" 어쩌고 하는 이상한 내용이 붙어있습니다. 

 

 

 

가장 뒤에도 중괄호가 아닌 );이 붙어있습니다. 

 

 

 

 

 

json을 제대로 디코드하기 위해서는 중괄호로 시작해서 중괄호로 끝내야합니다.

해결하는 방법은 쓸데없는 부분을 그냥 지워버리면 됩니다. 

 

앞 부분은 replace를 통해 없애버리고 뒷부분은 슬라이싱을 통해 마지막 두 글자를 받아오지 않았습니다.

# 쓸데없는 부분 지우고 중괄호로 시작해서 중괄호로 끝나도록 수정
url = url.text.replace("jQuery363006303414543543773_1711030196580(", "")[:-2]

 

 

 

 

 

다시 json에서 loads하면 이제 제대로 딕셔너리 형태로 변환되는 것을 확인할 수 있습니다.

night = json.loads(url)

 

 

 

 

 

아래는 위 방법을 이용해 원하는 웹툰 이름을 입력한 뒤, 최신화/전체화 여부와 베스트/전체 댓글 여부를 선택해주면 해당 내용을 크롤링해와 csv로 저장까지 해주는 연습 코드입니다. 처음부터 다 긁어오면 시간도 많이 걸리고 IP 차단을 당할 위험도 있기 때문에 최신화 기준 몇 화만 동작하도록 수정했습니다. 

 

원리만 알면 기본에서 응용의 영역입니다~!

# 옵션값에 따라 맞는거 가져와서 csv파일로 저장하기
# param : 웹툰이름, 최신화/전체 옵션, 베스트/전체 댓글 옵션
# return : csv파일 생성
import requests
import json
import pandas as pd

def get_review(name,isRecent,isBest):

    # 웹툰 이름으로 아이디값 및 주소 찾기
    webtoon_url = "https://comic.naver.com/api/webtoon/titlelist/weekday?order=user"
    name_url = requests.get(webtoon_url, headers = headers)
    name_url_info = json.loads(name_url.content)
    week = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]
    name_url_info_root = name_url_info["titleListMap"]
    name_day_idx = 0
    title_id = ""
    for day in week:
        for i in name_url_info_root[day]:
            if i["titleName"] == name:
                name_day_idx = week.index(day)
                title_id = i["titleId"]
    webtoon_url = f"https://comic.naver.com/api/article/list?titleId={title_id}&page=1"

    # 최신화 숫자 찾기
    recent_url = requests.get(webtoon_url)
    recent_url_info = json.loads(recent_url.content)
    endPage = int(recent_url_info["articleList"][0]["no"])

    # 댓글 긁어오기
    startPage = endPage if isRecent else 703
    bestNew = "best" if isBest else "new"
    # i화~끝화까지
    for i in range(startPage, endPage + 1):

        # 댓글 페이지 총 갯수
        page_url = requests.get(f"https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=comic&templateId=webtoon&pool=cbox3&_cv=20240319154009&_callback=jQuery363042627077631448373_1710902334452&lang=ko&country=KR&objectId={title_id}_{i}&categoryId=&pageSize=15&indexSize=10&groupId={title_id}&listType=OBJECT&pageType=more&page=1&currentPage=1&refresh=false&sort={bestNew}&current=465543055&prev=465656137&moreParam.direction=next&moreParam.prev=068fjrdx9vsrn&moreParam.next=068cb5jchh45b&_=1710902334458", headers = headers)
        page_url_info = page_url.text.replace('jQuery363042627077631448373_1710902334452({"success":true,"code":"1000","message":"요청을 성공적으로 처리하였습니다.","lang":"ko","country":"KR",', '{')
        page_resp = page_url_info[:-2]
        page_jsp = json.loads(page_resp)
        dic = page_jsp["result"]
        totalPages = int((dic["pageModel"]["totalPages"]))

        # 리뷰 가져오기
        comment = []
        for p in range(1, totalPages + 1):
            all_review_url = f"https://apis.naver.com/commentBox/cbox/web_naver_list_jsonp.json?ticket=comic&templateId=webtoon&pool=cbox3&_cv=20240319154009&_callback=jQuery363046600479701965636_1710895284718&lang=ko&country=KR&objectId={title_id}_{i}&categoryId=&pageSize=15&indexSize=10&groupId={title_id}&listType=OBJECT&pageType=more&page={p}&currentPage={p}&refresh=true&sort={bestNew}&_=1710835284729"
            url = requests.get(all_review_url, headers = headers)
            resp = url.text.replace('jQuery363046600479701965636_1710895284718({"success":true,"code":"1000","message":"요청을 성공적으로 처리하였습니다.","lang":"ko","country":"KR",', '{')
            resp = resp[:-2]
            jsp = json.loads(resp)


            comments = jsp["result"]["commentList"]

            for c in comments:
                comment.append(c["contents"])

        fileName = "베스트댓글" if isBest else "전체댓글"
        all_comment_df = pd.DataFrame(comment)
        all_comment_df.to_csv(f"{name}_{i}화_{fileName}.csv", encoding = "utf-8")

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글

💲 추천 글