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 게시물의 캐러셀 이미지를 모두 추출하도록 개선 +- 작성자의 프로필 이미지 정보 추출 기능 추가 + +**기타** +- 버전 업데이트 및 메타데이터 갱신 --- 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) diff --git a/src/services/scraper/platforms/instagram_scraper.py b/src/services/scraper/platforms/instagram_scraper.py index cf6a364..f39e7eb 100644 --- a/src/services/scraper/platforms/instagram_scraper.py +++ b/src/services/scraper/platforms/instagram_scraper.py @@ -74,27 +74,110 @@ 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: list[str] = [] # 순서 유지를 위해 리스트 사용 + seen_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: + if url not in seen_urls: + seen_urls.add(url) + collected_urls.append(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_author_profile_image(self) -> str | None: + """ + Instagram 작성자 프로필 이미지 URL 추출 + + Returns: + str | None: 작성자 프로필 이미지 URL 또는 None + """ + 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 author_profile_url: + logger.info("작성자 프로필 이미지 URL 추출 완료") + return author_profile_url + async def scrape_instagram_post(self, url: str, classification: UrlClassification) -> dict: """ Instagram 게시글/릴스 스크래핑 @@ -109,16 +192,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 +211,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 +224,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 추출...") + author_profile_image_url = await self.extract_author_profile_image() + return { "platform": classification.platform, "content_type": classification.content_type, @@ -156,7 +243,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, + "author_profile_image_url": author_profile_image_url } except HTTPException: 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"