Цель работы


  1. Парсим сайт, используя прокси-сервера.
  2. Сохраняем данные в формате CSV.
  3. Пишем поисковик по найденным данным.
  4. Строим интерфейс.




Использовать будем язык программирования 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'], ''))     
#каждому параметру присваиваем данные


Надеюсь, данная информация будет полезна в вашей работе. Желаю удачи.

Поделиться с друзьями
-->

Комментарии (7)


  1. ookami_kb
    26.02.2017 15:27
    +7

    Жесть. Это просто какая-то мешанина из объяснений синтаксиса питона, отвратительного кода, избыточности комментариев и издевательств над языком. Не надо так писать, даже для себя, а уж тем более учить такому других.


  1. rSedoy
    26.02.2017 15:40
    +3

    Очень хороший пример как не надо писать комментарии.


  1. alexmay
    26.02.2017 19:50
    +1

    Нормальные комментарии, малех избыточны, но лучше чем никаких, а для обучения -самое то. )))


  1. riot26
    27.02.2017 02:00
    +1

    https://www.ozon.ru/context/detail/id/21916535/
    Строго рекомендую.


  1. Zack
    27.02.2017 10:29

    В заключении хорошо бы выдавать готовый скрипт, для тех, кто хотел бы опробовать результат.


  1. nightvich
    27.02.2017 18:30

    Крайне рекомендую автору ознакомиться с PEP8.


  1. LingvoLena
    05.03.2017 15:11

    Интересная постановка задачи, но код требует доработки в плане оптимизации.