Статьи цикла
Быстрый анализ сайтов конкурентов через сайтмапы. Часть 1 - парсинг
Быстрый анализ сайтов конкурентов через сайтмапы. Часть 2 - простой анализ
Быстрый анализ сайтов конкурентов через сайтмапы. Часть 3 - более глубокий анализ
Как всегда надо начать с заманчивой картинки, но не пояснять ее смысл!
Пару дней назад ко мне обратился далекий знакомый, мол у нас политесы в компании и мы не можем никак выбрать направление развития. Ситуация типичная - лебедь, рак и щука и надо бы очень быстро проанализировать сайты его конкурентов, вот держи картинку со списком самых крупных компаний в этой сфере. Оказалось, что конкуренты из сферы форекса. Это меня смутило, но не остановило. Я посмотрел на их сайты (а, забегая вперед, некоторые из них просто огромны - миллионы страниц) и тут у меня родилась гениальная идея.
Я занимался ей пару суток почти без остановки и накопал относительно интересные результаты, которыми и хочу поделиться с вами.
Картинка, которую мне прислали. Даже nutshell не нужен.
После визита на пару сайтов, у меня в голове промелькнули такие мысли:
- Парсинг (или "скрепинг") сайтов в современном мире - это лучшее средство business intelligence. Ибо сейчас все выкладывают весь свой контент для индексации в интернет;
- Недавно я видел отличную и простую статью, про то, что мол парсинг это чуть ли ваша обязанность, если вы аналитик;
- У меня есть знакомый, которому один раз предлагали спарсить весь вконтакте за 300,000 рублей 1 раз. Так вот он говорил, что в одноразовом парсинге на питоне вообще нет ничего сложного. Потоковый парсинг сложнее, там надо иметь прокси, VPN и балансировщик нагрузки и очередь;
- Самые крупные сайты как правило имеют сайтмапы;
- По этой причине этот подход в принципе применим для любой отрасли, где присутствует много контента;
- Идея также расширяется до выборочного парсинга самих страниц, но это на порядок сложнее технически (оставим это более прошаренным коллегам);
Эта статья будет скорее оформлена в виде пошагового гайда, что немного в новинку для меня. Но почему нет, давайте попробуем?
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), что весьма неплохо для такого "наколеночного" решения!
Если вам понравился новый формат, пишите в личке, будем продолжать в таком же формате. Ну и эта статья - первая в цикле.