Exemple #1
0
class Crawler:
    def __init__(self, is_desktop):
        config = configparser.ConfigParser()
        config.read('config.ini')
        self.apk_directory = config.get('Setting', 'APK_DIRECTORY')
        self.is_desktop = is_desktop
        if (not is_desktop):
            self.display = Display(visible=0, size=(800, 600))
            self.display.start()

        self.chrome = webdriver.Chrome(config.get('Setting',\
            'CHROME_DRIVER_DIRECTORY'))
        self.category_list = config.items('PlayStoreURL')
        self.db_connector = DBController(config.get('Setting', 'DB_DIRECTORY'))
        self.db_connector.create_table()

    def __get_new_app_list(self, popular_url):
        """
        카테고리별 인기차트를 Selenium 를 사용해 300개 앱의
        메타정보를 가지고 온다.
        """
        self.chrome.get(popular_url)
        self.chrome.implicitly_wait(10)

        # 해당 페이지를 스크롤해야만 300위까지의 앱이 나타남
        for scroll in (10000, 20000, 30000, 40000, 50000):
            self.chrome.execute_script("window.scrollTo(0," \
                + str(scroll) + ");")
            time.sleep(2)

        package_list = []
        # selector를 사용해 300개의 앱 div를 가져옴
        div_app_list =  self.chrome.find_elements_by_css_selector(\
            ".card.no-rationale.square-cover.apps.small")
        for div_app in div_app_list:
            app_detail = div_app.find_element_by_class_name('details')
            url = app_detail.find_element_by_class_name('title')\
                .get_attribute('href')
            package_name = url.split('id=')[1]
            package_list.append(package_name)

        return package_list

    def __get_app_detail(self, package_list):
        """
        패키지 리스트를 입력으로 받아 앱별로 이름, 이미지소스,\
        업데이트날짜를 크롤링함
        """

        # 앱 상세정보 페이지에 들어가기위한 기본url
        # 뒤에 패키지 이름에 따라서 해당 앱 상세정보 페이지로 이동
        base_url = 'https://play.google.com/store/apps/details?id='
        detail_list = []

        for package in package_list:
            app_url = base_url + package

            self.chrome.get(app_url)
            self.chrome.implicitly_wait(10)

            try:
                name = self.chrome.\
                    find_element_by_css_selector('.id-app-title').text
                img_src = self.chrome.\
                    find_element_by_css_selector('.cover-image').\
                        get_attribute('src')
                updated_date = self.chrome.\
                    find_elements_by_css_selector('.content')[0].text
                ratings = self.chrome.find_elements_by_css_selector(
                    '.rating-count')[0].text
                if ',' in ratings:
                    ratings = ratings.replace(',', '')
            except:
                print(package + " 오류 발생")
                print(package + " name, img_src, update_date 가져오기 실패")
                continue

            # 마지막에 None은 isDownloaded 컬럼에 해당된다.
            detail_list.append([name, package, img_src, updated_date, False])

        return detail_list

    def __download_apk(self, package_name, download_url):
        """
        APK파일을 HTTP request를 통해 다운받는 함수
        리퀘스트를 보내는 도중 에러가 발생하면 False반환
        정상적으로 파일이 저장완료되면 True반환
        """
        file_name = str(package_name) + '.apk'
        try:
            r = requests.get(download_url, timeout=60)
            with open(self.apk_directory + file_name, 'wb') as apk:
                apk.write(r.content)
        except requests.exceptions.Timeout as e:
            print('time out')
            return False
        except Exception as e:
            print(e)
            return False
        return True

    def crawl_new(self):
        # 카레고리별 플레이스토어 인기차트 긁어오기
        for category in self.category_list:
            category_name = category[0]
            url = category[1]

            # Google Play Store를 크롤링하여 최신300개의 앱 메타정보를 가져오기
            new_package_list = self.__get_new_app_list(url)

            # 최신앱 메타정보로 갱신한 리스트를 입력으로 주고
            # 앱별로 상세정보를 크롤링함
            # 이름, 업데이트날짜, 이미지소스
            updated_app_list = self.__get_app_detail(new_package_list)

            # 새로 생긴된 데이터들을 DB에 업데이트
            self.db_connector.update_app(updated_app_list, category_name)

        self.db_connector.commit_n_close()

    def crawl_old(self):
        for category in self.category_list:
            category_name = category[0]
            url = category[1]

            # 기존 DB에 존재하던 카테고리별 패키지 리스트를 가져오기
            old_package_list = self.db_connector\
                .get_old_category_app_list(category)

            # 최신앱 메타정보로 갱신한 리스트를 입력으로 주고
            # 앱별로 상세정보를 크롤링함
            # 이름, 업데이트날짜, 이미지소스
            updated_app_list = self.__get_app_detail(old_package_list)

            # 새로 생긴된 데이터들을 DB에 업데이트
            self.db_connector.update_app(updated_app_list, category_name)
        self.db_connector.commit_n_close()

    def update_apk(self):
        not_updated_list = self.db_connector.not_updated_list()
        print(1)
        for package_row in not_updated_list:
            package_name = package_row[0]
            search_url = 'http://apkpure.com/search?q=' + package_name
            self.chrome.get(search_url)
            self.chrome.implicitly_wait(10)

            # 패키지명으로 검색하여 일치하는 앱 찾기
            search_titles = self.chrome.\
                find_elements_by_class_name('search-title')

            # APK pure사이트에서 검색이 되지 않는 APK는 통과
            if len(search_titles) == 0:
                logging.info(package_name + " is not searched")
                continue

            # 검색결과와 일치하는 앱은 href링크에 패키지 이름이 들어있음
            link = ''
            for title in search_titles:
                link = title.find_element_by_tag_name('a')
                link = link.get_attribute('href')

                if package_name in link:
                    break

            # 검색결과가 여러개 나오지만 일치하지 않는다면 통과
            if link == '':
                logging.info(package_name + ' is not searched in APKpure')
                continue

            print(link)  # debug
            self.chrome.get(link)
            self.chrome.implicitly_wait(10)

            a_list = self.chrome.find_elements_by_class_name(' down')
            try:
                for a in a_list:
                    link = a.get_attribute('href')
                    # href링크에 패키지 이름있는것이 있으면 발견!
                    if package_name in link:
                        self.chrome.get(link)
                        self.chrome.implicitly_wait(10)
                        break
                # 페이지 내부에 iframe을 못찾는 경우가 발생
                # 못찾는다면 해당 APK는 무시하고 다음APK로 이동
                iframe = self.chrome.find_element_by_id('iframe_download')

                src = iframe.get_attribute('src')
            except:
                logging.info(package_name + " does not have href or iframe")
                continue

            if (self.__download_apk(package_name, src)):
                self.db_connector.update_isdownload(package_name, True)
            else:
                self.db_connector.update_isdownload(package_name, False)

        self.db_connector.commit_n_close()

    def close(self):
        self.chrome.stop()
        if (not self.is_desktop):
            self.display.stop()
