Быстрый анализ сайтов конкурентов через сайтмапы. Часть 1 - парсинг

Или как минимальным ресурсом понять SEO модель целого рынка конкурентов за пару дней

Posted by snakers41 on June 24, 2017

Статьи цикла

Быстрый анализ сайтов конкурентов через сайтмапы. Часть 1 - парсинг

Быстрый анализ сайтов конкурентов через сайтмапы. Часть 2 - простой анализ

Быстрый анализ сайтов конкурентов через сайтмапы. Часть 3 - более глубокий анализ


Как всегда надо начать с заманчивой картинки, но не пояснять ее смысл!


Пару дней назад ко мне обратился далекий знакомый, мол у нас политесы в компании и мы не можем никак выбрать направление развития. Ситуация типичная - лебедь, рак и щука и надо бы очень быстро проанализировать сайты его конкурентов, вот держи картинку со списком самых крупных компаний в этой сфере. Оказалось, что конкуренты из сферы форекса. Это меня смутило, но не остановило. Я посмотрел на их сайты (а, забегая вперед, некоторые из них просто огромны - миллионы страниц) и тут у меня родилась гениальная идея.

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



Картинка, которую мне прислали. Даже nutshell не нужен.


После визита на пару сайтов, у меня в голове промелькнули такие мысли:

  1. Парсинг (или "скрепинг") сайтов в современном мире - это лучшее средство business intelligence. Ибо сейчас все выкладывают весь свой контент для индексации в интернет;
  2. Недавно я видел отличную и простую статью, про то, что мол парсинг это чуть ли ваша обязанность, если вы аналитик;
  3. У меня есть знакомый, которому один раз предлагали спарсить весь вконтакте за 300,000 рублей  1 раз. Так вот он говорил, что в одноразовом парсинге на питоне вообще нет ничего сложного. Потоковый парсинг сложнее, там надо иметь прокси, VPN и балансировщик нагрузки и очередь;
  4. Самые крупные сайты как правило имеют сайтмапы;
  5. По этой причине этот подход в принципе применим для любой отрасли, где присутствует много контента;
  6. Идея также расширяется до выборочного парсинга самих страниц, но это на порядок сложнее технически (оставим это более прошаренным коллегам);


Эта статья будет скорее оформлена в виде пошагового гайда, что немного в новинку для меня. Но почему нет, давайте попробуем?

1. Идея

Через секунду после того, как я подумал про сайтмапы, у меня сразу родился план действий:

  • Найти все сайтмапы сайтов;
  • Внести в массив, указав рекурсивные ли они;
  • Собрать итоговый список сайтмапов (а их явно будут сотни или тысячи);
  • Спарсить их;
  • Распарсить урлы на составляющие;
  • Сделать семантический анализ составляющий урлов;
  • Если хватит сил, прогнать bag-of-words анализ, снизить размерность через метод главных компонент (PCA) и посмотреть что будет;
  • Построить визуализации для самых популярных слов;
  • Сделать простейшие сводные таблицы;
  • Если будет нужно и полезно - подключить к процессу еще и словесные вектора (word2vec);


Забегая вперед, что получилось посмотреть можно тут:

Проще всего вам будет следить за повествованием установив себе зависимости, запустив jupyter notebook и выполняя код последовательно. Внимание (!) - иногда из-за размерности файлов отжирается вся память (на моей  песочнице 16ГБ) - будьте внимательны! Для простейшего мониторинга рекомендую glances.

2. Зависимости (Common libraries, Code progress utility)

Все делается на третьем питоне.

  • По сути вам нужен сервер с python3 и jupyter notebook (без этого будет гораздо дольше).
  • Также я очень советую поставить себе плагин сollapsable / expandable jupyter cells;
  • Список основных библиотек и зависимостей указан ниже (или я буду добавлять их в отдельных ячейках);


from __future__ import print_function
import os.path
from collections import defaultdict
import string
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
from sklearn.feature_extraction.text import CountVectorizer
import wordcloud
%matplotlib inline


Если у вас все работает, то .ipynb файл откроется примерно так (сверните ячейки, если они не свернуты по умолчанию):


Также отсюда я взял небольшую утилитку для демонстрации прогресса парсинга.  Инструкции по установке также по ссылке.


3. Список сайтмапов (Sitemap list)

Тут все банально, гуляем по сайтам, ищем сайтмапы (обычно они лежат в корне с названием sitemap.xml). Поиск google по сайту также помогает. Записываем в лист словарей.

