SEO
Влад Медведев
6955

Набор Python-скриптов для автоматизации рутинных задач SEO-специалиста

Статья будет полезна специалистам, которые хотели бы автоматизировать свою работу. Для работы со скриптами потребуются минимальные знания программирования и установленные библиотеки. Для каждого примера в конце есть ссылка на полный код, который нужно открывать в Jupyter Notebook.

В закладки
Аудио

С каждым годом процесс оптимизации требует всё больше технических навыков. Среди текущих требований к специалисту часто указывают, что необходимо знание какого-либо языка программирования, например, Python.

Python — простой и лаконичный язык, позволяющий автоматизировать значительную часть рутинных задач и анализировать данные, что особенно актуально при работе с крупными проектами.

Одно из преимуществ языка — наличие большого количества написанных библиотек. Так как многие сервисы отдают свои данные по API, у специалистов есть возможность без глубоких знаний программирования писать скрипты для решения рабочих задач.

В западных блогах часто можно встретить подборки специалистов, которые делятся своими наработками. В рунете такой информации пока мало, поэтому в этой статье я бы хотел рассказать о полезных библиотеках и поделиться своим набором скриптов, которые постоянно использую в работе.

1. Генерация RSS-фида для турбо-страниц «Яндекса»

Этот способ подходит для случаев, когда необходимо быстро запустить и протестировать турбо-страницы. Рекомендую использовать скрипт для генерации RSS-канала для статейных сайтов, контент которых изменяется редко. Этот метод подходит для быстрого запуска страниц с целью проверить теорию и посмотреть результаты внедрения технологии «Яндекса».

Что потребуется:

  • Netpeak Spider.
  • Базовые знания применения XPath.
  • Установленные Python-библиотеки.

Плюсы подхода:

  • Быстрое внедрение. Не требуется помощь программиста.
  • Не нужно подключение к базе, где хранится контент. Весь контент и его разметку берём прямо со страниц (одновременно минус).
  • Используем стандартные SEO-инструменты.

Минусы:

  • Необновляемый XML-файл. После изменения контента требуется пересобрать контент и формировать новый XML.
  • Новые страницы также не будут попадать в файл. Для них будет необходимо заново парсить контент и формировать XML.
  • Создаём нагрузку на свой сайт при сборе контента.

Ниже описана последовательность работ.

Подготавливаем данные

С помощью Screaming frog seo spider или Netpeak Spider парсим контент страниц, для которых будем подключать турбо-страницы.

На этом этапе подготавливаем данные для обязательных элементов, необходимых при формировании XML-файла.

Обязательные поля:

  • Link — URL страницы.
  • H1 — заголовок страницы.
  • Turbo:content — содержимое страницы.

Подробнее — на странице.

