Dev Blog

스크립트 & 소스코드 공유

실무에서 유용하게 쓰는 스크립트와 코드 조각을 블로그 게시판으로 저장하고 공유합니다.

총 110건의 글

PHP로 인스타그램 해시태그 이미지 파싱해오기

PHP의 cURL 함수로 인스타그램 태그 페이지(explore/tags/태그명/?__a=1)를 요청해 해시태그 게시물의 이미지 정보를 파싱하는 방법이다. 태그명과 가져올 개수를 지정하면 graphql 응답에서 shortcode와 thumbnail_src를 꺼내 원본 이미지 링크를 출력한다. 간단한 파싱 예제 코드를 제공한다.

function scrape_insta_hash($tag) {
$url = 'https://www.instagram.com/explore/tags/'.$tag.'/?__a=1';
$ch = curl_init();
$timeout = 5;
curl_setopt ($ch, CURLOPT_URL, $url);
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
$insta_source = curl_exec($ch);
$insta_array = json_decode($insta_source, TRUE);
return $insta_array;
}
$tag = '공양이'; // 태그 명
$limit = 16; // 가져 올 개수
$results_array = scrape_insta_hash($tag);
$image_array= array();
for ($i=0; $i < $limit; $i++) {
$latest_array = $results_array['graphql']['hashtag']['edge_hashtag_to_media']['edges'][$i]['node'];
$image_data  = '<a href="https://www.instagram.com/p/'.$latest_array['shortcode'].'" target="_blank"><img src="'.$latest_array['thumbnail_src'].'"></a>'; // thumbnail and same sizes
array_push($image_array, $image_data);
}
foreach ($image_array as $image) {
echo $image;
}

한글 종성에 맞는 조사(은/는, 이/가, 을/를) 자동 변환 클래스

UTF-8 한글 문자열에서 마지막 글자의 종성(받침) 유무에 따라 은/는, 이/가, 을/를, 과/와 같은 올바른 조사로 자동 치환해주는 PHP 클래스를 소개한다. 명사가 변수로 치환되는 메시지 조합 시 자연스러운 한국어 문장을 만들 수 있으며, 조사별 매핑과 종성 판단 로직이 담긴 전체 코드를 제공한다.

사용자에게 보여줄 메세지 문자열을 조합하는데,

일부 명사가 변수로 치환될 경우 치환될 단어에 따라 적절한 조사를 붙여줘야 함. (아니면 왈도체를...)

조사 바로 앞의 문자에 받침(종성)이 있냐 없냐에 따라 구분되는데,

UTF-8 이라 가정하고 종성이 있는지 없는지 판단하여 적절한 조사를 치환해주는 간단한 클래스를 만들었음.

// 게임 메세지라 가정하고 치환하는 예제
$name   = '백충덕';
$monster = '고블린';
$item1 = '포션';
$item2 = '엘릭서';

$str = '몬스터 '.$monster.'{가} 나타났다.'."\n";
$str .= $name.'{은} '.$monster.'{를} 물리치고 '.$item1.'{와} '.$item2.'{를} 얻었다.'."\n";
echo Postposition::conv($str);
// 출력
// 몬스터 고블린이 나타났다.
// 백충덕은 고블린을 물리치고 포션과 엘릭서를 얻었다.

