Здраствуйте, Хабровчане! Давно было желание написать статейку, но никак не осмеливался.
В моей серии постов будет описан мой путь от Junior'a до Middle'a, а потом возможно даже до Senior'a. Программировать будем на Python.

P.S. Документация к некоторым малоизвестным библиотекам будет прикреплена в конце.

Кратко о себе: Python начал изучать два года назад, особых продвижений не было.
Разочарованием стали сами основы, поскольку учил я их полтора года. Сейчас же углубленно изучаю язык, понимаю его структуру и как все происходит. В следующих статьях буду выкладывать код, использую все новые, и новые библиотеки, а их как вы знаете у питона очень много :)

С чего бы начать? Пару месяцев назад я написал свой первый парсер. Оказалось, что писать парсеры довольно просто и на них даже можно зарабатывать. Пока что покажу пару примеров, используя стек из bs4 + requests. Парсить будем наш любимый Хабр.

#для начала импортируем нужные нам модули
from bs4 import BeautifulSoup as bs
import requests
from fake_useragent import UserAgent

Не уверен, что все знакомы с библиотекой fake_useragent. Довольно удобна для парсинга, создает фейк user-agent'a.

ua = UserAgent()
headers = {'accept': '*/*', 'user-agent': ua.firefox}

Это следующий небольшой блок нашего кода. В первой строке, мы создали переменную ua, которая использует методы класса UserAgent. Во второй строке, мы создали словарь, который в будущем поможет нам при парсинге.

Теперь создаем саму функцию для парсинга.

def without_post(url, headers):
	response = requests.get(url, headers=headers)
	if response.status_code == 200:
		soup = bs(response.text, 'html.parser')
		links = {}
		for i in soup.find_all('a', {'class': 'post__title_link'}):
			links.update({i.text: i.get('href')})
		return links
	else:
		print("Connection Error")

Данная функция будет парсить ссылку, которую мы укажем на наличие тега с классом «post__title_link».

Самый простой способ сохранить данные — сохранить их в файл. Так и сделаем.

url = "https://habr.com/ru/all/"
links = without_post(url, headers)
with open('parsed.txt', 'w') as f_obj:
	for name, href in links.items():
		f_obj.write(name + ':\n' + href + '\n\n')

В конце получаем файл, в котором записаны названия и ссылки постов, которые мы собрали с первой страницы.

Исходный(полный) код, только уже без комментариев:

from bs4 import BeautifulSoup as bs
from fake_useragent import UserAgent
import requests

ua = UserAgent()
headers = {'accept': '*/*', 'user-agent': ua.firefox}

def without_post(url, headers):
	response = requests.get(url, headers=headers)
	if response.status_code == 200:
		soup = bs(response.text, 'html.parser')
		links = {}
		for i in soup.find_all('a', {'class': 'post__title_link'}):
			links.update({i.text: i.get('href')})
		return links
	else:
		print("Connection Error")

url = "https://habr.com/ru/all/"
links = without_post(url, headers)
with open('parsed.txt', 'w') as f_obj:
	for name, href in links.items():
		f_obj.write(name + ':\n' + href + '\n\n')

Хороший результат! Если не считать пропусков строк и комментариев, то мы уложились ровно в 20 строк. Для начала довольно-таки хорошо :)

Как и обещал, ссылки на документации использованных библиотек:

Requests: *жмяк*
bs4: *жмяк*
fake_useragent: *жмяк*

Всем спасибо за внимание! До встреч!

