Исследование Bitquark «наиболее популярные поддомены» оказалось очень полезным в качестве отправной точки для перечисления поддоменов популярных сайтов. Однако у меня было одно замечание к списку — количество китайских субдоменов. Поскольку мои цели обычно говорят по-английски, у них вряд ли будут поддомены, такие как ttyulecheng или aiwenbocai. Итак, я попытался составить свой собственный список доменов.

Начиная

Моей первой точкой вызова был тот же список, что и у Bitquark — набор данных Rapid7 Project Sonar Forward DNS. С 2016 года Rapid7 обновил свой процесс, который теперь генерирует файл JSON. Чтобы обработать это, я использовал jq, чтобы вытащить поле имя.

$(NAME_DATA_FILE): $(DATA_FILE)
   @echo "Step 1 (jq extraction): Creating $@ from $<"
   pv -cN input $< | \
      pigz -dc | \
      parallel --no-notice --pipe --line-buffer --block 50M jq -r .name | \
      pv -cN postparallel | \
      uniq | \
      pv -cN postuniq | \
      pigz -c | \
      pv -cN output > $@

Согласно руководству Анализ наборов данных от Rapid7, процесс пытается сохранить все данные в сжатом виде между этапами анализа, поскольку дисковый ввод-вывод в этом масштабе обходится дороже, чем накладные расходы на декодирование gzip. Он также использует инструмент parallel для разделения задачи между несколькими ядрами, поскольку jq изначально не является многопоточным. Это дало значительное ускорение.

Следующим шагом является извлечение субдоменов. Я мог бы сделать что-то простое с помощью awk или sed, но я хотел сделать все правильно. С этой целью я использовал сценарий Python в качестве оболочки для замечательной библиотеки tldextract. Это означает, что наши поддомены могут включать в себя несколько уровней; например www.news.

$(SUB_DATA_FILE): $(NAME_DATA_FILE)
   @echo "Step 2 (subdomain extraction): Creating $@ from $<"
   pv -cN input $< | \
      pigz -dc | \
      parallel --no-notice --pipe --line-buffer --block 50M python3 ./subdomain.py | \
      pv -cN postpython | \
      pigz -c | \
      pv -cN output > $@

При тестировании с использованием Parallel для распараллеливания скрипта Python 3 на самом деле было проще, чем многопоточность внутри скрипта, что кажется немного странным!

Теперь у нас есть список поддоменов. Следующий шаг — отсортировать их, а затем подсчитать уникальные экземпляры. К счастью, в Linux есть несколько стандартных утилит: sort и uniq. sort впечатляет тем, что изначально является многопоточным и имеет возможность сортировать наборы данных, которые намного превышают объем доступной памяти в системе, используя файловую систему в качестве временного хранилища.

$(SORTED_SUB_DATA_FILE): $(SUB_DATA_FILE)
   @echo "Step 3 (subdomain sorting): Creating $@ from $<"
   pv -cN input $< | \
      pigz -dc | \
      sort --parallel=$(NUM_PROCS) -S $(SORT_MEMORY_LIMIT) -T $(SORT_MEMORY_FOLDER) | \
      pigz -c | \
      pv -cN output > $@
$(COUNTED_SUB_DATA_FILE): $(SORTED_SUB_DATA_FILE)
   @echo "Step 4 (subdomain counting): Creating $@ from $<"
   pv -cN input $< | \
      pigz -dc | \
      uniq -c | \
      pigz -c | \
      pv -cN output > $@

Еще один раунд сортировки (на этот раз обратная сортировка по числовым, поскольку количество экземпляров предшествует поддомену), и мы почти у цели.

$(TOP_SUB_DATA_FILE): $(COUNTED_SUB_DATA_FILE)
   @echo "Step 5 (top subdomains): Creating $@ from $<"
   pv -cN input $< | \
      pigz -dc | \
      sort -rg --parallel=$(NUM_PROCS) -S $(SORT_MEMORY_LIMIT) -T $(SORT_MEMORY_FOLDER) | \
      pigz -c | \
      pv -cN output > $@

Теперь у нас есть вывод, аналогичный списку Bitquark:

# zcat step5.top_sub_20170818-fdns.json.gz | head -15 
21678317 www 
13261728 mail 
2667235 mx 
1244872 ns1 
1226027 ns2 
 662348 m 
 491697 ns 
 456890 mx0 
 337278 mail2 
 296522 remote 
 277020 mx1 
 227964 mailserver 
 212540 smtp 
 195492 blog 
 183159 webmail

Но этого недостаточно для моих целей; помните, я хочу избавиться от китайских доменов.

Изучение пиньинь

Одна важная вещь, которую нужно знать о китайских доменах в этом списке, это то, что они в основном находятся в Пиньине, который представляет собой метод латинизации для преобразования китайских символов в латиницу. Например, 爱问博彩 (которое, как мне достоверно сообщает Google Translate, означает Люблю делать ставки) латинизируется как aiwenbocai, а 娱乐城 латинизируется как yulecheng.

Мы хотим отфильтровать эти домены пиньинь. Поиск в Google «как определить пиньинь» не дает огромного количества полезной информации, поэтому пришло время разработать собственное решение.

