Да, сейчас стоит ограничение на две страницы. Поправьте здесь # for i in range(1,LastPage):
Сам спросил, сам и отвечу. Каждая строка csv-файла — это одна строка таблицы Т.е. у меня между данными, разделенными “;” внутри поля (данных) есть символ новой строки, соответственно после него данные помещаются в первый столбец и вся таблица рассинхронизируется. Еще рекомендую к прочтению статью “Редактируем CSV-файлы, чтобы не сломать данные”. Узнал для себя много нового.
Парсинг контактных данных(телефонов) пока не реализован, т.к. они формируются Ява скриптом, т.е. в готовом виде в карточке вакансии нет.
Долго матерился пока наконец-то не узнал, что pip install для Установки библиотек запускается не из питона, а из CMD.exe.
Исправил формирование полей CSV-файла. Теперь открывается в Экселе нормально (через данные-импорт). Но я теперь открываю в другой программе CSVed.exe.
P.S. Профи прошу не смотреть исходник, т.к. вас может стошнить.
# форматировать код Ctrl + Alt + L # установить символ комментария Ctrl + / # Alt+Enter, чтобы просмотреть список возможных исправлений # сохраняйте HTML-файл локально, чтобы протестировать его несколько раз # иначе сайт может заблокировать # Прежде чем парсить данные "супом", нужно проверять, доступны ли они # при отключенном JavaScript. Когда недоступны, открываем браузером # Выход - использовать Selenium webdriver. # https://parsemachine.com/articles/urok-1-pishem-parser-kataloga-tovarov-na-python/ # https://vc.ru/newtechaudit/109368-web-parsing-osnovy-na-python # https://pythonru.com/biblioteki/parsing-na-python-s-beautiful-soup # https://istories.media/workshops/2021/09/10/parsing-s-pomoshchyu-python-urok-1/ # https://evilinside.ru/parsing-sajtov-na-python-podrobnyj-videokurs-i-programmnyj-kod/# # Тэг, в общем случае, - это многострочная строка, коллекция строк # начинающаяся с символа <. Внутри тэга может быть много дополнительной информации # подключаю необходимые библиотеки import requests # что бы Получить HTML-код по URL-адреса from bs4 import BeautifulSoup, __version__ # парсер HTML from fake_useragent import UserAgent # генерация правдоподобных юзер-агентов import pyperclip # для работы с буфером обмена import time # для правдоподобных задержек import re # регулярка import csv # для работы с csv файлом # функция чтения HTML-файла (.txt????) def write_to_fileTXT(HTML_page): # запись в файл. try: with open("HTML_page.txt", "a") as fopen: # Open the txt file. txt_open = writer(fopen) txt_open.writerow(HTML_page) except: return False # функция записи списка в текстовый csv-файл (открывается экселем) def write_to_csv(list_date): ''' запись спарсенных данных в CSV файл. В Windows 'b', добавленный к режиму, открывает файл в двоичном режиме, поэтому существуют также такие режимы, как 'rb', 'wb' и 'r + b'. Python в Windows проводит различие между текстовыми и двоичными файлами; символы конца строки в текстовых файлах автоматически слегка изменяются при чтении или записи данных. Это скрытое изменение данных файла подходит для текстовых файлов ASCII, но оно приведет к повреждению двоичных данных, подобных тем, что содержатся в файлах JPEG или EXE. Будьте очень осторожны, используя двоичный режим при чтении и записи таких файлов. В Unix не повредит добавить 'b' к режиму, чтобы вы могли использовать его независимо от платформы для всех двоичных файлов. ''' try: # newline = '' что бы не было пустыхустых строк через одну запись with open("allvacancy.csv", "a") as fopen: # Open the csv file. csv_writer = csv.writer(fopen, delimiter = ";", lineterminator="\r") # delimiter- для разделения полей.По умолчанию это',' # lineterminator = "\r"-Это разделитель между строками таблицы, по умолчанию он"\r\n" csv_writer.writerow(list_date)# запись по одной строке # csv_writer.writerows(list_date) except: return False # запрашиваем данные с сервера (сайта) # Внимание! проверки нужно делать на каждом запросе/поиске!!! # Если сервер ответил <Response [200]>, тогда продолжаем def response_UserAgent(URLx): """ получаю URL (ссылку на нужный объект сайта) НАДО!!!прикидываюсь браузером (человеком за ПК) получаю ответ сервера (сайта) возвращаю ответ, который надо потом проверить """ # запрашиваем данные по ссылке response = requests.get(URLx, headers={'User-Agent': UserAgent().chrome}) if response.status_code == 200: return response # else requests.ConnectionError: else: print("50_def check_response: response(otvet) <> 200. Quit") return None # quit() # проверить список на непустоту def check_list(list): if list == []: print('72_def check_list: list is [] empty. quit()') quit() else: print('75_def check_list: list is not empty') # перевожу в суп содержимое переданной страницы def getsoup(ResponseX): """ возвращаю soup """ # получаем содержимое страницы и переводим в суп response_html = ResponseX.content # response_text = ResponseX.text # есть несколько парсеров: html.parser,lxml # soup = BeautifulSoup(response_html, 'html.parser') soup = BeautifulSoup(response_html, 'lxml') return soup # выберем режим ввода информации в программу: # 1-URL из буфера обмена # 2-HTML-файл с диска def InputURL(): Mode = int(input("select Mode. 1-input with clipboard, 2 -input with file:")) while True: try: if Mode == 1: inp_clipboard = pyperclip.paste()# вставляем URL из буфера обмена # pyperclip.copy(s) print('104_return inp_clipboard='+inp_clipboard) return inp_clipboard elif Mode == 2: # with open ('ффф.html','r',encoding='utf-8') as f: print('107_open (ффф.html1)') return '' except Exception: print("90_Clipboard is empty(bufer obmena pustoy).") return '' # ============ Начало программы ============ # изменилась структура. 28.11.2022 Сейчас сохранён url 2-й страницы, что бы видеть всю структуру запроса url = 'https://praca.by/search/vacancies/?page=2&search[cities-radius][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]=1&search[query-text-params][headline]=0&upped_period=1' # 13.11.2022 # url = 'https://praca.by/search/vacancies/?upped_period=1&search[city][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]=1' baseURL = 'https://praca.by/search/vacancies/?' pageURL = 'page=' EndURL = '&search[cities-radius][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]=1&search[query-text-params][headline]=0&upped_period=1' __version__ # заремировал, что бы работать с файлом с диска, а не сайтом # Запрос URL из буфера обмена url1= InputURL() if url1 == '': url1 = InputURL() #EndIf # проверяю, будет ли отработан запрос по этому урлу R1=response_UserAgent(url1)# ответ на запрос # Метод get библиотеки requests возвращает объект Response, # а не какой-то там html # s_Text=requests.get(url1).text # print(s_Text) if R1.status_code == 200: print("115_response(otvet)= Ok-> soup") else: print("117_ response(otvet) <> 200. Quit(vihod)") quit() # ResponseText показал, что есть 2432 строки, но раскрывать не стал # загружаю исходный текст полученной страницы в парсер BeautifulSoup 'lxml' # и создаю Первый его объект soup soup_root=getsoup(R1) # ===== для тестов открываю заранее сохраненный файл==== # with open ('ффф.html','r',encoding='utf-8') as f: # with open('ффф.html','r',encoding='utf-8') as fp: # soup_root = BeautifulSoup(fp, 'lxml') # soup это ПОЧТИ тот же response.content # У soup совсем другие методы работы с HTML кодом # можно бродить по вершинам (Чего?), указывая путь из тегов # soup.Tag1.Tag2.Tag3 (soup.html.head.title) # можно получить значение из места, на которе указали # soup.Tag1.Tag2.Tag3.text # Создал Переменные для названий тэгов и классов, что бы лучше # понимать работу методов soup, который использует # НЕ символы/строки, а ТЭГИ и дочерние атрибуты (КЛАССЫ CSS) NameTagDiv = 'div' NameTagLi = 'li' NameTagUl = 'ul' ClassNamePageIt = 'pagination__item' ClassNamePageLst = 'pagination__list' # в HTML (soup)нахожу блок/таблицу/элемент 'div', в котором # находятся строкИ кода по выбору найденных страниц (кнопки) # buttons = soup.find('ul', class_ = 'pagination__list') # print(buttons) # но можно и по другому: найти ВСЕ строки с тегом 'li' и классом = 'pagination__item' # objlistPages = soup.find_all('li', class_ = 'pagination__item') # возврашается Тип ОБЪЕКТ (список???). # "правильный" синтаксис для понимания soup.find: objlistPagesSoup = soup_root.find(NameTagUl, attrs={"class": ClassNamePageLst}) # Если ругается AttributeError: 'NoneType' object has no attribute... # то можно посмотреть командой print(type(objlistPages)) # Проверка, что ответ/список не пустой check_list(objlistPagesSoup) # что отобразит команда objlistPagesSoup.text не обёрнутая в print()? # '\n\n←123456789...→' не очень понятно # а вот так понятней: print(objlistPagesSoup.text) # ←123456789...→ так выглядит в браузере список страниц # Получить последний элемент списка. индекс=-1 [-1], # т.е. первый сзади. # Последняя страница (LastPage) в выданном результате LastPage1 = objlistPagesSoup.find_all('li', class_='pagination__item')[-1] # проверим список на непустоту check_list(LastPage1) # <li class="pagination__item"><a href="?page=45&upped_period=1&search[city][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]=1"><i class="mdi mdi-fast-forward"></i></a></li> # Теперь надо узнать какая это страница (?page=45) из содержимого Page = LastPage1.find('a') Page # [<a href="?page=45&upped_period=1&search[city][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]=1"><i class="mdi mdi-fast-forward"></i></a>] # Теперь вычленю цифру ?page=45. # Тип soup конвертирую в тип строка и ищу по строке = и разделяю по ней PageStr = str(Page.find).split('=') PageStr3 = PageStr[2] PageStr3 # '49&search[cities-radius][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]' Page4 = PageStr3.split('&') Page4 # ['49', 'amp;search[cities-radius][%D0%9C%D0%B8%D0%BD%D1%81%D0%BA]'] Page5 = Page4[0] Page5 # '45' LastPage = int(Page5) print('LastPage=',LastPage) NameTagA = 'a' ClassNameVac = 'vac-small__title-link' SleepPages=2 #задержка чтения страниц SleepKartochek=1 #задержка чтения карточки вакансии write_to_csv(["Вакансия", "Зарплата", "Наниматель", "Город", "МестоРаботы", "РежимРаботы", "ПолнЧастЗанятость", "ОпытРаботы", "Образование", "ОписаниеРаботы", "МестоРаботы", "URL_вакансии"]) # for i in range(1,LastPage): # боевой код for i in range(1, 2): # для отладки. только 1 страница time.sleep(SleepPages) #задержка чтения страниц # собираю полный URL (запрос) с указанием нужной страницы url_gen = baseURL + pageURL + str(i) + EndURL # запрос к сайту за страницами с вакансиями Resp_v = response_UserAgent(url_gen) # ответ на запрос print('*************************************204 i=',i) # проверка ответа if Resp_v.status_code == 200: print("207_response(otvet)= Ok-> soup") else: print("209_response(otvet) <> 200. Quit(vihod)") quit() soup_v = getsoup(Resp_v) # soup2 = BeautifulSoup(resp.content, 'lxml') # все вакансии(ссылки) выбранной(i) страницы vacansii_on_page = soup_v.find_all(NameTagA, attrs={"class": ClassNameVac}) # vacansii check_list(vacansii_on_page) # print(vacansii) # одна из строк <a class="vac-small__title-link" href="https://praca.by/vacancy/497531/" target="_blank">Повар</a> # 1 надо вычленить url, что бы пройти по нему и получить доп информацию # 2 отсюда могу взять название вакансии # переменные для парсинга карточки вакансии NameTagh1 = 'h1' ClassNameKart = 'no-margin-top no-margin-bottom' NameTagDiv = 'div' ClassNameSalary = 'vacancy__salary' n = 0 for k in vacansii_on_page: time.sleep(SleepKartochek) # задержка чтения карточек # значение атрибута href, т.е ссылка на карточку вакансии url_Kartochki = k.attrs["href"] name_vac = k.string print('262 ---------------Kartochka-----------') print('263_name_vac', name_vac) # запрашиваю карточку вакансии с сайта # resp = requests.get(url_Kartochki) Resp_k = response_UserAgent(url_Kartochki) # проверка ответа if Resp_k.status_code == 200: print("256_response(otvet)= Ok-> soup") else: print("258_response(otvet) <> 200. Quit(vihod)") quit() soup_k = getsoup(Resp_k) Kartochka = soup_k.find(NameTagh1, attrs={"class": ClassNameKart}).text.strip() print(Kartochka) # Зарплата +дополнительные условия if (soup_k.find(NameTagDiv, attrs={"class": "vacancy__salary"}) != None): salary = soup_k.find(NameTagDiv, attrs={"class": "vacancy__salary"}).text.strip() # print('275', salary) else: salary = '' print('302********************* Зарплата не указана') # Обязанности if (soup_k.find(NameTagDiv, attrs={"class": "description wysiwyg-st"}) != None): description = soup_k.find(NameTagDiv, attrs={"class": "description wysiwyg-st"}).text.strip() description = description.replace("\r", "") description = description.replace("\n", "") # Заменить перевод строки и лишние пробелы # description = Repl(description) # print('275', description) else: description = '' print('311********************* Обязанности не указаны') # Адрес работы if (soup_k.find(NameTagDiv, attrs={"class": "job-address"}) != None): job_address = soup_k.find(NameTagDiv, attrs={"class": "job-address"}).text.strip() # print('275', job_address) else: job_address = '' print('319********************* Адрес работы не указан') firma = soup_k.find('div', attrs={"class": "vacancy__org-name"}).text.strip() # print(firma) # город <div class="vacancy__city">Минск</div> city = soup_k.find('div', attrs={"class": "vacancy__city"}).text.strip() # print('288', city) all_harakt = soup_k.find_all('div', attrs={"class": "vacancy__item"}) #.text.strip() aaa=[] for elem in all_harakt: aa = [elem.text.strip().split('\t')[-1]] aaa.append(aa) # print(*aa, sep='\n')#* Распаковывает итерируемый объект в аргументы функции region1=aaa[0]# На территории работодателя # преобразую элемент списка в строку и применяю к ней strip() # иначе получаю в виде ['Минск'] region=" ".join(map(str,region1)).strip() time_work1=aaa[1]# Фиксированный / полный рабочий день time_work = " ".join(map(str, time_work1)).strip() full_partial_load1=aaa[2]# Полная full_partial_load = " ".join(map(str, full_partial_load1)).strip() experience1=aaa[3]# Опыт работы от 1 года experience = " ".join(map(str, experience1)).strip() education1=aaa[4]# Высшее образование education = " ".join(map(str, education1)).strip() # print(*time_work, sep='\n') n = n + 1 print('309 page=', i, 'nom kartochki=', n, 'SleepKartochek=',SleepKartochek,'SleepPages=',SleepPages) print('==========================================================') # Описание работы,description # Место работы,job_address # URL_вакансии write_to_csv([Kartochka, salary, firma, city, region, time_work, full_partial_load, experience, education, description, job_address, url_Kartochki])