Программирование на языке Python для сбора и анализа данных

Текст лекции: Будылин Р.Я., Щуров И.В., НИУ ВШЭ

Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.

Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.

Окружение 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()
Out[3]:
"print('Hello, world!!!')"

Это очень краткий, но не очень хороший синтаксис, потому что закрытие файла оставляется на откуп так называемой системе сборки мусора (garbage collector), и когда файл будет закрыт, точно неизвестно.

В стандартной реализации Python, которая называется CPython, garbage collector устроен таким образом, что файл закрывается сразу после выполнения этой строчки, но другие реализации могут вести себя по-другому и в некоторых ситуациях с таким кодом могут возникнуть проблемы.

Лучше действовать вот так:

In [4]:
with open(filename) as f:
    print(f.read())
print('Hello, world!!!')

При входе в конструкцию with выполняется строчка, эквивалентная f = open(filename). Дальше выполняются строчки с отступом, а когда отступ закончится, автоматически выполнится закрытие файла. Так что эти две строчки эквивалентны таким:

In [5]:
f = open(filename)
print(f.read())
f.close()
print('Hello, world!!!')

Вот ещё несколько примеров:

In [6]:
with open(filename) as f:
    print(f.read())
    f.seek(0)
    print(f.read())
print('Hello, world!!!')
print('Hello, world!!!')

Здесь мы используем конструкцию f.seek(0), чтобы «перемотать» файл на начало — в этом случае повторный f.read() опять выдаст его содержимое.

Теперь попробуем что-нибудь сделать с файлом после отступа.

In [7]:
with open(filename) as f:
    print(f.read())
    f.seek(0)
    print(f.read())
print(f.read())
print('Hello, world!!!')
print('Hello, world!!!')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-7-2b57747c6b28> in <module>()
      3     f.seek(0)
      4     print(f.read())
----> 5 print(f.read())

ValueError: I/O operation on closed file.

Как видим, сразу после окончания блока (выделенного, как обычно, отступом), файл оказывается закрытым.

И синтаксиса with есть несколько плюсов по сравнению с традиционным подходом. Во-первых, вы уж точно не забудете закрыть файл, потому что можно забыть написать f.close(), но нельзя забыть убрать отступ. Во-вторых, даже если вы не забудете f.close(), вы можете не дойти до него, потому что произошла какая-то ошибка по дороге.

Немного про исключения

Это продвинутый материал. Вы можете смело его пропустить на текущем этапе.

В коде ниже после того, как файл был открыт, происходит деление на 0. Конструкция

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())
    
print('Hello, world!!!')
Ups, I did it again!
print('Hello, world!!!')

А здесь ситуация такая: хотя мы и убежали по срочному звонку, но умный чайник тут же сам выключился. Как видим, при попытке читать из файла в блоке 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())
    
print('Hello, world!!!')
Ups, I did it again!
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-9-6c966bd01232> in <module>()
      3         print(f.read())
----> 4         print(10/0)
      5         print('This is never been printed')

ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-9-6c966bd01232> in <module>()
      6 except ZeroDivisionError:
      7     print("Ups, I did it again!")
----> 8     print(f.read())
      9 

ValueError: I/O operation on closed file.

(Конец продвинутого материала.)

Дописывание в файл

Нам часто нужно сделать с файлом что-то одно — или прочитать, или записать. Иногда нам нужно модифицировать файл. Чаще всего это делается так: файл сначала считывается в память, затем в памяти модифицируется и записывается «с нуля» на то же место, что и раньше. Если речь идёт о не очень больших файлах, то этот метод нормально работает.

В то же время, иногда нам нужно не перезаписать файл с нуля, а дописать какую-то информацию в конец файла. Чаще всего это приходится делать для записи логов, в которых сохраняется какая-то информация о работе программы (например, веб-сервер таким образом протоколирует, с каких адресов к нему обращались и какие страницы запрашивали). Чтобы дописать что-то в конец файла, его нужно открыть с модификатором '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())
print('Hello, world!!!')
print('Some new string')

Как видим, все ок.

