Импортируем библиотеки и функцию BeautifulSoup
(понадобятся для разных примеров ниже):
requests
для отправки запроса и получения кода HTML веб-страницы;bs4
для поиска тэгов в коде HTML;pandas
для обработки полученной информации и приведения ее к табличному виду.import requests
import pandas as pd
from bs4 import BeautifulSoup
У нас было домашнее задание на парсинг страницы фильма «Не покидай...» с сайта www.kino-teatr.ru. Сайт некоммерческий, довольно дружелюбный, позволяет свободно выгружать информацию. Но у него есть одна особенность: число лайков и дизлайков, поставленных актерам пользователями, загружается на страницу динамически, то есть автоматически «подтягивается» с сервера при загрузке страницы в определенный момент времени. На практике это выливается в то, что найти нужную информацию по тэгам просто невозможно, ее нет в основном коде HTML. Как быть? Понять, как выглядит запрос данных, который отправляется на сервер, и выяснить, где хранятся нужные нам данные. Мы рассмотрим несложный случай, когда сайт забирает информацию из строки JSON, которая находится на странице, но внутри кода, написанного на JavaScript. Такое можно встретить на страницах с результатами каких-нибудь игр или на сайтах, посвященных динамике цен или курсу валют (другой вопрос, что не всегда JSON прямо так явно находится в том же файле, где и код HTML).
Так как ранее мы обсуждали довольно базовый парсинг, в домашнем задании не требовалось собирать число лайков и дизлайков, эти значения были даны в виде готового списка словарей. Обновим задачу – теперь нам нужно собрать имена актеров и их id, а затем «подтянуть» к этой информации число голосов за и против.
Начало работы стандартное – загружаем код HTML страницы по ссылке и преобразуем его в объект BeautifulSoup:
page = requests.get("https://www.kino-teatr.ru/kino/movie/sov/4319/titr/")
soup = BeautifulSoup(page.text)
Ищем имена актеров – находим блоки с тэгами <div>
с классом film_name
и вытаскиваем из них «чистый» текст:
names_raw = soup.find_all("div", {"class" : "film_name"})
names = [name.text for name in names_raw]
print(names[0:10]) # первые 10 для примера
['Лидия Федосеева-Шукшина', 'Вячеслав Невинный', 'Игорь Красавин', 'Варвара Владимирова', 'Светлана Селезнёва', 'Регина Разума', 'Альберт Филозов', 'Артём Тынкасов', 'Елена Антонова', 'Владимир Ставицкий']
Теперь ищем id, они нам понадобятся для совмещения с информацией по числу голосов за и против:
divs = soup.find_all("div", {"class" : "actor_film_descript"})
print(divs[0:2])
[<div class="actor_film_descript" id="role_16801"> <div class="film_name"><a href="/kino/acter/w/sov/4484/bio/" itemprop="url" title="Лидия Федосеева-Шукшина"><strong itemprop="name">Лидия Федосеева-Шукшина</strong></a></div> <div class="film_role">Королева Флора — <span class="film_main_role">главная роль</span></div> <div class="film_role_descript">жена короля Теодора</div> <div class="film_rating"><span id="role_rating_16801"></span></div> </div>, <div class="actor_film_descript" id="role_16800"> <div class="film_name"><a href="/kino/acter/m/sov/3035/bio/" itemprop="url" title="Вячеслав Невинный"><strong itemprop="name">Вячеслав Невинный</strong></a></div> <div class="film_role">Король Теодор — <span class="film_main_role">главная роль</span></div> <div class="film_rating"><span id="role_rating_16800"></span></div> </div>]
Как можно заметить, числовых id здесь нет, но это легко исправить – забрать значения атрибута id
через метод .get()
(вспоминаем о сходстве объектов BeautifulSoup и словарей), разбить их по символу _
и забрать часть после _
с индексом 1:
ids = [i.get("id").split("_")[1] for i in divs]
print(ids[0:10])
['16801', '16800', '16803', '16802', '89473', '124124', '16804', '132138', '56008', '132139']
Теперь воспользуемся тем, что функция DataFrame()
из библиотеки pandas умеет превращать в датафрейм не только списки списков или словари, но и списки кортежей. Объединим элементы в список попарно через функцию zip()
и сконвертируем перечень пар-кортежей в датафрейм:
# напоминание: как выглядят элементы в zip()
list(zip(ids, names))[0:10]
[('16801', 'Лидия Федосеева-Шукшина'), ('16800', 'Вячеслав Невинный'), ('16803', 'Игорь Красавин'), ('16802', 'Варвара Владимирова'), ('89473', 'Светлана Селезнёва'), ('124124', 'Регина Разума'), ('16804', 'Альберт Филозов'), ('132138', 'Артём Тынкасов'), ('56008', 'Елена Антонова'), ('132139', 'Владимир Ставицкий')]
main = pd.DataFrame(zip(ids, names))
main.columns = ["id", "name"]
main
id | name | |
---|---|---|
0 | 16801 | Лидия Федосеева-Шукшина |
1 | 16800 | Вячеслав Невинный |
2 | 16803 | Игорь Красавин |
3 | 16802 | Варвара Владимирова |
4 | 89473 | Светлана Селезнёва |
5 | 124124 | Регина Разума |
6 | 16804 | Альберт Филозов |
7 | 132138 | Артём Тынкасов |
8 | 56008 | Елена Антонова |
9 | 132139 | Владимир Ставицкий |
10 | 72744 | Анатолий Рудаков |
11 | 132140 | Юрий Багинян |
12 | 62460 | Александр Денисов |
13 | 132141 | Валентин Букин |
14 | 83813 | Анатолий Голуб |
15 | 132142 | Саша Занько |
16 | 132143 | Вика Яблонская |
17 | 2030314 | А. Баутенко |
18 | 1973004 | С. Гецингер |
19 | 1973001 | Дмитрий Диджиокас |
20 | 1973002 | Владимир Зубенко |
21 | 1973003 | Владимир Корпусь |
22 | 1966145 | Ивар Кумник |
23 | 1973005 | Анна Маланкина |
24 | 1973000 | Валерий Мороз |
25 | 160325 | Леонид Нечаев |
26 | 1973006 | И. Окал |
27 | 1928641 | Анна Портная |
28 | 1973007 | Ростислав Рахт |
29 | 1973009 | В. Славуник |
30 | 1973008 | Юрий Шульга |
31 | 2088754 | Александр Макарцев |
32 | 1973010 | Евгений Герчаков |
33 | 1973011 | Сергей Дрейден |
34 | 222174 | Анатолий Тукиш |
35 | 1973015 | Виктор Борцов |
36 | 1973016 | Михаил Кокшенов |
37 | 1973012 | Ольга Машная |
38 | 1973017 | Леонид Нечаев |
39 | 1973020 | Илья Рутберг |
40 | 1973019 | Лариса Удовиченко |
41 | 1973013 | Елена Цыплакова |
42 | 1973014 | Борис Шувалов |
43 | 1973018 | Игорь Ясулович |
44 | 2496375 | Н. Острова |
45 | 2496376 | Анатолий Тукиш |
46 | 1914304 | Павел Бабаков |
47 | 2496373 | Улдис-Янис Вейспалс |
48 | 2022950 | Сергей Головкин |
Теперь переходим к более сложной части – поиску голосов за и против. Просто найти на странице кнопки красного и зеленого цвета и забрать с них текст не получится:
Поэтому для этого на нужно найти код JavaScript, где есть записи с числами plus
и minus
с привязкой к id актеров. Код JavaScript, если он не вынесен в отдельный файл, заключается в тэги <script>
. Если мы внимательно изучим исходный код страницы, мы заметим, что нужный нам блок кода имеет атрибут type
со значением text/javascript
, и среди таких блоков он находится на 7-ом месте:
# выполняем поиск и извлекаем текст – код JavaScript в виде строки
text = soup.find_all("script", {"type" : "text/javascript"})[6].text
text
'\r\n<!--\r\n\r\n// Рисуем кнопки в javascript\r\nfunction print_role_rating_buttons(role)\r\n{\r\n var span = $("#role_rating_"+role.id);\r\n var site = \'desktop\'\r\n\r\n if (span) {\r\n if (site == \'desktop\') {\r\n span.html("<input type=\'button\' value=\'+ " + role.plus +"\' class=\'acter_vote acter_vote_plus rating_button like\' title=\'Мне понравилась эта актёрская работа\' mark=\'plus\' clicked=\'0\' role_id=\'" + role.id +"\' onClick=\'return sendVote(this)\' mark=\'plus\'> <input type=\'button\' value=\'- " + role.minus + "\' class=\'acter_vote acter_vote_minus rating_button dislike\' title=\'Мне не понравилась эта актёрская работа\' mark=\'minus\' clicked=\'0\' role_id=\'" + role.id +"\' onClick=\'return sendVote(this)\' mark=\'minus\'>")\r\n } else {\r\n span.html("<button class=\'acter_vote acter_vote_plus ui-btn ui-btn-inline ui-corner-all ui-btn-text-left ui-btn-icon-left ui-icon-plus\' title=\'Мне понравилась эта актёрская работа\' mark=\'plus\' clicked=\'0\' role_id=\'" + role.id +"\' onClick=\'return sendVote(this)\' mark=\'plus\'>" + role.plus +"</button> <button class=\'acter_vote acter_vote_minus ui-btn ui-btn-inline ui-corner-all ui-btn-text-left ui-btn-icon-left ui-icon-minus\' title=\'Мне не понравилась эта актёрская работа\' mark=\'minus\' clicked=\'0\' role_id=\'" + role.id +"\' onClick=\'return sendVote(this)\' mark=\'minus\'>" + role.minus + "</button>")\r\n }\r\n span.addClass("role_rating nowrap");\r\n if (role.voted != \'\') {\r\n mark_voted_buttons(role.id, role.voted);\r\n }\r\n }\r\n}\r\n\r\n// Делаем кнопки неактивными\r\nfunction mark_voted_buttons(role_id, voted)\r\n{\r\n if (voted == "plus") {\r\n var inactive = "minus";\r\n } else {\r\n var inactive = "plus";\r\n }\r\n $("#role_rating_"+role_id+" .acter_vote_"+inactive).addClass("role_rating_inactive");\r\n $("#role_rating_"+role_id+" .acter_vote_"+inactive).attr(\'title\', \'\');\r\n $("#role_rating_"+role_id+" .acter_vote_"+voted).addClass("role_rating_voted");\r\n // Кликать больше нельзя\r\n $("#role_rating_"+role_id+" .acter_vote").attr("clicked", 1);\r\n var site = \'desktop\'\r\n if (site == \'desktop\') {\r\n // Кнопка вообще отключена\r\n //$("#role_rating_"+role_id+" .acter_vote").attr("disabled", true);\r\n } else {\r\n $("#role_rating_"+role_id+" .acter_vote_"+voted).addClass("voted");\r\n }\r\n}\r\n\r\n// Отправляем голос\r\nfunction sendVote(button)\r\n{\r\n var member_id = \'0\';\r\n var page = \'titr\';\r\n var site = \'desktop\'\r\n // Только зарегистрированные могут голосовать\r\n if(member_id < 1) {\r\n if (site == \'desktop\') {\r\n self.location = "/forum/auth/";\r\n /*\r\n $("#vote_dialog").dialogModal({\r\n buttons: {\r\n yes: {title: \'Да\', action: \'self.location=\\"/forum/reg/\\"\'},\r\n no: {title: \'Нет\', action: \'\'}\r\n }\r\n }); */\r\n } else {\r\n //$.mobile.changePage(\'#vote_dialog\', \'pop\', true, true);\r\n //$.mobile.changePage("/forum/auth/");\r\n self.location = "/forum/auth/";\r\n }\r\n return false;\r\n }\r\n // Если уже кликнули по кнопке - второй раз не обрабатываем\r\n if (button.getAttribute("clicked") == 1) {\r\n return;\r\n }\r\n var plus_button = button.parentNode.firstChild;\r\n var minus_button = button.parentNode.lastChild;\r\n plus_button.setAttribute("clicked", 1); // чтобы больше не кликали\r\n minus_button.setAttribute("clicked", 1); // чтобы больше не кликали\r\n\tvar scr = screen.availHeight + "#" + screen.availWidth + "#" + screen.colorDepth + "#" + screen.pixelDepth + "#" + screen.height + "#" + screen.width;\r\n\tvar mark = button.getAttribute("mark");\r\n\tvar role_id = button.getAttribute("role_id");\r\n // Делаем ajax-запрос\r\n if (site == \'desktop\') {\r\n $.getJSON(\'/vote/role/\', { screen: scr, mark: mark, role_id: role_id, page: page } , function (data) {\r\n if (data.error) {\r\n // Выводим сообщение об ошибке\r\n $("#alert_dialog").dialogModal({\r\n text: data.error,\r\n buttons: {\r\n ok: {title: \'ОК\', action: \'\'}\r\n }\r\n });\r\n return false;\r\n } else {\r\n plus_button.value = "+ " + data.plus;\r\n minus_button.value = "- " + data.minus;\r\n // Отмечаем кнопки как неактивные\r\n mark_voted_buttons(role_id, mark);\r\n ym(12718750,\'reachGoal\',\'role_vote\');\r\n }\r\n });\r\n } else {\r\n $.ajax({url: \'/vote/role/\',\r\n dataType: "json",\r\n async: true,\r\n data: { screen: scr, mark: mark, role_id: role_id, page: page },\r\n success: function (data) {\r\n if (data.error) {\r\n // to do - сделать диалог\r\n alert(data.error);\r\n } else {\r\n plus_button.innerHTML = data.plus;\r\n minus_button.innerHTML = data.minus;\r\n }\r\n // Отмечаем кнопки как неактивные\r\n mark_voted_buttons(role_id, mark);\r\n },\r\n error: function (request,error) {\r\n // Какая-то ошибка\r\n // alert(JSON.stringify(request))\r\n }\r\n });\r\n }\r\n}\r\n\r\nfunction print_all_role_rating_buttons ()\r\n{\r\n print_role_rating_buttons({ id:"160325", plus:"52", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"1928641", plus:"25", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1966145", plus:"21", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973000", plus:"21", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973001", plus:"21", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973002", plus:"23", minus:"0", voted:"" });\nprint_role_rating_buttons({ id:"1973003", plus:"23", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973005", plus:"23", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"1973007", plus:"26", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973008", plus:"20", minus:"1", voted:"" });\nprint_role_rating_buttons({ id:"1973010", plus:"30", minus:"4", voted:"" });\nprint_role_rating_buttons({ id:"1973011", plus:"32", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"2088754", plus:"27", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"16800", plus:"142", minus:"4", voted:"" });\nprint_role_rating_buttons({ id:"16801", plus:"99", minus:"23", voted:"" });\nprint_role_rating_buttons({ id:"16803", plus:"119", minus:"12", voted:"" });\nprint_role_rating_buttons({ id:"16802", plus:"133", minus:"7", voted:"" });\nprint_role_rating_buttons({ id:"16804", plus:"131", minus:"4", voted:"" });\nprint_role_rating_buttons({ id:"56008", plus:"92", minus:"10", voted:"" });\nprint_role_rating_buttons({ id:"62460", plus:"95", minus:"4", voted:"" });\nprint_role_rating_buttons({ id:"72744", plus:"116", minus:"3", voted:"" });\nprint_role_rating_buttons({ id:"83813", plus:"39", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"89473", plus:"92", minus:"20", voted:"" });\nprint_role_rating_buttons({ id:"124124", plus:"108", minus:"7", voted:"" });\nprint_role_rating_buttons({ id:"132138", plus:"123", minus:"2", voted:"" });\nprint_role_rating_buttons({ id:"132139", plus:"108", minus:"5", voted:"" });\nprint_role_rating_buttons({ id:"132140", plus:"74", minus:"3", voted:"" });\nprint_role_rating_buttons({ id:"132141", plus:"65", minus:"6", voted:"" });\n\r\n}\r\n// После загрузки страницы запускаем действо\r\nif (window.addEventListener) {\r\n window.addEventListener("DOMContentLoaded",function(event) {\r\n print_all_role_rating_buttons()\r\n });\r\n} else {\r\n $(document).ready( print_all_role_rating_buttons() );\r\n}\r\n\r\n//-->\r\n'
Блок с кодом довольно большой, в нем содержатся разные функции для отрисовки и обновления информации на кнопках (на них отображаются голоса за и против, на них же можно кликать после авторизации, чтобы записать свой голос). Нам же понадобится часть кода после функции print_role_rating_buttons ()
, так как именно под ней располагается необходимая информация. Воспользуемся методом .split()
и разобьем строку на части:
for_votes = text.split("function print_all_role_rating_buttons ()")[1]
print(for_votes)
{ print_role_rating_buttons({ id:"160325", plus:"52", minus:"2", voted:"" }); print_role_rating_buttons({ id:"1928641", plus:"25", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1966145", plus:"21", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973000", plus:"21", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973001", plus:"21", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973002", plus:"23", minus:"0", voted:"" }); print_role_rating_buttons({ id:"1973003", plus:"23", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973005", plus:"23", minus:"2", voted:"" }); print_role_rating_buttons({ id:"1973007", plus:"26", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973008", plus:"20", minus:"1", voted:"" }); print_role_rating_buttons({ id:"1973010", plus:"30", minus:"4", voted:"" }); print_role_rating_buttons({ id:"1973011", plus:"32", minus:"2", voted:"" }); print_role_rating_buttons({ id:"2088754", plus:"27", minus:"2", voted:"" }); print_role_rating_buttons({ id:"16800", plus:"142", minus:"4", voted:"" }); print_role_rating_buttons({ id:"16801", plus:"99", minus:"23", voted:"" }); print_role_rating_buttons({ id:"16803", plus:"119", minus:"12", voted:"" }); print_role_rating_buttons({ id:"16802", plus:"133", minus:"7", voted:"" }); print_role_rating_buttons({ id:"16804", plus:"131", minus:"4", voted:"" }); print_role_rating_buttons({ id:"56008", plus:"92", minus:"10", voted:"" }); print_role_rating_buttons({ id:"62460", plus:"95", minus:"4", voted:"" }); print_role_rating_buttons({ id:"72744", plus:"116", minus:"3", voted:"" }); print_role_rating_buttons({ id:"83813", plus:"39", minus:"2", voted:"" }); print_role_rating_buttons({ id:"89473", plus:"92", minus:"20", voted:"" }); print_role_rating_buttons({ id:"124124", plus:"108", minus:"7", voted:"" }); print_role_rating_buttons({ id:"132138", plus:"123", minus:"2", voted:"" }); print_role_rating_buttons({ id:"132139", plus:"108", minus:"5", voted:"" }); print_role_rating_buttons({ id:"132140", plus:"74", minus:"3", voted:"" }); print_role_rating_buttons({ id:"132141", plus:"65", minus:"6", voted:"" }); } // После загрузки страницы запускаем действо if (window.addEventListener) { window.addEventListener("DOMContentLoaded",function(event) { print_all_role_rating_buttons() }); } else { $(document).ready( print_all_role_rating_buttons() ); } //-->
Хотя с синтаксисом JavaScript знакомы немногие, не очень сложно догадаться, что означают записи такого вида:
print_role_rating_buttons({ id:"16800", plus:"140", minus:"4", voted:"" });
Такая строка кода активирует функцию print_role_rating_buttons()
– применяет ее к такому набору данных и наносит на кнопки, соответствующие актеру с id 16800 значения 140 (зеленая кнопка) и 4 (красная кнопка):
Примечание 1. Синхронизация не происходит мгновенно, актуальное значение 140 подставляется на кнопку не сразу, там вполне может стоять значение 139, а мы, благодаря значениям из JavaScript, обладаем более актуальной информацией. Это стоит иметь в виду при выгрузке информации, когда мы сверяем полученные результаты и то, что видим на экране.
Примечание 2. Значение voted
в коде выше является непустым, в случае, если пользователь зарегистрирован и оценивал игру актера (по крайней мере, какое-то время после голосования значение в voted
держится, потом на самой странице отметка о голосовании остается, а в JSON стирается). Сравним скрин и фрагменты кода для такого случая:
Код:
print_role_rating_buttons({ id:"56008", plus:"90", minus:"10", voted:"plus" });
print_role_rating_buttons({ id:"132139", plus:"106", minus:"4", voted:"plus" });
Все фрагменты кода выше – это обычные строки, объекты типа string. Поэтому с помощью регулярных выражений мы сможем найти в них записи в фигурных скобках, затем сделать из них словари, а из списка словарей с единообразными ключами собрать датафрейм!
Найдем все подстроки, соответствующие шаблону \{.+\}
, то есть просто все наборы символов, заключенных в фигурные скобки (скобки экранируем, чтобы не перепутать со специальными скобками в регулярных выражениях):
import re
votes_str = re.findall("\{.+\}", for_votes)
votes_str
['{ id:"160325", plus:"52", minus:"2", voted:"" }', '{ id:"1928641", plus:"25", minus:"1", voted:"" }', '{ id:"1966145", plus:"21", minus:"1", voted:"" }', '{ id:"1973000", plus:"21", minus:"1", voted:"" }', '{ id:"1973001", plus:"21", minus:"1", voted:"" }', '{ id:"1973002", plus:"23", minus:"0", voted:"" }', '{ id:"1973003", plus:"23", minus:"1", voted:"" }', '{ id:"1973005", plus:"23", minus:"2", voted:"" }', '{ id:"1973007", plus:"26", minus:"1", voted:"" }', '{ id:"1973008", plus:"20", minus:"1", voted:"" }', '{ id:"1973010", plus:"30", minus:"4", voted:"" }', '{ id:"1973011", plus:"32", minus:"2", voted:"" }', '{ id:"2088754", plus:"27", minus:"2", voted:"" }', '{ id:"16800", plus:"142", minus:"4", voted:"" }', '{ id:"16801", plus:"99", minus:"23", voted:"" }', '{ id:"16803", plus:"119", minus:"12", voted:"" }', '{ id:"16802", plus:"133", minus:"7", voted:"" }', '{ id:"16804", plus:"131", minus:"4", voted:"" }', '{ id:"56008", plus:"92", minus:"10", voted:"" }', '{ id:"62460", plus:"95", minus:"4", voted:"" }', '{ id:"72744", plus:"116", minus:"3", voted:"" }', '{ id:"83813", plus:"39", minus:"2", voted:"" }', '{ id:"89473", plus:"92", minus:"20", voted:"" }', '{ id:"124124", plus:"108", minus:"7", voted:"" }', '{ id:"132138", plus:"123", minus:"2", voted:"" }', '{ id:"132139", plus:"108", minus:"5", voted:"" }', '{ id:"132140", plus:"74", minus:"3", voted:"" }', '{ id:"132141", plus:"65", minus:"6", voted:"" }']
Выберем один элемент списка и изучим его:
v = votes_str[0]
v
'{ id:"160325", plus:"52", minus:"2", voted:"" }'
На первый взгляд, этот элемент представляет собой полноценный словарь, просто заключенный в кавычки.
Однако есть проблема: чтобы переделать строку в словарь, нужна строка, соответствующая формату JSON, а здесь не хватает кавычек вокруг ключей. Как эту проблему решить? Найти по какому-то паттерну эти ключи и доклеить вокруг них кавычки с помощью функции sub()
из модуля re
для «умной» замены (не обычная замена, так как мы не конкретный набор символов заменяем на другой, а ищем совпадения по некоторому общему шаблону и их изменяем).
Поиск будет простой – в строке с «неправильным» словарем содержатся либо цифры, либо буквы, либо пробелы с запятыми и знаками препинания, а нам нужны последовательности из одной и более букв:
re.findall("[a-z]+", v)
['id', 'plus', 'minus', 'voted']
Напишем функцию add_quotes()
, которая принимает на вход строку x
, доклеивает к ней кавычки и убирает пробелы. Нам нужно преобразовать строку x
в группу – объект из регулярных выражений – через +
доклеить кавычки и на всякий случай убрать лишние пробелы:
# x.group(): функция sub() из re для умной замены
# работает не со строками, а с объектом специального типа – группа символов
def add_quotes(x):
g = '"' + x.group() + '"'
return g.strip()
Применяем функцию sub
к одной строке v
и проверяем:
# аргументы:
# шаблон, по которому ищем, что заменять
# функция, которая выполняет преобразование – доклеивает к результатм поиска кавычки
# строка, по который выполняем поиск
re.sub("[a-z]+", add_quotes, v)
'{ "id":"160325", "plus":"52", "minus":"2", "voted":"" }'
Все работает, применяем ко всем строкам и получаем список валидных json-строк:
votes_new = [re.sub("[a-z]+", add_quotes, v) for v in votes_str]
votes_new
['{ "id":"160325", "plus":"52", "minus":"2", "voted":"" }', '{ "id":"1928641", "plus":"25", "minus":"1", "voted":"" }', '{ "id":"1966145", "plus":"21", "minus":"1", "voted":"" }', '{ "id":"1973000", "plus":"21", "minus":"1", "voted":"" }', '{ "id":"1973001", "plus":"21", "minus":"1", "voted":"" }', '{ "id":"1973002", "plus":"23", "minus":"0", "voted":"" }', '{ "id":"1973003", "plus":"23", "minus":"1", "voted":"" }', '{ "id":"1973005", "plus":"23", "minus":"2", "voted":"" }', '{ "id":"1973007", "plus":"26", "minus":"1", "voted":"" }', '{ "id":"1973008", "plus":"20", "minus":"1", "voted":"" }', '{ "id":"1973010", "plus":"30", "minus":"4", "voted":"" }', '{ "id":"1973011", "plus":"32", "minus":"2", "voted":"" }', '{ "id":"2088754", "plus":"27", "minus":"2", "voted":"" }', '{ "id":"16800", "plus":"142", "minus":"4", "voted":"" }', '{ "id":"16801", "plus":"99", "minus":"23", "voted":"" }', '{ "id":"16803", "plus":"119", "minus":"12", "voted":"" }', '{ "id":"16802", "plus":"133", "minus":"7", "voted":"" }', '{ "id":"16804", "plus":"131", "minus":"4", "voted":"" }', '{ "id":"56008", "plus":"92", "minus":"10", "voted":"" }', '{ "id":"62460", "plus":"95", "minus":"4", "voted":"" }', '{ "id":"72744", "plus":"116", "minus":"3", "voted":"" }', '{ "id":"83813", "plus":"39", "minus":"2", "voted":"" }', '{ "id":"89473", "plus":"92", "minus":"20", "voted":"" }', '{ "id":"124124", "plus":"108", "minus":"7", "voted":"" }', '{ "id":"132138", "plus":"123", "minus":"2", "voted":"" }', '{ "id":"132139", "plus":"108", "minus":"5", "voted":"" }', '{ "id":"132140", "plus":"74", "minus":"3", "voted":"" }', '{ "id":"132141", "plus":"65", "minus":"6", "voted":"" }']
Теперь нам осталось считать эти валидные JSON-строки с помощью Python – то есть превратить их в обычные питоновские словари. Импортируем модуль json
:
import json
Проверяем работу функции loads()
на примере первой строки из полученного списка votes_new
:
json.loads(votes_new[0])
{'id': '160325', 'plus': '52', 'minus': '2', 'voted': ''}
Напоминание: в модуле json
есть две похожих функции:
load()
загружает данные из файла с расширением .json
;loads()
– загружает данные из JSON-строки.Теперь применим эту функцию ко всем строкам в votes_new
и сразу преобразуем получившийся список словарей в датафрейм:
ratings = pd.DataFrame([json.loads(v) for v in votes_new])
ratings
id | plus | minus | voted | |
---|---|---|---|---|
0 | 160325 | 52 | 2 | |
1 | 1928641 | 25 | 1 | |
2 | 1966145 | 21 | 1 | |
3 | 1973000 | 21 | 1 | |
4 | 1973001 | 21 | 1 | |
5 | 1973002 | 23 | 0 | |
6 | 1973003 | 23 | 1 | |
7 | 1973005 | 23 | 2 | |
8 | 1973007 | 26 | 1 | |
9 | 1973008 | 20 | 1 | |
10 | 1973010 | 30 | 4 | |
11 | 1973011 | 32 | 2 | |
12 | 2088754 | 27 | 2 | |
13 | 16800 | 142 | 4 | |
14 | 16801 | 99 | 23 | |
15 | 16803 | 119 | 12 | |
16 | 16802 | 133 | 7 | |
17 | 16804 | 131 | 4 | |
18 | 56008 | 92 | 10 | |
19 | 62460 | 95 | 4 | |
20 | 72744 | 116 | 3 | |
21 | 83813 | 39 | 2 | |
22 | 89473 | 92 | 20 | |
23 | 124124 | 108 | 7 | |
24 | 132138 | 123 | 2 | |
25 | 132139 | 108 | 5 | |
26 | 132140 | 74 | 3 | |
27 | 132141 | 65 | 6 |
Ура! Вся относительно тяжелая работа проделана, осталось вспомнить, что у нас есть датафрейм main
, в который мы хотели добавить информацию о числе лайков и дизлайков:
main.head()
id | name | |
---|---|---|
0 | 16801 | Лидия Федосеева-Шукшина |
1 | 16800 | Вячеслав Невинный |
2 | 16803 | Игорь Красавин |
3 | 16802 | Варвара Владимирова |
4 | 89473 | Светлана Селезнёва |
Объединим два датафрейма, main
и ratings
по столбцу с названием id
с помощью метода .merge()
. Нам понадобится тип объединения left
, так как мы не хотим терять информацию по тем актерам, по которым голосования на странице не было, а хотим просто к первому (левому) датафрейму подтянуть информацию из второго (правого). Если такой информации нет, строка из main
не удалится, просто на соответствующих местах в объединенном датафрейме будут пропуски:
final = main.merge(ratings, on = "id", how = "left")
final
id | name | plus | minus | voted | |
---|---|---|---|---|---|
0 | 16801 | Лидия Федосеева-Шукшина | 99 | 23 | |
1 | 16800 | Вячеслав Невинный | 142 | 4 | |
2 | 16803 | Игорь Красавин | 119 | 12 | |
3 | 16802 | Варвара Владимирова | 133 | 7 | |
4 | 89473 | Светлана Селезнёва | 92 | 20 | |
5 | 124124 | Регина Разума | 108 | 7 | |
6 | 16804 | Альберт Филозов | 131 | 4 | |
7 | 132138 | Артём Тынкасов | 123 | 2 | |
8 | 56008 | Елена Антонова | 92 | 10 | |
9 | 132139 | Владимир Ставицкий | 108 | 5 | |
10 | 72744 | Анатолий Рудаков | 116 | 3 | |
11 | 132140 | Юрий Багинян | 74 | 3 | |
12 | 62460 | Александр Денисов | 95 | 4 | |
13 | 132141 | Валентин Букин | 65 | 6 | |
14 | 83813 | Анатолий Голуб | 39 | 2 | |
15 | 132142 | Саша Занько | NaN | NaN | NaN |
16 | 132143 | Вика Яблонская | NaN | NaN | NaN |
17 | 2030314 | А. Баутенко | NaN | NaN | NaN |
18 | 1973004 | С. Гецингер | NaN | NaN | NaN |
19 | 1973001 | Дмитрий Диджиокас | 21 | 1 | |
20 | 1973002 | Владимир Зубенко | 23 | 0 | |
21 | 1973003 | Владимир Корпусь | 23 | 1 | |
22 | 1966145 | Ивар Кумник | 21 | 1 | |
23 | 1973005 | Анна Маланкина | 23 | 2 | |
24 | 1973000 | Валерий Мороз | 21 | 1 | |
25 | 160325 | Леонид Нечаев | 52 | 2 | |
26 | 1973006 | И. Окал | NaN | NaN | NaN |
27 | 1928641 | Анна Портная | 25 | 1 | |
28 | 1973007 | Ростислав Рахт | 26 | 1 | |
29 | 1973009 | В. Славуник | NaN | NaN | NaN |
30 | 1973008 | Юрий Шульга | 20 | 1 | |
31 | 2088754 | Александр Макарцев | 27 | 2 | |
32 | 1973010 | Евгений Герчаков | 30 | 4 | |
33 | 1973011 | Сергей Дрейден | 32 | 2 | |
34 | 222174 | Анатолий Тукиш | NaN | NaN | NaN |
35 | 1973015 | Виктор Борцов | NaN | NaN | NaN |
36 | 1973016 | Михаил Кокшенов | NaN | NaN | NaN |
37 | 1973012 | Ольга Машная | NaN | NaN | NaN |
38 | 1973017 | Леонид Нечаев | NaN | NaN | NaN |
39 | 1973020 | Илья Рутберг | NaN | NaN | NaN |
40 | 1973019 | Лариса Удовиченко | NaN | NaN | NaN |
41 | 1973013 | Елена Цыплакова | NaN | NaN | NaN |
42 | 1973014 | Борис Шувалов | NaN | NaN | NaN |
43 | 1973018 | Игорь Ясулович | NaN | NaN | NaN |
44 | 2496375 | Н. Острова | NaN | NaN | NaN |
45 | 2496376 | Анатолий Тукиш | NaN | NaN | NaN |
46 | 1914304 | Павел Бабаков | NaN | NaN | NaN |
47 | 2496373 | Улдис-Янис Вейспалс | NaN | NaN | NaN |
48 | 2022950 | Сергей Головкин | NaN | NaN | NaN |
Отсортируем строки по числу положительных оценок:
final.sort_values("plus")
id | name | plus | minus | voted | |
---|---|---|---|---|---|
9 | 132139 | Владимир Ставицкий | 108 | 5 | |
5 | 124124 | Регина Разума | 108 | 7 | |
10 | 72744 | Анатолий Рудаков | 116 | 3 | |
2 | 16803 | Игорь Красавин | 119 | 12 | |
7 | 132138 | Артём Тынкасов | 123 | 2 | |
6 | 16804 | Альберт Филозов | 131 | 4 | |
3 | 16802 | Варвара Владимирова | 133 | 7 | |
1 | 16800 | Вячеслав Невинный | 142 | 4 | |
30 | 1973008 | Юрий Шульга | 20 | 1 | |
24 | 1973000 | Валерий Мороз | 21 | 1 | |
19 | 1973001 | Дмитрий Диджиокас | 21 | 1 | |
22 | 1966145 | Ивар Кумник | 21 | 1 | |
21 | 1973003 | Владимир Корпусь | 23 | 1 | |
23 | 1973005 | Анна Маланкина | 23 | 2 | |
20 | 1973002 | Владимир Зубенко | 23 | 0 | |
27 | 1928641 | Анна Портная | 25 | 1 | |
28 | 1973007 | Ростислав Рахт | 26 | 1 | |
31 | 2088754 | Александр Макарцев | 27 | 2 | |
32 | 1973010 | Евгений Герчаков | 30 | 4 | |
33 | 1973011 | Сергей Дрейден | 32 | 2 | |
14 | 83813 | Анатолий Голуб | 39 | 2 | |
25 | 160325 | Леонид Нечаев | 52 | 2 | |
13 | 132141 | Валентин Букин | 65 | 6 | |
11 | 132140 | Юрий Багинян | 74 | 3 | |
8 | 56008 | Елена Антонова | 92 | 10 | |
4 | 89473 | Светлана Селезнёва | 92 | 20 | |
12 | 62460 | Александр Денисов | 95 | 4 | |
0 | 16801 | Лидия Федосеева-Шукшина | 99 | 23 | |
15 | 132142 | Саша Занько | NaN | NaN | NaN |
16 | 132143 | Вика Яблонская | NaN | NaN | NaN |
17 | 2030314 | А. Баутенко | NaN | NaN | NaN |
18 | 1973004 | С. Гецингер | NaN | NaN | NaN |
26 | 1973006 | И. Окал | NaN | NaN | NaN |
29 | 1973009 | В. Славуник | NaN | NaN | NaN |
34 | 222174 | Анатолий Тукиш | NaN | NaN | NaN |
35 | 1973015 | Виктор Борцов | NaN | NaN | NaN |
36 | 1973016 | Михаил Кокшенов | NaN | NaN | NaN |
37 | 1973012 | Ольга Машная | NaN | NaN | NaN |
38 | 1973017 | Леонид Нечаев | NaN | NaN | NaN |
39 | 1973020 | Илья Рутберг | NaN | NaN | NaN |
40 | 1973019 | Лариса Удовиченко | NaN | NaN | NaN |
41 | 1973013 | Елена Цыплакова | NaN | NaN | NaN |
42 | 1973014 | Борис Шувалов | NaN | NaN | NaN |
43 | 1973018 | Игорь Ясулович | NaN | NaN | NaN |
44 | 2496375 | Н. Острова | NaN | NaN | NaN |
45 | 2496376 | Анатолий Тукиш | NaN | NaN | NaN |
46 | 1914304 | Павел Бабаков | NaN | NaN | NaN |
47 | 2496373 | Улдис-Янис Вейспалс | NaN | NaN | NaN |
48 | 2022950 | Сергей Головкин | NaN | NaN | NaN |
Результат сортировки получился странным: Владимир Ставицкий с числом положительных оценок 105 оказался на первом месте, Регина Разума с числом 106 – на втором, при этом Вячеслав Невинный с самым большим числом «плюсов» расположился на восьмом месте. Логика сортировки не похожа ни на убывание, ни на возрастание.
Почему так вышло? Проблема в том, что тип столбцов plus
и minus
остался строковым, а строки сортируются посимвольно: сравниваются первые символы, потом вторые, потом третьи... Сортировка по умолчанию идет по возрастанию, поэтому сначала идут строки, начинающиеся с 1, затем – с 2, и так далее, а внутри каждой группы сортировка проходит по второму символу, и если нужно, по третьему.
Преобразуем тип столбца в числовой и снова отсортируем, теперь уже точно правильно и по убыванию (тип столбца float
, так как сделать столбец с пропусками NaN
целочисленным pandas не позволит):
final["plus"] = final["plus"].astype(float)
final["minus"] = final["minus"].astype(float)
final.sort_values("plus", ascending = False)
id | name | plus | minus | voted | |
---|---|---|---|---|---|
1 | 16800 | Вячеслав Невинный | 142.0 | 4.0 | |
3 | 16802 | Варвара Владимирова | 133.0 | 7.0 | |
6 | 16804 | Альберт Филозов | 131.0 | 4.0 | |
7 | 132138 | Артём Тынкасов | 123.0 | 2.0 | |
2 | 16803 | Игорь Красавин | 119.0 | 12.0 | |
10 | 72744 | Анатолий Рудаков | 116.0 | 3.0 | |
5 | 124124 | Регина Разума | 108.0 | 7.0 | |
9 | 132139 | Владимир Ставицкий | 108.0 | 5.0 | |
0 | 16801 | Лидия Федосеева-Шукшина | 99.0 | 23.0 | |
12 | 62460 | Александр Денисов | 95.0 | 4.0 | |
4 | 89473 | Светлана Селезнёва | 92.0 | 20.0 | |
8 | 56008 | Елена Антонова | 92.0 | 10.0 | |
11 | 132140 | Юрий Багинян | 74.0 | 3.0 | |
13 | 132141 | Валентин Букин | 65.0 | 6.0 | |
25 | 160325 | Леонид Нечаев | 52.0 | 2.0 | |
14 | 83813 | Анатолий Голуб | 39.0 | 2.0 | |
33 | 1973011 | Сергей Дрейден | 32.0 | 2.0 | |
32 | 1973010 | Евгений Герчаков | 30.0 | 4.0 | |
31 | 2088754 | Александр Макарцев | 27.0 | 2.0 | |
28 | 1973007 | Ростислав Рахт | 26.0 | 1.0 | |
27 | 1928641 | Анна Портная | 25.0 | 1.0 | |
20 | 1973002 | Владимир Зубенко | 23.0 | 0.0 | |
21 | 1973003 | Владимир Корпусь | 23.0 | 1.0 | |
23 | 1973005 | Анна Маланкина | 23.0 | 2.0 | |
22 | 1966145 | Ивар Кумник | 21.0 | 1.0 | |
24 | 1973000 | Валерий Мороз | 21.0 | 1.0 | |
19 | 1973001 | Дмитрий Диджиокас | 21.0 | 1.0 | |
30 | 1973008 | Юрий Шульга | 20.0 | 1.0 | |
15 | 132142 | Саша Занько | NaN | NaN | NaN |
16 | 132143 | Вика Яблонская | NaN | NaN | NaN |
17 | 2030314 | А. Баутенко | NaN | NaN | NaN |
18 | 1973004 | С. Гецингер | NaN | NaN | NaN |
26 | 1973006 | И. Окал | NaN | NaN | NaN |
29 | 1973009 | В. Славуник | NaN | NaN | NaN |
34 | 222174 | Анатолий Тукиш | NaN | NaN | NaN |
35 | 1973015 | Виктор Борцов | NaN | NaN | NaN |
36 | 1973016 | Михаил Кокшенов | NaN | NaN | NaN |
37 | 1973012 | Ольга Машная | NaN | NaN | NaN |
38 | 1973017 | Леонид Нечаев | NaN | NaN | NaN |
39 | 1973020 | Илья Рутберг | NaN | NaN | NaN |
40 | 1973019 | Лариса Удовиченко | NaN | NaN | NaN |
41 | 1973013 | Елена Цыплакова | NaN | NaN | NaN |
42 | 1973014 | Борис Шувалов | NaN | NaN | NaN |
43 | 1973018 | Игорь Ясулович | NaN | NaN | NaN |
44 | 2496375 | Н. Острова | NaN | NaN | NaN |
45 | 2496376 | Анатолий Тукиш | NaN | NaN | NaN |
46 | 1914304 | Павел Бабаков | NaN | NaN | NaN |
47 | 2496373 | Улдис-Янис Вейспалс | NaN | NaN | NaN |
48 | 2022950 | Сергей Головкин | NaN | NaN | NaN |
Отлично, задача решена!