// 조사 치환 함수
// 치환대상 조사를 {가},{은} 식으로 감싼 문자을 넘기면 알아서 변환해 줌.
class Postposition
{
// 조사 리스트
// 0,2,4,6 = 종성있음
// 1,3,5,7 = 종성없음
const POSTPOSITION_LIST = '이가은는을를과와';

// 인스턴스
private static $instance = null;

// 문장에서 조사를 적절하게 변환
public static function conv($str)
{
return self::getInstance()->__conv($str);
}

// 인스턴스 반환
private static function getInstance()
{
if (self::$instance === null) self::$instance = new self;

return self::$instance;
}

// 생성자. internal encoding 세팅
private function __construct()
{
mb_internal_encoding('UTF-8');
}

// 조사 변환
private function __conv($str)
{
return preg_replace_callback("/(.)\\{([".self::POSTPOSITION_LIST."])\\}/u", array($this, 'replace'), $str);
}

// preg_replace_callback의 callback함수
private function replace($matches)
{
// 조사 바로 앞 한글자
$lastChar = $matches[1];
// 조사
$postpositionMatched = $matches[2];

// 종성 유/무에 따른 조사배열 세팅
$arrPostposition = array(
'종성없음' => $postpositionMatched,
'종성있음' => $postpositionMatched
);
$pos = mb_strpos(self::POSTPOSITION_LIST, $postpositionMatched);
if ($pos % 2 != 0) {
$arrPostposition['종성있음'] = mb_substr(self::POSTPOSITION_LIST, $pos-1, 1);
} else {
$arrPostposition['종성없음'] = mb_substr(self::POSTPOSITION_LIST, $pos+1, 1);
}

// 기본값 = '종성있음'
$lastCharStatus = '종성있음';

// 2바이트 이상 유니코드 문자
if (strlen($lastChar) > 1) {
$code = $this->utf8Ord($lastChar) - 44032;

// 한글일 경우 (가=0, 힣=11171)
if ($code > -1 && $code < 11172) {
// 초성
//$code / 588
// 중성
//$code % 588 / 28
// 종성
if ($code % 28 == 0) $lastCharStatus = '종성없음';
}
// 1바이트 ASCII
} else {
// 숫자중 2(이),4(사),5(오),9(구)는 종성이 없음
if (strpos('2459', $lastChar) > -1) {
$lastCharStatus = '종성없음';
}
}

// 종성 상태에 알맞는 조사를 붙여 반환
return $lastChar.$arrPostposition[$lastCharStatus];
}

// ord() UTF-8 버전
private function utf8Ord($char)
{
switch (strlen($char)) {
case 1:
return ord($char);
break;
case 2:
return ((ord($char[0]) & 0x1F) << 6) | (ord($char[1]) & 0x3F);
break;
case 3:
return ((ord($char[0]) & 0x0F) << 12) | ((ord($char[1]) & 0x3F) << 6) | (ord($char[2]) & 0x3F);
break;
case 4:
return ((ord($char[0]) & 0x07) << 18) | ((ord($char[1]) & 0x3F) << 12) | ((ord($char[2]) & 0x3F) << 6) | (ord($char[3]) & 0x3F);
break;
default:
return $char;
}
}
}

PHP 함수 - 한글 단어 마지막 글자 받침(종성) 확인

한글 문자열의 마지막 글자에 받침이 있는지 확인하는 PHP 함수 has_batchim()을 소개한다. UTF-8 문자열을 UTF-16BE로 변환해 마지막 글자의 유니코드 코드 포인트를 구하고, '가'의 기준값 44032를 뺀 값을 28로 나눈 나머지가 0보다 크면 받침이 있는 것으로 판별한다. 예시로 깃허브에 '을/를' 조사를 붙이는 사용법을 보여준다.

<?php
// License: Public Domain
//
// 마지막 글자에 받침이 있으면 0보다 큰 정수를 반환하고
// 받침이 없으면 0을 반환한다.
//
// 예:
// $word = '깃허브';
// echo $word . (has_batchim($word) ? '을' : '를');
function has_batchim($str, $charset = 'UTF-8') {
$str = mb_convert_encoding($str, 'UTF-16BE', $charset);
$str = str_split(substr($str, strlen($str) - 2));
$code_point = (ord($str[0]) * 256) + ord($str[1]);
if ($code_point < 44032 || $code_point > 55203) return 0;
return ($code_point - 44032) % 28;
}

해시태그로 인스타그램 데이터 불러오기 (PHP 파싱)

PHP의 cURL과 인스타그램 태그 URL을 이용해 해시태그별 게시물 데이터(본문, 썸네일, 좋아요 수 등)를 가져와 JSON 파일로 저장하고, 그누보드 게시판에서 이를 호출해 화면에 출력하는 전체 소스를 정리했다. 크론 작업으로 1시간마다 데이터를 갱신하는 구조와 여러 게시글의 태그를 관리하는 함수를 포함한다.