Извлечение данных из веб-страниц

Загрузка веб-страницы: модуль 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
Out[14]:
True

Значение True говорит о том, что все прошло нормально.

In [15]:
q = requests.get('http://vyshka.math.ru/ajlfdjalsdjf')
print(q.ok)
False

Мы попытались перейти по несуществующей странице и она не загрузилась. Вернемся к успешному запросу r. Посмотрим на html исходник страницы командой

In [16]:
print(r.text)
<html>

<head>

   <title>HSE: Math department</title>

   <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=koi8-r">

</head>

<body bgcolor="#F5DEB3">

<!--begin of Top100 code -->

<a href="http://top100.rambler.ru/top100/">

<img src="http://counter.rambler.ru/top100.cnt?188597" alt="" width=1
height=1 border=0></a>

<!--end of Top100 code-->


<h1>æÁËÕÌØÔÅÔ ÍÁÔÅÍÁÔÉËÉ îéõ ÷ûü</h1>

<table cellspacing=10>
<tr>
  <td align="left" valign="center" width=195>
    <img src="img/matfak.jpg"
         alt="äÏÍ ÎÁ ÷Á×ÉÌÏ×Á"
         width=193 height=398
         align="right">
  </td>
  <td align="left">
      üÔÏÔ ÓÁÊÔ, ÐÏÄÄÅÒÖÉ×ÁÅÍÙÊ ÎÅÓËÏÌØËÉÍÉ ÓÏÔÒÕÄÎÉËÁÍÉ ÆÁËÕÌØÔÅÔÁ, ÚÁÄÕÍÁÎ 
      ËÁË ÄÏÐÏÌÎÅÎÉÅ Ë <a href="http://math.hse.ru/">ÏÆÉÃÉÁÌØÎÏÍÕ ÓÁÊÔÕ 
      ÆÁËÕÌØÔÅÔÁ ÍÁÔÅÍÁÔÉËÉ</a> ÷ÙÓÛÅÊ ÛËÏÌÙ ÜËÏÎÏÍÉËÉ (<a 
      href="http://hse.ru">îéõ ÷ûü</a>). íÙ ÐÌÁÎÉÒÕÅÍ ÒÁÚÍÅÝÁÔØ ÚÄÅÓØ 
      ÍÁÔÅÒÉÁÌÙ ËÕÒÓÏ× É ÉÎÆÏÒÍÁÃÉÀ Ï ÔÅËÕÝÉÈ ÄÅÌÁÈ.
      </font>
  </td>
</table>

<h2>ëÕÒÓÙ 2015/16 ÕÞÅÂÎÏÇÏ ÇÏÄÁ</h2>
      
<ul>
<li>1 ËÕÒÓ ÂÁËÁÌÁ×ÒÉÁÔÁ.

<li>2 ËÕÒÓ ÂÁËÁÌÁ×ÒÉÁÔÁ.
    <ul>
    <li><a href="1516/topology2.php">÷×ÅÄÅÎÉÅ × ÔÏÐÏÌÏÇÉÀ</a>    <li><a href="1516/algebra2.php">áÌÇÅÂÒÁ</a>    </ul>
<li>3&ndash;4 ËÕÒÓÙ ÂÁËÁÌÁ×ÒÉÁÔÁ.
<br>(×ÓÅ ÐÒÅÄÍÅÔÙ ÐÏ ×ÙÂÏÒÕ)
    <ul>
    <li><a href="1516/funcan.php">æÕÎËÃÉÏÎÁÌØÎÙÊ ÁÎÁÌÉÚ</a>    <li><a href="1516/DiffGeom.php">äÉÆÆÅÒÅÎÃÉÁÌØÎÁÑ ÇÅÏÍÅÔÒÉÑ</a>    </ul>