sitemap_list = [ 
    {'url': 'https://www.ig.com/sitemap.xml', 'recursive': 1},
    {'url': 'https://www.home.saxo/sitemap.xml', 'recursive': 0},    
    {'url': 'https://www.fxcm.com/sitemap.xml', 'recursive': 1},  
    {'url': 'https://www.icmarkets.com/sitemap_index.xml', 'recursive': 1},  
    {'url': 'https://www.cmcmarkets.com/en/sitemap.xml', 'recursive': 0},
    {'url': 'https://www.oanda.com/sitemap.xml', 'recursive': 0},     
    {'url': 'http://www.fxpro.co.uk/en_sitemap.xml', 'recursive': 0}, 
    {'url': 'https://en.swissquote.com/sitemap.xml', 'recursive': 0}, 
    {'url': 'https://admiralmarkets.com/sitemap.xml', 'recursive': 0},     
    {'url': 'https://www.xtb.com/sitemap.xml', 'recursive': 1},       
    {'url': 'https://www.ufx.com/en-GB/sitemap.xml', 'recursive': 0},   
    {'url': 'https://www.markets.com/sitemap.xml', 'recursive': 0},   
    {'url': 'https://www.fxclub.org/sitemap.xml', 'recursive': 1},       
    {'url': 'https://www.teletrade.eu/sitemap.xml', 'recursive': 1},       
    {'url': 'https://bmfn.com/sitemap.xml', 'recursive': 0},       
    {'url': 'https://www.thinkmarkets.com/en/sitemap.xml', 'recursive': 0},  
    {'url': 'https://www.etoro.com/sitemap.xml', 'recursive': 1},  
    {'url': 'https://www.activtrades.com/en/sitemap_index.xml', 'recursive': 1},  
    {'url': 'http://www.fxprimus.com/sitemap.xml', 'recursive': 0}
]


Тут немаловажно, что часть сайтмапов рекурсивная (то есть содержит ссылки на другие сайтмапы), а часть нет.

4. Собственно сам сбор сайтмапов (Web scraping)

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

Для этого нам поможет библиотека fake_useragent:

from fake_useragent import UserAgent
ua = UserAgent()
headers = ua.chrome
headers = {'User-Agent': headers}


Если мы попробуем забрать один сайтмап, то мы увидим, что его надо декодировать

result = requests.get(sitemap_list[3]['url'])
c = result.content
c = c.decode("utf-8-sig")
c

Ответ выглядит примерно так:

'<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="//www.icmarkets.com/main-sitemap.xsl"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/post-sitemap.xml</loc>\n\t\t<lastmod>2016-12-16T07:13:32-01:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/page-sitemap.xml</loc>\n\t\t<lastmod>2017-06-20T07:11:01+00:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/attachment-sitemap1.xml</loc>\n\t\t<lastmod>2014-07-01T15:44:46+00:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/attachment-sitemap2.xml</loc>\n\t\t<lastmod>2014-10-29T02:36:07-01:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/attachment-sitemap3.xml</loc>\n\t\t<lastmod>2015-03-15T18:41:51-01:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/attachment-sitemap4.xml</loc>\n\t\t<lastmod>2017-05-30T12:33:34+00:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/category-sitemap.xml</loc>\n\t\t<lastmod>2016-12-16T07:13:32-01:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/post_tag-sitemap.xml</loc>\n\t\t<lastmod>2014-03-27T01:14:54-01:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/csscategory-sitemap.xml</loc>\n\t\t<lastmod>2013-06-11T00:02:10+00:00</lastmod>\n\t</sitemap>\n\t<sitemap>\n\t\t<loc>https://www.icmarkets.com/author-sitemap.xml</loc>\n\t\t<lastmod>2017-05-05T06:44:19+00:00</lastmod>\n\t</sitemap>\n</sitemapindex>\n<!-- XML Sitemap generated by Yoast SEO -->'


Эта функция найденная на просторах интернета поможет нам декодировать XML дерево, которым является сайтмап:

# xml tree parsing
import xml.etree.ElementTree as ET

def xml2df(xml_data):
    root = ET.XML(xml_data) # element tree
    all_records = []
    for i, child in enumerate(root):
        record = {}
        for subchild in child:
            record[subchild.tag] = subchild.text
            all_records.append(record)
    return pd.DataFrame(all_records)


Далее эта функция поможет нам собрать все сайтмапы в одном листе:

end_sitemap_list = []
for sitemap in log_progress(sitemap_list, every=1):
    if(sitemap['recursive']==1):
        try:
            result = requests.get(sitemap['url'], headers=headers)
            c = result.content
            c = c.decode("utf-8-sig")
            df = xml2df(c)
            end_sitemap_list.extend(list(df['{http://www.sitemaps.org/schemas/sitemap/0.9}loc'].values))
        except:
            print(sitemap)
    else:
        end_sitemap_list.extend([sitemap['url']])


В разное время у меня получалось от 200 до 250 сайтмапов.

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


result_df = pd.DataFrame(columns=['changefreq','loc','priority'])
for sitemap in log_progress(end_sitemap_list, every=1):
    
    result = requests.get(sitemap, headers=headers)
    c = result.content
    try:
        c = c.decode("utf-8-sig")
        df = xml2df(c)
        columns = [
            '{http://www.sitemaps.org/schemas/sitemap/0.9}changefreq',
            '{http://www.sitemaps.org/schemas/sitemap/0.9}loc',
            '{http://www.sitemaps.org/schemas/sitemap/0.9}priority'
        ]
        try: 
            df2 = df[columns]
            df2['source'] = sitemap
            df2.columns = ['changefreq','loc','priority','source']
        except:
            df2['loc'] = df['{http://www.sitemaps.org/schemas/sitemap/0.9}loc']
            df2['changefreq'] = ''
            df2['priority'] = ''
            df2['source'] = sitemap
        result_df = result_df.append(df2)
    except:
        print(sitemap)


После нескольких минут ожидания у нас получается таблица размером (14047393, 4), что весьма неплохо для такого "наколеночного" решения! 

Если вам понравился новый формат, пишите в личке, будем продолжать в таком же формате. Ну и эта статья - первая в цикле.