function _curl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$contents = curl_exec($ch);
curl_close($ch);
return $contents;

}
function scrape_insta_hash($tags = array(),$debug = false) {
$insta_array = array();
$output = array();
$output["update_date"] = date("Y-m-d H:i:s");
$index = 0;
foreach($tags as $tag){
$url = 'https://www.instagram.com/explore/tags/'.$tag.'/?__a=1';
$insta_source = _curl($url); // instagrame tag url
$limit = 60; // 가져 올 개수
$results_array = json_decode($insta_source, TRUE);

if($debug == true){
return $results_array;
}
for ($i=0; $i < $limit; $i++) {
$latest_array = $results_array['graphql']['hashtag']['edge_hashtag_to_media']['edges'][$i]['node'];
$text=$latest_array["edge_media_to_caption"]["edges"][0]["node"]["text"];
$shortcode=$latest_array['shortcode'];
$thumbnail_src=$latest_array['thumbnail_src'];
$taken_at_timestamp = $latest_array["taken_at_timestamp"];
$edge_media_preview_like = $latest_array["edge_media_preview_like"];
if($thumbnail_src){
$output[$index]["text"] = $text;
$output[$index]["shortcode"] = $shortcode;
$output[$index]["thumbnail_src"] = $thumbnail_src;
$output[$index]["taken_at_timestamp"] = $taken_at_timestamp;
$output[$index]["edge_media_preview_like"] = $edge_media_preview_like;
$index++;
}
}
}
if($index == 0){
return false;
}
return $output;
}

function get_insta_data($wr_id){
if($wr_id){
$output = array();
$insta_json_data = (array)json_decode(_curl("http://".$_SERVER["HTTP_HOST"].'/data/insta/insta_list.'.$wr_id.'.json'));
unset($insta_json_data["update_date"]);
foreach($insta_json_data as $k => $v){
$output[$v->taken_at_timestamp] = (array)$v;
}
krsort($output,SORT_ASC);
return $output;
}
}

function set_insta_data($tags = array(),$file_name,$mode = ""){
global $g5;
$insta_json_data = json_decode(_curl("http://".$_SERVER["HTTP_HOST"].'/data/insta/'.$file_name));
if($mode == "write"){
$insta_json_data = "";
}
$check_date = date("Y-m-d H:i:s",strtotime($insta_json_data->update_date." +1 hours"));
if($check_date < date("Y-m-d H:i:s") || $insta_json_data == ""){
//update
$output = scrape_insta_hash($tags);
if($output != false){
$outputJson = json_encode($output);
file_put_contents($_SERVER["DOCUMENT_ROOT"].'/data/insta/'.$file_name, $outputJson);
}
}
}
function get_sql_set($arr = array()){
$sql_set = "";
foreach($arr as $k => $v){
$sql_set .= " `".$k."` = '".$v."'";
if($index != count($arr)-1){
$sql_set .= ",";
}
$index++;
}
return $sql_set;
}

if(!function_exists('exe_query'))
{
function exe_query($query,$fetchType = 'default'){
if(function_exists('mysqli_query') && G5_MYSQLI_USE) {
$query = preg_replace('/\/\*.*?\*\/(\r\n|\n|\r)?/s', '', $query);
$query = trim($query);
$result = sql_query($query);
$output = array();
$i = 0;
while($row = $fetchType == 'default' || $fetchType == '' ? @mysqli_fetch_assoc($result) : $fetchType($result)){
foreach($row as $key=>$value){
$output[$i][$key] = $value;
}
$i++;
}
}
return $output;
}
}
function update_insta_data($wr_id){
global $g5;
if($wr_id > 0){
$result = exe_query("select * from ".$g5['write_prefix']."m11 where wr_id = '".$wr_id."'");
$tag_list = array();
foreach($result as $row){
$tags=$row["wr_3"];
if($tags){
$tag_arr = explode("#",$tags);
foreach($tag_arr as $tag){
$tag_list[$row["wr_id"]][] = $tag;
}
}
}
foreach($tag_list as $tag_wr_id => $tag_val){
set_insta_data($tag_val,"insta_list.".$tag_wr_id.".json","write");
}
}
}
function crontab_update_insta_data(){
global $g5;
$result = exe_query("select * from ".$g5['write_prefix']."m11");
$tag_list = array();
foreach($result as $row){
$tags=$row["wr_3"];
if($tags){
$tag_arr = explode("#",$tags);
foreach($tag_arr as $tag){
$tag_list[$row["wr_id"]][] = $tag;
}
}
}
foreach($tag_list as $tag_wr_id => $tag_val){
set_insta_data($tag_val,"insta_list.".$tag_wr_id.".json");
}
}

