Время прочтения: 6 мин.

Добрый день! В этой статье я поделюсь своим опытом парсинга большого количества сайтов. 
Задание:

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

Что необходимо сделать перед началом парсинга:

  1. Убедиться в корректности ссылок сайтов: 
    1. Корректно указанный протокол (исключаем протоколы: hhtp , hhtps , hhtps , hpp и прочие вариации  )
    1. Отсутствие пробелов, точек и прочих спец символов в начале и конце ссылки (прим. ./http://www.____.ru/.  )
  2. Проверить работоспособность сайтов
    1. Множество сайтов в датасете были недоступны по тем или иным причинам (прим. нерабочий сайт, ошибка 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_oldURLStatus
0111.ruhttps://111.ru200
1100.ruhttp://100.ru200
2www.ruwww.rubad_url
3https://vk.com/sitehttps://vk.com/site404
4http://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.
Описанным здесь методом не получится загрузить сайты, защищенные от парсинга.