페이스북을 통해서 포스팅을 하게 되면, 여러 사람들이 볼 수 있고 모바일 앱을 이용해 편하게 사용할 수 있긴 하지만, 내 생각과 자료들이 페이스북 플랫폼에 남게 되고 과거 글은 찾아보기 힘들어 불편한 점이 있습니다.
그래서 주기적으로 페이스북 포스팅들을 블로그에 Archive하기 위해 API를 사용하여 블로그에 저장하는 것을 시도해 보았습니다.
해당 결과물은 Facebook Posting Archive 에서 보실 수 있습니다.
순으로 진행해 보도록 하겠습니다.
본 홈페이지의 플랫폼인 dokuwiki는 PHP 기반으로 만들어졌기 때문에, PHP를 통해서 포스팅을 긁어올 수 있겠지만,
이라는 이유 때문에 데스크톱에서 주기적으로 실행시켜줘야 하지만, Python을 이용하기로 하고,
PHP는 최신 현황만 불러오는 간단한 스크립트를 작성하기로 했다.
이번 편에서는 1-2까지를 기술해 보도록 하겠다.
함수 선언부를 제외하고는 코드 순서대로 설명.
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구조-다음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')
우선 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
엑셀로 저장해서 보면, 컬럼명이 다음과 같이 잘 들어간 것을 볼 수 있다.
데이터들을 사용가능하도록 정리하고, 업로드가 가능한 형태로 수정하는 부분.
본 사이트는 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] + '...'
페이지id_포스팅id
형태로 되어 있기 때문에 포스팅id 부분을 통해서 facebook 링크를 생성
API를 통해서 정보는 original URL이 아닌, Facebook CDN 서버를 통하는 URL로 제공.
이 이미지 URL은 다음의 두 가지 경우가 존재한다.
https://scontent.xx.fbcdn.net
형식은 바로 접근이 가능하기 때문에 URL을 그대로 사용.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 함수
(다음편(이미지 및 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}}"
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
API를 통해서 제공하는 날짜 정보는 모두 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']} > <wrap group> <wrap column> {con['picture'].replace('!!None!!', '(No image)')} </wrap> <wrap column> {con['desc']} {con['link']} </wrap> </wrap> ---- """ 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()
을 설명하도록 하겠다.
또한, 4편(PHP최신현황)에서는, 바로 위 코드의 Line 1에서 ifram으로 가져오는 PHP 페이지에 대한 설명을 해보도록 하겠다.