<li>1 ËÕÒÓ ÍÁÇÉÓÔÒÁÔÕÒÙ.
<br>îÁ 2 ËÕÒÓÅ ÍÁÇÉÓÔÒÁÔÕÒÙ É × ÁÓÐÉÒÁÎÔÕÒÅ ÏÂÑÚÁÔÅÌØÎÙÈ ÚÁÎÑÔÉÊ ÎÅÔ, ÔÏÌØËÏ 
ÓÐÅÃËÕÒÓÙ É ÓÐÅÃÓÅÍÉÎÁÒÙ.
<li>óÐÅÃËÕÒÓÙ
    <ul>
    <li><a href="1516/tvs.php">ôÏÐÏÌÏÇÉÞÅÓËÉÅ ×ÅËÔÏÒÎÙÅ ÐÒÏÓÔÒÁÎÓÔ×Á</a>    </ul>
<li>óÐÅÃÓÅÍÉÎÁÒÙ
    <ul>
    <li><a href="1516/basic.php">ïÓÎÏ×ÎÙÅ ÐÏÎÑÔÉÑ ÍÁÔÅÍÁÔÉËÉ</a>    </ul> 

</ul>

<h2>ëÕÒÓÙ ÐÒÏÛÌÙÈ ÌÅÔ</h2>
<ul>
<li><a href="0809/0809.html">2008/09</a>
<li><a href="0910/0910.html">2009/10</a>
<li><a href="1011/1011.html">2010/11</a>
<li><a href="1112/1112.html">2011/12</a>
<li><a href="1213/1213.php">2012/13</a>
<li><a href="1415/1415.php">2014/15</a>
</ul>
</body>

</html>

Немного про HTML

То, что вы видите выше — HTML-страница. HTML (HyperText Markup Language) — это такой язык разметки, являющийся частным случаем стандарта SGML. Другим частным случаем SGML является XML, с которым мы еще встретимся.

Напишем простенькую HTML-страницу. Удобнее всего это делать в каком-либо редакторе. Но я запишу ее в файл через ноутбук.

In [40]:
my_html = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset = "UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello</h1>
<p>I'm a paragraph.</p>
<hr>
<ol>
    <li>One</li>
    <li>Two</li>
</ol>
    
</body>
</html>
'''
In [41]:
with open('my.html', 'w') as f:
    f.write(my_html)

Откройте my.html браузером и вы увидите простую веб-страничку. Видно что HTML разбит на специальные фрагменты, которые называются тегами. В тексте выше есть теги: <html>, <head>, <title> и т.д. Каждый тег отмечает какой-то кусочек веб-страницы. Тег <title> — это заголовок страницы. Тег <ol> отмечает упорядоченный список. Тег <li> отвечает элементу списка. Тег <p> — абзац (paragraph). Все перечисленные теги являются парными: они отмечают какой-то фрагмент текста (возможно, содержащий другие теги), помещая его между соответствующим открывающим и закрывающим тегом (например, <li> — открывающий тег, а </li> — закрывающий; всё, что между ними — это элемент списка). Исключением здесь является тег <hr>, который рисует горизонтальную линию (он работает и без </hr>).

Фактически HTML-страница представляет собой набор вложенных тегов. Можно сказать, что это дерево с корнем в теге <html>. У каждого тега есть потомки - те теги, которые непосредственно вложены в него. Например, у тега <body> потомками будут <h1>, <p>, <hr>, <ol>. Получается такое как бы генеалогическое древо.

HTML нас интересует с целью извлечения информации из такого дерева. Одним из наиболее популярных объектов для хранения информации являются таблицы, поэтмоу давайте вставим в наш файл небольшую таблицу: она обозначается тегом <table>, каждая строка таблицы выделяется тегом <tr> внутри <table>, а каждая ячейка — тегом <td> внутри <tr>.

In [19]:
my_html = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset = "UTF-8">
    <title>Title</title>
    <style type='text/css;'>
        table {
        border-collapse: collapse;
    }

    table, th, td {
        border: 1px solid black;
    }
    </style>
</head>
<body>
<h1>Hello</h1>
<p>I'm a paragraph.</p>
<hr>
<ol>
    <li>One</li>
    <li>Two</li>
</ol>
<table>
    <tr>
        <td>
            Cell 1
        </td>
        <td>
            Cell 2
        </td>
    </tr>
    <tr>
        <td>
            Cell 3
        </td>
        <td>
            Cell 4
        </td>
    </tr>
</table>
</body>
</html>
'''
with open('my.html', 'w') as f:
    f.write(my_html)