Используя XPath, парсим контент страниц со всей HTML-разметкой. Копируем через панель разработчика или пишем свой запрос (например, //div[@class='entry-content entry--item']).

Экспортируем полученные данные в CSV. В результате в CSV-файле должно быть три столбца:

  • Link.
  • H1.
  • Turbo:content.

Скрипт генерации файла

Подключаем нужные библиотеки.

import csv import pandas as pd import os import math

Считываем файл с подготовленными данными.

data = pd.read_csv('internal_all - internal_all.csv') #дописать ", header=1", если проблема при считывании заголовка data = data[['Address', 'H1-1', 'текст 1']] ## Если в таблицу попали лишние страницы, их можно легко отфильтровать. Ниже примеры. # data = data[data['Status Code']==200] # Фильтруем страницы с 200 ответом # data = data[~data['Address'].str.contains('page')] # Фильтруем страницы не содержащие "" # data = data.drop(index=0)

Выводим информацию о количестве строк в файле и итоговом количестве RSS-файлов, которые будут сгенерированы.

rows_in_rss = 1000 # количество строк в одном rss-канале total_rows = len(data) - 1 total_xml_file = math.ceil((total_rows-1)/rows_in_rss) print('Всего в файле строк:', total_rows) print('Будет сгенерировано xml-файлов:', total_xml_file)

Формируем структуру RSS-канала. Создаём функцию create_xml, отвечающую за создание начала файла.

def create_xml(item_count_next): rss_file = open('rss{:.0f}.xml'.format(item_count_next/rows_in_rss), 'w', encoding="utf-8") rss_file.write( """<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:yandex="http://news.yandex.ru" xmlns:media="http://search.yahoo.com/mrss/" xmlns:turbo="http://turbo.yandex.ru" version="2.0"> <channel>""") rss_file.close()

Функция close_xml будет закрывать файл.

def close_xml(item_count_next): rss_file = open('rss{:.0f}.xml'.format(item_count_next/rows_in_rss), 'a', encoding="utf-8") rss_file.write( ' </channel>' + '\n'+ '</rss>' ) rss_file.close()

В функцию data_for_rss передаём номер первой и последней строки. Для этого промежутка будем формировать RSS.

Построчно считываем строки в датафрейме и формируем <item>, записывая получившиеся данные в XML-файл. Каждая строка в датафрейме — новая страница.

def data_for_rss(item_count_prev, item_count_next): data_rss = data[item_count_prev:item_count_next] if len(data_rss) != 0: with open('rss{:.0f}.xml'.format(item_count_next/rows_in_rss), 'a', encoding="utf-8") as rss_file: for index, row in data_rss.iterrows(): url = str(row[0]) h1 = str(row[1]) text = str(row[2]) rss_file.write("""<item turbo="true"> <link>"""+ url + """</link> <turbo:content> <![CDATA[ <header> <h1>"""+ h1 +"""</h1> </header>""" + text+ """<div data-block="share" data-network="vkontakte,odnoklassniki,facebook,twitter"></div> ]]> </turbo:content> </item>""")

Делаем проверку размера получившихся фидов. Размер XML-файла не должен превышать 15 МБ. Если размер получился больше, изменяем количество строк в одном файле, изменяя значение переменной rows_in_rss.

def size_file(item_count_next): size_final_file_MB = os.path.getsize('rss{:.0f}.xml'.format(item_count_next/rows_in_rss))/1024/1024 if size_final_file_MB < 15: print('Файл создан') else: print('Нужно уменьшить шаг')

Финальный шаг — генерация фидов.

item_count_prev = 0 item_count_next = 1000 # должен быть равен rows_in_rss count_rss = 0 print(total_xml_file) while count_rss < total_xml_file: create_xml(item_count_next) data_list = data_for_rss(item_count_prev,item_count_next) close_xml(item_count_next) size_file(item_count_next) item_count_prev += rows_in_rss item_count_next += rows_in_rss count_rss += 1

Остаётся добавить RSS в личном кабинете «Яндекс.Вебмастера» и настроить меню, лого, счётчики систем аналитики.

Ссылка на скрипт (открывать в Jupyter Notebook).

2. Техническое задание для копирайтеров

ТЗ для копирайтеров — довольно рутинная работа, которая занимает много времени у специалиста. Ниже рассмотрим три варианта автоматизации этого процесса, используя различные сервисы:

  • Семантического ядра нет, статья не написана.
  • Семантическое ядро есть, статья не написана.
  • Статья написана, требуется рерайт.

Что потребуется:

  • Подписка на сервис с доступом к API.
  • Подписка на сервис с пополненным балансом.
  • Установленные Python-библиотеки.
  • Кластеризованное семантическое ядро (я использую KeyAssort) для случая, когда ядро есть, а статья не написана.

Ниже рассмотрим все варианты, для каждого я распишу плюсы и минусы этих подходов. Стоит помнить, что результаты, которые выдают сервисы, стоит перепроверять. Так как везде есть свои технические нюансы.

Вариант 1. Семантического ядра нет, статья не написана

Рассмотрим случай, когда нужно написать статью, но у вас нет готового семантического ядра. Для этого подхода нам понадобится только основной маркерный запрос статьи.

Плюсы подхода:

  • Не тратим время на сбор ядра (подходит для статей с широкой семантикой и хорошей видимостью URL конкурентов в топе).
  • В работу берём максимальное количество ключей, по которым конкуренты имеют видимость.

Минусы:

  • Нужна подписка на сервисы.
  • Данные, которые выдают сервисы, не всегда точны. Например, Megaindex не определяет длину текста меньше определённого количества знаков (около 200 символов). Поэтому показатели выборочно стоит перепроверить.
  • Не можем повлиять на кластеризацию.

Подключаем необходимые библиотеки.

import requests import json import pymorphy2 import re import urllib.request as urlrequest from urllib.parse import urlencode from collections import Counter

Нам понадобятся:

  • Token — токен API MegaIndex.
  • Ser_id — регион, по которому будут сниматься данные. Полный список можно получить, используя метод get_ser.
  • Keywords_list — список ключевых слов, для которых будем получать данные.
token = "xxxxxxxxxxxxxxxxxxx" ser_id = 174 #ID поисковой системы яндекс_спб keywords_list = ['основной маркерный запрос статьи №1', 'основной маркерный запрос статьи №2', 'основной маркерный запрос статьи №3'] morph = pymorphy2.MorphAnalyzer() # создаем экземпляр pymorphy2, понадобится нам дальше для морфологического анализа

Для получения ключевых слов по нужным нам маркерным запросам будем использовать метод url_keywords API Serpstat. Этот метод возвращает ключевые фразы в топе поисковой системы по заданному URL. Получать будем видимость конкурентов по URL, которые находятся в топе выбранной поисковой системы.

Для работы берём пример кода из документации и оборачиваем его в функцию serpstat_keywords. Подставляем свои значения для token и региона se, по которому будем получать данные. Получить полный список регионов можно здесь.

def serpstat_keywords(url): host = 'http://api.serpstat.com/v3' method = 'url_keywords' params = { 'query': '{}'.format(url), # string for get info 'se': 'y_213', # string search engine, y_2 - спб, y_213 - мск 'token': 'xxxxxxxxxxxxxxxxxxx', # string personal token } api_url = "{host}/{method}?{params}".format( host=host, method=method, params=urlencode(params) ) try: json_data = urlrequest.urlopen(api_url).read() except Exception as e0: print("API request error: {error}".format(error=e0)) pass data = json.loads(json_data) return data

Используя регулярное выражение, разбиваем исходную фразу на слова. Каждое слово лемматизируем, проверяем на часть речи и добавляем в результирующий список. Возвращаем готовый список.

def morph_word_lemma(key): meaningfullPoSes=['NPRO', 'PREP', 'CONJ', 'PRCL', 'INTJ'] # фильтруем граммемы https://pymorphy2.readthedocs.io/en/latest/user/grammemes.html reswords=[] for word in re.findall("([А-ЯЁа-яё0-9]+(-[А-ЯЁа-яё0-9]+)*)", key): # фразу бьем на слова word = word[0] word_normal_form = morph.parse(word)[0].normal_form form = morph.parse(word)[0].tag if form.POS in meaningfullPoSes: continue else: reswords.append(word_normal_form) return reswords

Не забываем, что pymorphy2 работает только с русским языком. Если в словосочетаниях будут фразы на другом языке, он их пропустит.

Составляем словарь вида «Лемма: [количество упоминаний леммы]».

def counter_dict_list(list_values): list_values_all=[] for item in list_values: list_values_word_lemma = morph_word_lemma(item) for item in list_values_word_lemma: list_values_all.append(item) dict_values_word_lemma = dict(Counter(list_values_all)) sorted_dict_values_word_lemma = list(dict_values_word_lemma.items()) sorted_dict_values_word_lemma.sort(key=lambda i: i[1], reverse=True) sorted_dict_values_word_lemma = dict(sorted_dict_values_word_lemma) return (sorted_dict_values_word_lemma)

Создаём финальный файл и записываем строку заголовка.

# чистим файл и записываем строку заголовка f = open('api.txt', 'w') f.write("key"+'\t' + "base_urls"+ '\t' + 'symbols_median' + '\t' + '\n') f.close()

Получаем данные по API и парсим полученный текст.

def megaindex_text_score(key): keyword_list = [] uniq_keyword_list = [] try: url = 'http://api.megaindex.com/visrep/text_score?key={}&words={}&ser_id={}'.format(token, key, ser_id) r = requests.get(url) json_string = r.text parsed_string = json.loads(json_string)['data'] list_base_urls = parsed_string['serps'][0]['base_urls'] symbols_median = parsed_string['old_api']['fragments']['long']['symbols_median'] except Exception as ex_megaindex: print("API megaindex request error: {error}".format(error=ex_megaindex)) list_base_urls = [] symbols_median = 'Данные не получены' for url in list_base_urls: url = url.replace('http:', 'https:') data = serpstat_keywords(url) try: for keyword in data['result']['hits']: keyword_list.append(keyword['keyword']) except: pass for item in set(keyword_list): uniq_keyword_list.append(item) count_lemma = counter_dict_list(uniq_keyword_list) return (list_base_urls, symbols_median, count_lemma)

Проходимся по списку маркерных запросов и генерируем задание.

print ('Всего будет сгенерировано ТЗ: ', len(keywords_list)) for keywords in keywords_list: print(keywords) try: list_base_urls, symbols_median, count_lemma = megaindex_text_score(keywords) except Exception as ex: pass print(f'Errow: {ex}') with open('api.txt', 'a') as f: f.write('{}\t{}\t{}\t\n\n'.format(keywords, list_base_urls, symbols_median)) f.write('Лемма' +'\t' + 'Количество повторений' + '\n') for key, value in count_lemma.items(): f.write('{}\t{}\n'.format(key, value)) f.write('\n'+'\n'+'\n') print ('end')

Получившийся результат переносим в «Google Таблицы». Пример ТЗ.

Нужно понимать, что «количество упоминаний леммы» в ТЗ — это сколько раз лемма встречалась в ключевых словах.

Ссылка на скрипт (открывать в Jupyter Notebook).

Вариант 2. Статья написана, требуется рерайт

Подход применим для случаев, когда статья уже написана, но не получает трафика.

Плюсы подхода:

  • В автоматическом режиме получаем средний объём текста в топ-10, объём анализируемого текста и разницу этих величин.
  • В работу берём максимальное количество ключей, по которым конкуренты имеют видимость.

Минусы (те же, что и у варианта номер один):

  • Нужна подписка на сервисы.
  • Данные, которые выдают сервисы, не всегда точны. Например, Megaindex не определяет длину текста меньше определённого количества знаков (около 200 символов). Поэтому показатели выборочно стоит перепроверить.
  • Не можем повлиять на кластеризацию.

Набор библиотек аналогичен варианту номер один, отличается набор входных параметров. Вместо списка основных маркерных запросов передаём словарь следующего вида:

{'основной маркерный запрос статьи №1':'url, соответствующий основному маркерному запросу'}

token = "xxxxxxxxxxxxxxxxxxx" ser_id = 174 #ID поисковой системы яндекс_спб - 174 keywords_url_dict = {'основной маркерный запрос статьи №1':'url_основного маркерного запроса статьи №1', 'основной маркерный запрос статьи №2':'url_основного маркерного запроса статьи №2'} morph = pymorphy2.MorphAnalyzer() # создаем экземпляр pymorphy2, понадобится нам дальше для морфологического анализа

Следующие функции копируем из первого варианта:

  • serpstat_keywords;
  • morph_word_lemma;
  • counter_dict_list.

Чистим файл и записываем строку заголовка.

f = open('api.txt', 'w') f.write("key"+'\t'+"compare_urls" + '\t' + "base_urls"+ '\t' + "relevance" + '\t' + 'symbols median' + '\t' +'symbols text'+ '\t' + 'symbols diff'+ '\t'+ 'words median' + '\t' + 'words value text' + '\t' + 'words diff' + '\n') f.close()

Получаем данные по API и парсим полученный текст. Получать будем следующие данные для ТЗ:

  • list_base_urls — список URL в топ-10 по маркерному запросу;
  • relevance — релевантность анализируемой страницы страницам в топе;
  • symbols_median — медиана длины текста (знаков без пробелов) по топу;
  • symbols_text — количество символов в анализируемом тексте;
  • symbols_diff — разница symbols_median и symbols_text;
  • words_median — медиана слова в URL по топу;
  • words_value_text — медиана слов в анализируемом тексте;
  • words_diff — разница слов;
  • count_lemma— посчитанные леммы.
def megaindex_text_score(key, key_url): keyword_list = [] uniq_keyword_list = [] try: url = 'http://api.megaindex.com/visrep/text_score?key={}&words={}&ser_id={}&compare_urls={}'.format(token, key, ser_id, key_url) r = requests.get(url) json_string = r.text parsed_string = json.loads(json_string)['data'] list_base_urls = parsed_string['serps'][0]['base_urls'] relevance = parsed_string['serps'][0]['compare_urls'][0]['relevance']*100 symbols_median = parsed_string['old_api']['fragments']['long']['symbols_median'] symbols_text = parsed_string['old_api']['compare_docs'][key_url]['fragments']['long']['symbols'] symbols_diff = symbols_median - symbols_text words_median = parsed_string['serps'][0]['compare_urls'][0]['diffs']['word_count']['long']['median'] words_value_text = parsed_string['serps'][0]['compare_urls'][0]['diffs']['word_count']['long']['value'] words_diff = parsed_string['serps'][0]['compare_urls'][0]['diffs']['word_count']['long']['diff'] except Exception as ex_megaindex: print("API megaindex request error: {error}".format(error=ex_megaindex)) list_base_urls = [] symbols_median = 'Данные не получены' for url in list_base_urls: url = url.replace('http:', 'https:') data = serpstat_keywords(url) try: for keyword in data['result']['hits']: keyword_list.append(keyword['keyword']) except: pass for item in set(keyword_list): uniq_keyword_list.append(item) count_lemma = counter_dict_list(uniq_keyword_list) return (list_base_urls, relevance, symbols_median, symbols_text, symbols_diff, words_median, words_value_text, words_diff, count_lemma)

Проходимся по списку маркерных запросов и генерируем задание.

print ('Всего будет сгенерировано ТЗ: ', len(keywords_url_dict)) for keywords in keywords_url_dict.keys(): print(keywords, keywords_url_dict[keywords]) try: list_base_urls, relevance, symbols_median, symbols_text, symbols_diff, words_median, words_value_text, words_diff, count_lemma = megaindex_text_score(keywords, keywords_url_dict[keywords]) except Exception as ex: pass print(f'Errow: {ex}') with open('api.txt', 'a') as f: f.write('{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t\n\n'.format(keywords, keywords_url_dict[keywords], list_base_urls, relevance, symbols_median, symbols_text, symbols_diff, words_median, words_value_text, words_diff)) f.write('Лемма' +'\t' + 'Количество повторений' + '\n') for key, value in count_lemma.items(): f.write('{}\t{}\n'.format(key, value)) f.write('\n'+'\n'+'\n') print ('end')

Ссылка на скрипт (открывать в Jupyter Notebook).

Вариант 3. Семантическое ядро есть, статья не написана

Рассмотрим ситуацию, когда у специалиста есть собранное и кластеризованное семантическое ядро.

Плюсы подхода:

  • Работаем уже с тщательно проработанным и кластеризованным семантическим ядром.

Минусы (почти те же, что и у первого варианта):

  • Нужна подписка на сервисы.
  • Данные, которые выдают сервисы, не всегда точны. Например, Megaindex не определяет длину текста меньше определённого количества знаков (около 200 символов). Поэтому показатели выборочно стоит перепроверить.

Подключаем необходимые библиотеки, указываем токен для работы с Megaindex и ser_id нужного региона.

import pymorphy2 import requests import json import re morph = pymorphy2.MorphAnalyzer() token = "xxxxxxxxxxxxxxxxxxxxx" ser_id = 174 #174 #ID поисковой системы яндекс_спб

Для работы скрипта нам понадобится txt-файл ('data_tz.txt') с кластеризованным ядром.

Формат файла: Ключ → Группа; разделитель табуляция.

item_dict = {} flag = True with open('data_tz.txt') as file: for line in file: if flag: flag = False # пропускаем строку заголовка else: line = line.strip().split(' ') word = line[0] group = line[1] if group not in item_dict: item_dict[group] = [] item_dict[group].append(word) else: item_dict[group].append(word)

Работаем со словарём, полученным на предыдущем шаге. Для каждой группы обходим все ключевые фразы, разбиваем их на слова, нормализуем и добавляем в словарь.

group_word_count_dict = {} for key, value in item_dict.items(): group_word_count_dict.setdefault(key, {}) for item in value: for word in re.findall("([А-ЯЁа-яё0-9]+(-[А-ЯЁа-яё0-9]+)*)", item): word = word[0] word = morph.parse(word)[0].normal_form form = morph.parse(word)[0].tag #не добавляем в словарь местоимение-существительное, предлог, союз, частица, междометие if ('NPRO' in form or 'PREP' in form or 'CONJ' in form or 'PRCL' in form or 'INTJ' in form): continue else: group_word_count_dict[key].setdefault(word, 0) if word in group_word_count_dict[key]: group_word_count_dict[key][word] += 1 #Сортировка получивщегося словаря for key, value in group_word_count_dict.items(): sorted_group_word_count_dict = list(value.items()) sorted_group_word_count_dict.sort(key=lambda i: i[1], reverse=True) sorted_group_word_count_dict = dict(sorted_group_word_count_dict) group_word_count_dict[key] = sorted_group_word_count_dict print(group_word_count_dict) print('end')

Получаем данные по API и парсим полученный текст.

def megaindex_text_score(key): try: url = 'http://api.megaindex.com/visrep/text_score?key={}&words={}&ser_id={}'.format(token, key, ser_id) r = requests.get(url) json_string = r.text parsed_string = json.loads(json_string)['data'] list_base_urls = parsed_string['serps'][0]['base_urls'] symbols_median = parsed_string['old_api']['fragments']['long']['symbols_median'] except Exception as ex_megaindex: print("API megaindex request error: {error}".format(error=ex_megaindex)) list_base_urls = ['Данные не получены'] symbols_median = 0 return(list_base_urls, symbols_median)

Подготавливаем финальный файл.

# чистим файл f = open('group_word_lemma.txt', 'w') f.write('Группа' +'\t' + 'Конкуренты' +'\t' + 'Символов ЗБП'+ '\n') f.close() with open('group_word_lemma.txt' , 'a') as f: for key_dict, value_dict in group_word_count_dict.items(): base_urls, symbols_median = megaindex_text_score(key_dict) if symbols_median < 8000: # Ограничение по количеству символов print(key_dict, base_urls, symbols_median) f.write('{}\t{}\t{}\n\n'.format(key_dict, base_urls, symbols_median)) f.write('Лемма' +'\t' + 'Количество повторений' + '\n') for key, value in value_dict.items(): print(key, value) f.write('{}\t{}\n'.format(key, value)) f.write('\n'+'\n'+'\n') print('end')

Проходимся по списку групп и генерируем задание.

with open('group_word_lemma.txt' , 'a') as f: for key_dict, value_dict in group_word_count_dict.items(): base_urls, symbols_median = megaindex_text_score(key_dict) if symbols_median < 8000: # Ограничение по количеству символов print(key_dict, base_urls, symbols_median) f.write('{}\t{}\t{}\n\n'.format(key_dict, base_urls, symbols_median)) f.write('Лемма' +'\t' + 'Количество повторений' + '\n') for key, value in value_dict.items(): print(key, value) f.write('{}\t{}\n'.format(key, value)) f.write('\n'+'\n'+'\n') print('end')

Так как основной маркерный запрос в этом случае — название категории, нужно следить за полнотой и правильностью её написания.

Аналогично первому варианту, получившийся результат переносим в «Google Таблицы». Получившееся ТЗ в таком же формате.

Ссылка на скрипт (открывать в Jupyter Notebook).

Предложенные скрипты можно дорабатывать, добавляя в них и другие важные на ваш взгляд требования к тексту.

Имея список URL конкурентов, можно парсить:

  • Title страниц.
  • Заголовки H1 — H6.
  • Количество нумерованных, маркированных списков, изображений на странице и так далее.

Аналогичным способом делать морфологический анализ тегов, заголовков и выдавать рекомендации по количеству элементов и упоминаний лемм в этих тегах.

3. Анализ логов

При техническом аудите сайтов полезно анализировать логи сайта. Возможные варианты анализа:

  • Использовать возможности, которые предоставляет хостер. Чаще всего это решение в виде надстройки, например, AWStats. Минусы: не гибко, чаще всего предоставляется определённый набор графиков, которые никак не изменить.
  • Использовать платные решения. Например, Screaming Frog SEO Log File Analyser — бесплатная версия работает с файлами до 1000 строк. Минусы: цена, не всегда логи вашего сервера будут соответствовать тому виду, который требуется для работы в программе.
  • Использовать ELK-стек (elastic + logstash + kibana). Минусы: требуются знания по настройке хранилища и передаче в него данных.
  • Решение на Python с использованием библиотек.

Подробнее что про то, что такое логи, их структуру и содержание можно почитать в статье. Перейдём к скрипту.

Что потребуется:

  • Лог-файлы сайта.
  • Установленные Python-библиотеки.

Плюсы подхода:

  • Бесплатное решение.
  • Можно быстро проанализировать лог-файл в любом формате.
  • Легко обрабатывает большие файлы на несколько миллионов записей.

Минусы:

  • Хранение данных на своём устройстве (если работаете не на выделенном сервере).
  • Чтобы проанализировать данные за новый период, необходимо заново считать данные, разобрать и записать их в анализируемый CSV-файл.
  • В приведённом скрипте только базовые универсальные примеры анализа.

Для работы будем использовать библиотеку apache-log-parser, подробная документация по ссылке на GitHub.

import apache_log_parser import csv

Для начала обработаем наш лог-файл и запишем данные в CSV. Если файлов несколько, склеить их можно следующей командой:

!cat access.log.1 access.log.2 access.log.3 > all_log.log

Создаем файл log.csv и записываем в него строку заголовка с названием столбцов. Столбцы определяются в соответствии с вашим лог-файлом.

csv_file = open('log.csv', 'w') data = [['remote_host', 'server_name2', 'query_string', 'time_received_isoformat', 'request_method', 'request_url', 'request_http_ver', 'request_url_scheme', 'request_url_query', 'status', 'response_bytes_clf', 'request_header_user_agent', 'request_header_user_agent__browser__family', 'request_header_user_agent__browser__version_string', 'request_header_user_agent__os__family', 'request_header_user_agent__os__version_string', 'request_header_user_agent__is_mobile']] with csv_file: writer = csv.writer(csv_file) writer.writerows(data) csv_file.close()

Читаем построчно access.log, парсим строку и записываем разобранные данные в CSV. Используем функцию make_parser, которая принимает строку из файла журнала в указанном нами формате и возвращает проанализированные значения в виде словаря.

Формат строки из журнала указывается в make_parserс помощью поддерживаемых значений, указанных в документации, — supported values.

Пример строки

54.36.148.252 example.ru — [13/Oct/2019:12:00:01 +0300] "GET /lenta/example/example/p1 HTTP/1.1" 301 5 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" 0.137 0.137 .

Пример разбора

with open('all_log.log') as file: for line in file: line = line.strip() line_parser = apache_log_parser.make_parser("%h %V %q %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"") log_line_data = line_parser(f'{line}') #Пишем в файл нужные данные data = [[log_line_data['remote_host'], log_line_data['server_name2'], log_line_data['query_string'], log_line_data['time_received_isoformat'], log_line_data['request_method'], log_line_data['request_url'], log_line_data['request_http_ver'], log_line_data['request_url_scheme'], log_line_data['request_url_query'], log_line_data['status'], log_line_data['response_bytes_clf'], log_line_data['request_header_user_agent'], log_line_data['request_header_user_agent__browser__family'], log_line_data['request_header_user_agent__browser__version_string'], log_line_data['request_header_user_agent__os__family'], log_line_data['request_header_user_agent__os__version_string'], log_line_data['request_header_user_agent__is_mobile']]] csv_file = open('log.csv', 'a') with csv_file: writer = csv.writer(csv_file) writer.writerows(data)

Далее анализируем полученный CSV-файл. Анализ можно провести в Excel или любом другом удобном инструменте. Для примера рассмотрим несколько вариантом получения данных на Python.

Подключаем библиотеку для анализа данных и считываем файл.

import pandas as pd data = pd.read_csv('log.csv')

Посмотрим распределение страниц по статус коду страниц.

status_code_count = data['status'].value_counts() print(status_code_count)

Посчитаем количество страниц со статусом 410 для каждого user-agent.

data[data['status']==410]['request_header_user_agent__browser__family'].value_counts()

В результате работы скрипта мы получили готовый CSV-файл с разобранными по столбцам записями из лог-файла. Далее можно анализировать данные в соответствии с вашими целями.

Ссылка на скрипт (открывать в Jupyter Notebook).

Материал опубликован пользователем.
Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Влад Медведев", "author_type": "self", "tags": ["\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0430","\u043f\u0438\u0448\u0435\u043c","\u043d\u0435","\u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b","\u0434\u043e\u043f\u0438\u0441\u0430\u0442\u044c","id","174"], "comments": 9, "likes": 54, "favorites": 238, "is_advertisement": false, "subsite_label": "seo", "id": 91963, "is_wide": false, "is_ugc": true, "date": "Mon, 11 Nov 2019 16:00:10 +0300", "is_special": false }
Объявление на vc.ru
Финансы
Госдума приняла законопроект, который упрощает режим использования зарубежных счетов и вкладов
В среду, 20 ноября, Государственная Дума в третьем чтении приняла законопроект, который упрощает жизнь валютным…
0
{ "id": 91963, "author_id": 218681, "diff_limit": 1000, "urls": {"diff":"\/comments\/91963\/get","add":"\/comments\/91963\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/91963"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 199127, "last_count_and_date": null }
9 комментариев
Популярные
По порядку
Написать комментарий...
1

Больше автоматизации!! Еще и наработки есть?

Ответить
2

Да, есть. Но в основном все специфичные, под конкретные задачи, конкретного сайта. В статье описал наиболее универсальные

Ответить
2

Примеры решения специфичных задач тоже интересно посмотреть.

Ответить
1

Более 100 человек посчитали полезным. Менее 20 решили поблагодарить плюсиком. Три человека написали комментарии. Из них 1 по делу, один автор, один я.

Такая себе вороночка)

Ответить
0

+ многие пишут сразу в лс, так что в итоге неплохо получается

Ответить
0

Это отлично! Статья ведь годная.

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

Ответить
1

С такими материалами так чаще всего и бывает. В закладки чтобы почитать потом.

Ответить
0

не могу поставить +

Ответить
0

У меня недавно спрашивали, почему Python вдруг стал таким популярным. Да вот почему.

Ответить

Комментарий удален

{ "page_type": "article" }

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fizc" } } }, { "id": 4, "label": "Article Branding", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "cfovx", "p2": "glug" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "bscsh", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-223676-0", "render_to": "inpage_VI-223676-0-1104503429", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=bugf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Баннер в ленте на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudx", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byzqf", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvc" } } }, { "id": 19, "disable": true, "label": "Тизер на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "p1": "cbltd", "p2": "gazs" } } }, { "id": 20, "label": "Кнопка в сайдбаре", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "cgxmr", "p2": "gnwc" } } } ] { "page_type": "default" }