НЛТК

Набор инструментов Python Natural Language Tool Kit имеет ряд полезных функций для анализа текста. Важнейшим из них для наших целей является классификация текстов. По сути, мы определяем ряд «признаков», которые можно определить по фрагменту текста. Для моей функциональной функции мы проверяем каждый слог пиньинь, а также все ли слоги пиньинь. На данный момент мы на самом деле не знаем, какие слоги являются лучшими индикаторами пиньиньства, поэтому поливать грязью стену, чтобы увидеть, что приживется.

def pinyin_features(subdomain):
    """
    A function which extracts information about the subdomain and tld pair in hand.
    :param subdomain_tld: A colon joined tuple of subdomain and TLD.
    :return: a dictionary of feature information.
    """
    domain_lower = subdomain.lower()

    # Start to build up a dictionary of features.
    features = {
        "length": len(subdomain)
    }

    syllable_count = 0
    for syllable in nlp.SYLLABLES:
        key = "has({0})".format(syllable)
        features[key] = (syllable in domain_lower)

    for syllable in nlp.COMPOUND_SYLLABLES:
        key = "has({0})".format(syllable)
        features[key] = (syllable in domain_lower)

    features["is(pinyin)"] = True if PINYIN_RE.match(domain_lower) else False
    features["is(pinyinnum)"] = True if PINYINNUM_RE.match(domain_lower) else False
    return features

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

Теперь мы используем NLTK для обучения классификатора Наивного Байеса с нашими обучающими данными. Проверка этого на некоторых тестовых данных дает следующий результат:

DEBUG Loaded 9207 records 
INFO  Training classifier 
INFO  Classifier trained; accuracy 0.995656 
INFO  xingxi125                     : guessed pinyin False, was pinyin True 
INFO  zuohg                         : guessed pinyin False, was pinyin True 
INFO  888daren                      : guessed pinyin False, was pinyin True 
INFO  ent.qingdaonews.com           : guessed pinyin True, was pinyin False 
INFO  yulin                         : guessed pinyin False, was pinyin True 
INFO  zhifu                         : guessed pinyin False, was pinyin True 
INFO  rizhao                        : guessed pinyin True, was pinyin False 
INFO  xinxi825                      : guessed pinyin False, was pinyin True 
INFO  julian                        : guessed pinyin True, was pinyin False 
INFO  xingxi295                     : guessed pinyin False, was pinyin True 
Most Informative Features 
                has(eng) = True                1 : 2      =    723.4 : 1.0 
               has(chen) = True                1 : 2      =    591.0 : 1.0 
                 has(zu) = True                1 : 2      =    411.9 : 1.0 
                has(hen) = True                1 : 2      =    364.7 : 1.0 
                 has(gu) = True                1 : 2      =    320.3 : 1.0 
                has(bai) = True                1 : 2      =    312.3 : 1.0 
                 has(ji) = True                1 : 2      =    311.8 : 1.0 
                 has(qi) = True                1 : 2      =    279.4 : 1.0 
              is(pinyin) = True                1 : 2      =    254.9 : 1.0 
                has(han) = True                1 : 2      =    248.9 : 1.0

Точность 99,5 % — это довольно хорошо, хотя это может быть ложью. Очевидно, что этот процесс не идеален. Однако мы можем внести в белый и черный список любые явно неправильно классифицированные домены; например, «exchange» и «secure» обычно определяются как пиньинь, поэтому мы можем выделить их как особый случай.

Выполнение этого процесса классификации для 2000000 лучших доменов отфильтровывает 405084 субдомена пиньинь; это почти четверть!

50 лучших субдоменов, не относящихся к пиньинь (2017 г.)

Итак, без лишних слов, топ-50 субдоменов, не относящихся к пиньинь, по состоянию на 18 августа 2017 года:

21678317 www 
13261728 mail 
2667235 mx 
1244872 ns1 
1226027 ns2 
 662348 m 
 491697 ns 
 456890 mx0 
 337278 mail2 
 296522 remote 
 277020 mx1 
 227964 mailserver 
 212540 smtp 
 195492 blog 
 183159 webmail 
 170200 server 
 156743 ns3 
 128062 mx2 
  96903 mail1 
  89788 redbusprimarydns 
  89760 redbussecondarydns 
  85941 vpn 
  81053 dev 
  80927 mx7 
  74248 secure 
  73883 shop 
  70482 ns4 
  64261 cloud 
  61913 ftp 
  59321 mx01 
  55806 api 
  54913 portal 
  54693 dns1 
  53482 test 
  52216 dns2 
  50731 email 
  49777 host 
  49596 app 
  48128 support 
  45356 ww1 
  45227 mailin2 
  45199 mailin1 
  43292 pop 
  42677 bbs 
  42661 web 
  41336 r.1 
  41243 r.2 
  41135 r.3 
  37818 forum 
  37806 owa

Так что тогда особо ничего и не изменилось. Одним интересным моментом является добавление «r.1», «r.2» и «r.3», которых не было в исходном исследовании.

Если вы хотите увидеть инструменты и данные, используемые для этой работы, вы можете найти их на моей странице Github: https://github.com/cmeister2/dauntless.