Моя реализация не задействует deviantart api
[/mpc_alert]Данный проект реализован не идеально, и не претендует на звание хорошего кода. Это мой первый опыт по анализу и сбору данных. Изначально скорость и стабильность парсера оставлял желать лучшего, прежде всего связано это с избранным мною инструментом которые изначально был сделан для тестов, но я использовал его для выгрузки фида, и дальнейшего сбора исходного кода страницы. Конечно опытный разработчик ужаснётся такой реализации, всё равно что вместо инструмента для тонкой работы использовать бензопилу , однако саму цель я достиг следовательно это можно назвать успехом.
Итак для начала что нам нужно что бы получить ссылку на страницу пользователя, правильно найти её)) и искать её естественно проще всего в ленте новостей, где собираются все новые работы. В связи с особенностью сайта я решил в первой реализации пойти простым способ (как оказалось не самым надёжным и быстрым). Алгоритм бота был таков, открывать первую работу на сайте и нажимая на копку “next” переходить по всем работам собирая попутно ссылки на их авторов.
Однако авторы сайта очевидно оказались умнее меня) и предусмотрели подобные попытки сбора, как же спросите вы?! а очень просто, при длительном нажатии данной кнопки работы начинали зацикливаться, и часто попадали повторы, проверку которых к слову я реализовал не сразу) точно сказать реализация была, вот только она не работала как нужно. итак рассмотрим пример кода который ещё не задействовал библиотеку bs4 а полностью справлялся силами селениума.
from selenium import webdriver
import time
"""Данная конфигурация реализует базовую настройку с использованием профиля пользователя """
from tqdm import tqdm
import progressbar
options = webdriver.ChromeOptions()
options.add_argument('headless') # Без GUI
options.add_argument(r"user-data-dir=/home/moonz/chrome/profile")
driver = webdriver.Chrome(executable_path="/home/moonz/chrome/chromedriver", chrome_options=options)
# driver.page_source
driver.get("https://www.deviantart.com/newest/")
driver.find_element_by_xpath("//a[@class='torpedo-thumb-link']").click()
time.sleep(1)
progress = progressbar.ProgressBar()
def clean():
try:
for i in range(1,50):
a = driver.find_element_by_xpath \
("//div[@class='dev-view-about']").find_element_by_xpath("//span[@class='dev-title-avatar']") \
.find_element_by_css_selector('a').get_attribute('href')
time.sleep(2)
with open('links.txt', 'r+') as c:
prov = c.readlines() # Читаем все строки.
if str(a + '\n') not in prov: # Реализуем проверку вхождений линка в списке.
c.writelines(a + '\n')
else:
driver.find_element_by_xpath("//img[@alt='Right']").click()
driver.find_element_by_xpath("//img[@alt='Right']").click()
time.sleep(1)
driver.get("https://www.deviantart.com/newest/")
time.sleep(1)
driver.find_element_by_xpath("//a[@class='torpedo-thumb-link']").click()
time.sleep(1)
except Exception as error:
print('ОШИБКА ! | ', error)
driver.find_element_by_xpath("//img[@alt='Right']").click()
time.sleep(1)
try:
# for i in progress(range(1,100)):
for x in tqdm(range(1,1000)):
clean()
except Exception as er:
print(er)
driver.get("https://www.deviantart.com/newest/")
finally:
driver.close()
time.sleep(2)
driver.quit()
time.sleep(2)
При работе с selenium нужно усвоить один очень важный факт, если Вы явно не прекращаете его работу, то он зависает в процессах до ручного завершения.
finally:
driver.close()
time.sleep(2)
driver.quit()
time.sleep(2)
Данная конструкция решает проблему, советую использовать её именно в таком виде, что бы независимо от ситуации всегда срабатывал блок finally: даже если программа завершилась с ошибкой или вы внезапно решили её остановить. В ином же случае есть риск забить оперативную память и получить кучу мёртвых процессов. driver.close()
отвечает за корректное завершение работы веб браузера, а driver.quit()
в свою очередь отвечает за завершение процесса самого веб драйвера. Особое внимание следует уделить таймингу, ведь если попытаться выполнить блок кода без задержек, можно получить исключение, потому что веб драйвер не сможет завершиться пока полностью не остановлена работа с бразуером. Опытным путём я выяснил что задержка в 2 сек, вполне приемлема и может использоваться повсеместно.
Итак спустя день раздумей я все же решил не усложнять себе жизнь, и попытался реализовать изначальный подход, это загрузка ленты сверху в низ, так-как это было предусмотрено разработчиками изначально. Такой метод парсинга было бы попросту невозможно изолировать, ведь бот выполняет действия потенциального посетителя сайта.
Трудности
Первая трудность с которой я столкнулся это естественно нагрузка и потребление ресурсов. Сайт полн различных баннеров, да и сами миниатюры картинок он тоже подгружает создавая нагрузку на оперативную память. В этот момент я ощутил полноту преимущества работы с SELENIUM, и в частности с бразуемом google chrome, проблему с получаемым данными я решил всего двумя плагинами, это Adguard популярный блокировщик рекламы, на мой взгляд намного лучше попсового adblock, и конечно же я решил избавить систему от нагрузки в процессе загрузки изображений, с помощью плагина Block image который никак не портит работу сайта, просто блокирует загрузку. Таким образом я снизил потребление RAM до 100мб работы скрипта и бразуера. На мой взгляд это безуможно высокий результат, ведь если посмотреть на мой прошлый проект где я парсил данные из ютуба, там у меня потребление памяти буквально текло из под крана. За час работы прошлый скрипт сжирал всю доступную оперативную память, и решалось это только перезагрузкой скрипта. Сейчас очевидно разработчики проделали большую работу по оптемизации веб драйвера, потому что иначе я это никак объяснить не могу.
К слову использовал я
chromedriver_linux64.zip | 2018-12-10 23:20:22 | 5.14MB |
На скрине видно количество итераций, это был эксперимент с целью понять зависимость загружаемого контента на оперативную память. В данном случае веб драйвер совершил почти тысячу прокруток вниз, что привело к загрузке более тысячи изображений (на скрине ещё не был включен плагин их блокировки). Таким образом становится очевидно что реализация не такая уж и плохая, тем более если бразуер работает в headless режиме без GUI, и никому не мешает лишними окнами + не загружает процессор как открытая вкладка, все мы знаем насколько сам по себе хром прожорливый.
Итак перейдем непосредственно к реализации, вот код:
from selenium import webdriver
import time
from bs4 import BeautifulSoup
from tqdm import tqdm
PROXY = '144.217.163.93:8888'
options = webdriver.ChromeOptions()
options.add_argument('headless') # Без GUI
options.add_argument(r"user-data-dir=/home/moonz/chrome/profile")
#options.add_argument('--proxy-server=%s' % PROXY)
driver = webdriver.Chrome(executable_path="/home/moonz/chrome/chromedriver", chrome_options=options)
# Чистим закладки
def start():
time.sleep(2) # Ждём загрузку
for i in range(20):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # Прокручиваем страницу.
time.sleep(1)
# Реализуем сбор загруженных данных.
soup = BeautifulSoup(driver.page_source, 'html.parser') # На вход сразу подаём html из selenium
test = soup.find(id='browse-results-page-1').find_all(class_='artist')
adders = int(0)
for sup in test: # Реализуем проверку каждой ссылки на наличие её в Уже сформированном листе.
try:
user = sup.find('a').get('href')
with open('links.txt', 'r') as filter_file:
copy = filter_file.readlines()
if str(user + '\n') not in copy:
with open('links.txt', 'a') as file:
file.writelines(user + '\n')
adders += 1
except Exception as error:
continue
print('Было давлено ссылок: ',adders)
try:
for i in tqdm(range(100)):
driver.get("https://www.deviantart.com/newest/")
start()
driver.get("https://www.deviantart.com/photography/newest/")
start()
driver.get("https://www.deviantart.com/fanart/newest/")
start()
driver.get("https://www.deviantart.com/manga/newest/")
start()
time.sleep(200)
except Exception as er:
print(er)
finally:
driver.close()
time.sleep(2)
driver.quit()
time.sleep(2)
Внимательный читатель наверняка обратил внимание что в данной реализации код дополнен опцией работы с прокси, и это не случайно, ведь постоянная нагрузка с одного ip может вызвать подозрения у анти-спам системы сайта, по этому настоятельно рекомендую маскировать свои действия хотя бы таким примитивным способом, что всегда иметь возможность получить доступ с своего реального ip.
На скрине ниже я демонстрирую работу уже обновленной версии бразуера с плагином. Из консоли видно что данная версия запущено аж 4 с половиной час назад, и за это время нагрузка на оперативную память не изменилась от слова Совсем! Я не случайно делаю акцент на этом моменте, потому что лично для меня это была самая большая проблема, прямо вот Ахиллесова пята работы с веб драйвром, которую всё же удалось победить. Данная реализация работала у меня всю ночь, и весь день (потом), и нагрузка по прежнему не менялась. Я бы сказал что готов радоваться этому вечно =)
Отдельно хочется рассказать немного о прогрессбаре который я использовал в этом проекте впервые. Реализован силами tqdm которой крайне просто пользоваться, обернув цикл получаем вывод в консоль. В отличии от библиотеки progressbar которая к слову не хуже (просто визуализирует прогресс), библиотека tqdm обладает рядом преимуществ главное из которых это поддержка работы в IDE PyCharm, поскольку изначально я пишу код именно там, для меня это было важно. Ещё одним неоспоримым плюсом является поддержка отображения времени, не только затраченного, но и приблизительный расчёт до окончания работы.
Пришло время рассказать о непосредственно сборе данных, в первой версии скрипта (на данный момент она одна), я собираю исключительно Страну юзера, если таковая имеется в профиле, если нет то ссылка пропускается. Реализовал я это уже с помощью BeautifulSoup и requests, они идеально подходят для решения такой простой задачи. Однако в данном проекте я использовал новый для себя метод работы с requests, это requests.Session() который имитируют живого человека, работает он конечно же в купе с user agent, собственно сама реализация:
import requests
from bs4 import BeautifulSoup as bs # Такая запись позволяет обернуть BeautifulSoup в bs
import time
from tqdm import tqdm
def give_me_html(url):
https_proxy = "144.217.22.128:8080"
proxyDict = {
"https": https_proxy,
}
headers = {'accept': '*/*',
'user-agent': 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.52 Safari/536.5'}
session = requests.Session() # Иметирует сессию клиента.
#request = session.get(url, headers=headers, proxies=proxyDict)
request = session.get(url, headers=headers)
return request.content
def find_data():
with open('/home/moonz/PycharmProjects/BeautifulSoup/venv/users.txt', 'r') as file:
num = (sum(1 for _ in file)) # Считаем сколько ссылок(строк) в файле.
# Не знаю почему НО, данная реализация после считывания забирает в себя все данные.
with open('/home/moonz/PycharmProjects/BeautifulSoup/venv/users.txt', 'r') as file:
for i in tqdm(range(num)): # Прогрессбар для наглядности :3
url = file.readline()
html = give_me_html(url.replace('\n',''))
soup = bs(html, 'html.parser')
location = soup.find('div', id='aboutme-personal-info')
# На будущи, что бы собрать и города.
location_more = soup.find('table', class_='f')
if location != None:
data = location.text
if data != None and data.istitle() == True: # Проверяем не пустота ли вместо страны.
# Данный метод реализует проверку на заглавную букву, были ситуации когда записывал пустоту.
# Велосепед который забирает ужасный текст, а отадёт прекрсный :3
b = location.text.split() # Полученная строка имеет отступы, проблемы, и что-то ещё. Убираем лишнее
c = ' '.join(b) # В связи с тем что метод выше возвращает список, делаем из него строку.
with open('user_locatin.txt', 'a') as files:
files.writelines(c + '\n')
time.sleep(2)
find_data()
К сожалению данная реализация имеет множество недостатков, самый главный это естественно работа в одним потоком, вторая не по значимости проблема это прокси, для данного проекта мне пришлось писать валидатор проксей и парсер, да да, ещё один парсер для проксей (потом правда я нашел огромный txt с более чем 5к проксей), но их всё равно нужно было как валидировать, ведь большинство из них были мёртвые. По этому у меня возникло сразу две задачи, как быстро проверить прокси, к тому же качественно. Вот тут то я первый раз познакомился с библиотекой multiprocessing и методом Pool. Спасибо Олегу Молчанову за информацию, которую к слову я получил из его платного курса (не разу не желаю что купил). Потом начались долгие эксперименты по количеству запускаемых процессов, и я понял что количество далеко не всегда коллерирует с качеством. Если я увеличивал число “ботов”, то на выходе в списке валидных проксей получал чуть меньше чем при запуске меньшего количества процессов. В общем в итоге я пришел к вот такой реализации:
import requests
from tqdm import tqdm
from multiprocessing import Pool
def get_proxy_list():
proxy = []
proxy_list = '/home/moonz/PycharmProjects/BeautifulSoup/proxy_validator/prxoy_list.txt'
with open(proxy_list, 'r') as file:
num = (sum(1 for _ in file))
with open(proxy_list, 'r') as file_proxy:
for i in range(num):
url = 'https://ya.ru'
https_proxy = str(file_proxy.readline())
a = https_proxy.replace('\n', '')
proxy.append(a)
return proxy
def validator(proxy):
try:
proxyDict = {"https": proxy}
headers = {'accept': '*/*',
'user-agent': 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.52 Safari/536.5'}
url = 'https://ya.ru'
session = requests.Session() # Иметирует сессию клиента.
# Таймаут нужен что бы не ждать ошибок, а вызивать их :3
request = session.get(url, headers=headers, proxies=proxyDict, timeout=1)
if str(request) == '<Response [200]>':
with open('proxy_sort.txt', 'a') as spisok:
spisok.writelines(proxy + '\n')
except Exception as er:
a = 'ok'
try:
proxy = get_proxy_list()
# Прогрессбар для мультипроцессинга :3
pool = Pool(processes=40)
for _ in tqdm(pool.imap_unordered(validator, proxy), total=len(proxy)):
pass
except Exception as error:
print(error)
Первые результаты
После модификации основго парсера стран под прокси, мне удалось получить первые данные. Это было 9079 строк стран которые получились в результате анализа 14.768 ссылок, неплохой результат, если бы только парсинг не занял у меня 19 часов =(
На эту реализацию я без скромности потратил весь день, это был незабываемый опыт), потому что я пытался решить банальную задачу, но никак не могу осознать её решение. Суть такова: обычно для итерации по списку я использую как и все нормальные люди Цикл, но в случае с многопоточностью данный подход невозможен, ведь в таком случае каждый поток создает свой цикл, и итерируется независимо от всех остальных. А мне было нужно, что бы каждый процесс использовал уникальный набор прокси и юзер агентов. Я не придумал ничего лучше чем использовать обычный list и вынимать из него прокси с удаление его из списка. То есть, каждый новый пул берет нулевой элемент и удаляет его за собой, что бы следующий пул мог взять уже новый элемент, и так до конца списка. Но вот беда, я не могу понять как именно мне “перезагрузить” список, ведь использовать конструкция присвоения я не могу, аля: старый_список = новый_список, по этому я начал долгие поиски решения. Помогла как всегда документация :3 я нашел метод extend, который и осуществят загрузку одного списка в другой. Я не хотел изобретать велосипед с новым циклом который бы наполнял список каждый раз, в нашей работе важность имеет скорость, и конечно же ресурсы. По этому я реализовал бесконечный цикл внутри которого осуществлялась проверка на длинную списка, если элементов меньше чем общее количество пулов, производим дозаправку) Вот таким незамысловатым образом я решил эту задачу, получив море позитивных ощущений от созидания работающего кода.
Многопоточная реализация:
import requests
from bs4 import BeautifulSoup as bs # Такая запись позволяет обернуть BeautifulSoup в bs
import time
from tqdm import tqdm
from multiprocessing import Pool
def ping_test(proxy, user):
''' Данная функция реализует проверку доступности проки. К данному решению пришлось прибегнуть
из-за большого количества битых ссылок, которые вызывали исключения. '''
try:
url = 'https://www.deviantart.com/'
proxyDict = {"https": proxy, }
headers = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'user-agent': user}
session = requests.Session() # Имитируется сессию клиента.
request = session.get(url, headers=headers, proxies=proxyDict, timeout=1)
if str(request) == '<Response [200]>':
return True
else:
return False
except:
return False
def get_proxys():
proxys = []
with open('proxy_validator/proxy_ok.txt', 'r') as file:
num = (sum(1 for _ in file))
with open('proxy_validator/proxy_ok.txt', 'r') as file1:
for i in range(num):
proxy = file1.readline()
text = proxy.replace('\n', '')
proxys.append(text)
return proxys
def get_user_agent():
user_agetn = []
with open('agent.txt', 'r') as file1:
num = (sum(1 for _ in file1))
with open('agent.txt', 'r') as file:
for i in range(num):
users = file.readline()
data = users.replace('\n', '')
user_agetn.append(data)
return user_agetn
def give_me_html(url, proxy, user):
try:
proxyDict = {"https": proxy,}
headers = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'user-agent': user}
session = requests.Session() # Имитируется сессию клиента.
request = session.get(url, headers=headers, proxies=proxyDict, timeout=1)
if str(request) == '<Response [200]>':
return request.content
else:
return None
except Exception as error:
return 'non'
def find_data(html):
soup = bs(html, 'html.parser')
location = soup.find('div', id='aboutme-personal-info')
# На будущие, что бы собрать и города.
location_more = soup.find('table', class_='f')
if location != None:
data = location.text
if data != None and data.istitle() == True: # Проверяем не пустота ли вместо страны.
# Данный метод реализует проверку на заглавную букву, были ситуации когда записывал пустоту.
# Велосипед который забирает ужасный текст, а отдаёт прекрасный :3
b = location.text.split() # Полученная строка имеет отступы, проблемы, и что-то ещё. Убираем лишнее
c = ' '.join(b) # В связи с тем что метод выше возвращает список, делаем из него строку.
with open('user_locatin.txt', 'a') as files:
files.writelines(c + '\n')
time.sleep(2)
def get_urls():
urls = []
with open('links.txt', 'r') as numbers:
num = (sum(1 for _ in numbers)) # Считаем сколько нужно сделать итераций.
with open('links.txt', 'r') as user_urls:
for _ in range(num):
link = user_urls.readline().replace('\n', '')
urls.append(link)
return urls
proxys = get_proxys()
agent = get_user_agent()
spisok1 = []
spisok1.extend(proxys)
spisok2 = []
spisok2.extend(agent)
urls = get_urls() # Список юзеров.
def main(links):
while True:
try:
if len(spisok1) < 10:
# Ох как же долго я искал это решения для перезагрузки списка.
spisok1.extend(proxys)
spisok2.extend(agent)
else:
p = spisok1[0]
spisok1.remove(p)
a = spisok2[0]
spisok2.remove(a)
html = give_me_html(links,p,a)
if html != None:
find_data(html)
break
elif html == 'non':
break
else:
continue
except Exception as errros:
print(errros)
spisok1.extend(proxys) # Подстраховка
spisok2.extend(agent)
continue
pool = Pool(processes=10)
for _ in tqdm(pool.imap_unordered(main, urls), total=len(urls)):
pass
Данная версия справляется всего за час с небольшим, против 19 часов прошлой реализации. При этом я задействую всего десяток пулов, что создаёт жалкую нагрузку в 10%, в сравнении с приростом в скорости работы это конечно пустяк. Разумеется данная реализация была бы невозможна без такого количества проксей, в моё случае я использовал базу из тысячи с небольшим. Разумеется в последней версии моей реализации был предусмотрен промежуточный тест на доступность именно ответа с сайта. То есть все прокси были проверены на валидность перед использованием, проверялись они на ya.ru, ответ получили 200, но наш сайт не такой простой, у него и свой блэк лист имеется, по этому перед запросом на целевой url я делал тестовый пинг на главную страницу сайт, таким образом я релизовал проверку именно доступности, что бы можно было отличить от битой url. То есть, если бы данной проверки не было, сложно было-бы сказать, действительно ссылка ведет на удалённый аккаунт, либо же сайт не отвечает через эту проксю) В общем огород вышел дичайший. Я и не планировал столько мороки. Но всё же приятно осознавать что я справился :3
p.s при 30 пулах, время работы скрипта сократилась до 20 минут)
Данные полученные из ссылок (страны), я упаковал в список, и подсчитал количество вхождений. Наверное можно было как-то проще это всё реализовать, ноо я пока не умею). Для визуализации я использовал встроенную в юпитер библиотеку matplotlib, дабы упросить себе жизнь. Данных не много, по этому я решил представить их в виде круговой диаграммы.