From b4eb5c0a7446d89379a90d82d7930c7cd4ab5279 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 11 Jan 2026 15:34:23 +0000 Subject: [PATCH 1/5] =?UTF-8?q?MapSee-AI=20=EB=B2=84=EC=A0=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20:=20docs=20:=20v0.1.1=20README=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f39c935..dc13973 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MapSee-AI -## 최신 버전 : v0.0.11 (2026-01-11) +## 최신 버전 : v0.1.1 (2026-01-11) [전체 버전 기록 보기](CHANGELOG.md) From d5345b2e6f8ac4c5ce87c835b237908ce02ba9a7 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN <83532821+Cassiiopeia@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:58:08 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=83=80=20url=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=8B=A4=EB=A5=B8=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EC=B6=94=EC=B6=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20:=20feat=20:=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20post=20=EC=9D=B4=EC=99=B8=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=EC=9D=98=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0.=20=EA=B0=81=EA=B0=81=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EC=B6=9C=ED=95=98=EC=97=AC=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=B4=20https://github.com/MapSee-Lab/MapSee-AI/is?= =?UTF-8?q?sues/9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scraper/platforms/instagram_scraper.py | 135 ++++++++++++++---- 1 file changed, 110 insertions(+), 25 deletions(-) diff --git a/src/services/scraper/platforms/instagram_scraper.py b/src/services/scraper/platforms/instagram_scraper.py index cf6a364..c514e10 100644 --- a/src/services/scraper/platforms/instagram_scraper.py +++ b/src/services/scraper/platforms/instagram_scraper.py @@ -74,27 +74,107 @@ def parse_instagram_description(self, description: str) -> dict: async def extract_instagram_image_urls(self) -> list[str]: """ - Instagram 이미지 URL 추출 (cdninstagram.com 도메인만) + Instagram 게시글 이미지 URL 추출 (캐러셀 슬라이드 네비게이션 포함) + + 캐러셀의 경우 Next 버튼을 클릭하며 모든 이미지를 수집합니다. Returns: - list[str]: 이미지 URL 목록 + list[str]: 게시글 이미지 URL 목록 (다른 게시글 썸네일 제외) """ - image_urls = await self.browser_controller.page.evaluate('''() => { - const imgs = document.querySelectorAll('img[src*="cdninstagram.com"]'); - const urls = []; - imgs.forEach(img => { - const src = img.src; - // 프로필 이미지 제외 (보통 작은 크기) - if (src && !src.includes('150x150') && !src.includes('44x44')) { - urls.push(src); - } - }); - // 중복 제거 - return [...new Set(urls)]; + page = self.browser_controller.page + + # 1. 캐러셀 존재 여부 확인 + has_carousel = await page.evaluate('''() => { + return !!document.querySelector('ul._acay'); }''') - logger.info(f"이미지 URL 추출: {len(image_urls)}개") + + if has_carousel: + # 2. 캐러셀: 슬라이드를 넘기며 모든 이미지 수집 + image_urls = await self._extract_carousel_images() + else: + # 3. 단일 이미지: article 내 메인 이미지 추출 + image_urls = await page.evaluate('''() => { + const article = document.querySelector('article'); + if (!article) return []; + + const mainImg = article.querySelector('div._aagv img[src*="cdninstagram.com"]'); + return mainImg && mainImg.src ? [mainImg.src] : []; + }''') + + logger.info(f"게시글 이미지 URL 추출: {len(image_urls)}개") return image_urls + async def _extract_carousel_images(self) -> list[str]: + """ + 캐러셀 슬라이드를 넘기며 모든 이미지 URL 수집 + + Returns: + list[str]: 캐러셀 내 모든 이미지 URL + """ + import asyncio + page = self.browser_controller.page + collected_urls: set[str] = set() + + # 현재 로드된 이미지 수집 함수 + async def collect_current_images(): + urls = await page.evaluate('''() => { + const carousel = document.querySelector('ul._acay'); + if (!carousel) return []; + + const imgs = carousel.querySelectorAll('li._acaz img[src*="cdninstagram.com"]'); + return Array.from(imgs).map(img => img.src).filter(Boolean); + }''') + for url in urls: + collected_urls.add(url) + + # 초기 이미지 수집 + await collect_current_images() + + # 슬라이드 개수 확인 (인디케이터 도트로 확인) + total_slides = await page.evaluate('''() => { + // 캐러셀 인디케이터 도트 개수로 전체 슬라이드 수 확인 + const dots = document.querySelectorAll('div._acnb'); + return dots.length || 1; + }''') + + logger.info(f"캐러셀 슬라이드 개수: {total_slides}") + + # Next 버튼 클릭하며 이미지 수집 + for i in range(total_slides - 1): + # JavaScript로 직접 Next 버튼 클릭 (가려진 요소 무시) + clicked = await page.evaluate('''() => { + const btn = document.querySelector('button[aria-label="Next"]'); + if (btn) { + btn.click(); + return true; + } + return false; + }''') + + if not clicked: + break + + await asyncio.sleep(0.4) # 이미지 로드 대기 + await collect_current_images() + + return list(collected_urls) + + async def extract_instagram_profile_image(self) -> str | None: + """ + Instagram 작성자 프로필 이미지 URL 추출 + + Returns: + str | None: 프로필 이미지 URL 또는 None + """ + profile_url = await self.browser_controller.page.evaluate('''() => { + // 프로필 이미지 셀렉터: alt 속성에 "profile picture" 포함 + const profileImg = document.querySelector('img[alt*="profile picture"]'); + return profileImg ? profileImg.src : null; + }''') + if profile_url: + logger.info(f"프로필 이미지 URL 추출 완료") + return profile_url + async def scrape_instagram_post(self, url: str, classification: UrlClassification) -> dict: """ Instagram 게시글/릴스 스크래핑 @@ -109,16 +189,16 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio Raises: HTTPException: 스크래핑 실패 시 """ - logger.info(f"[1/5] Instagram 스크래핑 시작: {url} (type={classification.content_type})") + logger.info(f"[1/6] Instagram 스크래핑 시작: {url} (type={classification.content_type})") async with async_playwright() as playwright: try: - # [2/5] 브라우저 생성 - logger.info("[2/5] 브라우저 초기화...") + # [2/6] 브라우저 생성 + logger.info("[2/6] 브라우저 초기화...") await self.browser_controller.create_browser_and_context(playwright) - # [3/5] 페이지 로드 - logger.info("[3/5] 페이지 로드...") + # [3/6] 페이지 로드 + logger.info("[3/6] 페이지 로드...") response = await self.browser_controller.load_page(url) if response and response.status >= 400: @@ -128,8 +208,8 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio detail=f"Instagram 응답 오류: {response.status}" ) - # [4/5] 메타데이터 추출 - logger.info("[4/5] 메타데이터 추출...") + # [4/6] 메타데이터 추출 + logger.info("[4/6] 메타데이터 추출...") open_graph_metadata = await self.browser_controller.extract_open_graph_tags() # og:description 파싱 @@ -141,10 +221,14 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio f"likes={parsed_metadata['likes_count']}, comments={parsed_metadata['comments_count']}" ) - # [5/5] 이미지 URL 추출 - logger.info("[5/5] 이미지 URL 추출...") + # [5/6] 이미지 URL 추출 + logger.info("[5/6] 게시글 이미지 URL 추출...") image_urls = await self.extract_instagram_image_urls() + # [6/6] 프로필 이미지 URL 추출 + logger.info("[6/6] 프로필 이미지 URL 추출...") + profile_image_url = await self.extract_instagram_profile_image() + return { "platform": classification.platform, "content_type": classification.content_type, @@ -156,7 +240,8 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio "posted_at": parsed_metadata["posted_at"], "hashtags": parsed_metadata["hashtags"], "og_image": open_graph_metadata.get('image'), - "image_urls": image_urls + "image_urls": image_urls, + "profile_image_url": profile_image_url } except HTTPException: From 7424fb85619fce439926c9a1e37026d095dbefa8 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN <83532821+Cassiiopeia@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:05:30 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=83=80=20url=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=8B=A4=EB=A5=B8=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EC=B6=94=EC=B6=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20:=20feat=20:=20post=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A7=8C=20=EC=B6=94=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(=EC=88=9C=EC=84=9C?= =?UTF-8?q?=EA=B7=B8=EB=8C=80=EB=A1=9C),=20profile=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EA=B0=99=EC=9D=B4=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=ED=95=A0=EC=88=98=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20https://github.com/MapSee-Lab/MapSee-AI/issues/9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scraper/platforms/instagram_scraper.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/services/scraper/platforms/instagram_scraper.py b/src/services/scraper/platforms/instagram_scraper.py index c514e10..f39e7eb 100644 --- a/src/services/scraper/platforms/instagram_scraper.py +++ b/src/services/scraper/platforms/instagram_scraper.py @@ -106,14 +106,15 @@ async def extract_instagram_image_urls(self) -> list[str]: async def _extract_carousel_images(self) -> list[str]: """ - 캐러셀 슬라이드를 넘기며 모든 이미지 URL 수집 + 캐러셀 슬라이드를 넘기며 모든 이미지 URL 수집 (순서 유지) Returns: - list[str]: 캐러셀 내 모든 이미지 URL + list[str]: 캐러셀 내 모든 이미지 URL (게시글 순서대로) """ import asyncio page = self.browser_controller.page - collected_urls: set[str] = set() + collected_urls: list[str] = [] # 순서 유지를 위해 리스트 사용 + seen_urls: set[str] = set() # 중복 체크용 # 현재 로드된 이미지 수집 함수 async def collect_current_images(): @@ -125,7 +126,9 @@ async def collect_current_images(): return Array.from(imgs).map(img => img.src).filter(Boolean); }''') for url in urls: - collected_urls.add(url) + if url not in seen_urls: + seen_urls.add(url) + collected_urls.append(url) # 초기 이미지 수집 await collect_current_images() @@ -159,21 +162,21 @@ async def collect_current_images(): return list(collected_urls) - async def extract_instagram_profile_image(self) -> str | None: + async def extract_author_profile_image(self) -> str | None: """ Instagram 작성자 프로필 이미지 URL 추출 Returns: - str | None: 프로필 이미지 URL 또는 None + str | None: 작성자 프로필 이미지 URL 또는 None """ - profile_url = await self.browser_controller.page.evaluate('''() => { + author_profile_url = await self.browser_controller.page.evaluate('''() => { // 프로필 이미지 셀렉터: alt 속성에 "profile picture" 포함 const profileImg = document.querySelector('img[alt*="profile picture"]'); return profileImg ? profileImg.src : null; }''') - if profile_url: - logger.info(f"프로필 이미지 URL 추출 완료") - return profile_url + if author_profile_url: + logger.info("작성자 프로필 이미지 URL 추출 완료") + return author_profile_url async def scrape_instagram_post(self, url: str, classification: UrlClassification) -> dict: """ @@ -225,9 +228,9 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio logger.info("[5/6] 게시글 이미지 URL 추출...") image_urls = await self.extract_instagram_image_urls() - # [6/6] 프로필 이미지 URL 추출 - logger.info("[6/6] 프로필 이미지 URL 추출...") - profile_image_url = await self.extract_instagram_profile_image() + # [6/6] 작성자 프로필 이미지 URL 추출 + logger.info("[6/6] 작성자 프로필 이미지 URL 추출...") + author_profile_image_url = await self.extract_author_profile_image() return { "platform": classification.platform, @@ -241,7 +244,7 @@ async def scrape_instagram_post(self, url: str, classification: UrlClassificatio "hashtags": parsed_metadata["hashtags"], "og_image": open_graph_metadata.get('image'), "image_urls": image_urls, - "profile_image_url": profile_image_url + "author_profile_image_url": author_profile_image_url } except HTTPException: From 887ae20c242efefd9390621618bf5fe0f4ee114f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 15:05:49 +0000 Subject: [PATCH 4/5] =?UTF-8?q?MapSee-AI=20=EB=B2=84=EC=A0=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=200.1.2=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- version.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.yml b/version.yml index bb940ac..2b7a5ed 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "0.1.1" -version_code: 14 # app build number +version: "0.1.2" +version_code: 15 # app build number project_type: "python" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-01-11 15:32:48" + last_updated: "2026-01-13 15:05:48" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" From b4d5b4df27a14ea026aa36fb3233595617f4a46c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 13 Jan 2026 15:12:41 +0000 Subject: [PATCH 5/5] =?UTF-8?q?MapSee-AI=20=EB=B2=84=EC=A0=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20:=20docs=20:=20v0.1.2=20=EB=A6=B4=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?(PR=20#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.json | 29 ++++++++++++++++++++++++++--- CHANGELOG.md | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.json b/CHANGELOG.json index 147f811..5925822 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,11 +1,34 @@ { "metadata": { - "lastUpdated": "2026-01-11T15:34:03Z", - "currentVersion": "0.1.1", + "lastUpdated": "2026-01-13T15:12:41Z", + "currentVersion": "0.1.2", "projectType": "python", - "totalReleases": 2 + "totalReleases": 3 }, "releases": [ + { + "version": "0.1.2", + "project_type": "python", + "date": "2026-01-13", + "pr_number": 10, + "raw_summary": "## Summary by CodeRabbit\n\n## 릴리스 노트\n\n* **새로운 기능**\n * Instagram 게시물의 캐러셀 이미지를 모두 추출하도록 개선\n * 작성자의 프로필 이미지 정보 추출 기능 추가\n\n* **기타**\n * 버전 업데이트 및 메타데이터 갱신", + "parsed_changes": { + "새로운_기능": { + "title": "새로운 기능", + "items": [ + "Instagram 게시물의 캐러셀 이미지를 모두 추출하도록 개선", + "작성자의 프로필 이미지 정보 추출 기능 추가" + ] + }, + "기타": { + "title": "기타", + "items": [ + "버전 업데이트 및 메타데이터 갱신" + ] + } + }, + "parse_method": "markdown" + }, { "version": "0.1.1", "project_type": "python", diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a11ce9..7411132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,20 @@ # Changelog -**현재 버전:** 0.1.1 -**마지막 업데이트:** 2026-01-11T15:34:03Z +**현재 버전:** 0.1.2 +**마지막 업데이트:** 2026-01-13T15:12:41Z + +--- + +## [0.1.2] - 2026-01-13 + +**PR:** #10 + +**새로운 기능** +- Instagram 게시물의 캐러셀 이미지를 모두 추출하도록 개선 +- 작성자의 프로필 이미지 정보 추출 기능 추가 + +**기타** +- 버전 업데이트 및 메타데이터 갱신 ---