` с классом `info`:"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [],
"source": [
"divs = soup.find_all(\"div\", {\"class\" : \"info\"})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Возьмём первого пользователя и поймём, как извлечь информацию по нему:"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"
"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"d = divs[0]\n",
"d"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Найдём элемент, соответствующий имени и извлечём оттуда текст и ссылку на профиль:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [],
"source": [
"person = d.find(\"div\", {\"class\" : \"labeled name\"})\n",
"name = person.text\n",
"href = person.find(\"a\")[\"href\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Напишем функцию для извлечения имени и ссылки на профиль:"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [],
"source": [
"def get_person(d):\n",
" person = d.find(\"div\", {\"class\" : \"labeled name\"})\n",
" name = person.text\n",
" href = person.find(\"a\")[\"href\"]\n",
" return name, href"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"И применим её ко всем пользователям в списке `divs`:"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"people = []\n",
"for d in divs:\n",
" p = get_person(d)\n",
" people.append(p)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[('Артемий Лебедев', '/temalebedev'),\n",
" ('Jama Buldozer', '/jama_buldozer'),\n",
" ('Павел Дуров', '/durov'),\n",
" ('Вован Тарасов', '/id579521433'),\n",
" ('Сергей Крылов', '/id11010073'),\n",
" ('Ян Топлес', '/yanlapotkov'),\n",
" ('Сергей Миронов', '/mironov_sergey'),\n",
" ('Александр Кондрашов', '/justdoit150'),\n",
" ('Денис Джиган', '/iamgeegun'),\n",
" ('Стас Васильев', '/stas_satori'),\n",
" ('Амиран Сардаров', '/amirans'),\n",
" ('Константин Малофеев', '/kvmalofeev'),\n",
" ('Митя Фомин', '/id87211536'),\n",
" ('Сергей Домогацкий', '/domogatskiy'),\n",
" ('Стас Костюшкин', '/stas_kostyushkin'),\n",
" ('Сергей Жуков', '/sergeyzhukov.official'),\n",
" ('Роберт Григорян', '/scorty'),\n",
" ('Александр Соколовский', '/id8006'),\n",
" ('Олег Абрамов', '/tapog'),\n",
" ('Андрей Воробьев', '/andreyvorobiev'),\n",
" ('Тимур Батрутдинов', '/batr'),\n",
" ('Ранис Гайсин', '/ranto'),\n",
" ('Алексей Русских', '/russkihay'),\n",
" ('Мария Ивакова', '/maria_ivakova'),\n",
" ('Борис Александрович', '/borune'),\n",
" ('Samir Vishniakov', '/samir_vishniakov'),\n",
" ('Владислав Плотников', '/donatworry'),\n",
" ('Эдуард Перец', '/eduard_perets'),\n",
" ('Дмитрий Глуховский', '/dg'),\n",
" ('Данила Поперечный', '/spoontamer'),\n",
" ('Тимур Юнусов', '/timatimusic'),\n",
" ('Элвин Грей', '/elvingrey'),\n",
" ('Денис Кукояка', '/denisque'),\n",
" ('Константин Ивлев', '/ivlevchef'),\n",
" ('Ратмир Мавлиев', '/ratmir.mavliev'),\n",
" ('Стас Барецкий', '/stasbareckiy'),\n",
" ('Сергей Сергеев', '/rusgamespc'),\n",
" ('Геннадий Зюганов', '/gennadiy_zyuganov'),\n",
" ('Сергей Червяков', '/id59443037'),\n",
" ('Денис Ступаков', '/nexus26')]"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"people"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Отлично! При желании ссылки на профили можно сделать полными и кликабельными:"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [],
"source": [
"people_new = []\n",
"for p in people:\n",
" new_link = \"https://vk.com\" + p[1] \n",
" people_new.append((p[0], new_link)) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Проверяем:"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('Артемий Лебедев', 'https://vk.com/temalebedev')"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"people_new[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Осталась одна проблема – мы извлекли далеко не все результаты, а только те результаты, которые «видит» браузер, то есть то, что мы как пользователи видим до скроллинга. Библиотека `selenium` умеет скроллить страницы, точнее, активировать запуск кода на JavaScript, который отвечает за скроллинг. В общем виде строка с кодом для скроллинга выглядит так (`Y` – на сколько пикселей нужно проскроллить):\n",
"\n",
" br.execute_script(\"window.scrollTo(0, Y)\") \n",
" \n",
"Если нужно проскроллить до конца страницы, то тогда вместо `Y` нужно вписать значение, которое извлекается из тела документа HTML:\n",
"\n",
" document.body.scrollHeight"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Чтобы извлечь все результаты и проскроллить всё до самого конца, нам понадобится цикл. \n",
"Аглоритм такой: \n",
"\n",
"1. Выгружаем исходный код страницы.\n",
"2. Находим в этом коде необходимые элементы – извлекаем имена и ссылки на профили.\n",
"3. Скроллим вниз до конца страницы.\n",
"4. Повторяем пункты 1-3 до тех пор пока не достигнем последнего результата.\n",
"\n",
"Тут может возникнуть логичный вопрос: почему нельзя сразу проскроллить страницу до самого последнего результата, а потом собрать все данные? Причина проста: раз `webdriver` «видит» браузер глазами пользователя, при скроллинге вниз более ранние результаты будут пролистываться и уходить наверх, а значит, доступа к ним уже не будет. Поэтому нужно действовать поэтапно: проскроллили – сразу выгрузили информацию, ещё проскроллили – снова выгрузили."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Для упрощения задачи напишем функцию, которая сохраняет исходный код страницы, обращаясь к драйверу `br`, и извлекает информацию о пользователях."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [],
"source": [
"def get_users_info(br):\n",
" html = br.page_source\n",
" soup = BeautifulSoup(html)\n",
" divs = soup.find_all(\"div\", {\"class\" : \"info\"})\n",
" \n",
" people = []\n",
" for d in divs:\n",
" p = get_person(d)\n",
" people.append(p)\n",
" \n",
" return people"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Осталось запустить долгий цикл и на каждой итерации добавлять новые результаты после скроллинга. Импортируем функцию `sleep`, чтобы выставлять задержки между скроллингами:"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [],
"source": [
"from time import sleep"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь напишем код (адаптированный ответ с [StackOverflow](https://stackoverflow.com/questions/20986631/how-can-i-scroll-a-web-page-using-selenium-webdriver-in-python)) для цикла. Какой тип цикла нам нужен? Нам нужен цикл, который умеет повторять операции до тех пор, пока мы не дойдём до последнего результата, то есть до того момента, когда скроллить будет некуда. Воспользуемся конструкцией `while True`, бесконечным вариантом цикла `while`, который будет запускаться до тех пор, пока не дойдёт до кода с оператором `break` (выход из цикла) или не столкнётся с ошибкой. "
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {},
"outputs": [],
"source": [
"all_results = []\n",
"last_height = br.execute_script(\"return document.body.scrollHeight\")\n",
"\n",
"while True:\n",
" res = get_users_info(br)\n",
" all_results.extend(res) \n",
" \n",
" br.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n",
" new_height = br.execute_script(\"return document.body.scrollHeight\")\n",
" sleep(1)\n",
" \n",
" if new_height == last_height:\n",
" break\n",
" \n",
" last_height = new_height"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Пояснения к коду.\n",
"\n",
"1. Сохраняем в переменную `last_height` величину, на которую мы можем проскроллить страницу за один раз в данный момент времени, чтобы дойти до конца страницы. \n",
"\n",
"2. На каждой итерации цикла `while` мы выгружаем информацию, добавляем её в список `all_results` и скроллим страницу до самого низа. После скроллинга проверяем, на сколько ещё можно проскроллить, сохраняем полученное значение в `new_height`. \n",
"\n",
"3. Если скроллить уже некуда, если мы находимся в самом низу «бесконечной» страницы с результатами поиска, то `new_height` совпадает с `last_height`. Значит, нам нужно остановить исполнение кода – выходим из цикла с помощью `break`. \n",
"\n",
"4. Если мы ещё не закончили скроллить, обновляем значение `last_height`, заменяя его на `new_height` (теперь уже в нём хранится величина, на которую мы можем проскроллить страницу за один раз в данный момент времени). Продолжаем выполнять выгрузку информации и скроллинг."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"После недолгой работы кода с ячейке выше (у меня исполнение заняло примерно 2.5 минуты), мы получаем список `all_results` с именами пользователей и ссылками на их профили."
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"26297"
]
},
"execution_count": 38,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# 26297 результатов\n",
"\n",
"len(all_results)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Преобразуем список в датафрейм:"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" 0 | \n",
" 1 | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" Артемий Лебедев | \n",
" /temalebedev | \n",
"
\n",
" \n",
" 1 | \n",
" Jama Buldozer | \n",
" /jama_buldozer | \n",
"
\n",
" \n",
" 2 | \n",
" Павел Дуров | \n",
" /durov | \n",
"
\n",
" \n",
" 3 | \n",
" Вован Тарасов | \n",
" /id579521433 | \n",
"
\n",
" \n",
" 4 | \n",
" Сергей Крылов | \n",
" /id11010073 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" 0 1\n",
"0 Артемий Лебедев /temalebedev\n",
"1 Jama Buldozer /jama_buldozer\n",
"2 Павел Дуров /durov\n",
"3 Вован Тарасов /id579521433\n",
"4 Сергей Крылов /id11010073"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dat = pd.DataFrame(all_results)\n",
"dat.head(5) # первые 5 строк"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Переименуем столбцы и сделаем ссылки на профили полными:"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [],
"source": [
"dat.columns = [\"name\", \"link\"]"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [],
"source": [
"dat[\"link_full\"] = dat[\"link\"].apply(lambda x: \"https://vk.com/\" + x)"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" name | \n",
" link | \n",
" link_full | \n",
"
\n",
" \n",
" \n",
" \n",
" 0 | \n",
" Артемий Лебедев | \n",
" /temalebedev | \n",
" https://vk.com//temalebedev | \n",
"
\n",
" \n",
" 1 | \n",
" Jama Buldozer | \n",
" /jama_buldozer | \n",
" https://vk.com//jama_buldozer | \n",
"
\n",
" \n",
" 2 | \n",
" Павел Дуров | \n",
" /durov | \n",
" https://vk.com//durov | \n",
"
\n",
" \n",
" 3 | \n",
" Вован Тарасов | \n",
" /id579521433 | \n",
" https://vk.com//id579521433 | \n",
"
\n",
" \n",
" 4 | \n",
" Сергей Крылов | \n",
" /id11010073 | \n",
" https://vk.com//id11010073 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" name link link_full\n",
"0 Артемий Лебедев /temalebedev https://vk.com//temalebedev\n",
"1 Jama Buldozer /jama_buldozer https://vk.com//jama_buldozer\n",
"2 Павел Дуров /durov https://vk.com//durov\n",
"3 Вован Тарасов /id579521433 https://vk.com//id579521433\n",
"4 Сергей Крылов /id11010073 https://vk.com//id11010073"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dat.head(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь можно писать другой код, код, который проходит по ссылкам на профили и выгружает оттуда доступную всем информацию :) Но это уже другая история, в следующий раз попробуем поработать с ВКонтакте уже с помощью API."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}