Этот проект реализует пользовательский алгоритм для извлечения наиболее важных предложений и ключевых слов из новостных статей испанского и английского языка.
Он был полностью разработан в Python , и он вдохновлен аналогичными проектами, которые можно увидеть на субредатах Reddit News, которые используют частоту частоты с частотой - вызванным документом ( tf–idf ).
3 наиболее важных файла:
scraper.py : сценарий Python, который выполняет соскабливание веб -сайта на данном HTML -источнике, он извлекает заголовок статьи, дату и тело.
summary.py : сценарий Python, который применяет пользовательский алгоритм к строке текста и извлекает верхние предложения и слова.
bot.py : бот Reddit, который проверяет субреддит на свои последние представления. Он управляет списком уже обработанных представлений, чтобы избежать дубликатов.
Этот проект использует следующие библиотеки Python
spaCy : использовался для токенизации статьи на предложения и слова.PRAW : очень проще использует API Reddit.Requests : выполнить HTTP get запросов на URL -адреса статей.BeautifulSoup : используется для извлечения текста статьи.html5lib : Этот анализатор стал лучшей совместимостью при использовании с BeautifulSoup .tldextract : используется для извлечения домена из URL.wordcloud : используется для создания облаков слов с текстом статьи. После установки библиотеки spaCy необходимо установить языковую модель, чтобы иметь возможность токенизировать статью.
Для Spanish вы можете запустить это:
python -m spacy download es_core_news_sm
На другие языки, пожалуйста, проверьте следующую ссылку: https://spacy.io/usage/models
Бот простой по своей природе, он использует библиотеку PRAW , которая очень проста в использовании. BOT опрометчивы в субреддите каждые 10 минут, чтобы получить свои последние представления.
Сначала он обнаруживает, не обработана ли представление, а затем проверяет, находится ли URL -адрес отправки в белом списке. Этот белый список в настоящее время курируется мной.
Если сообщение и его URL проходят оба проверки, то к URL -адресу применяется процесс соскобки веб -сериала, именно здесь все начинает становиться интересными.
Прежде чем ответить на первоначальное представление, он проверяет процент достигнутого сокращения, если оно слишком низкое или слишком высокое, оно пропускает и перемещается в следующее представление.
В настоящее время в белом списке уже более 300 различных сайтов новостных статей и блогов. Создание специализированных веб -скребков для каждого из них просто невозможно.
Второе лучшее, что можно сделать, это сделать скребок максимально точным.
Мы начинаем веб -скребок с обычного способа, с Requests и библиотеками BeautifulSoup .
with requests . get ( article_url ) as response :
if response . encoding == "ISO-8859-1" :
response . encoding = "utf-8"
html_source = response . text
for item in [ "</p>" , "</blockquote>" , "</div>" , "</h2>" , "</h3>" ]:
html_source = html_source . replace ( item , item + " n " )
soup = BeautifulSoup ( html_source , "html5lib" ) Очень несколько раз у меня возникли проблемы с кодированием, вызванные неправильным догадением кодирования. Чтобы избежать этой проблемы, я заставляю Requests декодировать с помощью utf-8 .
Теперь, когда у нас есть наша статья, разрабатываемая в soup -объект, мы начнем с извлечения названия и опубликованного времени.
Я использовал аналогичные методы, чтобы извлечь оба значения, сначала проверяю наиболее распространенные теги и отступление на следующие общие альтернативы.
Не все веб -сайты разоблачают свою опубликованную дату, мы иногда заканчиваемся пустой строкой.
article_title = soup . find ( "title" ). text . replace ( " n " , " " ). strip ()
# If our title is too short we fallback to the first h1 tag.
if len ( article_title ) <= 5 :
article_title = soup . find ( "h1" ). text . replace ( " n " , " " ). strip ()
article_date = ""
# We look for the first meta tag that has the word 'time' in it.
for item in soup . find_all ( "meta" ):
if "time" in item . get ( "property" , "" ):
clean_date = item [ "content" ]. split ( "+" )[ 0 ]. replace ( "Z" , "" )
# Use your preferred time formatting.
article_date = "{:%d-%m-%Y a las %H:%M:%S}" . format (
datetime . fromisoformat ( clean_date ))
break
# If we didn't find any meta tag with a datetime we look for a 'time' tag.
if len ( article_date ) <= 5 :
try :
article_date = soup . find ( "time" ). text . strip ()
except :
passПри извлечении текста из разных тегов я часто получал струны без разделения. Я реализовал небольшой взломан, чтобы добавить новые строки в каждый тег, который обычно содержит текст. Это значительно улучшило общую точность токенизатора.
Моя оригинальная идея заключалась в том, чтобы принять только веб -сайты, которые использовали тег <article> . Это работало нормально для первых веб -сайтов, которые я протестировал, но вскоре я понял, что очень немногие веб -сайты используют его, и те, кто его использует, не используют его правильно.
article = soup . find ( "article" ). text При доступе к свойству .text по тегу <article> я заметил, что я также получаю код JavaScript. Я немного отступил и удалил все теги, которые могли бы добавить шум к тексту статьи.
[ tag . extract () for tag in soup . find_all (
[ "script" , "img" , "ul" , "time" , "h1" , "h2" , "h3" , "iframe" , "style" , "form" , "footer" , "figcaption" ])]
# These class names/ids are known to add noise or duplicate text to the article.
noisy_names = [ "image" , "img" , "video" , "subheadline" ,
"hidden" , "tract" , "caption" , "tweet" , "expert" ]
for tag in soup . find_all ( "div" ):
tag_id = tag [ "id" ]. lower ()
for item in noisy_names :
if item in tag_id :
tag . extract ()Приведенный выше код удалил большинство подписей, которые обычно повторяют то, что находится внутри в статье.
После этого я применил 3 -ступенчатый процесс, чтобы получить текст статьи.
Сначала я проверил все теги <article> и схватил один с самым длинным текстом.
article = ""
for article_tag in soup . find_all ( "article" ):
if len ( article_tag . text ) >= len ( article ):
article = article_tag . text Это отлично сработало для веб -сайтов, которые правильно использовали тег <article> . Самая длинная метка почти всегда содержит основную статью.
Но это не сработало, как и ожидалось, я заметил низкое качество в результатах, иногда я получал выдержки для других статей.
Именно тогда я решил добавить запасную сторону, Lnstead только поиска тега <article> , я буду искать теги <div> и <section> с часто используемыми id's .
# These names commonly hold the article text.
common_names = [ "artic" , "summary" , "cont" , "note" , "cuerpo" , "body" ]
# If the article is too short we look somewhere else.
if len ( article ) <= 650 :
for tag in soup . find_all ([ "div" , "section" ]):
tag_id = tag [ "id" ]. lower ()
for item in common_names :
if item in tag_id :
# We guarantee to get the longest div.
if len ( tag . text ) >= len ( article ):
article = tag . text Это увеличило точность, я повторил код, но вместо атрибута id я также искал атрибут class .
# The article is still too short, let's try one more time.
if len ( article ) <= 650 :
for tag in soup . find_all ([ "div" , "section" ]):
tag_class = "" . join ( tag [ "class" ]). lower ()
for item in common_names :
if item in tag_class :
# We guarantee to get the longest div.
if len ( tag . text ) >= len ( article ):
article = tag . textИспользование всех предыдущих методов значительно увеличило общую точность скребка. В некоторых случаях я использовал частичные слова, которые разделяют одни и те же буквы на английском и испанском (Artic -> article/articulo). Скребок теперь был совместим со всеми URL -адресами, которые я протестировал.
Мы делаем окончательную проверку, и если статья все еще слишком короткая, мы прерваем процесс и переходим к следующему URL, в противном случае мы переходим к сводному алгоритму.
Этот алгоритм был разработан, чтобы работать в основном на испанских письменных статьях. Он состоит на нескольких шагах:
Прежде чем начать, нам нужно инициализировать библиотеку spaCy .
NLP = spacy . load ( "es_core_news_sm" ) Эта линия кода загрузит Spanish модель, которую я использую больше всего. Если вы используете другой язык, обратитесь к разделу Requirements , чтобы вы знали, как установить соответствующую модель.
При извлечении текста из статьи мы обычно получаем много пробелов, в основном от разрывов линий ( n ).
Мы разделили текст этим символом, затем разделим все пробелы и снова присоединяемся к нему. Это не требуется строго, но очень помогает, отладки всего процесса.
В верхней части сценария мы объявляем путь текстовых файлов Stop Words. Эти стоп -слова будут добавлены в set , гарантируя никаких дубликатов.
Я также добавил список с некоторыми испанскими и английскими словами, которые не останавливают слова, но они не добавляют ничего существенного в статью. Моим личным предпочтением было жесткое кодирование их в строчной форме.
Затем я добавил копию каждого слова в прописной форме и форме заголовка. Это означает, что set будет в 3 раза превышает исходный размер.
with open ( ES_STOPWORDS_FILE , "r" , encoding = "utf-8" ) as temp_file :
for word in temp_file . read (). splitlines ():
COMMON_WORDS . add ( word )
with open ( EN_STOPWORDS_FILE , "r" , encoding = "utf-8" ) as temp_file :
for word in temp_file . read (). splitlines ():
COMMON_WORDS . add ( word )
extra_words = list ()
for word in COMMON_WORDS :
extra_words . append ( word . title ())
extra_words . append ( word . upper ())
for word in extra_words :
COMMON_WORDS . add ( word ) Прежде чем начать токенизировать наши слова, мы должны сначала передать нашу очищенную статью в трубопровод NLP , это делается с одной строкой кода.
doc = NLP ( cleaned_article ) Этот объект doc содержит несколько итераторов, 2, которые мы будем использовать, - это tokens и sents (предложения).
В этот момент я добавил личный контакт в алгоритм. Сначала я сделал копию статьи, а затем удалил из нее все общие слова.
После этого я использовал объект collections.Counter , чтобы сделать начальный счет.
Затем я применил бонус множителя к словам, которые начинаются в верхнем регионе и равны или дольше 4 символов. Большую часть времени эти слова являются именами мест, людей или организаций.
Наконец, я установил ноль счет для всех слов, которые на самом деле являются цифрами.
words_of_interest = [
token . text for token in doc if token . text not in COMMON_WORDS ]
scored_words = Counter ( words_of_interest )
for word in scored_words :
if word [ 0 ]. isupper () and len ( word ) >= 4 :
scored_words [ word ] *= 3
if word . isdigit ():
scored_words [ word ] = 0Теперь, когда у нас есть окончательные оценки для каждого слова, пришло время забить каждое предложение из статьи.
Для этого нам сначала нужно разделить статью на предложения. Я попробовал различные подходы, включая RegEx , но лучше всего работала библиотека spaCy .
Мы снова повторим объект doc , который мы определили на предыдущем шаге, но на этот раз мы будем повторять его свойство sents .
Следует отметить, что мы создаем список tokens предложений и внутри этих токенов, мы можем извлечь текст предложений, получив их text свойство.
article_sentences = [ sent for sent in doc . sents ]
scored_sentences = list ()
or index , sent in enumerate ( article_sentences ):
# In some edge cases we have duplicated sentences, we make sure that doesn't happen.
if sent . text not in [ sent for score , index , sent in scored_sentences ]:
scored_sentences . append (
[ score_line ( sent , scored_words ), index , sent . text ]) scored_sentences - это список списков. Каждый внутренний список содержит 3 значения. Оценка предложения, его индекс и само предложение. Эти значения будут использоваться на следующем шаге.
Код ниже показывает, как оцениваются строки.
def score_line ( line , scored_words ):
# We remove the common words.
cleaned_line = [
token . text for token in line if token . text not in COMMON_WORDS ]
# We now sum the total number of ocurrences for all words.
temp_score = 0
for word in cleaned_line :
temp_score += scored_words [ word ]
# We apply a bonus score to sentences that contain financial information.
line_lowercase = line . text . lower ()
for word in FINANCIAL_WORDS :
if word in line_lowercase :
temp_score *= 1.5
break
return temp_score Мы применяем множитель к предложениям, которые содержат любое слово, которое относится к деньгам или финансам.
Это последняя часть алгоритма, мы используем функцию sorted() чтобы получить верхние предложения, а затем переупорядочить их в их исходных позициях.
Мы сортируем scored_sentences в обратном порядке, это сначала даст нам лучшие забитые предложения. Мы запускаем небольшую переменную счетчика, поэтому она разбивает петлю, как только он достигнет 5. Мы также отказываемся от всех предложений, которые составляют 3 символа или меньше (иногда существуют хитрость символов с нулевой шириной).
top_sentences = list ()
counter = 0
for score , index , sentence in sorted ( scored_sentences , reverse = True ):
if counter >= 5 :
break
# When the article is too small the sentences may come empty.
if len ( sentence ) >= 3 :
# We append the sentence and its index so we can sort in chronological order.
top_sentences . append ([ index , sentence ])
counter += 1
return [ sentence for index , sentence in sorted ( top_sentences )]В конце мы используем понимание списка, чтобы вернуть только предложения, которые уже отсортированы в хронологическом порядке.
Просто для развлечения я добавил облако слов в каждую статью. Для этого я использовал библиотеку wordcloud . Эта библиотека очень проста в использовании, вам нужно только объявить объект WordCloud и использовать метод generate со строкой текста в качестве его параметра.
wc = wordcloud . WordCloud () # See cloud.py for full parameters.
wc . generate ( prepared_article )
wc . to_file ( "./temp.png" ) После создания изображения я загрузил его в Imgur , вернул ссылку на URL и добавил его в сообщение Markdown .

Это был очень забавный и интересный проект для работы. Возможно, я заново изобрел колесо, но, по крайней мере, я узнал несколько крутых вещей.
Я доволен общим качеством результатов, и я буду продолжать настраивать алгоритм и применять усовершенствования совместимости.
В качестве примечания, при тестировании сценария я случайно запросил твиты, сообщения на Facebook и написанные статьи на английском языке. Все они получили приемлемые результаты, но, поскольку эти сайты не были целью, я удалил их из белого списка.
После нескольких недель обратной связи я решил добавить поддержку английскому языку. Это требовало немного рефакторинга.
Чтобы он работал с другими языками, вам потребуется только текстовый файл, содержащий все остановки из упомянутого языка, и копировать несколько строк кода (см. Раздел «Удалить общие и остановить слова»).