====== Facebook API 포스팅 가져오기 #2 python으로 정리 ====== {{tag>blogs 페이스북API Facebook API 자동화 Python}} {{section>blog:facebook_api_get_posting_1_api#intro &noindent&nolink&nouser&noeditbtn&nocomments¬ags&nodate}} ===== Why Python? ===== 본 홈페이지의 플랫폼인 dokuwiki는 PHP 기반으로 만들어졌기 때문에, PHP를 통해서 포스팅을 긁어올 수 있겠지만, - 저가 웹호스팅을 쓰기 때문에 트래픽 문제가 발생할 수 있음. - 접속할 때마다 데이터를 불러오면 로딩 시간이 늘어남 - 오프라인에도 자료를 저장하고 싶음 - 데이터 정리나 라이브러리 사용이 용이함. 익숙하기 때문에 개발 시간이 단축 이라는 이유 때문에 데스크톱에서 주기적으로 실행시켜줘야 하지만, Python을 이용하기로 하고, PHP는 최신 현황만 불러오는 간단한 스크립트를 작성하기로 했다. - 데이터 호출 및 테이블로 정리 - wiki 문법으로 변환 - 포스팅에 연관된 이미지 다운로드 - FTP를 통한 이미지 업로드 - wiki 문서 수정하기 이번 편에서는 1-2까지를 기술해 보도록 하겠다. 함수 선언부를 제외하고는 코드 순서대로 설명. ===== 데이터 받아오기 ===== ==== API 호출 ==== [[blog:facebook_api_get_posting_1_api|1편(API 사용하기)]] 에서 받아온 cURL 과 액세스 토큰을 통해서 데이터를 받아온다. requests 라이브러리의 get 함수를 사용해서 데이터를 받아서 json을 dict 형식으로 변환한다. (Line 1의 utils 는 nested_json, get_images, UTCtoKST 등 아래에서 설명할 함수들이 포함된 파일) from utils import * import pandas as pd import requests from urllib.parse import unquote import re output = pd.DataFrame() token = "[액세스토큰]" url = f"https://graph.facebook.com/v6.0/100300361542753/posts?fields=created_time%2Cfull_picture%2Cicon%2Cid%2Cmessage%2Cmessage_tags%2Cpicture%2Cattachments.limit(10)%7Burl%2Cmedia%2Cunshimmed_url%2Cmedia_type%2Ctitle%2Cdescription%2Cdescription_tags%2Csubattachments%2Ctype%2Ctarget%7D%2Cshares&limit=100&pretty=0&access_token={token}" resp = requests.get(url=url) json_out = resp.json() output = nested_json(json_out) ==== 다음페이지 호출 ==== [[#json을_DataFrame으로|json구조-다음section]]는 1-depth 에서 data와 paging 으로 구분되어 있고, paging > next 에 다음페이지 URL이 있기 때문에 존재하면 계속 받아오도록 한다. while True: if 'next' in json_out['paging'].keys(): print('===============Next Page==================') url2 = json_out['paging']['next'] resp = requests.get(url=url2) json_out = resp.json() output2 = nested_json(json_out) output = output.append(output2) else: break output.to_excel('facebook.xlsx') ==== json을 DataFrame으로 ==== 우선 Archiving과 추후 페이스북 포스팅 효과 분석 등을 위해서 눈으로 볼 수 있는 DataFrame 형식으로 변환을 하고 싶었다. 하지만 받아오는 json이 아래와 같이 attachments 가 다시 dict 구조로 되어 있기 때문에 일반적인 방법으로는 DataFrame 형식으로 바꾸기 어려워 보였다. json_out = dict({ ㄴdata : [ 첫번째포스팅({ title, created_time, message 등 attachments : dict( { data: [ 첫번째attachment{ ㄴ url ㄴ media ㄴ image : height, width, src ㄴ source ㄴ ... }, 두번째attachment{} ] }) }),두번째포스팅,..] ㄴpaging : ... ㄴ next : 다음페이지 주소 )} 그래서 recursion을 이용해서 nested json 형식을 DataFrame으로 바꿀 수 있도록 11번 라인의 nested_json 함수를 다음과 같이 만들어 보았다. def nested_json(json_out): df_out = pd.DataFrame() #리스트 값 추출 if json_out == list: data = json_out else: data = json_out[next(iter(json_out))] # 한줄씩 Series로 만들어서 DataFrame에 추가한다. for i,_ in enumerate(data): output = nested_json_row(data[i]) df_out = df_out.append(output.to_frame().T, ignore_index=True) return df_out nested_json 에서 데이터의 리스트 element 하나(DataFrame에서 한 행이 될 부분)에 대해서 nested_json_row 를 실행해서 DataFrame을 만들게 된다. nested_json_row 에서는 아래와 같이 dict나 list가 아닌 '값'이 나올 때까지 recursion을 이용해서 column명-값 을 가져오도록 했다. dict일 때는 dict의 값을 컬럼명으로, list일 때는 index를 컬럼명에 추가해서 구분이 가능하도록 하였다. def nested_json_row(dict_data): out = pd.Series() for k,v in dict_data.items(): if type(v) == dict: out_child = nested_json_row(v) out_child.index = k + '_' + out_child.index out = out.append(out_child) elif type(v) == list: for idx,it in enumerate(v): out_child = nested_json_row(it) out_child.index = f"{k}_{idx}_" + out_child.index out = out.append(out_child) else: out[k] = v return out 엑셀로 저장해서 보면, 컬럼명이 다음과 같이 잘 들어간 것을 볼 수 있다. {{blog:pasted:20200218-210029.png?200}} ===== wiki문법으로 변환 ===== 데이터들을 사용가능하도록 정리하고, 업로드가 가능한 형태로 수정하는 부분. 본 사이트는 [[https://www.dokuwiki.org/dokuwiki|dokuwiki]] 를 사용하여 만들었기 때문에, 이 테이블을 wiki 문법으로 바꾸어줘야 했다. 아래 코드에서 wiki 문법을 HTML 이나 Markdown 문법 등으로 수정해서 사용하면 다른 곳에서도 사용할 수 있을 것이다. ==== 기본정보 처리 ==== from tqdm import tqdm output = output.fillna('!!None!!') output['img_base64']='' contents = '' #for loop 시작 for idx,row in tqdm(output.iterrows()): con = { 'title': row['attachments_data_0_title'], 'message': row['message'].replace('!!None!!', ''), 'desc': row['attachments_data_0_description'].replace('!!None!!',''), 'url': "[[https://www.facebook.com/data.triviaz/posts/"+row['id'].split('_')[1]+"|페이스북에서 보기]]", 'picture': row['attachments_data_0_media_image_src'], 'type': row['attachments_data_0_media_type'], } #제목 처리 : 없으면 내용 or No title if con['title'] == '!!None!!': if con['desc'] == '' and con['message'] == '': con['title'] = 'No title' else: con['title'] = con['message'][:20] + '...' * 제목title : 주로 링크를 공유하는 포스팅을 올리기 때문에 attachment에 있는 title을 사용 * Line 15의 제목처리 부분에서 링크 공유가 아닌 경우 내용의 앞 20 글자를 가져오도록 처리 * 내용message : message 컬럼 * Description : 페이스북에서 링크 내용을 자동스크랩해서 보여주는 부분 * url : id는 ''페이지id_포스팅id'' 형태로 되어 있기 때문에 포스팅id 부분을 통해서 facebook 링크를 생성 * picture : 링크 내의 이미지 또는 제가 업로드한 이미지 URL * type : 포스팅 종류 (link, picutre, video 등) ==== Image 처리 ==== API를 통해서 정보는 original URL이 아닌, Facebook CDN 서버를 통하는 URL로 제공. 이 이미지 URL은 다음의 두 가지 경우가 존재한다. - Facebook 내부 이미지(line3) : ''https://scontent.xx.fbcdn.net'' 형식은 바로 접근이 가능하기 때문에 URL을 그대로 사용. - 외부 이미지(line6~) : ''https://external.xx.fbcdn.net/safe_image.php'' 의 ''&url='' 부분이 외부 이미지 URL. 여기에 몇가지 예외 처리를 추가하여 다음과 같이 이미지 URL 을 추출하였다. # image 처리 ################ img_url = '' if 'scontent' in con['picture']: #Facebook 내부 img_url = row['picture'] else: #외부 이미지 if '&url=' in con['picture']: #url 부터 cfs 까지 img_url = con['picture'][con['picture'].index('&url')+5:con['picture'].index('&cfs')] img_url = unquote(img_url) if img_url[-3:] in ['jpg','png'] or 'daum' in img_url: #daum은 확장자 없음 img_url = img_url else: img_url = img_url.split('?')[0] #가끔 ? 붙은게 있음 img_url = img_url.replace('%3A',':').replace('%2F',"/") 그리고 아래와 같이 ''get_images 함수'' ([[blog:facebook_api_get_posting_3_image_ftp_write|다음편(이미지 및 FTP)]]에서 설명)를 호출하여 Archiving을 위한, - 이미지를 다운로드 - 이미지를 HTML에서 바로 사용할 수 있도록 base64로 인코딩하여 DataFrame에 추가 하도록 했다. if img_url != '': img_base64 = get_images(img_url, row['id']) output.loc[idx,'img_base64'] = img_base64 con['picture'] = "{{blogs_facebook_upload:" + row['id']+ ".png?100}}" ==== 기타 처리 ==== * 포스팅의 대표 링크는 페이스북에서 보이는 것처럼 도메인 이름만 추출하도록 하고, * message 내에 있는 링크가 길이가 길 때는 말 줄임표로 줄이도록 하고. * 또한 참고로 dokuwiki문법 적용시 개행을 의미하는 ''가 f-string에서는 적용되지 않기 때문에 따로 처리하였다. con['link'] = "[[" + row['attachments_data_0_unshimmed_url'] + "|"+ row['attachments_data_0_unshimmed_url'].replace("https://","").replace("http://","").split("/")[0] + "]]" #내용 안에 있는 링크는 줄임표로 줄인다 urlfound = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\), ]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', con['message']) for urlf in urlfound: if len(urlf) > 30: con['message'] = con['message'].replace(urlf, f"[[{urlf}|{urlf[:30]}...]]") con['message'] = con['message'].replace('\n', ' ') # SyntaxError: f-string expression part cannot include a backslash ==== 날짜 처리 및 Template 완성 ==== API를 통해서 제공하는 날짜 정보는 모두 [[https://ko.wikipedia.org/wiki/%ED%98%91%EC%A0%95_%EC%84%B8%EA%B3%84%EC%8B%9C|UTC]] 기준으로 되어 있다. 한국은 UTC+9 시간이기 때문에 아래와 같이 timezone 라이브러리를 이용하여 변환해주는 작업이 필요하다. from pytz import timezone import datetime def UTCtoKST(timestr): KST = timezone('Asia/Seoul') #https://blog.kimkevin.net/python-utc-to-kst/ return datetime.datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S%z').astimezone(KST).strftime( '%Y-%m-%d %H:%M:%S') 지금까지 작업을 바탕으로 아래처럼 각 포스팅별로 wiki문법을 적용한 Template을 만들었다. content = f""" === {con['title']} === | {con['type'].upper()} | {UTCtoKST(row['created_time'])} | {con['url']} | {con['message']} > {con['picture'].replace('!!None!!', '(No image)')} {con['desc']} {con['link']} ---- """ contents += content # END of for ################################## 지금까지 for loop 를 통해서 각 포스팅 들을 만들었고, 아래처럼 Header와 Footer 를 추가하고, txt 파일로 저장하기까지 완성 iframe = "{{url>fb_newest.php?date="+max(output['created_time']).replace('+','%2B')+"&format=m/d%20H&front=[최신:%20&mid=%EC%8B%9C%EA%B9%8C%EC%A7%80%20&end=%20%ED%8F%AC%EC%8A%A4%ED%8C%85%EC%9D%B4%20%EB%8D%94%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4]&style=font-size:11pt;font-color:%23333333;font-family:Helvetica,Arial,sans-serif; 100%,30 noscroll noborder left|no iframe error}}" contents = """====== Facebook Posting Archive ====== {{tag> blog Facebook 페이스북 페이지}} """ + f""" > {UTCtoKST(max(output['created_time']))} 까지 총 {len(output)} 개 포스팅 Archived > {iframe} > 최신 포스팅과 더 많은 소식은 [[https://facebook.com/data.triviaz|Data.triviaz]] 좋아요, 팔로잉 해주세요 ---- [[weblog:facebook_api_포스팅_가져오기_1_api사용|API사용,Python데이터정리,PHP최신현황 방법]] ---- """ + contents + """ ~~~DISCUSSION~~~ """ # end of HEADER ################################### f = open('facebook_posting.txt','w+', encoding="utf-8") f.write(contents) f.close() ===== 추후 작업 / 다음 편 ===== [[blog:facebook_api_get_posting_3_image_ftp_write|다음편(이미지 및 FTP)]] 에서 - Image 처리부분에서 호출한 get_images 함수 부분 : Image 다운로드, base64 로 인코딩 - FTP 를 통한 Image 업로드 - txt파일로 저장한 글로 웹 상의 문서 생성/수정 을 설명하도록 하겠다. 또한, [[blog:facebook_api_get_posting_4_php|4편(PHP최신현황)]]에서는, 바로 위 코드의 Line 1에서 ifram으로 가져오는 PHP 페이지에 대한 설명을 해보도록 하겠다. ~~DISCUSSION~~