diff --git a/.gitignore b/.gitignore index 2577e997..be7a0f10 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ **/.idea/** /**/screenshot.png /**/*.log -3. AppWEB_Habr.md \ No newline at end of file diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList.PNG b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList.PNG new file mode 100644 index 00000000..8b2a3728 Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList.PNG differ diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList_CSS.PNG b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList_CSS.PNG new file mode 100644 index 00000000..4180174e Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANOfferList_CSS.PNG differ diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Human.png b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Human.png new file mode 100644 index 00000000..6f268dd3 Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Human.png differ diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Robot.png b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Robot.png new file mode 100644 index 00000000..dae684c2 Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. CIANWhatSee_Robot.png differ diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. MonitoringCIAN_Run_64.py b/Wiki/RUS_Tutorial/WebGUI_Habr/3. MonitoringCIAN_Run_64.py index a4457797..5f649197 100644 --- a/Wiki/RUS_Tutorial/WebGUI_Habr/3. MonitoringCIAN_Run_64.py +++ b/Wiki/RUS_Tutorial/WebGUI_Habr/3. MonitoringCIAN_Run_64.py @@ -5,15 +5,19 @@ import time import re # Regexp to extract info from string import json import datetime +import os +import re +import copy +import logging # Store structure (.json) """ { - "SearchKeyStr": "SPB", - "SearchTitleStr": "Search in SPB, Russia", # Title of the search [str] - "SearchURLStr": "https://spb.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=30.2815_59.9821%2C30.2844_59.9821%2C30.2874_59.9821%2C30.29_59.9821%2C30.293_59.9822%2C30.2957_59.9824%2C30.2984_59.9824%2C30.3019_59.9824%2C30.3048_59.9824%2C30.3074_59.9824%2C30.3088_59.9835%2C30.3085_59.9848%2C30.3065_59.9859%2C30.3049_59.987%2C30.3035_59.9885%2C30.302_59.9897%2C30.2991_59.9902%2C30.2961_59.9904%2C30.2934_59.9903%2C30.2904_59.9898%2C30.2879_59.9893%2C30.2855_59.9888%2C30.2825_59.9882%2C30.2799_59.9879%2C30.2768_59.9874%2C30.2741_59.987%2C30.2716_59.9867%2C30.2688_59.9867%2C30.2657_59.9867%2C30.2626_59.9867%2C30.26_59.9867%2C30.2577_59.986%2C30.2576_59.9846%2C30.2588_59.9834%2C30.2611_59.9827%2C30.2641_59.9822%2C30.2667_59.9819%2C30.2697_59.9819%2C30.2726_59.9816%2C30.2753_59.9815%2C30.2781_59.9818%2C30.2807_59.9823%2C30.2833_59.9823%2C30.2815_59.9821&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str] + "SearchKeyStr": "МСК_Тверской", + "SearchTitleStr": "Москва, район Тверской", # Title of the search [str] + "SearchURLStr": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str] "SearchDatetimeStr": "2020-08-01 09:33:00.838081", # Date of data extraction, [str] "SearchItems": { - "https://spb.cian.ru/sale/flat/777928777/:": { # Item URL with https + "https://www.cian.ru/sale/flat/219924574/:": { # Item URL with https "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] "PriceFloat": 10000000.0, # Price [float] "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] @@ -54,68 +58,115 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +# def to extract list of offers from one job +def OffersByJobExtractDict(inLogger, inWebDriver, inJob): + # BUG 0 - if timeout - retry the job + + # BUG 1 - do mouse scroll to to emulate user activity - cian can hold the robot + # BUG 2 - check the page to retry job offer if page is not next + + # BUG 3 - RE fall on Апартаменты-студия, 85,6 м², 4/8 этаж + + lRetryJobBool = True # Init flag if some error is raised - retry + while lRetryJobBool: + lRetryJobBool = False # Set false until some another action will appear + lResult = copy.deepcopy(inJob) # do copy the structure + lFilterURLStr = lResult["SearchURLStr"] + inWebDriver.get(lFilterURLStr) # Open the URL + lDatetimeNowStr = str(datetime.datetime.now()) + lResult.update({ + "SearchDatetimeStr": lDatetimeNowStr, # Date of data extraction, [str] + "SearchItems": {} # prepare the result + }) + # Get List of the page + lNextPageItemCSS = 'div[data-name="Pagination"] li[class*="active"] + li a' + lNextPageItem = inWebDriver.find_element_by_css_selector(lNextPageItemCSS) + lPageCounterInt = 1 # Init the page counter + while lNextPageItem: + lPageNumberInt = int(inWebDriver.find_element_by_css_selector(css_selector='li[class*="--active--"] span').text) # Get the current page int from web and check with iterator (if not equal - retry all job) + if lPageNumberInt == lPageCounterInt: + lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])' + lOfferList = inWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr) + for lOfferItem in lOfferList: # Processing the item, extract info + lOfferItemInfo = { # Item URL with https + "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] + "PriceFloat": 10000000.0, # Price [float] + "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] + "SqMFloat": 31.4, # Square meters in flat [float] + "FloorCurrentInt": 5, # Current floor [int] + "FloorTotalInt": 8, # Current floor [int] + "RoomCountInt": 3 # Room couint [int] + } + lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text # Extract title text + if inLogger: inLogger.info(f"Старт обработки предложения: {lTitleStr}") + lPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"] > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text # Extract total price + lURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") # Extract offer URL + lOfferItemInfo["TitleStr"] = lTitleStr # set the title + lPriceStr = lPriceStr.replace(" ","").replace("₽","") # Remove some extra symbols + lOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Convert price to the float type + #Check if Апартаменты + if "АПАРТАМЕНТЫ" in lTitleStr.upper(): + lREResult = re.search(r".*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re + lOfferItemInfo["RoomCountInt"] = 1 # Room count + lSqmStr = lREResult.group(1) + lSqmStr= lSqmStr.replace(",",".") + lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count + lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(2)) # Floor current + lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(3)) # Floor total + lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M + else: + lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re + lOfferItemInfo["RoomCountInt"] = int(lREResult.group(1)) # Room count + lSqmStr = lREResult.group(2) + lSqmStr= lSqmStr.replace(",",".") + lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count + lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor current + lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor total + lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M + lResult['SearchItems'][lURLStr] = lOfferItemInfo # Set item in result dict + # Click next page item + lNextPageItem = None + lNextPageList = inWebDriver.find_elements_by_css_selector(lNextPageItemCSS) + if len(lNextPageList)>0: + lNextPageItem = lNextPageList[0] + try: + #lNextPageItem = WebDriverWait(lWebDriver, 10).until(EC.visibility_of_element_located((By.CSS_SELECTOR, 'div[data-name="Pagination"]'))) + #lNextPageItem.click() + inWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""") + except Exception as e: + print(e) + time.sleep(0.5) # some init operations + # wait while preloader is active. If timeout - retry all job + lTimeFromFLoat = time.time() # get current time in float (seconds) + lDoWaitBool = True + while lDoWaitBool: + lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') + if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait + else: lDoWaitBool = False # Stop wait if preloader is dissappear + if (time.time() - lTimeFromFLoat) > 15: # check if timeout is more than 15 seconds + lRetryJobBool = True # Loading error on page - do break, then retry the job + if inLogger: inLogger.warning(f"Ожидание загрузки страницы более {15} с., Робот повторит задание сначала") + break # break the loop + if lRetryJobBool == True: # break the loop if RetryJobBool is true + break + lPageCounterInt = lPageCounterInt + 1 # Increment the page counter + else: + lRetryJobBool = True + if inLogger: inLogger.warning( + f"Следующая страница по списку не была загружена. Была загружена страница: {lPageNumberInt}, Ожидалась страница: {lPageCounterInt}") + if lRetryJobBool == False: # break the loop if RetryJobBool is true + # Check dir - create if not exists + if not os.path.exists(os.path.join('Datasets',lResult['SearchKeyStr'])): + os.makedirs(os.path.join('Datasets',lResult['SearchKeyStr'])) + # Save result in file + lFile = open(f"{os.path.join('Datasets',lResult['SearchKeyStr'],lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_').replace('.','_'))}.json","w",encoding="utf-8") + lFile.write(json.dumps(lResult)) + lFile.close() + # Инициализировать Google Chrome with selenium web driver lWebDriver = WebDriverInit(inWebDriverFullPath = gWebDriverFullPath, inChromeExeFullPath = gChromeExeFullPath, inExtensionFullPathList = gExtensionFullPathList) -lFilterURLStr = "https://spb.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=30.2815_59.9821%2C30.2844_59.9821%2C30.2874_59.9821%2C30.29_59.9821%2C30.293_59.9822%2C30.2957_59.9824%2C30.2984_59.9824%2C30.3019_59.9824%2C30.3048_59.9824%2C30.3074_59.9824%2C30.3088_59.9835%2C30.3085_59.9848%2C30.3065_59.9859%2C30.3049_59.987%2C30.3035_59.9885%2C30.302_59.9897%2C30.2991_59.9902%2C30.2961_59.9904%2C30.2934_59.9903%2C30.2904_59.9898%2C30.2879_59.9893%2C30.2855_59.9888%2C30.2825_59.9882%2C30.2799_59.9879%2C30.2768_59.9874%2C30.2741_59.987%2C30.2716_59.9867%2C30.2688_59.9867%2C30.2657_59.9867%2C30.2626_59.9867%2C30.26_59.9867%2C30.2577_59.986%2C30.2576_59.9846%2C30.2588_59.9834%2C30.2611_59.9827%2C30.2641_59.9822%2C30.2667_59.9819%2C30.2697_59.9819%2C30.2726_59.9816%2C30.2753_59.9815%2C30.2781_59.9818%2C30.2807_59.9823%2C30.2833_59.9823&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1" -lWebDriver.get(lFilterURLStr) # Open the URL -lDatetimeNowStr = str(datetime.datetime.now()) -lResult = { - "SearchKeyStr": "SPB", - "SearchTitleStr": "Search in SPB, Russia", # Title of the search [str] - "SearchURLStr": "https://spb.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=30.2815_59.9821%2C30.2844_59.9821%2C30.2874_59.9821%2C30.29_59.9821%2C30.293_59.9822%2C30.2957_59.9824%2C30.2984_59.9824%2C30.3019_59.9824%2C30.3048_59.9824%2C30.3074_59.9824%2C30.3088_59.9835%2C30.3085_59.9848%2C30.3065_59.9859%2C30.3049_59.987%2C30.3035_59.9885%2C30.302_59.9897%2C30.2991_59.9902%2C30.2961_59.9904%2C30.2934_59.9903%2C30.2904_59.9898%2C30.2879_59.9893%2C30.2855_59.9888%2C30.2825_59.9882%2C30.2799_59.9879%2C30.2768_59.9874%2C30.2741_59.987%2C30.2716_59.9867%2C30.2688_59.9867%2C30.2657_59.9867%2C30.2626_59.9867%2C30.26_59.9867%2C30.2577_59.986%2C30.2576_59.9846%2C30.2588_59.9834%2C30.2611_59.9827%2C30.2641_59.9822%2C30.2667_59.9819%2C30.2697_59.9819%2C30.2726_59.9816%2C30.2753_59.9815%2C30.2781_59.9818%2C30.2807_59.9823%2C30.2833_59.9823%2C30.2815_59.9821&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str] - "SearchDatetimeStr": lDatetimeNowStr, # Date of data extraction, [str] - "SearchItems": {} # prepare the result +lFilterURLStr = "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1" +lJobItem = { + "SearchKeyStr": "МСК_Тверской", + "SearchTitleStr": "Москва, район Тверской", # Title of the search [str] + "SearchURLStr": lFilterURLStr, + # URL of the CIAN search [str] } -# Get List of the page -lNextPageItemCSS = 'div[data-name="Pagination"] li[class*="active"] + li a' -lNextPageItem = lWebDriver.find_element_by_css_selector(lNextPageItemCSS) -while lNextPageItem: - lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])' - lOfferList = lWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr) - for lOfferItem in lOfferList: # Processing the item, extract info - lOfferItemInfo = { # Item URL with https - "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] - "PriceFloat": 10000000.0, # Price [float] - "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] - "SqMFloat": 31.4, # Square meters in flat [float] - "FloorCurrentInt": 5, # Current floor [int] - "FloorTotalInt": 8, # Current floor [int] - "RoomCountInt": 3 # Room couint [int] - } - lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text - lPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"] > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text - lURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") - lOfferItemInfo["TitleStr"] = lTitleStr # set the title - lPriceStr = lPriceStr.replace(" ","").replace("₽","") - lOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Set the price - lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re - lOfferItemInfo["RoomCountInt"] = lREResult.group(1) # Room count - lSqmStr = lREResult.group(2) - lSqmStr= lSqmStr.replace(",",".") - lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count - lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor current - lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor total - lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M - lResult['SearchItems'][lURLStr] = lOfferItemInfo # Set item in result dict - # Click next page item - lNextPageItem = None - lNextPageList = lWebDriver.find_elements_by_css_selector(lNextPageItemCSS) - if len(lNextPageList)>0: - lNextPageItem = lNextPageList[0] - try: - #lNextPageItem = WebDriverWait(lWebDriver, 10).until(EC.visibility_of_element_located((By.CSS_SELECTOR, 'div[data-name="Pagination"]'))) - #lNextPageItem.click() - lWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""") - except Exception as e: - print(e) - time.sleep(0.5) # some init operations - # wait while preloader is active - lDoWaitBool = True - while lDoWaitBool: - lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') - if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait - else: lDoWaitBool = False # Stop wait if preloader is dissappear -# Save result in file -lFile = open(f"{lResult['SearchKeyStr']}_{lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_')}","w",encoding="utf-8") -lFile.write(json.dumps(lResult)) -lFile.close() \ No newline at end of file +OffersByJobExtractDict(inLogger = logging, inWebDriver = lWebDriver, inJob = lJobItem) \ No newline at end of file diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. PortableGoogleChrome_Main_DevTools.PNG b/Wiki/RUS_Tutorial/WebGUI_Habr/3. PortableGoogleChrome_Main_DevTools.PNG new file mode 100644 index 00000000..685fdcfc Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. PortableGoogleChrome_Main_DevTools.PNG differ diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. WebGUI_Habr.md b/Wiki/RUS_Tutorial/WebGUI_Habr/3. WebGUI_Habr.md new file mode 100644 index 00000000..1258998a --- /dev/null +++ b/Wiki/RUS_Tutorial/WebGUI_Habr/3. WebGUI_Habr.md @@ -0,0 +1,503 @@ +Долгожданный туториал по управлению сторонними WEB приложениями с помощью pyOpenRPA. Во 2-й части мы разберем принципы роботизированного воздействия на HTML/JS. А также своими руками сделаем небольшого, но очень показательного робота. + +Этот робот будет полезен тем, для кого актуальна тема покупки/продажи недвижимости. + +![pyOpenRPA туториал. Управление WEB приложениями](https://habrastorage.org/webt/gt/rq/mo/gtrqmo2cvzjaajye98dlpvxo-_u.png) + +# Для тех, кто с нами впервые +[pyOpenRPA](https://gitlab.com/UnicodeLabs/OpenRPA) - это open source [RPA платформа](https://ru.wikipedia.org/wiki/Robotic_process_automation), которая **в полной мере позволяет заменить топовые коммерческие RPA платформы**. + +Подробнее про то, чем же она полезна, можно [почитать здесь](https://habr.com/ru/post/506766/). + +# Навигация по туториалам pyOpenRPA +Туториал сверстан в виде серии статей, в которых будут освещаться ключевые технологии, необходимые для RPA. + +Освоив эти технологии, у вас появится возможность углубиться в специфику той задачи, которая поставлена перед вами. + +**Перечень статей-туториалов (опубликованные и планируемые):** +- [Отказываемся от платных RPA платформ и базируемся на OpenSource (pyOpenRPA)](https://habr.com/ru/post/506766/) +- [pyOpenRPA туториал. Управление оконными GUI приложениями](https://habr.com/ru/post/509644/) +- \>> [pyOpenRPA туториал. Управление WEB приложениями (то, что мы смотрим в Chrome, Firefox, Opera)](https://habr.com/ru/post/515310/) +- pyOpenRPA туториал. Управление клавиатурой & мышью +- pyOpenRPA туториал. Распознавание графических объектов на экране + +А теперь перейдем к самому туториалу. + + + +# Немного теории и терминов + +[Из википедии] + +Веб-приложение — клиент-серверное приложение, в котором клиент взаимодействует с веб-сервером при помощи браузера. Логика веб-приложения распределена между сервером и клиентом, хранение данных осуществляется, преимущественно, на сервере, обмен информацией происходит по сети. Одним из преимуществ такого подхода является тот факт, что клиенты не зависят от конкретной операционной системы пользователя, поэтому веб-приложения являются межплатформенными службами. + +Веб приложения стали широко использоваться в конце 1990-х — начале 2000-х годов. + +[Ссылка на источник](https://ru.wikipedia.org/wiki/%D0%92%D0%B5%D0%B1-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5) + +Ок, с выдержкой из вики все #КрутоУмно, но от этого не легче (для тех, кто в этой теме дилетант). Продемонстрирую устройство WEB приложения на примере "Что видим мы?"/"Что видит робот?". Для этого отправимся на сайт одной известной WEB площадки по объявлениям по недвижимости + +## Что видим мы? +Мы видим красиво сверстанный сайт с интуитивно понятным интерфейсом, на котором можно найти интересные объявления о продаже/сдаче в аренду недвижимости. + +![Как мы видим WEB приложение](https://habrastorage.org/webt/af/sf/ku/afsfkuhlchm-prpeh_-hzrachmw.png) + +## Что видит робот? +Робот видит огромную гипертекстовую разметку [HTML](https://ru.wikipedia.org/wiki/HTML) с примесью алгоритмического кода [JS](https://ru.wikipedia.org/wiki/JavaScript) и завернутого в каскадную таблицу стилей [CSS](https://ru.wikipedia.org/wiki/CSS). Увлекательно, правда? :) + +![Как робот видит WEB приложение](https://habrastorage.org/webt/gz/15/hy/gz15hycrdih3s9paxjsuocsnq6e.png) + +## Интерпретация +WEB приложения - это **один из самых легко роботизируемых классов приложений**. Обилие инструментов + технологий позволяют реализовывать практически любую поставленную задачу в кооперации с ними. + +Управлять WEB страницей можно с помощью разных технологий адресации: CSS, XPath, id, class, attribute. Мы будем взаимодействовать со страницей с помощью CSS селекторов. + + + +# (По шагам) робот своими руками +В этом туториале мы будем заниматься разработкой робота, который оперирует на одном из самых популярных порталов по объявлениям по недвижимости в РФ (тема одна из актуальных для многих). + +В качестве примера поставим себе следующую задачу: **Разработать робота, который будет извлекать список всех объявлений по ранее преднастроенному фильтру**. Все извлеченные объявления сохранить как датасет в файл .json со следующей структурой: +``` +{ + "SearchKeyStr": "МСК_Тверской", # Ключевое слово поиска + "SearchTitleStr": "Москва, район Тверской", # Заголовок поиска + "SearchURLStr": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str] + "SearchDatetimeStr": "2020-08-01 09:33:00.838081", # Дата, на которую была сформирована выгрузка + "SearchItems": { # Перечень извлеченных ценовых объявлений + "https://www.cian.ru/sale/flat/219924574/:": { # URL ссылка на ценовое объявление + "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Заголовок ценового объявления + "PriceFloat": 10000000.0, # Стоимость общая + "PriceSqmFloat": 133333.0, # Стоимость на 1 кв. м. + "SqMFloat": 31.4, # Кол-во кв. м. + "FloorCurrentInt": 5, # Этаж лота по объявлению + "FloorTotalInt": 8, # Этажей в доме всего + "RoomCountInt": 3 # Кол-во комнат + } + } + +} +``` + + + +## Шаг 0. Подготовим проект для нового робота (развернем pyOpenRPA) +В отличии от подавляющего большинства RPA платформ, в pyOpenRPA реализован принципиально иной подход по подключению к проекту, а именно: если в них структуру проекта определяет сама RPA платформа, то в pyOpenRPA структуру проекта определяете Вы и только Вы. Это дает больше гибкости и возможности по использованию этой RPA технологии в других направлениях (использовать pyOpenRPA как обычную библиотеку Python). + +**Доступно несколько вариантов загрузки [pyOpenRPA](https://gitlab.com/UnicodeLabs/OpenRPA):** +- Вариант 1, простой. Скачать преднастроенную портативную версию с [GitLab страницы проекта](https://gitlab.com/UnicodeLabs/OpenRPA) +- Вариант 2, сложный. Установить pyOpenRPA в свою версию интерпретатора Python 3 (pip install pyOpenRPA) + +Я рекомендую воспользоваться простым вариантом (вариант 1). Преднастроенная версия не требуется каких-либо настроек инфраструктуры. Здесь в лучших традициях pyOpenRPA реализован принцип, когда пользователь скачивает репозиторий, и у него уже все настроено из коробки - пользователю остается лишь писать скрипт робота. #Enjoy :) + + +## Шаг 1. Создать проект робота +Для того, чтобы начать проект робота, необходимо создать папку проекта. В дальнейшем я затрону тему организации папок проектов для промышленных программных роботов. Но на текущий момент не буду заострять внимание на этом, чтобы сконцентрироваться непосредственно на основном - на логике работы с WEB страницами. + +**Ниже приведу зависимости проекта от сторонних компонентов:** +- Selenium WebDriver +- Google Chrome или Mozilla Firefox или Internet Explorer +- Python 3 + +Если вы пошли по варианту 1 (см. шаг 0), то у Вас все эти компоненты уже будут развернуты и настроены внутри скачанного репозитория pyOpenRPA (#Удобно). Репозиторий pyOpenRPA уже содержит все необходимые portable версии требуемых программ (Google Chrome, Mozilla Firefox, Python3 32|64 и т.д.). + +Вы наверняка заметили, что в pyOpenRPA используется [Selenium](https://habr.com/ru/post/152653/). Этот компонент является одним из лучших отказоустойчивых компонентов по внедрению в WEB. Именно поэтому мы его и будем использовать в pyOpenRPA. + +**Создадим следующую структуру проекта:** +- Репозиторий pyOpenRPA > Wiki > RUS_Tutorial > WebGUI_Habr: + - Файл "3. MonitoringCIAN_Run_64.py" - скрипт робота, который мониторит WEB площадку + - Файл "3. MonitoringCIAN_Run_64.cmd" - скрипт запуска робота с 1-го клика по аналогии с .exe файлами + +**Ниже приведу пример "3. MonitoringCIAN_Run_64.cmd" файла:** +``` +cd %~dp0..\..\..\Sources +..\Resources\WPy64-3720\python-3.7.2.amd64\python.exe "..\Wiki\RUS_Tutorial\WebGUI_Habr\3. MonitoringCIAN_Run_64.py" +pause >nul +``` + +**Для инициализации Selenium WebDriver воспользуемся следующей функцией:** + +``` +########################## +# Init the Chrome web driver +########################### +def WebDriverInit(inWebDriverFullPath, inChromeExeFullPath, inExtensionFullPathList): + # Set full path to exe of the chrome + lWebDriverChromeOptionsInstance = webdriver.ChromeOptions() + lWebDriverChromeOptionsInstance.binary_location = inChromeExeFullPath + # Add extensions + for lExtensionItemFullPath in inExtensionFullPathList: + lWebDriverChromeOptionsInstance.add_extension (lExtensionItemFullPath) + # Run chrome instance + lWebDriverInstance = None + if inWebDriverFullPath: + # Run with specified web driver path + lWebDriverInstance = webdriver.Chrome(executable_path = inWebDriverFullPath, options=lWebDriverChromeOptionsInstance) + else: + lWebDriverInstance = webdriver.Chrome(options = lWebDriverChromeOptionsInstance) + # Return the result + return lWebDriverInstance +``` + + +## Шаг 2. Запустить WEB инструменты разработчика и сформировать CSS селекторы +В нашем случае WEB инструменты разработчика мы будем использовать из Google Chrome, который предустановлен в репозитории pyOpenRPA (вариант 1 из шага 0). + +**Откроем Google Chrome и инструменты разработчика** (pyOpenRPA repo\\Resources\GoogleChromePortable\App\Chrome-bin\chrome.exe, после чего Ctrl + Shift + i) + ![Portable Google Chrome + Dev Tools](https://habrastorage.org/webt/2p/nq/pc/2pnqpcdew_gkdrsvc2rhya1wc8k.png) + +**Откроем в браузере сайт, который мы будем анализировать. Сформируем область поиска и отобразить обнаруженные ценовые предложения в виде списка.** + + [Пример поискового запроса](https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1) + +![Список ценовых предложений по фильтру](https://habrastorage.org/webt/qc/sp/po/qcspporaxqux_kcimoncngnjtcg.png) + +Для того, чтобы подобрать CSS селектор нам помогут инструменты разработчика Google Chrome. Подробнее узнать про устройство CSS селекторов можно [здесь по ссылке](https://learn.javascript.ru/css-selectors) +Для проверки правильности CSS селектора я буду делать следующую проверку в инструментах разработчика на вкладке "Console". На картинке представлен пример того, как проводится проверки правильности CSS селектора для извлечения списка ценовых предложений. + +![Пример проверки CSS селектора](https://habrastorage.org/webt/o-/zy/x9/o-zyx9fqur5fq6mu1xedb7srskg.png) + +**Подберем CSS селектор для выборки списка ценовых предложений на странице.** + +При составлении селектора выяснилось, что в список объявлений встроены рекламные баннеры, которые не содержат информацию о ценовом предложении. + +И таких видов рекламных баннеров было обнаружено несколько видов: +- div[data-name="BannerServicePlaceInternal"] +- div[data-name="getBannerMarkup"] +- div[data-name="AdFoxBannerTracker"] + +В связи с этим CSS селектор должен быть скорректирован таким образом, чтобы исключить из выборки такие виды баннеров. Ниже приведен готовый CSS селектор. + +- CSS селектор, Список ценовых предложений: div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"]) + +**Подберем CSS селекторы по извлечению параметров ценового предложения: Заголовок, Стоимость общая, URL ссылка на карточку.** + +- CSS селектор, Заголовок: div[data-name="TopTitle"],div[data-name="Title"] +- CSS селектор, Стоимость общая: div[data-name="Price"] > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"] +- CSS селектор, URL ссылка на карточку: a[class*="--header--"] + +**Подберем CSS селектор для извлечения кнопки на следующую страницу.** + +- CSS селектор, Указатель на следующую страницу: div[data-name="Pagination"] li[class*="active"] + li a + + + +## Шаг 3. Обработать/преобразовать получаемые данные +На предыдущем шаге мы успешно подобрали все необходимые CSS селекторы. Теперь нам нужно грамотно извлечь информацию, а потом и обработать ее. + +В результате обработки ценового предложения у нас будет сформирована структура следующего вида: +``` +lOfferItemInfo = { # Item URL with https + "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] + "PriceFloat": 10000000.0, # Price [float] + "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] + "SqMFloat": 31.4, # Square meters in flat [float] + "FloorCurrentInt": 5, # Current floor [int] + "FloorTotalInt": 8, # Current floor [int] + "RoomCountInt": 3 # Room couint [int] +} +``` +**Для начала получим список элементов ценовых предложений.** +``` +lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])' +lOfferList = inWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr) +``` + +**Далее циклическая обработка каждого ценового предложения.** +``` +for lOfferItem in lOfferList: +``` + +**Извлечем параметры из WEB страницы: Заголовок, Стоимость общая, URL на карточку.** +``` +lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text # Extract title text +lPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"] > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text # Extract total price +lURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") # Extract offer URL +lOfferItemInfo["TitleStr"] = lTitleStr # set the title +lPriceStr = lPriceStr.replace(" ","").replace("₽","") # Remove some extra symbols +lOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Convert price to the float type +``` + +**Извлечем недостающие параметры алгоритмическим путем.** +- Если в заголовке **содержится** слово "Апартаменты" +``` +lREResult = re.search(r".*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re +lOfferItemInfo["RoomCountInt"] = 1 # Room count +lSqmStr = lREResult.group(1) +lSqmStr= lSqmStr.replace(",",".") +lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count +lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(2)) # Floor current +lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(3)) # Floor total +lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M +``` + +- Если в заголовке **не содержится** слово "Апартаменты" +``` +lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re +lOfferItemInfo["RoomCountInt"] = int(lREResult.group(1)) # Room count +lSqmStr = lREResult.group(2) +lSqmStr= lSqmStr.replace(",",".") +lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count +lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor current +lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor total +lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M +``` + +В примере выше применяется магия [регулярных выражений](https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%83%D0%BB%D1%8F%D1%80%D0%BD%D1%8B%D0%B5_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F) + +Для подбора правильных регулярных выражений я пользуюсь [online валидаторами типа таких](https://www.google.com/search?q=online+regex&oq=online+reg&aqs=chrome.0.0l2j69i57j0l5.5699j0j7&sourceid=chrome&ie=UTF-8) + +**По окончанию обработки ценовых предложений выполним проверку на наличие указателя на следующую страницу, и (если такой указатель имеется) выполним переход на нее.** + +Выше (на шаге 2) мы уже находили CSS селектор указателя на следующую страницу. Нам нужно выполнить действие клика .click() по этому элементу. +Но при тестировании выяснилось, что функция .click от Selenium отрабатывает некорректно для этой страницы (не происходит переключение). В связи с этим у нас есть уникальная возможность использовать функциональность JavaScript на самой странице через Selenium. А уже из JavaScript выяснилось, что функция нажатия по указателю страницы отрабатывает корректно. Для этого выполним следующую команду: +``` +inWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""") +``` + +После того как был отправлен сигнал на переключение страницы, необходимо дождаться ее загрузки. Только после появления новой страницы мы сможем перейти к обработке новых ценовых предложений. +``` +# wait while preloader is active +lDoWaitBool = True +while lDoWaitBool: + lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') # So hard to catch the element :) + if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait + else: lDoWaitBool = False # Stop wait if preloader is dissappear +``` + +**Итоговую структуру сохраним в .json файл.** +``` +# Check dir - create if not exists +if not os.path.exists(os.path.join('Datasets',lResult['SearchKeyStr'])): + os.makedirs(os.path.join('Datasets',lResult['SearchKeyStr'])) +# Save result in file +lFile = open(f"{os.path.join('Datasets',lResult['SearchKeyStr'],lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_').replace('.','_'))}.json","w",encoding="utf-8") +lFile.write(json.dumps(lResult)) +lFile.close() +``` + +## Шаг 4. Обработка нештатных ситуаций +Этап тестирования - это один из самых важных этапов, который позволяет конвертировать вложенные усилия в реальный эффект. При тестировании кода на этом WEB приложении выяснилось, что могут происходить некоторые сбои. Ниже привожу те виды сбоев, которые встретились у меня: +- Зависает ползунок загрузки при переключении на сл. страницу +- При переключении на следующую страницу открывается совсем не следующая страница (иногда, но случалось :) ) + +Но роботы не боятся таких проблем (на то они и роботы :) ). + +**Для каждого вида сбоя мы предусмотрим сценарий восстановления, который позволит роботы доделать свою работу до конца.** +- Зависает ползунок загрузки при переключении на сл. страницу +``` +# wait while preloader is active. If timeout - retry all job +lTimeFromFLoat = time.time() # get current time in float (seconds) +lDoWaitBool = True +while lDoWaitBool: + lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') + if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait + else: lDoWaitBool = False # Stop wait if preloader is dissappear + if (time.time() - lTimeFromFLoat) > 15: # check if timeout is more than 15 seconds + lRetryJobBool = True # Loading error on page - do break, then retry the job + if inLogger: inLogger.warning(f"Ожидание загрузки страницы более {15} с., Робот повторит задание сначала") + break # break the loop +if lRetryJobBool == True: # break the loop if RetryJobBool is true + break +``` + +- При переключении на следующую страницу открывается совсем не следующая страница (иногда, но случалось :) ) +``` +lPageNumberInt = int(inWebDriver.find_element_by_css_selector(css_selector='li[class*="--active--"] span').text) # Get the current page int from web and check with iterator (if not equal - retry all job) +if lPageNumberInt == lPageCounterInt: + ... Код робота ... +else: + lRetryJobBool = True + if inLogger: inLogger.warning( + f"Следующая страница по списку не была загружена. Была загружена страница: {lPageNumberInt}, Ожидалась страница: {lPageCounterInt}") +``` + +## Шаг 5. Консолидировать код в проекте робота +**Соберем все блоки воедино.** + +Получим следующий пакет ([открыть на GitLab](https://gitlab.com/UnicodeLabs/OpenRPA/-/blob/master/Wiki/RUS_Tutorial/WebGUI_Habr/3.%20MonitoringCIAN_Run_64.py)): +``` +# Init Chrome web driver with extensions (if applicable) +# Import section +from selenium import webdriver +import time +import re # Regexp to extract info from string +import json +import datetime +import os +import re +import copy +import logging +# Store structure (.json) +""" +{ + "SearchKeyStr": "МСК_Тверской", + "SearchTitleStr": "Москва, район Тверской", # Title of the search [str] + "SearchURLStr": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str] + "SearchDatetimeStr": "2020-08-01 09:33:00.838081", # Date of data extraction, [str] + "SearchItems": { + "https://www.cian.ru/sale/flat/219924574/:": { # Item URL with https + "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] + "PriceFloat": 10000000.0, # Price [float] + "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] + "SqMFloat": 31.4, # Square meters in flat [float] + "FloorCurrentInt": 5, # Current floor [int] + "FloorTotalInt": 8, # Current floor [int] + "RoomCountInt": 3 # Room couint [int] + } + } + +} +""" + +########################## +# Init the Chrome web driver +########################### +gChromeExeFullPath = r'..\Resources\GoogleChromePortable\App\Chrome-bin\chrome.exe' +gExtensionFullPathList = [] +gWebDriverFullPath = r'..\Resources\SeleniumWebDrivers\Chrome\chromedriver_win32 v84.0.4147.30\chromedriver.exe' +def WebDriverInit(inWebDriverFullPath, inChromeExeFullPath, inExtensionFullPathList): + # Set full path to exe of the chrome + lWebDriverChromeOptionsInstance = webdriver.ChromeOptions() + lWebDriverChromeOptionsInstance.binary_location = inChromeExeFullPath + # Add extensions + for lExtensionItemFullPath in inExtensionFullPathList: + lWebDriverChromeOptionsInstance.add_extension (lExtensionItemFullPath) + # Run chrome instance + lWebDriverInstance = None + if inWebDriverFullPath: + # Run with specified web driver path + lWebDriverInstance = webdriver.Chrome(executable_path = inWebDriverFullPath, options=lWebDriverChromeOptionsInstance) + else: + lWebDriverInstance = webdriver.Chrome(options = lWebDriverChromeOptionsInstance) + # Return the result + return lWebDriverInstance + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +# def to extract list of offers from one job +def OffersByJobExtractDict(inLogger, inWebDriver, inJob): + # BUG 0 - if timeout - retry the job + + # BUG 1 - do mouse scroll to to emulate user activity - cian can hold the robot + # BUG 2 - check the page to retry job offer if page is not next + + # BUG 3 - RE fall on Апартаменты-студия, 85,6 м², 4/8 этаж + + lRetryJobBool = True # Init flag if some error is raised - retry + while lRetryJobBool: + lRetryJobBool = False # Set false until some another action will appear + lResult = copy.deepcopy(inJob) # do copy the structure + lFilterURLStr = lResult["SearchURLStr"] + inWebDriver.get(lFilterURLStr) # Open the URL + lDatetimeNowStr = str(datetime.datetime.now()) + lResult.update({ + "SearchDatetimeStr": lDatetimeNowStr, # Date of data extraction, [str] + "SearchItems": {} # prepare the result + }) + # Get List of the page + lNextPageItemCSS = 'div[data-name="Pagination"] li[class*="active"] + li a' + lNextPageItem = inWebDriver.find_element_by_css_selector(lNextPageItemCSS) + lPageCounterInt = 1 # Init the page counter + while lNextPageItem: + lPageNumberInt = int(inWebDriver.find_element_by_css_selector(css_selector='li[class*="--active--"] span').text) # Get the current page int from web and check with iterator (if not equal - retry all job) + if lPageNumberInt == lPageCounterInt: + lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])' + lOfferList = inWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr) + for lOfferItem in lOfferList: # Processing the item, extract info + lOfferItemInfo = { # Item URL with https + "TitleStr": "3-комн. кв., 31,4 м², 5/8 этаж", # Offer title [str] + "PriceFloat": 10000000.0, # Price [float] + "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float] + "SqMFloat": 31.4, # Square meters in flat [float] + "FloorCurrentInt": 5, # Current floor [int] + "FloorTotalInt": 8, # Current floor [int] + "RoomCountInt": 3 # Room couint [int] + } + lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text # Extract title text + if inLogger: inLogger.info(f"Старт обработки предложения: {lTitleStr}") + lPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"] > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text # Extract total price + lURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") # Extract offer URL + lOfferItemInfo["TitleStr"] = lTitleStr # set the title + lPriceStr = lPriceStr.replace(" ","").replace("₽","") # Remove some extra symbols + lOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Convert price to the float type + #Check if Апартаменты + if "АПАРТАМЕНТЫ" in lTitleStr.upper(): + lREResult = re.search(r".*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re + lOfferItemInfo["RoomCountInt"] = 1 # Room count + lSqmStr = lREResult.group(1) + lSqmStr= lSqmStr.replace(",",".") + lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count + lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(2)) # Floor current + lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(3)) # Floor total + lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M + else: + lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м², (\d*)/(\d*) эта.", lTitleStr) # run the re + lOfferItemInfo["RoomCountInt"] = int(lREResult.group(1)) # Room count + lSqmStr = lREResult.group(2) + lSqmStr= lSqmStr.replace(",",".") + lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count + lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor current + lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor total + lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M + lResult['SearchItems'][lURLStr] = lOfferItemInfo # Set item in result dict + # Click next page item + lNextPageItem = None + lNextPageList = inWebDriver.find_elements_by_css_selector(lNextPageItemCSS) + if len(lNextPageList)>0: + lNextPageItem = lNextPageList[0] + try: + #lNextPageItem = WebDriverWait(lWebDriver, 10).until(EC.visibility_of_element_located((By.CSS_SELECTOR, 'div[data-name="Pagination"]'))) + #lNextPageItem.click() + inWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""") + except Exception as e: + print(e) + time.sleep(0.5) # some init operations + # wait while preloader is active. If timeout - retry all job + lTimeFromFLoat = time.time() # get current time in float (seconds) + lDoWaitBool = True + while lDoWaitBool: + lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') + if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait + else: lDoWaitBool = False # Stop wait if preloader is dissappear + if (time.time() - lTimeFromFLoat) > 15: # check if timeout is more than 15 seconds + lRetryJobBool = True # Loading error on page - do break, then retry the job + if inLogger: inLogger.warning(f"Ожидание загрузки страницы более {15} с., Робот повторит задание сначала") + break # break the loop + if lRetryJobBool == True: # break the loop if RetryJobBool is true + break + lPageCounterInt = lPageCounterInt + 1 # Increment the page counter + else: + lRetryJobBool = True + if inLogger: inLogger.warning( + f"Следующая страница по списку не была загружена. Была загружена страница: {lPageNumberInt}, Ожидалась страница: {lPageCounterInt}") + if lRetryJobBool == False: # break the loop if RetryJobBool is true + # Check dir - create if not exists + if not os.path.exists(os.path.join('Datasets',lResult['SearchKeyStr'])): + os.makedirs(os.path.join('Datasets',lResult['SearchKeyStr'])) + # Save result in file + lFile = open(f"{os.path.join('Datasets',lResult['SearchKeyStr'],lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_').replace('.','_'))}.json","w",encoding="utf-8") + lFile.write(json.dumps(lResult)) + lFile.close() + +# Инициализировать Google Chrome with selenium web driver +lWebDriver = WebDriverInit(inWebDriverFullPath = gWebDriverFullPath, inChromeExeFullPath = gChromeExeFullPath, inExtensionFullPathList = gExtensionFullPathList) +lFilterURLStr = "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1" +lJobItem = { + "SearchKeyStr": "МСК_Тверской", + "SearchTitleStr": "Москва, район Тверской", # Title of the search [str] + "SearchURLStr": lFilterURLStr, + # URL of the CIAN search [str] +} +OffersByJobExtractDict(inLogger = logging, inWebDriver = lWebDriver, inJob = lJobItem) +``` + + +# Подведем итоги +Уважаемые роботизаторы. + +Мы успешно преодолели вторую серию туториалов по созданию роботов в WEB приложениях с помощью open source pyOpenRPA. Готовый проект робота Вы можете найти в репозитории pyOpenRPA [по ссылочке](https://gitlab.com/UnicodeLabs/OpenRPA/-/tree/master/Wiki/RUS_Tutorial/WebGUI_Habr). +В нашем аресенале уже имеются изученные технологии упраления Desktop и WEB приложениями. В следующей статье-туториале мы остановимся на особенностях роботизированного управления мышью и клавиатурой. + +Пишите комменты, внедряйте бесплатных роботов, будьте счастливы :) + +**До скорых публикаций!** \ No newline at end of file diff --git a/Wiki/RUS_Tutorial/WebGUI_Habr/3. pyOpenRPA Tutorial Habr welcome.png b/Wiki/RUS_Tutorial/WebGUI_Habr/3. pyOpenRPA Tutorial Habr welcome.png new file mode 100644 index 00000000..3605761f Binary files /dev/null and b/Wiki/RUS_Tutorial/WebGUI_Habr/3. pyOpenRPA Tutorial Habr welcome.png differ