인스타그램 데이타를 호출하는 부분

<div class="mright">
<img src="/img/t_insta.jpg"> <span style="float: right;"><?=$write[wr_3] ?></span>
<?php $insta_data= get_insta_data($wr_id); ?>
<div class="insta_data" >
<? $index = 1; ?>
<ul>
<? foreach($insta_data as $row){ if($index > 15) break; ?>
<? if($row["thumbnail_src"]){ ?>
<li class="hoverEffect">
<a href="https://www.instagram.com/p/<?=$row["shortcode"] ?>" target="_blank">
<img class="card-img-top" src="<?=$row["thumbnail_src"] ?>" class="">
<p class="txt"><i class="fa fa-heart-o" aria-hidden="true"></i> <?=$row["edge_media_preview_like"]->count ?></p>
</a>
</li>
<? $index++;} ?>
<? } ?>
</ul>
</div>
</div>

JavaScript로 동영상을 연속 재생하는 배너 (onended 이벤트)

여러 개의 동영상을 순서대로 연속 재생하는 메인 배너 구현 예제다. video 태그의 onended 이벤트로 현재 영상이 끝나면 다음 영상이 자동 재생되도록 하고, 하단 닷(dot) 버튼을 클릭하면 해당 영상으로 이동해 재생한다. 배너 레이아웃 CSS와 jQuery 제어 코드를 포함한 전체 예제를 제공한다.

<style>
#video-box {min-width:1200px; position:relative;}
#video-box .main-banner {display:none; width:100%; height:750px;  overflow:hidden;position:relative; background-color:#000;}
#video-box .main-banner.open{display:block;}
#video-box .main-banner video {position:absolute;width:100%;top:50%; left:50%;transform:translate(-50%,-50%);border:0;}
#video-box .dot-box {position:absolute; width:100%; bottom:0; text-align:center;}
#video-box .dot-box a{display:inline-block; margin:0.5em; width:20px;height:20px; border-radius:50%; background-color:#06F;text-indent:-9999px;}
#video-box .dot-box a.open{background-color:yellowgreen;}
@media screen and (max-width:1400px) { #video-box .main-banner {height:550px;} }
</style>
<div id="video-box">
<div class="main-banner open">
<video autoplay muted>
<source src="https://www.bodyfriend.co.kr/video/1.mp4" type="video/mp4">
</video>
</div>
<div class="main-banner">
<video muted>
<source src="https://www.bodyfriend.co.kr/video/main02.mp4" type="video/mp4">
</video>
</div>
<div class="main-banner">
<video muted>
<source src="https://www.bodyfriend.co.kr/video/main03.mp4" type="video/mp4">
</video>
</div>
<div class="dot-box"></div>
</div>
<script>
$(function() {
var len = $('.main-banner').length
$('.main-banner').each(function(index, element) {
$(this).addClass('v' + index);
var video = $(this).find('video')[0];
video.onended = function(e) {
$('.main-banner').removeClass('open');
$('.main-banner.v'+((index+1) % len)).addClass('open');
$('.main-banner.v'+((index+1) % len)).find('video')[0].play();
$('#video-box a.dot').removeClass('open');
$('#video-box a.dot.v'+((index+1) % len)).addClass('open');
};
$('#video-box .dot-box').append('<a href="#" class="dot v'+index+'" data-idx="'+index+'">'+(index+1)+'번째 비디오</a>');
});
$('#video-box a.dot').click(function(e) {
var idx = $(this).data('idx');
$('.main-banner.open').find('video')[0].pause();
$('.main-banner').removeClass('open');
$('.main-banner.v'+(idx % len)).addClass('open');
$('.main-banner.v'+(idx % len)).find('video')[0].play();
$('#video-box a.dot').removeClass('open');
$('#video-box a.dot.v'+(idx % len)).addClass('open');
});
$('#video-box a.dot.v0').addClass('open');
});
</script>


Let's create something great together.

프로젝트 문의 및 견적 요청은 언제든 환영합니다.