Время прочтения: 6 мин.
Добрый день! В этой статье я поделюсь своим опытом парсинга большого количества сайтов.
Задание:
Получить текст с сайтов (порядка 70к), затем выполнить его последующую обработку, в соответствии с требованиями.
Что необходимо сделать перед началом парсинга:
- Убедиться в корректности ссылок сайтов:
- Корректно указанный протокол (исключаем протоколы: hhtp , hhtps , hhtps , hpp и прочие вариации )
- Отсутствие пробелов, точек и прочих спец символов в начале и конце ссылки (прим. ./http://www.____.ru/. )
- Проверить работоспособность сайтов
- Множество сайтов в датасете были недоступны по тем или иным причинам (прим. нерабочий сайт, ошибка 404, 403 и т.д.)
Для реализации обработки выбран Python, использованы библиотеки:
# Парсинг
import requests
from bs4 import BeautifulSoup as BS
# Обработка текста
import re
# Работа с файлами
import json
import os
#Очистка вывода
from IPython.display import clear_output
Если вы уверены в корректности своих ссылок, этот пункт можно пропустить.
Для первичной обработки пунктов 1 и 2 использовалась функция check_url() , которая принимает в себя ссылку , а возвращает её же исправленную, и код ответа с сайта :
#Список найденных неправильных протоколов
bad_list = ['https','http', 'hhtp://' ,'hhtps://' ,'hhtps://', 'hpp:', 'hpps:', 'hppt', 'hppts', 'htt', 'htth', 'htto', 'htpps', '^', 'httpp', 'httt','ttps']
def cheek_url(url):
s = url
# Удаление протоколов из ссылки
for i in bad_list:
s = s.replace(i,'')
# Удаление первого символа из спец символов в списке
while s[0] in ['/','.',';',':',',']:
s = s[1:]
# Удаление последнего символа из спец символов в списке
while s[-1] in ['/','.',';',':',',']:
s = s[:-1]
url = s
s_c = 'bad_url'
try:
url = f'https://{s}'
# Попытка обратиться к сайту с обновленным защищенным протоколом
response = requests.get(url,timeout = 60)
s_c = response.status_code
if s_c == 200:
return url, s_c
elif s_c != 'bad_url':
return url, s_c
except:
try:
# Попытка обратиться к сайту с обновленным незащищенным протоколом
url = f'http://{s}'
response = requests.get(url,timeout = 60)
s_c = response.status_code
if s_c == 200:
return url, s_c
elif s_c != 'bad_url':
return url, s_c
except:
return url, s_c
Помимо нерабочих протоколов, я удалял и правильные, потому что попадались сайты, не загружаются по протоколу http, но на https все окей, и наоборот.
Далее предстояло определиться: необходимо ли долговременное хранение текста с сайтов, или будет достаточно обработки без дальнейшего сохранения данных.
Долговременное хранение потребует проработки архитектуры хранения сайтов, для последующей работы с ними. Также стоит учитывать вес загруженных текстов и, соответственно, место для хранения. Некоторых сайты с принятыми ограничениями указанными ниже весили до 7ГБ, 5000 сайтов весят в среднем ~ 50 ГБ в не заархивированном состоянии ( в архиве они же ~ 7 ГБ).
Для поставленной задачи, хранение данных было необходимо. Поэтому было принято решение хранить каждый сайт в отдельном json файле, где:
имя файла — id сайта в изначальной таблице
ключи внутри — ссылки страниц
file = { "ссылка на сайт" : {
"status" : "статус сайта (200,404 и т.д.)",
"text" : "текст, загруженный с сайта ",
"img" : [список ссылок на картинки с сайта]
},
"ссылка на сайт" : {...},
...
}
И последнее при подготовке — это определиться, как вы будете хранить данные о кодах сайтов, данные обработки и т.д.
Я использовал для этого таблицы .csv и .xlsx, а работу с ними в python производил с помощью библиотеки pandas.
import pandas as pd
df = pd.read_csv('sites.csv', index_col=0)
df
URL_old | URL | Status | |
0 | 111.ru | https://111.ru | 200 |
1 | 100.ru | http://100.ru | 200 |
2 | www.ru | www.ru | bad_url |
3 | https://vk.com/site | https://vk.com/site | 404 |
4 | http://orrr.ru/ | http://orrr.ru/ | 200 |
2. Парсинг
Закончив с первичной обработкой, приступаем к парсингу рабочих сайтов.
Поставленная передо мною задача требовала обработки большого количества сайтов, при этом была достаточно ограничена по времени. Было принято решение ограничить парсинг загрузкой текста с главной страницы, а также со страниц, на которые можно перейти с главной, исключая тем самым некоторые переходы по внутренним ссылкам сайтов.
Для этого я использовал функцию get_all_links(), которая принимает ссылку на сайт, и возвращает список всех ссылок, найденных на нем
def get_all_links(page):
try:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0'}
html_text = requests.get(page, headers=HEADERS, timeout = 60).text
soup = BS(html_text)
arr = []
end = ['.jpg','.pdf','.exe','.mp4','.jpeg']
for link in soup.find_all('a', href=True):
#Пропуск ссылок, оканчивающиеся на расширения файлов
ch = False
for e in end:
if link.endswith(e):
ch = True
if ch:
continue
l = link['href']
#добавление имени главной страницы сайта, если таковая отсутствует
if l.find("//") == -1 :
try:
if l[0] != "/":
l = page + '/' + l
else:
l = page + l
except:
pass
if l!='':
arr.append(l)
return [el for el, _ in groupby(arr)]
except:
return[]
И наконец можно приступить непосредственно к парсингу. Проходясь по готовому дата фрейму, очищенному от нерабочих сайтов, методично парсим сайты один за одним, и сохраняем каждый в папку.
%%time
i = 0
start_time = time()
directory = 'sites'
#Создание папки под файлы, если еще не создана
if not os.path.isdir(directory):
os.mkdir(directory)
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0'}
l1 = len(df)
for row in df.itertuples(index=True, name='Pandas'):
print( f"Url_n {row.Index} of {l1}. URL = {row.URL}" )
data= {}
# Проверка, не создали ли мы уже файл с таким индексом
if os.path.isfile(f'{directory}/{row.Index}.json'):
i+=1
continue
j = 0
#Получаем ссылки со страницы, и ставим главную страницу на первое место в списке
list_url = get_all_links(row.URL)
list_url.insert(0,row.URL)
error = 0
imgUrls = []
for url in list_url:
clear_output(True)
print(f"i = {i} / {l1}. URL = {row.URL} . name = {row.Index }")
print(f"page {j} of {len(list_url)}. URL = {url}. Error = {error} . Time = {time() - start_time}")
j+=1
list_img = []
try:
try:
html = requests.get(url,headers=HEADERS,timeout = 60).text
except:
data[url]= {'status':'cheek one more',"text":'','img': imgUrls}
continue
soup = BS(html)
text = soup.body.get_text()
#Получаем список с ссылками на все картинки на странице
imgUrls = re.findall('img .*?src="(.*?)"', html)
data[url] = {'status':'ok',"text":text,'img': imgUrls}
except:
error +=1
data[url] = {'status':'error load page',"text":'','img': imgUrls}
#Сохраняем json файл с загруженным сайтом
with open( f'{directory}/{row.Index}.json','w',encoding="utf-8") as file:
json.dump(data,file)
i+=1
Данный процесс можно ускорить, запустив несколько вкладок в Jupyter Notebook, обрабатывая разные части дата фрейма.
Заключение.
По итогу мы получаем файлы с текстом, готовые к обработке.
Важно отметить, что при парсинге обязательно необходим стабильный, желательно скоростной интернет. При прерывание соединения, парсинг не остановится, и будут загружаться пустые страницы, и будет проблематично отследить, когда случилась беда.
Также важно не перегружать компьютер, в том числе оперативную память.
При обработке тяжелых сайтов ( ~4гб) может закончится озу, и загруженные данные обрезаются.
Сложно оценить скорость парсинга и последующей обработки, так как это зависит от производительности всего ПК, скорости интернета и размера каждого сайта, и времени отклика каждого из них.
P.S.
Описанным здесь методом не получится загрузить сайты, защищенные от парсинга.