P.S. Если будет какой-либо фидбек, то следующая статейка не заставит себя ждать

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


  1. fougasse
    28.12.2019 18:48

    А обработка ошибок где?
    Если файл не открылся, например. Почему код только 200?
    Как-то не очень на миддла, если честно.


    1. Krau5 Автор
      28.12.2019 19:29

      Я не миддл, вроде писал, что вместе с вами буду описывать свой путь от Юниора до Миддла
      Извините, если ошибся)


      1. Maksclub
        28.12.2019 20:10
        +1

        А обработка ошибок где?
        Я не миддл
        Обработка ошибок то где? :) Или джунам не нужно обрабатывать?


        1. Anshi85
          29.12.2019 03:30

          Ошибки обрабатывать конечно нужно всем) но не каждый обжигается на этом,
          я работаю джуниор разработчиком полгода, стек Python/Django, поначалу конечно тоже не использовал try/exception, обычно пихал везде logger.info() вместо print, чтобы в случае чего понять где ошибка, потом понял что так жить нельзя, так как моим кодом будут пользоваться люди, я то ладно в логе посмотрю, что там поломалось, а они как поймут? Начал заворачивать все в if/else, но где то на 2 месяц работы понял что это тоже не дело и начал использовать try/exception внезапно оказалось очень удобно, мой сервис не падает и мне сухо и спокойно)
          Я подозреваю, что автор пишет в основном для себя или для одного клиента, какие-то разовые заказы, поэтому и не сталкивался с проблемами, когда сервисом пользуется много людей и есть интеграция со сторонними сервисами и очень важно, чтобы все действия в сервисе были максимально информативны ну и нужна какая никакая отказоустойчивость сервиса в плане возникновения ошибок или сбоя в коде.


          1. Krau5 Автор
            29.12.2019 22:59

            Раньше писал для себя, учил, работал на фрилансе с какими-либо минимальными заказами и использовал правило Главное, чтобы работало. Сейчас же приходится переучиватся, вспоминать PEP8, ибо когда передаешь код получаешь тонну критику в сторону качества.


            1. Anshi85
              30.12.2019 03:55

              Вы главное не воспринимайте критику близко к сердцу, а постарайтесь сделать для себя выводы и уроки на будущее. Главное не унывать и стремиться к совершенству, я знаете единственный разработчик на проекте был полгода, учиться особо не у кого было, сперва писал как мог, а потом через пару дней узнав что то новое переписывал все что писал до этого. Помните каждый синьор — помидор был когда то как мы джуниором) Желаю вам да и всем пользователям хабра в новом году здоровья и меньше багов.


  1. Zoolander
    28.12.2019 18:52

    я не питонист, поэтому могу ошибиться
    по названиям функций и методов пришел к выводу, что данный код генерирует список ссылок на посты только с первой страницы общей ленты Хабра. Это верно?


    1. Krau5 Автор
      28.12.2019 19:30

      Да, все верно.


  1. tabbols95
    28.12.2019 19:55

    Что-то очень плохо для начала. Ожидал большего. Просто собрать пару ссылок и записать его в txt, карл, даже не в csv.


  1. LazyTalent
    28.12.2019 20:01

    bs4 ~в 10 раз медленнее, чем lxml
    requests, чтобы парсер работал еще быстрее, то лучше использовать Session.

    session = requests.Session()
    r = session.get('http://some.url')
    

    fake_useragent — полезный инструмент, но необходимо использовать только последнию версию (иначе может поломаться). И, наверное, вместо конкретного браузера лучше использовать ua.random ("# and the best one, random via real world browser usage statistic")


    1. Krau5 Автор
      28.12.2019 20:17
      -1

      к сожалению с lxml не работал


    1. just_another_user
      30.12.2019 11:35

      Для одного запроса разве будет преимущество в скорости от использования Session?


  1. shep
    28.12.2019 22:49
    +1

    Откройте пожалуйста для себя итераторы, try...except, lxml, logging. Форматирование строк в стиле php, попробуйте f'...'. Определитесь какие вы хотите использовать кавычки для строк и делайте единообразно. Узнайте про локальную и глобальную области видимости. Вот после этого уже будет ближе к джуну.


    Попробуйте писать в PyCharm и внимательно анализируйте все предупреждения — он будет неким судьёй вашему коду.


    Хотя бы вручную тестируйре код на разные вариации. Если хабр не вернёт http200, то Вы получите ни о чем не говорящую ошибку, создадите пустой файл и после этого скрипт вылетит с исключением.


    1. Krau5 Автор
      28.12.2019 23:46

      Спасибо, прочитаю, выучу. Думаю следующая статья будет уже намного лучше по качеству кода.
      P.S работаю в VS Code, потому что PyCharm как-то не зашел.


  1. CaerDarrow
    28.12.2019 23:42

    А какая разница сколько строк? Питон выразительный язык, но зачем каждый раз писать сколько строк программа?)


    1. Krau5 Автор
      28.12.2019 23:44

      Незнаю, как другим, но по-моему чем меньше код, тем лучше.
      P.S. а считая что первый парсер я делал с помощью регуляров, то это довольно таки хороший результат)


      1. Tihon_V
        29.12.2019 20:32

        import this

        ...readability counts.


  1. soymiguel
    29.12.2019 00:56

    Ситуация «когда джун и дилетант не синонимы».

    Впечатлил даже не суп в 2019 году, а это:

    ua = UserAgent()
    headers = {'accept': '*/*', 'user-agent': ua.firefox}
    


    Вы прикидываетесь реальным браузером с acceptом */*. Какую глубокую цель вы преследуете этим двумя строками, пусть даже не обращая внимания на то, что requests и так выставляет Accept: */* по умолчанию?


    1. Krau5 Автор
      29.12.2019 01:36

      Не знал, что Accept: "/" — по умолчанию.
      Реальным браузером прикидываюсь потому, что при строке:
      response = requests.get(url)
      Мне выкидывало ConnectionError или что-то на подобии этого.


  1. kbaa
    29.12.2019 01:18

    статья-демонстрация того, что автор смог. так-то можно было и код не выкладывать, а просто написать что-то типа «ребята, я нашел пару прикольных питонячих библиотек и с ними парсить сайты проще, чем регулярками, всё ништяк, начинаю путь в миддлы»
    хочется пожелать автору 2 вещи — успешного профессионального роста (без сарказма, тут я добрый) и обдумывать, несет ли хоть какую-то пользу для окружающих подобного рода «статья» (а вот тут я злой — потому что на запрос «python scraping» гугл выдает кучу материала по requests и bs)


    1. Krau5 Автор
      29.12.2019 01:32

      Спасибо, за пожелания. На счет пользы статьи. Во время своих первых попыток парсинга я не понимал что я делаю, в то время я даже основы ООП не выучил. К сожалению про lxml я не знал, а Scrapy стал для меня слишком сложным, потому решил сделать парсер на bs4 + reuqests, зная, что хабр, кроме опытных программистов читают такие же джуны, как и я.


      1. kbaa
        29.12.2019 02:00

        вот в этом и дело. лично я, например, когда ищу что-то, проклинаю авторов статей, в которых весь материал сводится к описанию того, как они использовали пару стандартных приемов. порой много времени уходит только на то, чтобы найти нормальную публикацию по интересующему вопросу. а уж конкретно связку bs4 + requests видел даже добавляя «java» в запрос.
        если не секрет, сколько времени и/или выполненных задач прошло от «первых попыток парсинга» до «решил сделать парсер на bs4 + requests»? просто у меня в свое время на подобное (только на java) ушло пару вечеров (1й вечер поиск популярных решений, 2й вечер — чтение доков к выбранной библиотеке)


        1. Krau5 Автор
          29.12.2019 10:26

          Когда я проходил книгу Эрика Метиза, то уже тогда был зареган на фрилансе. Часто видел, что люди заказывали парсеры и за довольно большую цену. Подмечу, что на тот момент по книге я даже до классов не дошел. В результате, на то чтобы понять что такое парсер и как его писать ушло около 2-3 месяцев.


  1. dom1n1k
    29.12.2019 14:37

    В моей серии постов будет описан мой путь от Junior'a до Middle'a

    Не совсем понятно, в какой части пути вы сейчас находитесь? Это анонс будущих статей или ретроспектива? И если первое, то на какие ориентировочно календарные сроки рассчитана серия?


    1. Krau5 Автор
      29.12.2019 21:40

      Скорее анонс будущих статей. По срокам, думаю что год, возможно меньше, возможно больше.
      Тут уже как выйдет.


  1. Tihon_V
    29.12.2019 20:39

    Посмотрите на более современные человечные решения для crawling'а сайтов.


    1. Krau5 Автор
      29.12.2019 21:33

      Спасибо, заинтересовало.


  1. xelaxela
    29.12.2019 22:06

    посмотрите в сторону Scrapy, но если есть желание написать свой парсер, то в сторону асинхронности asyncio


    1. Krau5 Автор
      29.12.2019 23:47

      На счет Scrapy — подумаю, а вот asyncio — сложно.
      P.S. сложно для меня, для других оно возможно и классно