このプロジェクトは、スペイン語と英語のニュース記事から最も重要な文とキーワードを抽出するために、カスタムアルゴリズムを実装しています。
Pythonで完全に開発され、 Reddit News Subredditsで見られる同様のプロジェクトに触発され、周波数–Inverseドキュメント頻度( tf–idf )という用語を使用しています。
最も重要な3つのファイルは次のとおりです。
scraper.py :特定のHTMLソースでWebスクレイピングを実行するPythonスクリプト、記事のタイトル、日付、およびボディを抽出します。
summary.py :カスタムアルゴリズムを一連のテキストに適用し、上位の文章と単語を抽出するPythonスクリプト。
bot.py :最新の提出のためにsubredditをチェックするredditボット。重複を避けるために、すでに処理された提出物のリストを管理します。
このプロジェクトでは、次のPythonライブラリを使用します
spaCy :記事を文章や言葉にトークン化するために使用されます。PRAW :Reddit APIの使用を非常に簡単にします。Requests :HTTPを実行するには、記事のURLにリクエストget 。BeautifulSoup :記事テキストの抽出に使用されます。html5lib :このパーサーは、 BeautifulSoupで使用すると互換性が向上しました。tldextract :URLからドメインを抽出するために使用されます。wordcloud :記事テキストで単語クラウドを作成するために使用されます。 spaCyライブラリをインストールした後、記事をトークン化できるように言語モデルをインストールする必要があります。
Spanishの場合、これを実行できます。
python -m spacy download es_core_news_sm
他の言語については、次のリンクを確認してください:https://spacy.io/usage/models
ボットは本質的にシンプルで、使用するのが非常に簡単なPRAWライブラリを使用しています。ボットは、最新の提出を取得するために10分ごとにsubredditを投票します。
最初に、提出物がまだ処理されていないかどうかを検出し、次に送信URLがホワイトリストにあるかどうかをチェックします。このホワイトリストは現在、自分でキュレーションされています。
投稿とそのURLが両方のチェックに合格した場合、WebスクレイピングのプロセスがURLに適用されます。これは、物事が面白くなり始める場所です。
元の提出物に返信する前に、達成された削減の割合をチェックします。それが低すぎるか高すぎる場合、それをスキップして次の提出に移動します。
現在、ホワイトリストには、すでに300を超えるニュース記事やブログのWebサイトがあります。それぞれに特殊なウェブスクレーパーを作成することは、単に実行可能ではありません。
2番目に最良のことは、スクレーパーをできるだけ正確にすることです。
RequestsとBeautifulSoupライブラリを使用して、通常の方法でWebスクレーパーを開始します。
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" )数回、誤ったエンコード推測によって引き起こされるエンコードの問題が発生しました。この問題を回避するために、 utf-8でデコードするようRequests強制します。
記事がsoupオブジェクトに解析されたので、タイトルと公開された時間を抽出することから始めます。
同様の方法を使用して両方の値を抽出しました。最初に、最も一般的なタグと次の一般的な代替案へのフォールバックをチェックしました。
すべてのWebサイトが公開された日付を公開するわけではありませんが、空の文字列で終了することもあります。
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>タグを使用したWebサイトのみを受け入れることでした。私がテストした最初のウェブサイトでは問題なく動作しましたが、すぐにそれを使用するウェブサイトはほとんどなく、それを使用する人はそれを正しく使用しないことに気付きました。
article = soup . find ( "article" ). text <article>タグの.textプロパティにアクセスするとき、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>タグを適切に使用したWebサイトで正常に機能しました。最も長いタグには、ほとんどの場合、メインの記事が含まれています。
しかし、それは期待どおりに機能しませんでした。結果の質が低いことに気づきました。時には他の記事の抜粋を取得していました。
そのとき、私はフォールバックを追加することにしました。これは、一般的に使用されているid'sを使用して<div>および<section>タグを探している<article>タグのみを探しています。
# 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 )からです。
そのキャラクターでテキストを分割し、すべての白人を剥がして再び参加します。これは厳密には必要ではありませんが、プロセス全体をデバッグしながら大いに役立ちます。
スクリプトの上部では、停止ワードテキストファイルのパスを宣言します。これらの停止単語は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パイプラインに渡す必要があります。これは1つのコードで行われます。
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 Libraryでした。
前のステップで定義した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の投稿、英語の書かれた記事を要求しました。それらはすべて受け入れられる出力を得ましたが、それらのサイトはターゲットではなかったため、ホワイトリストからそれらを削除しました。
数週間のフィードバックの後、私は英語のサポートを追加することにしました。これには、少しリファクタリングが必要でした。
他の言語で動作させるには、言語からのすべての停止単語を含むテキストファイルのみが必要になり、コードの数行をコピーします(一般的な単語の削除セクションを参照してください)。