Прочитав пост Эмиля в блоге о темных данных несколько недель назад, я заинтриговался попыткой найти какую-то структуру в свободных текстовых данных и подумал «Как я встретил стенограммы вашей матери», это было бы хорошим началом.
Я нашел веб-сайт, на котором есть стенограммы для всех эпизодов, а затем вручную загрузил две страницы с перечнем всех эпизодов и написал сценарий для захвата каждой из стенограмм, чтобы я мог использовать их на своем компьютере.
Я хотел немного изучить Python, и мой коллега Найджел указал мне на запросы и библиотеки BeautifulSoup, чтобы помочь мне с моей задачей. Скрипт для захвата стенограммы выглядит так:
import requests from bs4 import BeautifulSoup from soupselect import select episodes = {} for i in range(1,3): page = open("data/transcripts/page-" + str(i) + ".html", 'r') soup = BeautifulSoup(page.read()) for row in select(soup, "td.topic-titles a"): parts = row.text.split(" - ") episodes[parts[0]] = {"title": parts[1], "link": row.get("href")} for key, value in episodes.iteritems(): parts = key.split("x") season = int(parts[0]) episode = int(parts[1]) filename = "data/transcripts/S%d-Ep%d" %(season, episode) print filename with open(filename, 'wb') as handle: headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} response = requests.get("transcript website" + value["link"], headers = headers) if response.ok: for block in response.iter_content(1024): if not block: break handle.write(block)
{Примечание редактора: по запросу владельца стенограммы сайта мы удалили конкретные ссылки на этот сайт.}
файлы, содержащие списки эпизодов, называются «page-1» и «page-2»
Код достаточно прост — мы находим все ссылки внутри таблицы, помещаем их в словарь, затем перебираем словарь и загружаем файлы на диск. Код для сохранения файла немного чудовищен, но, похоже, не существует метода «сохранения», который я мог бы использовать.
Загрузив файлы, я обдумал все умные вещи, которые мог сделать, включая создание модели пакета слов для каждого эпизода или выполнение анализа настроений для каждого предложения, о котором я узнал из учебника Kaggle .
В конце я решил начать с простого и извлечь все слова из стенограмм и посчитать, сколько раз слово встречалось в данном эпизоде.
Я закончил со следующим сценарием, который создал словарь (эпизод -> слова + вхождения):
import csv import nltk import re from bs4 import BeautifulSoup from soupselect import select from nltk.corpus import stopwords from collections import Counter from nltk.tokenize import word_tokenize def count_words(words): tally=Counter() for elem in words: tally[elem] += 1 return tally episodes_dict = {} with open('data/import/episodes.csv', 'r') as episodes: reader = csv.reader(episodes, delimiter=',') reader.next() for row in reader: print row transcript = open("data/transcripts/S%s-Ep%s" %(row[3], row[1])).read() soup = BeautifulSoup(transcript) rows = select(soup, "table.tablebg tr td.post-body div.postbody") raw_text = rows[0] [ad.extract() for ad in select(raw_text, "div.ads-topic")] [ad.extract() for ad in select(raw_text, "div.t-foot-links")] text = re.sub("[^a-zA-Z]", " ", raw_text.text.strip()) words = [w for w in nltk.word_tokenize(text) if not w.lower() in stopwords.words("english")] episodes_dict[row[0]] = count_words(words)
Затем я хотел немного изучить данные, чтобы увидеть, какие слова встречались в эпизодах или какое слово встречалось чаще всего, и понял, что это будет намного проще, если я буду где-то хранить данные.
с / где-то / в Neo4j
Язык запросов Neo4j, Cypher, имеет действительно хороший инструмент ETL-esque, называемый «LOAD CSV» для загрузки в файлы CSV (как следует из названия!), Поэтому я добавил некоторый код для сохранения своих слов на диск:
with open("data/import/words.csv", "w") as words: writer = csv.writer(words, delimiter=",") writer.writerow(["EpisodeId", "Word", "Occurrences"]) for episode_id, words in episodes_dict.iteritems(): for word in words: writer.writerow([episode_id, word, words[word]])
Вот как выглядит содержимое файла CSV:
$ head -n 10 data/import/words.csv EpisodeId,Word,Occurrences 165,secondly,1 165,focus,1 165,baby,1 165,spiders,1 165,go,4 165,apartment,1 165,buddy,1 165,Exactly,1 165,young,1
Теперь нам нужно написать Cypher, чтобы получить данные в Neo4j:
// words LOAD CSV WITH HEADERS FROM "file:/Users/markneedham/projects/neo4j-himym/data/import/words.csv" AS row MERGE (word:Word {value: row.Word})
// episodes LOAD CSV WITH HEADERS FROM "file:/Users/markneedham/projects/neo4j-himym/data/import/words.csv" AS row MERGE (episode:Episode {id: TOINT(row.EpisodeId)})
// words to episodes LOAD CSV WITH HEADERS FROM "file:/Users/markneedham/projects/neo4j-himym/data/import/words.csv" AS row MATCH (word:Word {value: row.Word}) MATCH (episode:Episode {id: TOINT(row.EpisodeId)}) MERGE (word)-[:USED_IN_EPISODE {times: TOINT(row.Occurrences) }]->(episode);
Сделав это, мы можем написать несколько простых запросов, чтобы изучить слова, использованные в книге «Как я встретил вашу маму»:
MATCH (word:Word)-[r:USED_IN_EPISODE]->(episode) RETURN word.value, COUNT(episode) AS episodes, SUM(r.times) AS occurrences ORDER BY occurrences DESC LIMIT 10 ==> +-------------------------------------+ ==> | word.value | episodes | occurrences | ==> +-------------------------------------+ ==> | "Ted" | 207 | 11437 | ==> | "Barney" | 208 | 8052 | ==> | "Marshall" | 208 | 7236 | ==> | "Robin" | 205 | 6626 | ==> | "Lily" | 207 | 6330 | ==> | "m" | 208 | 4777 | ==> | "re" | 208 | 4097 | ==> | "know" | 208 | 3489 | ==> | "Oh" | 197 | 3448 | ==> | "like" | 208 | 2498 | ==> +-------------------------------------+ ==> 10 rows
Основные 5 персонажей занимают 5 верхних позиций, что, вероятно, и следовало ожидать. Я не уверен, почему ‘m’ и ‘re’ находятся в следующих двух позициях — я ожидаю, что это может быть ошибкой!
Наш следующий запрос может сфокусироваться на проверке того, какой символ ссылается на пост в каждом эпизоде:
WITH ["Ted", "Barney", "Robin", "Lily", "Marshall"] as mainCharacters MATCH (word:Word) WHERE word.value IN mainCharacters MATCH (episode:Episode)<-[r:USED_IN_EPISODE]-(word) WITH episode, word, r ORDER BY episode.id, r.times DESC WITH episode, COLLECT({word: word.value, times: r.times})[0] AS topWord RETURN episode.id, topWord.word AS word, topWord.times AS occurrences LIMIT 10 ==> +---------------------------------------+ ==> | episode.id | word | occurrences | ==> +---------------------------------------+ ==> | 72 | "Barney" | 75 | ==> | 143 | "Ted" | 16 | ==> | 43 | "Lily" | 74 | ==> | 156 | "Ted" | 12 | ==> | 206 | "Barney" | 23 | ==> | 50 | "Marshall" | 51 | ==> | 113 | "Ted" | 76 | ==> | 178 | "Barney" | 21 | ==> | 182 | "Barney" | 22 | ==> | 67 | "Ted" | 84 | ==> +---------------------------------------+ ==> 10 rows
Если мы углубимся в это дальше, то на самом деле количество раз, когда упоминается главный герой в каждом эпизоде, довольно немного отличается, что, вероятно, снова говорит о данных:
WITH ["Ted", "Barney", "Robin", "Lily", "Marshall"] as mainCharacters MATCH (word:Word) WHERE word.value IN mainCharacters MATCH (episode:Episode)<-[r:USED_IN_EPISODE]-(word) WITH episode, word, r ORDER BY episode.id, r.times DESC WITH episode, COLLECT({word: word.value, times: r.times})[0] AS topWord RETURN MIN(topWord.times), MAX(topWord.times), AVG(topWord.times), STDEV(topWord.times) ==> +-------------------------------------------------------------------------------------+ ==> | MIN(topWord.times) | MAX(topWord.times) | AVG(topWord.times) | STDEV(topWord.times) | ==> +-------------------------------------------------------------------------------------+ ==> | 3 | 259 | 63.90865384615385 | 42.36255207691068 | ==> +-------------------------------------------------------------------------------------+ ==> 1 row
Очевидно, что это очень простой способ получения структуры из текста, вот некоторые из вещей, которые я хочу попробовать в следующем:
- Обнаружение общих фраз / мемов / фраз, используемых в шоу (например, желтый зонтик) — это должно быть возможно путем создания n-грамм различной длины и последующего поиска этих фраз по всему корпусу.
- Вытащите сцены — некоторые из транскриптов используют ключевое слово «сцена», чтобы обозначить это, хотя некоторые из них не делают. В зависимости от того, сколько транскриптов содержат разметки сцен, возможно, мы могли бы обучить классификатор, чтобы определить, где должны быть сцены в транскриптах, у которых нет сцен.
- Проанализируйте, кто разговаривает друг с другом или кто чаще всего говорит друг о друге
- Создайте график бесед, о которых мои коллеги Макс и Майкл ранее писали в блоге.
Будьте общительны, делитесь!