class Crawler:
    def __init__(self, is_desktop):
        """
        생성자
        is_desktop : 서버환경에서 실행시키는지, 데크스탑환경(GUI)에서 실행시키는지\
                     (true, false)
        """
        # config.ini파일의 변수 가져오기
        config = configparser.ConfigParser()
        config.read('config.ini')
        self.apk_directory = config.get('Setting', 'APK_DIRECTORY')
        os.makedirs(self.apk_directory, exist_ok=True)
        self.is_desktop = is_desktop

        # 서버모드로 실행시켰다면 가상디스플레이 실행
        chrome_options = webdriver.ChromeOptions()
        if (not is_desktop):
            self.display = Display(visible=0, size=(1024, 768))
            self.display.start()
            chrome_options.add_argument('--headless')

        # 크롬 드라이버 실행
        self.chrome = webdriver.Chrome(config.get('Setting',
                                                  'CHROME_DRIVER_DIRECTORY'),
                                       chrome_options=chrome_options)
        self.chrome.set_window_size(1024, 768)
        #self.chrome.set_page_load_timeout(30)

        # 크롤링할 디렉토리 리스트 저장
        self.category_list = config.items('PlayStoreURL')

        # 데이터를 저장하고 제어할 DBController객체 생성
        self.db_connector = DBController(config.get('Setting', 'DB_DIRECTORY'))

        # 메타데이터가 저장될 SQLite 테이블 생성
        self.db_connector.create_table()

    def __get_new_app_list(self, popular_url):
        """
        (private)
        입력받은 인기차트 url의 상위 300개 앱 메타데이터 수집
        popular_url : 특정 카테고리의 인기차트 URL
        """

        # 크롬 드라이버 url 이동 및 완료 대기
        self.chrome.get(popular_url)
        self.chrome.implicitly_wait(10)

        # 해당 페이지를 스크롤해야만 300위까지의 앱이 나타남
        for scroll in (10000, 20000, 30000, 40000, 50000):
            self.chrome.execute_script("window.scrollTo(0," \
                + str(scroll) + ");")
            time.sleep(2)

        package_list = []
        # selector를 사용해 300개의 앱 div를 가져옴
        div_app_list =  self.chrome.find_elements_by_css_selector(\
            ".card.no-rationale.square-cover.apps.small")

        # 300개의 div태그를 반복하면서 패키지 이름을 추출하여 리스트에 저장
        for div_app in div_app_list:
            app_detail = div_app.find_element_by_class_name('details')
            url = app_detail.find_element_by_class_name('title')\
                .get_attribute('href')
            package_name = url.split('id=')[1]
            package_list.append(package_name)

        #return package_list
        return package_list

    def __get_app_detail(self, package_list):
        """
        (priavte)
        패키지 리스트를 입력으로 받아 해당 패키지의 앱 이름, 이미지소스,\
        업데이트날짜, 별점 개수 (ratings)를 크롤링함
        """

        # 앱 상세정보 페이지에 들어가기위한 기본url
        # 뒤에 패키지 이름에 따라서 해당 앱 상세정보 페이지로 이동
        base_url = 'https://play.google.com/store/apps/details?id='
        detail_list = []

        for package in package_list:
            app_url = base_url + package

            # 크롬 드라이버 페이지 이동 및 완료 대기
            self.chrome.get(app_url)
            self.chrome.implicitly_wait(10)

            # 앱 이름, 이미지 소스, 최근 업데이트 날짜, 별점을 조회
            try:
                name = self.chrome.find_element_by_css_selector(
                    'h1[itemprop="name"]').text.strip()
            except:
                name = package
            try:
                img_src = self.chrome.find_element_by_css_selector(
                    'img[alt="Cover art"]').get_attribute('src')
            except:
                img_src = 'https://upload.wikimedia.org/wikipedia/en/4/48/Blank.JPG'
            try:
                updated_date = self.chrome.find_element_by_css_selector(
                    'span[class="htlgb"]').text.strip()
            except:
                updated_date = 'January 1, 2000'
            try:
                ratings = self.chrome.find_element_by_css_selector(
                    'meta[itemprop="ratingValue"]').get_attribute('content')
            except:
                ratings = -1

            # [앱 이름, 패키지 이름, 이미지 소스, 최신업데이트 날짜, 평점, APK다운 여부]
            print('FromPlayStore', name, package, img_src, updated_date,
                  ratings)
            detail_list.append(
                [name, package, img_src, updated_date, ratings, False])
            time.sleep(2)

        return detail_list

    def __download_apk(self, package_name, download_url):
        """
        (private)
        HTTP request를 통해 APK파일을 다운받음
        리퀘스트를 보내는 도중 에러가 발생하면 False반환
        정상적으로 파일이 저장완료되면 True반환
        package_name : 다운받으려는 패키지 이름
        download_url : HTTP request를 날리는 url 이름
        """
        file_name = str(package_name) + '.apk'

        # timout 1분으로 설정하여 반응이 없는 것들은 예외처리
        try:
            r = requests.get(download_url, timeout=60)
            # apk directory에 패키지이름.apk 형태로 저장
            with open(self.apk_directory + file_name, 'wb') as apk:
                apk.write(r.content)
        except requests.exceptions.Timeout as e:
            print('time out')
            return False
        except Exception as e:
            print(e)
            return False
        return True

    def crawl_new(self):
        """
        (public)
        카테고리 별 플레이스토어 인기차트 크롤링 및 DB 저장
        """
        # TODO: list ranomization needed
        for category in self.category_list:
            category_name = category[0]
            url = category[1]

            # 하나의 카테고리 인기차트에서 300개의 앱 패키지 이름 가져오기
            new_package_list = self.__get_new_app_list(url)

            # 카테고리의 300개 앱 패키지 이름으로 300개 앱 상세정보 수집
            # (앱 이름, 최신 업데이트 날짜, 이미지 소스, 레이팅)
            updated_app_list = self.__get_app_detail(new_package_list)

            # 300개의 앱 메타 데이터를 DB에 업데이트
            # 동일한 앱이 존재한다면 그대로 유지
            # 하지만 동일한 앱에도 업데이트가 존재한다면 메타정보 업데이트
            # 앱 이름이 DB에 없다면 새로 추가
            self.db_connector.update_app(updated_app_list, category_name)

        self.db_connector.commit_n_close()

    def crawl_old(self):
        """
        (public)
        기존 DB에 저장된 앱 메타데이터를 최신으로 업데이트
        """
        for category in self.category_list:
            category_name = category[0]
            url = category[1]

            # 기존 DB에 존재하던 카테고리별 패키지 리스트를 가져오기
            old_package_list = self.db_connector.get_old_category_app_list(
                category)

            # 기존 DB 메타데이터의 상세정보를 플레이스토어에서 크롤링
            updated_app_list = self.__get_app_detail(old_package_list)

            # 새로 생긴된 데이터들을 DB에 업데이트
            self.db_connector.update_app(updated_app_list, category_name)
        self.db_connector.commit_n_close()

    def update_apk(self):
        """
        DB에서 다운받지 않은 APK파일을 찾아 APK파일을 다운로드
        """

        # DB에서 아직 다운받지 않은 APK파일의 리스트를 가져옴
        not_updated_list = self.db_connector.not_updated_list()

        for package_row in not_updated_list:
            package_name = package_row[0]
            # apkpure.com에 패키지 이름으로 검색
            search_url = 'http://apkpure.com/search?q=' + package_name
            self.chrome.get(search_url)
            self.chrome.implicitly_wait(10)

            # 일치하는 앱이 검색되었는지 확인
            search_titles = self.chrome.\
                find_elements_by_class_name('search-title')

            # 검색결과가 없으면 apk를 다운받을 수 없으므로 통과
            if len(search_titles) == 0:
                print(package_name + " is not searched")
                continue

            # 검색결과와 일치하는 앱의 href 속성에서 다운로드 링크 추출
            # 검색결과가 여러개일 경우가 있으므로 패키지 이름으로 다시 확인
            link = ''
            for title in search_titles:
                link = title.find_element_by_tag_name('a')
                link = link.get_attribute('href')

                if package_name in link:
                    break

            # 검색결과가 여러개 나오지만 패키지명이 일치하지 않는다면 통과
            if link == '':
                print(package_name + ' is not searched in APKpure')
                continue

            # apk download링크로 이동
            self.chrome.get(link)
            self.chrome.implicitly_wait(10)

            a_list = self.chrome.find_elements_by_class_name(' down')
            try:
                for a in a_list:
                    link = a.get_attribute('href')
                    # href링크에 패키지 이름있는것이 있으면 발견!
                    if package_name in link:
                        self.chrome.get(link)
                        self.chrome.implicitly_wait(10)
                        break
                # 페이지 내부에 iframe을 못찾는 경우가 발생
                # 못찾는다면 해당 APK는 무시하고 다음APK로 이동
                iframe = self.chrome.find_element_by_id('iframe_download')

                src = iframe.get_attribute('src')
            except:
                print(package_name + " does not have href or iframe")
                continue

            # apk 파일 다운로드가 성공하면 db에 True로 저장, 실패시 False로 저장
            if (self.__download_apk(package_name, src)):
                self.db_connector.update_is_downloaded(package_name, True)
                print(package_name, 'downloaded')
            else:
                self.db_connector.update_is_downloaded(package_name, False)
                print(package_name, 'no-downloaded')
            time.sleep(2)

        self.db_connector.commit_n_close()

    def close(self):
        self.chrome.close()
        if (not self.is_desktop):
            self.display.stop()