Вот так выглядит эта страница:

Вот такая страничка с табличкой

Допустим, что она лежит где-то на удалённом сайте. Давайте загрузим ее с помощью 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
Out[23]:
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="utf-8">
<title>Title</title>
<style type="text/css;">
        table {
        border-collapse: collapse;
    }

    table, th, td {
        border: 1px solid black;
    }
    </style>
</meta></head>
<body>
<h1>Hello</h1>
<p>I'm a paragraph.</p>
<hr>
<ol>
<li>One</li>
<li>Two</li>
</ol>
<table>
<tr>
<td>
            Cell 1
        </td>
<td>
            Cell 2
        </td>
</tr>
<tr>
<td>
            Cell 3
        </td>
<td>
            Cell 4
        </td>
</tr>
</table>
</hr></body>
</html>

Мы видим, что объект page очень похож на строку, но, на самом деле, это не просто строка. К page можно делать запросы. Например:

In [24]:
page.html
Out[24]:
<html lang="en">
<head>
<meta charset="utf-8">
<title>Title</title>
<style type="text/css;">
        table {
        border-collapse: collapse;
    }

    table, th, td {
        border: 1px solid black;
    }
    </style>
</meta></head>
<body>
<h1>Hello</h1>
<p>I'm a paragraph.</p>
<hr>
<ol>
<li>One</li>
<li>Two</li>
</ol>
<table>
<tr>
<td>
            Cell 1
        </td>
<td>
            Cell 2
        </td>
</tr>
<tr>
<td>
            Cell 3
        </td>
<td>
            Cell 4
        </td>
</tr>
</table>
</hr></body>
</html>

Мы видим то, что внутри тега <html> (это почти вся страница, но самая первая строчка «отрезалась»). Можно пойти вглубь и посмотреть на содержимое <head>.

In [25]:
page.html.head
Out[25]:
<head>
<meta charset="utf-8">
<title>Title</title>
<style type="text/css;">
        table {
        border-collapse: collapse;
    }

    table, th, td {
        border: 1px solid black;
    }
    </style>
</meta></head>

Теперь мы видим только то, что внутри тега <head>. Мы можем пойти еще глубже, и получить то, что находится внутри тега <title>, который в свою очередь находится внутри тега <head> (говорят, что <title> является потомком <head>:

In [26]:
page.html.head.title
Out[26]:
<title>Title</title>

Впрочем, можно было бы и не писать так подробно — поскольку в документе есть только один тег <title>, мы бы могли не указывать, что он находится внутри <head>, который находится внутри <html>.

In [27]:
page.head.title
Out[27]:
<title>Title</title>
In [28]:
page.title
Out[28]:
<title>Title</title>

Одним из потомков <body> является <table>. Ее можно получить вот так.

In [29]:
page.body.table
Out[29]:
<table>
<tr>
<td>
            Cell 1
        </td>
<td>
            Cell 2
        </td>
</tr>
<tr>
<td>
            Cell 3
        </td>
<td>
            Cell 4
        </td>
</tr>
</table>

Допустим, что мне нужно получить несколько элементов с одинаковым тегом, например, все строки <tr>. Для этого используется такой синтаксис:

In [30]:
rows = page.body.table.findAll('tr')
rows
Out[30]:
[<tr>
 <td>
             Cell 1
         </td>
 <td>
             Cell 2
         </td>
 </tr>, <tr>
 <td>
             Cell 3
         </td>
 <td>
             Cell 4
         </td>
 </tr>]
In [31]:
len(rows)
Out[31]:
2

Мы видим, что это список из двух элементов. Так что по нему можно пройти циклом.

In [32]:
for i, row in enumerate(rows):
    print(i)
    print(row)
0
<tr>
<td>
            Cell 1
        </td>
<td>
            Cell 2
        </td>
</tr>
1
<tr>
<td>
            Cell 3
        </td>
<td>
            Cell 4
        </td>
</tr>

У нас есть 2 строчки и каждая из них является таким же объектом BeautifulSoup, как и все предыдущие. Так что к ним можно применить конструкцию row.td

In [33]:
for i, row in enumerate(rows):
    print(i)
    print(row.td)
0
<td>
            Cell 1
        </td>
1
<td>
            Cell 3
        </td>

Мы видим, что если внутри тега <row> есть несколько тегов <td>, то row.td возьмет первый из них. Поэтому мы получили первый столбец. Но нас интересует не сам тег <td>, а строка, которая там лежит. Её можно напечатать вот так.

In [34]:
for i, row in enumerate(rows):
    print(i)
    print(row.td.string)
0

            Cell 1
        
1

            Cell 3
        

Видно, что перед строкой идут ненужные пробелы. Удалим их командой strip

In [35]:
for i, row in enumerate(rows):
    print(i)
    print(row.td.string.strip())
0
Cell 1
1
Cell 3

Давайте загрузим таблицу в виде списка списков

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)
[['Cell 1', 'Cell 2'], ['Cell 3', 'Cell 4']]

