Web Analytics

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java 오늘의집 베스트 크롤링 Playwright 샘플
    개발 2026. 3. 9. 22:52

    목차

      Java 오늘의집 베스트 크롤링 Playwright 샘플
      Java 오늘의집 베스트 크롤링 Playwright 샘플



      Java와 Playwright를 사용하면 오늘의집 실시간 베스트 상품을 자동으로 수집할 수 있습니다.
      메인 페이지와 스토어 페이지를 차례로 방문하고 베스트 상품 페이지에 접속합니다.
      자연스럽게 스크롤도 한 번 합니다.
      그리고 DOM 구조에 맞는 CSS 셀렉터를 지정해서 데이터를 취득합니다.

      Playwright의 비디오 녹화 기능을 이용해서 브라우저 실행 장면을 녹화합니다.

       


      Playwright

       

      [개발] - Java 이클립스 Gradle 프로젝트 설정 방법

       

      Java 이클립스 Gradle 프로젝트 설정 방법

      Gradle은 프로젝트의 소스 코드를 컴파일하고 필요한 라이브러리를 가져옵니다. 그리고 실행 파일 ( JAR, WAR, APK ) 로 묶어주는 빌드 자동화 도구입니다. 2012년에 나와 현재는 안드로이드 앱 개발의

      crawling.baobtree.com

       

       

      [개발] - Java 이클립스 Maven 프로젝트 설정 방법

       

      Java 이클립스 Maven 프로젝트 설정 방법

      Maven은 자바 프로젝트를 만들 때 필요한 외부 라이브러리들을 알아서 가져옵니다. 그리고 실행 파일까지 만들어주는 자동화 도구이기도 합니다. MavenMaven은 자바 프로젝트를 자동으로 관리 및 빌

      crawling.baobtree.com

       

       

      1. Maven 프로젝트 생성
      IntelliJ나 Eclipse에서 새 Maven 프로젝트를 만듭니다.
      2. pom.xml에 Playwright 의존성 추가합니다.
      https://mvnrepository.com/artifact/com.microsoft.playwright/playwright

       

      Maven Repository: com.microsoft.playwright » playwright

      Java library to automate Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast. This is the main package that provides Playwright client. Overview Versions

      mvnrepository.com

       

      Chromium 브라우저 실행 옵션 설정

      BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
      	// 브라우저 창을 화면에 표시하지 않는 헤드리스 모드
      	.setHeadless(false)
      	// 각 동작 사이에 100ms 지연 ( 안정성 향상 )
      	.setSlowMo(100);



      VPN 접속

      String vpnUrl = VPN_TYPE + "://" + VPN_IP + ":" + VPN_PORT;
      launchOptions.setProxy(new Proxy(vpnUrl));



      브라우저 컨텍스트 옵션 설정 ( 탭 / 세션 단위 설정 )

      BrowserContext context = browser.newContext(
      		new Browser.NewContextOptions()
      			// 일반 사용자처럼 보이도록 User-Agent 설정 (봇 감지 우회)
      			.setUserAgent(userAgent)
      			// 뷰포트 ( 화면 ) 크기 설정
      			.setViewportSize(1280, 800)
      			// 자바스크립트 활성화 여부 ( 기본값 true, 명시적 설정 )
      			.setJavaScriptEnabled(true)
      			// 로케일 설정 ( 한국어 )
      			.setLocale("ko-KR")
      			// 비디오 녹화 활성화 ( videos / 폴더 자동 생성 )
      			.setRecordVideoDir(Paths.get("videos/"))
      			// 비디오 크기 설정
      			.setRecordVideoSize(1280, 800)
      			// HTTP 헤더를 설정
      			.setExtraHTTPHeaders(java.util.Map.of(
      				"Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
      				"Accept",          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
      				"Accept-Encoding", "gzip, deflate, br",
      				"Sec-Fetch-Dest",  "document",
      				"Sec-Fetch-Mode",  "navigate",
      				"Sec-Fetch-Site",  "none",
      				"Sec-Fetch-User",  "?1",
      				"Upgrade-Insecure-Requests", "1"
      			))
      );

       

       


      오늘의집 베스트

      1. 메인 페이지를 방문합니다.
      2. 스토어 메인 페이지를 경유합니다.
      3. 랭킹 페이지를 로딩합니다.
      4. DOM 구조에 맞는 CSS 셀렉터를 사용합니다.
      5. 스크롤을 합니다.
      6. 상품 데이터를 추출합니다.

      반응형
      import java.net.InetSocketAddress;                      // TCP 소켓 연결을 위한 주소 /포 트 표현
      import java.net.Socket;                                 // 저수준 TCP 소켓 연결
      import java.nio.file.Paths;                             // 파일 경로 처리
      import java.util.ArrayList;                             // 동적 리스트
      import java.util.List;                                  // 컬렉션 인터페이스
      import java.util.Random;                                // 무작위 선택
      
      import com.microsoft.playwright.Browser;                // 브라우저 ( Chromium / Firefox / WebKit ) 인스턴스
      import com.microsoft.playwright.BrowserContext;         // 쿠키/스토리지 분리용 컨텍스트
      import com.microsoft.playwright.BrowserType;            // 브라우저 타입 및 옵션
      import com.microsoft.playwright.Page;                   // 실제 탭 ( 페이지 )
      import com.microsoft.playwright.Playwright;             // Playwright 엔진 생성
      import com.microsoft.playwright.PlaywrightException;    // 예외 처리
      import com.microsoft.playwright.Response;               // Response
      import com.microsoft.playwright.options.Proxy;          // 프록시 설정
      import com.microsoft.playwright.options.WaitUntilState; // 언제 성공적으로 완료된 것으로 간주할지 결정
      
      /**
       * 오늘의 집 베스트 크롤링
       * Playwright
       * @since 2026-03-09
       */
      public class OhouRanksPlaywrightrawler {
      
          // URL : 오늘의 집 베스트
          private static final String TARGET_URL  = "https://store.ohou.se/ranks";
          // 무작위
          private static final Random RANDOM      = new Random(); 
      
          // ──────────────────────────────────────────────
          //  VPN 설정 (서버 IP 직접 지정, 인증 없음)
          //
          //  VPN_IP  : VPN 서버 IP 주소
          //  VPN_PORT: VPN 서버 포트
          //    - HTTP  프록시 일반 포트 → 3128, 8080
          //    - SOCKS5 일반 포트       → 1080
          //  VPN_TYPE: "http" 또는 "socks5"
          // ──────────────────────────────────────────────
          private static final boolean USE_VPN  = false;             // VPN 사용 여부 (false = 직접 접속)
          private static final String  VPN_TYPE = "socks5 ";         // "http" 또는 "socks5"
          private static final String  VPN_IP   = "xxx.xxx.xxx.xxx"; // VPN 서버 IP 입력
          private static final int     VPN_PORT = 80;                // VPN 서버 포트 입력
          
          // ──────────────────────────────────────────────
          //  타임아웃 설정 (ms)
          //  VPN 환경은 느리므로 넉넉하게 설정
          // ──────────────────────────────────────────────
          private static final int TIMEOUT_NAVIGATE = 60_000;  // 페이지 이동 60초
          private static final int TIMEOUT_VPN_PING =  5_000;  // VPN 소켓 연결 확인 5초
      
          // ──────────────────────────────────────────────
          //  User-Agent 풀 ( 실제 브라우저 UA 로테이션 )
          // ──────────────────────────────────────────────
          private static final String[] USER_AGENTS = {
              "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
              "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
              "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0",
              "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15"
          };
          
          public static void main(String[] args) {
              // ── 사전 진단: VPN 소켓 연결 확인 ────────────
              if ( USE_VPN ) {
                  System.out.println("=== VPN 연결 진단 ===");
                  boolean vpnReachable = checkTcpConnection(VPN_IP, VPN_PORT, TIMEOUT_VPN_PING);
                  if ( !vpnReachable ) {
                      System.err.println("[FAIL] VPN 서버 " + VPN_IP + ":" + VPN_PORT + " 에 연결할 수 없습니다.");
                      System.err.println("       → VPN_IP / VPN_PORT 값을 확인하세요.");
                      System.err.println("       → USE_VPN = false 로 바꾸면 VPN 없이 실행됩니다.");
                      return;
                  }
                  System.out.println("[OK]  VPN 서버 TCP 연결 성공: " + VPN_IP + ":" + VPN_PORT);
              }
              
              // user Agent
              String userAgent = USER_AGENTS[RANDOM.nextInt(USER_AGENTS.length)];
              
              // try-with-resources로 Playwright 자원 자동 정리
              try ( Playwright playwright = Playwright.create() ) {
                  // Chromium 브라우저 실행 옵션 설정
                  BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
                          // 브라우저 창을 화면에 표시하지 않는 헤드리스 모드
                          .setHeadless(false)
                          // 각 동작 사이에 100ms 지연 ( 안정성 향상 )
                          .setSlowMo(100);
                  
                  // VPN 사용 유무
                  if ( USE_VPN ) {
                      // VPN 접속
                      String vpnUrl = VPN_TYPE + "://" + VPN_IP + ":" + VPN_PORT;
                      launchOptions.setProxy(new Proxy(vpnUrl));
                      System.out.println("VPN 연결 : " + vpnUrl);
                  }
                  
                  // Chromium 브라우저 실행 ( Chrome/Edge 대신 내장 Chromium 사용 )
                  Browser browser = playwright.chromium().launch(launchOptions);
                  
                  // 브라우저 컨텍스트 옵션 설정 ( 탭 / 세션 단위 설정 )
                  BrowserContext context = browser.newContext(
                          new Browser.NewContextOptions()
                              // 일반 사용자처럼 보이도록 User-Agent 설정 (봇 감지 우회)
                              .setUserAgent(userAgent)
                              // 뷰포트 ( 화면 ) 크기 설정
                              .setViewportSize(1280, 800)
                              // 자바스크립트 활성화 여부 ( 기본값 true, 명시적 설정 )
                              .setJavaScriptEnabled(true)
                              // 로케일 설정 ( 한국어 )
                              .setLocale("ko-KR")
                              // 비디오 녹화 활성화 ( videos / 폴더 자동 생성 )
                              .setRecordVideoDir(Paths.get("videos/"))
                              // 비디오 크기 설정
                              .setRecordVideoSize(1280, 800)
                              // HTTP 헤더를 설정
                              .setExtraHTTPHeaders(java.util.Map.of(
                                  "Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
                                  "Accept",          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
                                  "Accept-Encoding", "gzip, deflate, br",
                                  "Sec-Fetch-Dest",  "document",
                                  "Sec-Fetch-Mode",  "navigate",
                                  "Sec-Fetch-Site",  "none",
                                  "Sec-Fetch-User",  "?1",
                                  "Upgrade-Insecure-Requests", "1"
                              ))
                  );
                  
                  // 컨텍스트에서 새 페이지 ( 탭 ) 생성
                  Page page = context.newPage();
                  
                  // ── Stealth: navigator.webdriver = false 로 덮어쓰기 ──
                  // ── Stealth ───────────────────────────────
                  page.addInitScript("""
                      Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
                      Object.defineProperty(navigator, 'plugins',   { get: () => [1,2,3,4,5] });
                      Object.defineProperty(navigator, 'languages', { get: () => ['ko-KR','ko','en-US','en'] });
                      window.chrome = { runtime: {} };
                  """);
                  
                  System.out.println("사용 UA : " + userAgent);
                  
                  try {
                      // ── 1. 메인 페이지 먼저 방문 ( 자연스러운 진입 경로 ) ──
                      System.out.println("메인 페이지 방문 중...");
                      page.navigate("https://ohou.se", new Page.NavigateOptions()
                          .setWaitUntil(WaitUntilState.DOMCONTENTLOADED)
                          .setTimeout(20000)
                      );
                      randomDelay(1500, 3000); // 사람처럼 잠깐 대기
                      
                      // ── 2. 스토어 메인 경유 ──────────────────
                      System.out.println("스토어 메인 경유 중...");
                      page.navigate("https://store.ohou.se", new Page.NavigateOptions()
                          .setWaitUntil(WaitUntilState.DOMCONTENTLOADED)
                          .setTimeout(20000)
                      );
                      randomDelay(1000, 2500);
                      
                      // ── 3. 랭킹 페이지 접근 ──────────────────
                      System.out.println("랭킹 페이지 로딩 중: " + TARGET_URL);
                      page.setExtraHTTPHeaders(java.util.Map.of("Referer", "https://store.ohou.se/"));
                     
                      Response response = null;
                      try {
                          response = page.navigate(TARGET_URL, new Page.NavigateOptions()
                              .setWaitUntil(WaitUntilState.DOMCONTENTLOADED) // NETWORKIDLE → DOMCONTENTLOADED (더 빠름)
                              .setTimeout(TIMEOUT_NAVIGATE)
                          );
                      } catch ( PlaywrightException e ) {
                          System.err.println("페이지 로드 실패: " + e.getMessage().split("\n")[0]);
                          page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("load_screenshot.png")).setFullPage(true));
                      }
                      
                      // HTTP 상태 확인
                      int statusCode = response != null ? response.status() : -1;
                      System.out.println("HTTP 상태: " + statusCode);
                      
                      if ( response.status() == 403 ) {
                          System.err.println("[403] 서버가 접근을 차단했습니다.");
                          System.err.println("      → VPN IP가 차단된 경우 다른 VPN 서버를 사용하세요.");
                          System.err.println("      → setHeadless(false) 로 변경 후 재시도해보세요.");
                          page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get("h403_screenshot.png")).setFullPage(true));
                      }
                      
                      // 동적 콘텐츠 렌더링 대기
                      page.waitForTimeout(2500);
                      
                      // ────────────────────────────────────────
                      //  DOM 덤프 저장 ( 셀렉터 분석용 )
                      //  page_dom.html 을 열어 실제 클래스명 확인
                      // ────────────────────────────────────────
      //                String fullHtml = (String) page.evaluate("() => document.documentElement.outerHTML");
      //                Files.writeString(Paths.get(DOM_DUMP), fullHtml);
      //                System.out.println("DOM 덤프 저장: " + DOM_DUMP + " (" + fullHtml.length() + " chars)");
                         
                      // 4. DOM 구조에 맞는 CSS 셀렉터를 사용해야 함
                      // 크롬 개발자도구 ( F12 ) 로 실제 class 명을 확인 후 수정
                      // 상품 카드 셀렉터 ( 실제 DOM 구조에 따라 조정 필요 )
                      // 상품 목록 대기
                      System.out.println("렌더링 대기 중...");
                      try {
                          page.waitForSelector("article[data-element='ProductCard']",
                              new Page.WaitForSelectorOptions().setTimeout(20_000));
                      } catch ( PlaywrightException e ) {
                          System.out.println("기본 셀렉터 미검출, 전체 구조 확인 중...");
                          
                          // 페이지 구조 디버깅용 출력
                          String bodyText = page.innerHTML("body");
                          System.out.println("페이지 일부 내용: " + bodyText.substring(0, Math.min(500, bodyText.length())));
                      }
                      
                      // 스크롤
                      scrollDown(page, 1);
                      
                      // ── 상품 데이터 추출 ──────────────────────
                      Object detected = page.evaluate("""
                          () => {
                              const selectors = [
                                  '[class*="product-name"]',
                                  '[class*="price"]',
                                  '[class*="product-thumbnail"]',
                                  '[class*="e1bro5mc2"]',
                                  '[class*="etj6rb20"]'
                              ];
                          const found = {};
                          selectors.forEach(sel => {
                              const els = document.querySelectorAll(sel);
                              if (els.length > 0) found[sel] = els.length;
                          });
                          return found;
                          }
                      """);
                      
                      System.out.println("=== 감지된 셀렉터 ===");
                      if ( detected instanceof java.util.Map ) {
                          ((java.util.Map<?,?>) detected).forEach((k, v) ->
                              System.out.println("  " + k + " → " + v + "개")
                          );
                      }
                      
                      // ────────────────────────────────────────
                      //  실제 DOM 기반 상품 추출
                      //  카드:     article[data-element="ProductCard"]
                      //  순위:     span[data-element="Rank"]
                      //  브랜드:   div.product-brand
                      //  상품명:   span.product-name
                      //  가격:     div.price 내 두 번째 <span> 텍스트    ( 할인가 )
                      //  링크:     article > a[href]                     ( 카드 첫 번째 자식 <a> )
                      //  이미지:   img.thumbnail-image                   ( alt="상품-썸네일-이미지" )
                      // ────────────────────────────────────────
                      Object result = page.evaluate("""
                              () => {
                                  const cards = Array.from(
                                      document.querySelectorAll('article[data-element="ProductCard"]')
                                  );
      
                                  return cards.map((card, index) => {
      
                                      // ── 순위 ──
                                      const rankEl = card.querySelector('span[data-element="Rank"]');
                                      const rank   = rankEl ? rankEl.textContent.trim() : String(index + 1);
      
                                      // ── 브랜드 ──
                                      const brandEl = card.querySelector('.product-brand');
                                      const brand   = brandEl ? brandEl.textContent.trim() : '';
      
                                      // ── 상품명 ──
                                      const nameEl = card.querySelector('span.product-name');
                                      const name   = nameEl ? nameEl.textContent.trim() : '';
      
                                      // ── 할인율 ──
                                      const perEl = card.querySelector('[class*="e175igv96"]');
                                      const per   = perEl ? perEl.textContent.trim() : '';
      
                                      // ── 가격 ──
                                      const priceEl = card.querySelector('[class*="e175igv95"]');
                                      const price   = priceEl ? priceEl.textContent.trim() : '';
      
                                      // ── 링크 ──
                                      let link = '';
                                      const anchors = card.querySelectorAll('a[href]');
                                      for (const a of anchors) {
                                          if (a.href.includes('/goods/') || a.href.includes('/productions/')) {
                                              link = a.href;
                                              break;
                                          }
                                      }
                                      // /goods/ 없으면 첫 번째 <a> 사용
                                      if (!link && anchors.length > 0) link = anchors[0].href;
      
                                      // ── 상품 이미지: alt="상품-썸네일-이미지" ──
                                      const imgEl    = card.querySelector('img.thumbnail-image');
                                      const imageUrl = imgEl ? imgEl.src : '';
      
                                      // ── 리뷰 ──
                                      const avgEl   = card.querySelector('strong.avg');
                                      const cntEl   = card.querySelector('span.count');
                                      const review  = avgEl ? avgEl.textContent.trim() : '';
                                      const reviewCount = cntEl ? cntEl.textContent.replace(/[^0-9]/g, '') : '';
      
                                      return { rank, brand, name, per, price, imageUrl, link, review, reviewCount };
                                  }).filter(item => item.name.length > 0);
                              }
                          """);
                      
                      List<ProductItem> products = new ArrayList<>();
                      
                      if ( result instanceof List ) {
                          List<?> rawList = (List<?>) result;
                          System.out.println("추출된 상품 수: " + rawList.size());
                          
                          for ( int i = 0 ; i < rawList.size() ; i++ ) {
                              Object item = rawList.get(i);
                              if ( item instanceof java.util.Map ) {
                                  @SuppressWarnings("unchecked")
                                  java.util.Map<String, Object> map = (java.util.Map<String, Object>) rawList.get(i);
                                  ProductItem p = new ProductItem();
                                  p.rank        = parseString(map.get("rank"),        String.valueOf(i + 1));
                                  p.brand       = parseString(map.get("brand"),       "");
                                  p.name        = parseString(map.get("name"),        "");
                                  p.per         = parseString(map.get("per"),       "");
                                  p.price       = parseString(map.get("price"),       "");
                                  p.imageUrl    = parseString(map.get("imageUrl"),    "");
                                  p.link        = parseString(map.get("link"),        "");
                                  p.review      = parseString(map.get("review"),      "");
                                  p.reviewCount = parseString(map.get("reviewCount"), "");
                                  if ( !p.name.isEmpty() ) {
                                      products.add(p);
                                      System.out.printf("[%s위] %s | %s%n", p.rank, p.name, p.price);
                                  }
                                  
                                  if ( i + 1 == 8 ) break;
                              }
                          }
                      }
                      
                      // 구분선 출력
                      System.out.println("=".repeat(60));
                      
                      for ( ProductItem item : products ) {
                          String rank = item.rank;
                          String name = item.name;
                          String price = item.price;
                          String link = item.link;
                          String img = item.imageUrl;
                          String brand = item.brand;
                          String per = item.per;
                          String review = item.review;
                          String reviewCount = item.reviewCount;
                          
                          System.out.printf("[%s위] %n", rank);
                          System.out.printf("상품   : %s%n", name);
                          System.out.printf("할인율 : %s%n", per);
                          System.out.printf("가격   : %s원%n", price);
                          System.out.printf("링크   : %s%n", link);
                          System.out.printf("이미지 : %s%n", img);
                          System.out.printf("브랜드 : %s%n", brand);
                          System.out.printf("평점   : %s%n", review);
                          System.out.printf("리뷰수 : %s%n", reviewCount);
                          
                          // 상품 간 구분선 출력
                          System.out.println("-".repeat(60));
                      }
                  } catch ( Exception e ) {
                      // 페이지 접속 또는 대기 중 발생한 예외 메시지 출력
                      System.err.println("크롤링 중 오류 발생: " + e.getMessage());
                      
                      // 예외 스택 트레이스 전체 출력 (디버깅용)
                      e.printStackTrace();
                      
                      // 오류 발생 시 페이지 스크린샷 저장 (디버깅용)
                      page.screenshot(new Page.ScreenshotOptions()
                              .setPath(java.nio.file.Paths.get("error_screenshot.png")));
                      System.out.println("스크린샷 저장 완료: error_screenshot.png");
                      
                  } finally {
                      // 브라우저 컨텍스트 종료
                      context.close();
                      
                      // 브라우저 종료
                      browser.close();
                      
                      // 종료 로그 출력
                      System.out.println("브라우저 종료");
                  }
              }
          }
          
          /**
           * TCP 소켓으로 VPN 서버 접근 가능 여부 사전 확인
           * @param host      String
           * @param port      int
           * @param timeoutMs int
           * @return boolean
           */
          private static boolean checkTcpConnection(String host, int port, int timeoutMs) {
              try ( Socket socket = new Socket() ) {
                  socket.connect(new InetSocketAddress(host, port), timeoutMs);
                  return true;
              } catch ( Exception e ) {
                  System.err.println("TCP 연결 실패: " + e.getMessage());
                  return false;
              }
          }
      
          /**
           * 랜덤 딜레이
           * ( 자연스러운 접근 패턴 )
           * @param minMs     int
           * @param maxMs     int
           */
          private static void randomDelay(int minMs, int maxMs) {
              try {
                  Thread.sleep(minMs + RANDOM.nextInt(maxMs - minMs));
              } catch ( InterruptedException e ) {
                  Thread.currentThread().interrupt();
              }
          }
      
          /**
           * Playwright 페이지 스크롤
           * @param page      Page
           * @param times     int
           */
          private static void scrollDown(Page page, int times) {
              for ( int i = 0 ; i < times ; i++ ) {
                  page.mouse().wheel(0, 250);
                  page.waitForTimeout(2000); // 로딩 대기
              }
          }
          
          /**
           * Null-safe 문자열 변환
           * @param v         Object 변환할 객체
           * @param d         String 기본값
           * @return String
           */
          private static String parseString(Object v, String d) {
              return v != null ? v.toString() : d;
          }
          
          static class ProductItem {
              String rank, brand, name, per, price, imageUrl, link, review, reviewCount;
          }
      }

       

      오늘의 집 베스트
      오늘의 집 베스트
      오늘의 집 베스트
      오늘의 집 베스트

       

      Playwright 녹회 기능
      Playwright 녹회 기능

       

       


      요약

      1. 오늘의집 https://ohou.se
      2. 오늘의집 스토어 https://store.ohou.se
      3. 오늘의집 베스트 https://store.ohou.se/ranks
      4. 브랜드 .product-brand
      5. 상품명 span.product-name
      6. 할인율 .e175igv96
      7. 가격 .e175igv95
      8. 이미지 img.thumbnail-image
      9. 평점 strong.avg
      10. 리뷰수 span.count

      반응형
    Designed by Tistory.