Цель работы
- Парсим сайт, используя прокси-сервера.
- Сохраняем данные в формате CSV.
- Пишем поисковик по найденным данным.
- Строим интерфейс.
Использовать будем язык программирования Python. Сайт, с которого мы будем качать данные — www.weblancer.net (парсинг старой версии этого сайта был размещен здесь), в нем есть предложения работы по адресу www.weblancer.net/jobs. С него мы и будем получать данные — это название, цена, количество заявок, категория, краткое описание предлагаемой работы.
Вход с использованием прокси означает — вход на сайт под ненастоящим адресом. Пригодится для парсинга сайта с защитой бана по IP адресу (то есть, если вы слишком часто, за короткий отрезок времени, входите на сайт).
Импорт модулей
Модули для непосредственно парсинга: requests и BeautifulSoup, их нам будет достаточно. Сохранять данные в формате csv нам поможет модуль с аналогичным названием — csv. В работе с интерфейсом нам поможет, до боли простой, модуль tkinter (кто желает получить более качественный интерфейс, советую воспользоваться модулем pyQt5). Работу по поиску и замене данных осуществит модуль re.
import requests #осуществляет работу с HTTP-запросами import urllib.request #библиотека HTTP from lxml import html #библиотека для обработки разметки xml и html, импортируем только для работы с html import re #осуществляет работу с регулярными выражениями from bs4 import BeautifulSoup #осуществляет синтаксический разбор документов HTML import csv #осуществляет запись файла в формате CSV import tkinter #создание интерфейса from tkinter.filedialog import * #диалоговые окна
Переменные
Создаем массив, где будем хранить уже использованные ранее прокси, и две текстовые переменные, к первой приравниваем адрес сайта, а вторую объявляем глобальной (есть варианты, когда использование глобальных переменных может негативно отразиться на работоспособности программы, подробнее о ее использовании — здесь), для получения ее данных в функциях.
global proxy1 #объвляем глобальную переменную для запоминания прокси на следующий проход цикла proxy1 = '' #и приравниваем к пустому тексту BASE_URL = 'https://www.weblancer.net/jobs/' #адрес сайта для парсинга massiv = [] #массив для хранения прокси
Переменные для tkinter:
root = Tk() #главное окно root.geometry('850x500') #ширина и высота главного окна в пикселях txt1 = Text(root, width = 18, heigh = 2) #текстовое поле для ввода поисковых слов txt2 = Text(root, width = 60, heigh = 22) #текстовое поле для вывода данных lbl4 = Label(root, text = '') #надпись для вывода прокси btn1 = Button(root, text = 'Отпарсить сайт') #кнопка для парсинга btn2 = Button(root, text = 'Найти по слову') #кнопка для поиска btn3 = Button(root, text = 'Очистить поля') #кнопка для очистки полей lbl1 = Label(root, text = 'Впишите ключевые слова для поиска') #надпись для поиска lbl2 = Label(root, text = '') #надпись для вывода процента парсинга lbl3 = Label(root, text = '') #надпись для вывода количества страниц
Переменная.grid(строка, колонка) — определяем местоположение элемента в окне отображения. Bind — нажатие клавиши. Следующий код помещаем в самый конец программы:
btn1.bind('<Button-1>', main) #при нажатии клавиши вызывает основную функцию btn2.bind('<Button-1>', poisk) #вызывает функцию поиска нужных заказов btn3.bind('<Button-1>', delete) #вызывает функцию очистки полей lbl2.grid(row = 4, column = 1) lbl4.grid(row = 5, column = 1) lbl3.grid(row = 3, column = 1) btn1.grid(row = 1, column = 1) btn3.grid(row = 2, column = 1) btn2.grid(row = 1, column = 2) lbl1.grid(row = 2, column = 2) txt1.grid(row = 3, column = 2) txt2.grid(row = 6, column = 3) root.mainloop() #запуск приложения
Основная функция
Первым делом, напишем главную функцию (почему функция, а не процедура? В будущем нам будет необходимо запускать ее с помощью bind (нажатие клавиши), это легче сделать именно с функцией), а позже будем добавлять прочие функции. Процедуры, которые нам пригодятся:
- config — вносит изменения в элементы виджетов. К примеру, мы будем заменять текст в виджетах Label.
- update — используется для обновления виджета. Столкнемся с проблемой — виджет будет изменен только после завершения цикла, update позволяет обновлять содержимое виджета каждый проход цикла.
- re.sub(шаблон, изменяемая строка, строка) — находит шаблон в строке и заменяет его на указанную подстроку. Если шаблон не найден, строка остается неизменно��.
- get — осуществляет http-запрос, если он равен «200» — вход на сайт был удачен.
- content — позволяет получить html-код.
- L.extend(K) — расширяет список L, добавляя в конец все элементы списка K
def main(event): #запуск функции с передачей переменной event (для работы виджетов) page_count = get_page_count(get_html(BASE_URL)) #переменную присваиваем функции пересчета страниц, где сначала выполняется другая функция, получающая http-адрес от переменной BASE_URL lbl3.config(text='Всего найдено страниц: '+str(page_count)) #меняем текстовую часть переменной lbl3 на количество найденных страниц page = 1 #переменная для счетчика projects = [] #массив для хранения всей искомой информации while page_count != page: #цикл выполняется, пока переменная page не равна количеству найденных страниц proxy = Proxy() #присваиваем классу, где зададим нужные параметры proxy = proxy.get_proxy() #получать proxy-адрес lbl4.update() #обновляем виджет lbl4.config(text='Прокси: '+proxy) #и приравниваем к полученному прокси global proxy1 #глобальная переменная proxy1 = proxy #приравниваем переменные для дальнейшей проверки их совпадения try: #обработчик исключительных ситуаций for i in range(1,10): #этот цикл будет прогонять полученный прокси определенное количество раз (range - определяет, сколько раз будем его использовать для входа на сайт). Можно и каждый раз брать новый прокси, но это существенно замедлит скорость работы программы page += 1 #счетчик необходим для подсчета выполненной работы lbl2.update() #обновляем виджет lbl2.config(text='Парсинг %d%%'%(page / page_count * 100)) #меняет процент сделанной работы от 100% r = requests.get(BASE_URL + '?page=%d' % page, proxies={'https': proxy}) #получаем данные со страницы сайта parsing = BeautifulSoup(r.content, "lxml") #получаем html-код по средству BeautifulSoup (чтобы позже использовать поисковые возможности этого модуля) для дальнейшей передачи переменной в функцию projects.extend(parse(BASE_URL + '?page=%d' % page, parsing)) #получаем данные из функции parse (передавая адрес страницы и html-код) и добавляем их в массив save(projects, 'proj.csv') #вызываем функцию сохранения данных в csv, передаем туда массив projects except requests.exceptions.ProxyError: #неудача при подключеннии с прокси continue #продолжаем цикл while except requests.exceptions.ConnectionError: #не удалось сформировать запрос continue #продолжаем цикл while except requests.exceptions.ChunkedEncodingError: #сделана попытка доступа к сокету методом, запрещенным правами доступа continue #продолжаем цикл while
Подсчет страниц сайта
Пишем функцию для получения url:
def get_html(url): #объявление функции и передача в нее переменной url, которая является page_count[count] response = urllib.request.urlopen(url) #это надстройка над «низкоуровневой» библиотекой httplib, то есть, функция обрабатывает переменную для дальнейшего взаимодействия с самим железом return response.read() #возвращаем полученную переменную с заданным параметром read для корректного отображения
Теперь с url ищем все страницы:
def get_page_count(html): #функция с переданной переменной html soup = BeautifulSoup(html, 'html.parser') #получаем html-код от url сайта, который парсим paggination = soup('ul')[3:4] #берем только данные, связанные с количеством страниц lis = [li for ul in paggination for li in ul.findAll('li')][-1] #перебираем все страницы и заносим в массив lis, писать так циклы куда лучше для работоспособности программы for link in lis.find_all('a'): #циклом ищем все данные связанные с порядковым номером страницы var1 = (link.get('href')) #и присваиваем переменной var2 = var1[-3:] #создаем срез, чтобы получить лишь число return int(var2) #возвращаем переменную как числовой тип данных
Получение прокси
Код частично был взят у Игоря Данилова. Будем использовать __init__(self) — конструктор класса, где self — элемент, на место которого подставляется объект в момент его создания. Важно! __init__ по два подчеркивания с каждой стороны.
class Proxy: #создаем класс proxy_url = 'http://www.ip-adress.com/proxy_list/' #переменной присваиваем ссылку сайта, выставляющего прокси-сервера proxy_list = [] #пустой массив для заполнения def __init__(self): #функция конструктора класса с передачей параметра self r = requests.get(self.proxy_url) #http-запрос методом get, запрос нужно осуществлять только с полным url str = html.fromstring(r.content) #преобразование документа к типу lxml.html.HtmlElement result = str.xpath("//tr[@class='odd']/td[1]/text()") #берем содержимое тега вместе с внутренними тегами для получение списка прокси for i in result: #перебираем все найденные прокси if i in massiv: #если есть совпадение с прокси уже использованными yy = result.index(i) #переменная равна индексу от совпавшего прокси в result del result[yy] #удаляем в result этот прокси self.list = result #конструктору класса приравниваем прокси def get_proxy(self): #функция с передачей параметра self for proxy in self.list: #в цикле перебираем все найденные прокси if 'https://'+proxy == proxy1: #проверяем, совпдает ли до этого взятый прокси с новым, если да: global massiv #massiv объявляем глобальным massiv = massiv + [proxy] #добавляем прокси к массиву url = 'https://'+proxy #прибавляем протокол к прокси return url #возвращаем данные
Парсинг страниц
Теперь находим на каждой странице сайта нужные нам данные. Новые процедуры:
- find_all — в html-коде страницы ищет блоки и элементы, в нем находящиеся.
- text — получение из html-кода только текст отображенный на сайте.
- L.append(K) — добавляет элемент K в конец списка L.
def parse(html,parsing): #запуск функции с получением переменных html и parsing projects = [] #создаем пустой массив, где будем хранить все полученные данные table = parsing.find('div' , {'class' : 'container-fluid cols_table show_visited'}) #находим часть html-кода, хранящую название, категорию, цену, количество заявок, краткое описание for row in table.find_all('div' , {'class' : 'row'}): #отбираем каждую запись cols = row.find_all('div') #получаем название записи price = row.find_all('div' , {'class' : 'col-sm-1 amount title'}) #получаем цену записи cols1 = row.find_all('div' , {'class' : 'col-xs-12' , 'style' : 'margin-top: -10px; margin-bottom: -10px'}) #получаем краткое описание записи if cols1==[]: #если массив остался пуст, application_text = '' #то присваиваем пустую строку else: #если не пуст application_text = cols1[0].text #приравниваем к тексту из html-кода cols2 = [category.text for category in row.find_all('a' , {'class' : 'text-muted'})] #с помощью цикла получаем категорию и заявку записи projects.append({'title': cols[0].a.text, 'category' : cols2[0], 'applications' : cols[2].text.strip(), 'price' : price[0].text.strip() , 'description' : application_text}) #в массив projects помещаем поочередно все найденные данные return projects #возвращаем проект для сохранения
Функция очистки
Единственная, нам необходимая процедура delete — удаляет объект по указанному идентификатору или тегу.
def delete(event): #запуск функции txt1.delete(1.0, END) #удаляет текст с вводимыми данными txt2.delete(1.0, END) #удаляет текст с выведенными данными
Поиск данных
Функция будет осуществлять поиск предложений, в описании которых упоминаются необходимые нам слова. Запись в поле придется осуществлять с учетом знаний регулярных выражений (к примеру, python|Python, С\+\+).
- csv.DictReader — конструктор возвращает объекты-итераторы для чтения
данных из файла. - split — разбивает строку на части, используя разделитель, и возвращает эти части списком.
- join — преобразовывает список в строку, рассматривая каждый элемент как строку.
- insert — добавление элементы в список по индексу.
def poisk(event): #запуск функции с передачей переменной event для работоспособности интерфейса file = open("proj.csv", "r") #открытие файла, где мы сохранили все данные rdr = csv.DictReader(file, fieldnames = ['name', 'categori', 'zajavki', 'case', 'opisanie']) #читаем данные из файла по столбцам poisk = txt1.get(1.0, END) #получаем данные из поля для поиска соответствий poisk = poisk[0:len(r)-1] #конкотенация необходима для отбрасывания последнего символа, который программа добавляет самостоятельно ('\n') for rec in rdr: #запуск цикла, проход по каждой строке csv-файла data = rec['opisanie'].split(';') #к переменной приравниваем данные по описанию задания data1 = rec['case'].split(';') #к переменной приравниваем данные по цене задания data = ('').join(data) #преобразовываем в строку data1 = ('').join(data1) #преобразовываем в строку w = re.findall(poisk, data) #ищем в описании совпадение с поисковыми словами if w != []: #условие, если переменная w не равна пустому массиву, то продолжать if data1 == '': #условие проверяющее, если цена не была получена, то продолжать data1 = 'Договорная' #заменяем пустое значение на текст txt2.insert(END, data+'--'+data1+'\n'+'---------------'+'\n') #соединяем краткое описание заказа, его цену, переход на новую строку, символы, разделяющие заказы и снова переход на новую строку
Сохранение данных
Как уже говорил: данные будем сохранять в формате csv. При желании можно переписать функцию под любой другой формат.
def save(projects, path): #функция с переданной переменной и названием файла как переменная path with open(path, 'w') as csvfile: #открываем файл как path и w (Открывает файл только для записи. Указатель стоит в начале файла. Создает файл с именем имя_файла, если такового не существует) writer = csv.writer(csvfile) #writer - осуществляет запись файла, csv - определяет формат файла writer.writerow(('Проект', 'Категории', 'Заявки' , 'Цена' , 'Описание')) #writerow - создает заглавия каждого заполняемого столбца for project in projects: #перебираем элементы в массиве try: #обработчик исключительных ситуаций writer.writerow((project['title'], project['category'], project['applications'], project['price'], project['description'])) #каждому параметру присвоим данные except UnicodeEncodeError: #в description иногда будут попадаться символы из других кодировок, придется брать как пустую строку writer.writerow((project['title'], project['category'], project['applications'], project['price'], '')) #каждому параметру присваиваем данные
Надеюсь, данная информация будет полезна в вашей работе. Желаю удачи.