Вот то же самое, но короче с помощью list comprehensions:

In [37]:
table = []
for row in rows:
    table.append([cell.string.strip() for cell in row.findAll('td')]) 
print(table)
[['Cell 1', 'Cell 2'], ['Cell 3', 'Cell 4']]

Или еще короче (но заковыристее):

In [38]:
table = [[cell.string.strip() for cell in row.findAll('td')] 
         for row in rows]
print(table)
[['Cell 1', 'Cell 2'], ['Cell 3', 'Cell 4']]

Заметим, что вместо 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)
[['Cell 1', 'Cell 2'], ['Cell 3', 'Cell 4']]

У тегов, кроме названия, бывают еще свойства — например, в строчке <html lang="en"> мы видим свойство lang у тега <html>, имеющее значение "en". Другим важным примером тега со свойствами является тег <a>, который создает ссылку. У него есть свойство href, которое хранит собственно ссылку.

Например, строка <a href="http://math-info.hse.ru/s15/m">Курс по Python</a> превращается в ссылку Курс по Python, ведущую на страницу нашего курса.

Теперь представим себе, что мы хотим сделать робота, который будет ходить по веб-страницам, и переходить с одной страницы на другую по ссылкам. Тогда мы сталкиваемся с задачей извлечь из страницы все гиперссылки. Для этого нужно найти все теги <a> на странице, и у всех них взять параметр <href>. Для начала покажем как получить свойство объекта, например, lang у html. Это делается так как будто наш объект словарь, и мы берем его значение по ключу.

In [103]:
page.html['lang']
Out[103]:
'en'

Если запросить свойство, которое тег не имеет, то мы получим KeyError, как и со словарем.

In [104]:
page.html['strange']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-104-e19396244582> in <module>()
----> 1 page.html['strange']

/usr/local/lib/python3.5/site-packages/bs4/element.py in __getitem__(self, key)
    956         """tag[key] returns the value of the 'key' attribute for the tag,
    957         and throws an exception if it's not there."""
--> 958         return self.attrs[key]
    959 
    960     def __iter__(self):

KeyError: 'strange'

Так же, как у словаря, есть метод get(), который ничего не возвращает, если такого свойства нет. Или возвращает значение по умолчанию, определенное нами.

In [105]:
page.html.get('strange')
In [106]:
page.html.get('strange', 'no-such-tag')
Out[106]:
'no-such-tag'

Теперь извлечем все ссылки с какого-нибудь сайта

In [108]:
r = requests.get('http://vyshka.math.ru')
page = BeautifulSoup(r.text, 'html.parser')

Вот все ссылки на нашей странице.

