#!/usr/bin/env python
# coding: utf-8
# # Программирование на языке Python для сбора и анализа данных
#
# *Текст лекции: Будылин Р.Я., Щуров И.В., НИУ ВШЭ*
#
# Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии [Creative Commons Attribution-Share Alike 4.0](http://creativecommons.org/licenses/by-sa/4.0/). При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на [страницу курса](http://math-info.hse.ru/s15/m). Фрагменты кода, включенные в этот notebook, публикуются как [общественное достояние](http://creativecommons.org/publicdomain/zero/1.0/).
#
# Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на [странице курса](http://math-info.hse.ru/s15/m).
# ## Окружение with для открытия файлов
# В прошлый раз мы обсуждали работу с файлами, но я не успел рассказать про важную конструкцию `with`, которая часто используется для того, чтобы автоматически закрывать открытые файлы. Рассмотрим применение этой конструкции на примере.
#
# Для начала создадим файл, который будем открывать. Пусть это будет `test_123.py`. Если у вас в папке с ноутбуком лежит файл с таким названием, который очень ценен для вас, то замените значение переменной `filename` на что-то другое.
# In[1]:
filename = 'test_123.py'
# In[2]:
f = open(filename, 'w')
f.write("print('Hello, world!!!')")
f.close()
# Стандартная проблема с файлами состоит в том, чтобы любой открытый файл должен быть закрыт. На предыдущей лекции я показал вам такой синтаксис:
# In[3]:
open(filename).read()
# Это очень краткий, но не очень хороший синтаксис, потому что закрытие файла оставляется на откуп так называемой системе сборки мусора (garbage collector), и когда файл будет закрыт, точно неизвестно.
#
# > В стандартной реализации Python, которая называется CPython, garbage collector устроен таким образом, что файл закрывается сразу после выполнения этой строчки, но другие реализации могут вести себя по-другому и в некоторых ситуациях с таким кодом могут возникнуть проблемы.
#
# Лучше действовать вот так:
# In[4]:
with open(filename) as f:
print(f.read())
# При входе в конструкцию `with` выполняется строчка, эквивалентная `f = open(filename)`. Дальше выполняются строчки с отступом, а когда отступ закончится, автоматически выполнится закрытие файла. Так что эти две строчки эквивалентны таким:
# In[5]:
f = open(filename)
print(f.read())
f.close()
# Вот ещё несколько примеров:
# In[6]:
with open(filename) as f:
print(f.read())
f.seek(0)
print(f.read())
# Здесь мы используем конструкцию `f.seek(0)`, чтобы «перемотать» файл на начало — в этом случае повторный `f.read()` опять выдаст его содержимое.
#
# Теперь попробуем что-нибудь сделать с файлом после отступа.
# In[7]:
with open(filename) as f:
print(f.read())
f.seek(0)
print(f.read())
print(f.read())
# Как видим, сразу после окончания блока (выделенного, как обычно, отступом), файл оказывается закрытым.
#
# И синтаксиса `with` есть несколько плюсов по сравнению с традиционным подходом. Во-первых, вы уж точно не забудете закрыть файл, потому что можно забыть написать `f.close()`, но нельзя забыть убрать отступ. Во-вторых, даже если вы не забудете `f.close()`, вы можете не дойти до него, потому что произошла какая-то ошибка по дороге.
# ### Немного про исключения
# *Это продвинутый материал. Вы можете смело его пропустить на текущем этапе.*
#
# > В коде ниже после того, как файл был открыт, происходит деление на 0. Конструкция
#
# ```python
# try:
# something
# except Name_of_some_error:
# do_something_else
# ```
#
# > позволяет в случае, если произошла ошибка типа Name_of_some_error не заканчивать программу со словами «Все пропало! Ошибка!», а тут же передать управление блоку do_something_else, который что-нибудь сделает. Интересно, что в блоке do_something_else в примере ниже файл оказался все еще открытым, что плохо. Это можно сравнить с ситуацией: вы поставили чайник на плиту, но тут вам срочно позвонили и вы убежали, а огонь остался непогашенным.
# In[8]:
try:
f = open(filename)
print(f.read())
print(10/0)
print('This is never been printed')
f.close()
except ZeroDivisionError:
print("Ups, I did it again!")
f.seek(0)
print(f.read())
# А здесь ситуация такая: хотя мы и убежали по срочному звонку, но умный чайник тут же сам выключился. Как видим, при попытке читать из файла в блоке `except` мы получаем ошибку, и это хорошо, значит, файл закрылся, несмотря на ошибку.
# In[9]:
try:
with open(filename) as f:
print(f.read())
print(10/0)
print('This is never been printed')
except ZeroDivisionError:
print("Ups, I did it again!")
print(f.read())
# *(Конец продвинутого материала.)*
#
# ## Дописывание в файл
# Нам часто нужно сделать с файлом что-то одно — или прочитать, или записать. Иногда нам нужно модифицировать файл. Чаще всего это делается так: файл сначала считывается в память, затем в памяти модифицируется и записывается «с нуля» на то же место, что и раньше. Если речь идёт о не очень больших файлах, то этот метод нормально работает.
#
# В то же время, иногда нам нужно не перезаписать файл с нуля, а дописать какую-то информацию в конец файла. Чаще всего это приходится делать для записи логов, в которых сохраняется какая-то информация о работе программы (например, веб-сервер таким образом протоколирует, с каких адресов к нему обращались и какие страницы запрашивали). Чтобы дописать что-то в конец файла, его нужно открыть с модификатором `'a'` (от слова `append`) вот так:
# In[10]:
with open(filename, 'a') as f:
print("\n" + "print('Some new string')", file = f)
# Проверим, что старое содержимое осталось на месте
# In[11]:
with open(filename) as f:
print(f.read())
# Как видим, все ок.
# ## Извлечение данных из веб-страниц
# ### Загрузка веб-страницы: модуль `requests`
# Если у вас не сработает строчка ниже, то сделайте `pip install requests` или `conda install requests` в командной строке (например, в *Anaconda Prompt*).
# In[12]:
import requests
# Модуль `requests` позволяет получать доступ к веб-страницам. Я не буду вдаваться в подробности протокола `http`, но надо понимать, что есть два распространенных способа доступа к веб-страницам: запрос типа *get* и типа *post* (хотя на самом деле видов http-запросов гораздо больше). Запрос типа get - это когда вы передаете серверу какую-то информацию в адресной строке. Например, если вы перейдете по такому адресу: https://www.google.ru/?q=python+анализ+данных+вшэ, то этим вы просите гугл искать по запросу "python анализ данных вшэ". post-запрос - это когда вам нужно ввести информацию в какую-нибудь форму, например, ввести логин-пароль, который не будет отображать в адресной строке браузера.
#
# Мы пока будем использовать get-запросы.
# In[13]:
r = requests.get('http://vyshka.math.ru')
# Чтобы проверить, что страница нормально загрузилась есть команда
# In[14]:
r.ok
# Значение True говорит о том, что все прошло нормально.
# In[15]:
q = requests.get('http://vyshka.math.ru/ajlfdjalsdjf')
print(q.ok)
# Мы попытались перейти по несуществующей странице и она не загрузилась. Вернемся к успешному запросу `r`. Посмотрим на html исходник страницы командой
# In[16]:
print(r.text)
# ### Немного про HTML
#
# То, что вы видите выше — HTML-страница. HTML (HyperText Markup Language) — это такой язык разметки, являющийся частным случаем стандарта SGML. Другим частным случаем SGML является XML, с которым мы еще встретимся.
#
# Напишем простенькую HTML-страницу. Удобнее всего это делать в каком-либо редакторе. Но я запишу ее в файл через ноутбук.
# In[40]:
my_html = '''
Title
Hello
I'm a paragraph.
- One
- Two
'''
# In[41]:
with open('my.html', 'w') as f:
f.write(my_html)
# Откройте `my.html` браузером и вы увидите простую веб-страничку. Видно что HTML разбит на специальные фрагменты, которые называются тегами. В тексте выше есть теги: ``, ``, `` и т.д. Каждый тег отмечает какой-то кусочек веб-страницы. Тег `` — это заголовок страницы. Тег `` отмечает упорядоченный список. Тег `- ` отвечает элементу списка. Тег `
` — абзац (paragraph). Все перечисленные теги являются *парными*: они отмечают какой-то фрагмент текста (возможно, содержащий другие теги), помещая его между соответствующим открывающим и закрывающим тегом (например, `
- ` — открывающий тег, а `
` — закрывающий; всё, что между ними — это элемент списка). Исключением здесь является тег `
`, который рисует горизонтальную линию (он работает и без ``).
#
# Фактически HTML-страница представляет собой набор вложенных тегов. Можно сказать, что это дерево с корнем в теге ``. У каждого тега есть потомки - те теги, которые непосредственно вложены в него. Например, у тега `` потомками будут ``, `
`, `
`, ``. Получается такое как бы генеалогическое древо.
# HTML нас интересует с целью извлечения информации из такого дерева. Одним из наиболее популярных объектов для хранения информации являются таблицы, поэтмоу давайте вставим в наш файл небольшую таблицу: она обозначается тегом ``, каждая строка таблицы выделяется тегом `` внутри ``, а каждая ячейка — тегом `` внутри ` | `.
# In[19]:
my_html = '''
Title
Hello
I'm a paragraph.
- One
- Two
Cell 1
|
Cell 2
|
Cell 3
|
Cell 4
|
'''
with open('my.html', 'w') as f:
f.write(my_html)
# Вот так выглядит эта страница:
#
# ![Вот такая страничка с табличкой](http://math-info.hse.ru/f/2015-16/all-py/my-html.png)
#
# Допустим, что она лежит где-то на удалённом сайте. Давайте загрузим ее с помощью `requests` и попробуем извлечь какую-то информацию.
# In[20]:
r = requests.get('http://math-info.hse.ru/f/2015-16/all-py/my.html')
# ## BeautifulSoup
# Для обработки веб-страниц существует множество пакетов. Проблема с HTML в том, что большинство браузеров ведет себя «прощающе», и поэтому в вебе много плохо-написанных (не по стандарту HTML) HTML-страниц. Впрочем, обработка даже не вполне корректного HTML-кода не так сложна, если под рукой есть подходящие инструменты.
#
# Мы будем пользоваться пакетом *Beautiful Soup 4*. Он входит в стандартную поставку *Anaconda*, но если вы используете другой дистрибутив Python, возможно, вам придётся его установить вручную с помощью `pip install beautifulsoup4`.
#
# > Пакет под названием `BeautifulSoup` — скорее всего, не то, что вам нужно. Это третья версия (*Beautiful Soup 3*), а мы будем использовать четвертую. Так что нам нужен пакет `beautifulsoup4`. Чтобы было совсем весело, при импорте нужно указывать другое название пакета — `bs4`, а импортировать функцию под названием `BeautifulSoup`. В общем, сначала легко запутаться, но эти трудности нужно преодолеть однажды, а потом будет проще.
# In[21]:
from bs4 import BeautifulSoup
# Чтобы использовать *Beautiful Soup*, нужно передать функции `BeautifulSoup` текст веб-страницы (в виде одной строки). Чтобы он не ругался, я также вручную указываю название парсера (той программы, которая как раз и осуществляет обработку HTML) — с целью совместимости я использую `html.parser` (он входит в поставку Python и не требует установки), но вы можете также попробовать использовать `lxml`, если он у вас установлен.
# In[22]:
page = BeautifulSoup(r.text, 'html.parser')
# Что теперь лежит в переменной `page`? Давайте посмотрим.
# In[23]:
page
# Мы видим, что объект `page` очень похож на строку, но, на самом деле, это не просто строка. К `page` можно делать запросы. Например:
# In[24]:
page.html
# Мы видим то, что внутри тега `` (это почти вся страница, но самая первая строчка «отрезалась»). Можно пойти вглубь и посмотреть на содержимое ``.
# In[25]:
page.html.head
# Теперь мы видим только то, что внутри тега ``. Мы можем пойти еще глубже, и получить то, что находится внутри тега ``, который в свою очередь находится внутри тега `` (говорят, что `` является *потомком* ``:
# In[26]:
page.html.head.title
# Впрочем, можно было бы и не писать так подробно — поскольку в документе есть только один тег ``, мы бы могли не указывать, что он находится внутри ``, который находится внутри ``.
# In[27]:
page.head.title
# In[28]:
page.title
# Одним из потомков `` является ``. Ее можно получить вот так.
# In[29]:
page.body.table
# Допустим, что мне нужно получить несколько элементов с одинаковым тегом, например, все строки ``. Для этого используется такой синтаксис:
# In[30]:
rows = page.body.table.findAll('tr')
rows
# In[31]:
len(rows)
# Мы видим, что это список из двух элементов. Так что по нему можно пройти циклом.
# In[32]:
for i, row in enumerate(rows):
print(i)
print(row)
# У нас есть 2 строчки и каждая из них является таким же объектом BeautifulSoup, как и все предыдущие. Так что к ним можно применить конструкцию row.td
# In[33]:
for i, row in enumerate(rows):
print(i)
print(row.td)
# Мы видим, что если внутри тега `` есть несколько тегов ``, то row.td возьмет первый из них. Поэтому мы получили первый столбец. Но нас интересует не сам тег ` | `, а строка, которая там лежит. Её можно напечатать вот так.
# In[34]:
for i, row in enumerate(rows):
print(i)
print(row.td.string)
# Видно, что перед строкой идут ненужные пробелы. Удалим их командой strip
# In[35]:
for i, row in enumerate(rows):
print(i)
print(row.td.string.strip())
# Давайте загрузим таблицу в виде списка списков
# In[36]:
table = []
for i, row in enumerate(rows):
table.append([])
for cell in row.findAll('td'):
table[-1].append(cell.string.strip())
print(table)
# Вот то же самое, но короче с помощью list comprehensions:
# In[37]:
table = []
for row in rows:
table.append([cell.string.strip() for cell in row.findAll('td')])
print(table)
# Или еще короче (но заковыристее):
# In[38]:
table = [[cell.string.strip() for cell in row.findAll('td')]
for row in rows]
print(table)
# Заметим, что вместо `some_beautiful_soup_objec.findAll('sometag')` можно писать короче `some_beautiful_soup_object('sometag')`. Так что можно написать еще короче
# In[39]:
table = [[cell.string.strip() for cell in row('td')]
for row in rows]
print(table)
# У тегов, кроме названия, бывают еще свойства — например, в строчке `` мы видим свойство `lang` у тега ``, имеющее значение `"en"`. Другим важным примером тега со свойствами является тег ``, который создает ссылку. У него есть свойство `href`, которое хранит собственно ссылку.
# > Например, строка
# > `Курс по Python`
# > превращается в ссылку
# > Курс по Python,
# > ведущую на страницу нашего курса.
# Теперь представим себе, что мы хотим сделать робота, который будет ходить по веб-страницам, и переходить с одной страницы на другую по ссылкам. Тогда мы сталкиваемся с задачей извлечь из страницы все гиперссылки.
# Для этого нужно найти все теги `` на странице, и у всех них взять параметр ``. Для начала покажем как получить свойство объекта, например, `lang` у `html`. Это делается так как будто наш объект словарь, и мы берем его значение по ключу.
# In[103]:
page.html['lang']
# Если запросить свойство, которое тег не имеет, то мы получим KeyError, как и со словарем.
# In[104]:
page.html['strange']
# Так же, как у словаря, есть метод `get()`, который ничего не возвращает, если такого свойства нет. Или возвращает значение по умолчанию, определенное нами.
# In[105]:
page.html.get('strange')
# In[106]:
page.html.get('strange', 'no-such-tag')
# Теперь извлечем все ссылки с какого-нибудь сайта
# In[108]:
r = requests.get('http://vyshka.math.ru')
page = BeautifulSoup(r.text, 'html.parser')
# Вот все ссылки на нашей странице.
# In[109]:
page('a')
# Как видим, метод `findAll()` (или его сокращённая форма записи в виде просто скобочек) ищет не только по непосредственным «детям» какой-то вершины (в генеалогических терминах), но и по всем потомкам.
# Напечатаем сами ссылки
# In[110]:
for link in page("a"):
print(link['href'])
# Тут есть внешние гиперссылки, которые начинаются с `http`, и локальные, которые ведут на тот же сайт и носят относительный характер (то есть перед `1516/topology2.php` нужно написать `http://math.hse.ru/`, чтобы получить полную ссылку на соответствующий документ).
#
# Теперь понятно, как должен действовать наш робот: для каждой из полученных ссылок он должен загрузить соответствующую страницу, найти на ней все ссылки, добавить их в очередь для исследования и т.д. Примерно так работают веб-краулеры поисковых систем. (Хотя, конечно, они устроены гораздо сложнее.)
#
# ## P.S. Документация — ваш друг
# До сих пор мы старались включать в конспекты все материалы, которые необходимы для выполнения домашних заданий. Начиная с этого момента ситуация меняется: мы изучаем библиотеки, обладающие огромным количеством функций, и не можем включить их полное описание в конспект. Отныне лекции будут скорее примером использования конкретной библиотеки, демонстрирующим общие принципы, но описание всех функций и способов работы с ними вам придётся искать в официальной документации. Чаще всего её можно найти на странице соответствующей библиотеки, которая находится любым веб-поиском по названию. Для *Beautiful Soup* документация лежит [здесь](http://www.crummy.com/software/BeautifulSoup/bs4/doc/), а для *requests* [здесь](http://docs.python-requests.org/en/latest/) (начните с Quickstart). Конечно, она на английском языке, но, как говорил мой преподаватель по программированию, «через полгода занятий программированием вы будете считать английский язык подмножеством русского».
#
# Другой источник информации о библиотеках — всё тот же веб-поиск, который чаще всего будет выдавать ссылки на сайт с вопросами и ответами http://stackoverflow.com/. Например, набрав [how to parse table with beautifulsoup](https://www.google.ru/search?q=how+to+parse+table+with+beautifulsoup&gws_rd=cr&ei=wXaJVvzQKIfXyQO4v4PYDw) вы получите несколько ссылок на stackoverflow с примерами кода. Кстати, на stackoverflow можно задавать и свои вопросы — но прежде нужно убедиться, что на них не ответили раньше.
# In[ ]:
|