In [109]:
page('a')
Out[109]:
[<a href="http://top100.rambler.ru/top100/">
 <img alt="" border="0" height="1" src="http://counter.rambler.ru/top100.cnt?188597" width="1"/></a>,
 <a href="http://math.hse.ru/">ÏÆÉÃÉÁÌØÎÏÍÕ ÓÁÊÔÕ 
       ÆÁËÕÌØÔÅÔÁ ÍÁÔÅÍÁÔÉËÉ</a>,
 <a href="http://hse.ru">îéõ ÷ûü</a>,
 <a href="1516/topology2.php">÷×ÅÄÅÎÉÅ × ÔÏÐÏÌÏÇÉÀ</a>,
 <a href="1516/algebra2.php">áÌÇÅÂÒÁ</a>,
 <a href="1516/funcan.php">æÕÎËÃÉÏÎÁÌØÎÙÊ ÁÎÁÌÉÚ</a>,
 <a href="1516/DiffGeom.php">äÉÆÆÅÒÅÎÃÉÁÌØÎÁÑ ÇÅÏÍÅÔÒÉÑ</a>,
 <a href="1516/tvs.php">ôÏÐÏÌÏÇÉÞÅÓËÉÅ ×ÅËÔÏÒÎÙÅ ÐÒÏÓÔÒÁÎÓÔ×Á</a>,
 <a href="1516/basic.php">ïÓÎÏ×ÎÙÅ ÐÏÎÑÔÉÑ ÍÁÔÅÍÁÔÉËÉ</a>,
 <a href="0809/0809.html">2008/09</a>,
 <a href="0910/0910.html">2009/10</a>,
 <a href="1011/1011.html">2010/11</a>,
 <a href="1112/1112.html">2011/12</a>,
 <a href="1213/1213.php">2012/13</a>,
 <a href="1415/1415.php">2014/15</a>]

Как видим, метод findAll() (или его сокращённая форма записи в виде просто скобочек) ищет не только по непосредственным «детям» какой-то вершины (в генеалогических терминах), но и по всем потомкам.

Напечатаем сами ссылки

In [110]:
for link in page("a"):
    print(link['href'])
http://top100.rambler.ru/top100/
http://math.hse.ru/
http://hse.ru
1516/topology2.php
1516/algebra2.php
1516/funcan.php
1516/DiffGeom.php
1516/tvs.php
1516/basic.php
0809/0809.html
0910/0910.html
1011/1011.html
1112/1112.html
1213/1213.php
1415/1415.php

Тут есть внешние гиперссылки, которые начинаются с http, и локальные, которые ведут на тот же сайт и носят относительный характер (то есть перед 1516/topology2.php нужно написать http://math.hse.ru/, чтобы получить полную ссылку на соответствующий документ).

Теперь понятно, как должен действовать наш робот: для каждой из полученных ссылок он должен загрузить соответствующую страницу, найти на ней все ссылки, добавить их в очередь для исследования и т.д. Примерно так работают веб-краулеры поисковых систем. (Хотя, конечно, они устроены гораздо сложнее.)

P.S. Документация — ваш друг

До сих пор мы старались включать в конспекты все материалы, которые необходимы для выполнения домашних заданий. Начиная с этого момента ситуация меняется: мы изучаем библиотеки, обладающие огромным количеством функций, и не можем включить их полное описание в конспект. Отныне лекции будут скорее примером использования конкретной библиотеки, демонстрирующим общие принципы, но описание всех функций и способов работы с ними вам придётся искать в официальной документации. Чаще всего её можно найти на странице соответствующей библиотеки, которая находится любым веб-поиском по названию. Для Beautiful Soup документация лежит здесь, а для requests здесь (начните с Quickstart). Конечно, она на английском языке, но, как говорил мой преподаватель по программированию, «через полгода занятий программированием вы будете считать английский язык подмножеством русского».

Другой источник информации о библиотеках — всё тот же веб-поиск, который чаще всего будет выдавать ссылки на сайт с вопросами и ответами http://stackoverflow.com/. Например, набрав how to parse table with beautifulsoup вы получите несколько ссылок на stackoverflow с примерами кода. Кстати, на stackoverflow можно задавать и свои вопросы — но прежде нужно убедиться, что на них не ответили раньше.