Sentence: ってなんぞ? sumy/spaCy/GiNZAを使った文章要約

Pythonを使って文章要約

趣味の一環でPythonを使った文章要約にチャレンジしました。

おおまかな流れはこんな感じです。

  1. BeautifulSoupを使ってスクレイピング。記事サイトの文章を抽出。
  2. spaCyとGiNZAで形態素解析
  3. sumyで文章要約
  4. アウトプット

アウトプットに関しては何でもいいんだけれども、wordpressやtwitter自動投稿を想定していました。

実装したコードよりも、躓いたポイントだけ紹介します。

にしかた
実際の仕組みは、もっと立派なサイトが紹介してるぜ!

実装したコード

GoogleアラートからRSS取得

まずGoogleアラートで、RSSを取得します。

この方法も割愛。注意点は『ダイジェスト配信』を有効にしているとRSSを選択できなくなります。

# RSSを取得する
rssurl = 'https://www.google.co.jp/alerts/feeds/********'
response = urllib.request.urlopen(rssurl)
rss = response.read().decode('utf-8')

# 記事リスト
lineup = []

# RSSからデータを抽出する
soup = BeautifulSoup(rss, 'html.parser')
for entry in soup.find_all('entry'):

	# 記事配列
	article = []

	# タイトル
	title = entry.find('title').string
	title = title.replace('<b>','').replace('</b>','')

	# リンク先URLの処理
	url = entry.find('link').get('href')
	idx = url.rfind('url=')
	url = url[idx+len('url='):]
	idx = url.find('&')
	url = url[:idx]

	# ドメインの処理
	domain = url.replace('https://','')
	dom = domain.find('/')
	domain = domain[:dom]

最終的にlineupという配列に、articleという配列を入れる、二次元配列の構造にしています。

そしてなぜドメインの処理を行っているかというと、その後の処理でBeautifulSoupを使ってスクレイピングを行うにあたってのクラス指定をするためです。

『このドメインは、このクラスでスクレイピングをする』という辞書型の一覧を別のpythonファイルで扱っており、そのkeyをドメインにしているわけです。

 

スクレイピングの処理はここでは説明しませんが、最終的にarticleという配列には、[記事タイトル, URL, 記事の内容]が入るようになっています。

要約の処理

このサイトのコードをほぼ丸コピペでいけました。

はてだBlog(仮称)

要約・キーフレーズ抽出について sumy は、Pythonで実装された、抽出型のドキュメント要約ライブラリです。 3行で…

import spacy
from sumy.parsers.plaintext import PlaintextParser

#要約アルゴリズム一覧
from sumy.summarizers.lex_rank import LexRankSummarizer
from sumy.summarizers.lsa import LsaSummarizer
from sumy.summarizers.reduction import ReductionSummarizer
from sumy.summarizers.luhn import LuhnSummarizer
from sumy.summarizers.sum_basic import SumBasicSummarizer
from sumy.summarizers.kl import KLSummarizer
from sumy.summarizers.edmundson import EdmundsonSummarizer

#先ほど処理した記事リストを読み込む
import lineup

#GiNZA/spaCyの初期化
nlp = spacy.load('ja_ginza_electra')

class myTokenizer:
    @staticmethod
    def to_sentences(text) :
        return [str(s) for s in nlp(text).sents] 

    @staticmethod
    def to_words(sentence) :
        l = next(nlp(sentence).sents).lemma_
        return l.split(' ')

#先ほど処理した記事リストの処理
doc_len = len(lineup.lineup)
for i in range(0, doc_len):

    #ドキュメントの読み込み
    doc_str = lineup.lineup[i][2]
    
    #要約する行数の指定
    sentences_count = 5

    #パーサーの設定
    parser = PlaintextParser.from_string(doc_str, myTokenizer())

    #要約
    sumer = LexRankSummarizer()
    sum_text = sumer(document=parser.document, sentences_count=sentences_count)

    list_text = ''

    for sentence in sum_text:
        list_text = list_text + '\n\n' + sentence.__str__()

    #あとで説明
    lineup.lineup[i][2] = list_text


sumlist = lineup.lineup

さきほど[記事タイトル, URL, 記事の内容]このような配列を作ったので、記事の内容 → 記事の要約に帰るために『lineup.lineup[i][2] = list_text』という処理を行いました。

つまづきポイント

BeautifulSoupでの記事テキスト取得

にしかた
sumyに辿り着く前に、こんなところでつまづいちゃったぜ
色々試行錯誤したものの、記事の文章だけ取ってくることに難航しました。
結局どのような処理をおこなったかというと、
import scls
html = rp.read().decode(scls.scha[domain], 'ignore') #たまーに文字コードがshift-jisのサイトがある
soup = BeautifulSoup(html, 'html.parser')
main_text = str(soup.find('div', class_=scls.scls[domain])) #クラスの指定

#ここからが文章の処理
main_text = re.sub(r'<[^>]*?>','',main_text)
main_text = re.sub('(.+?)','', main_text)
main_text = re.sub('\(.+?\)', '', main_text)
main_text = re.sub('「|」','', main_text)
main_text = ''.join(main_text.split())
main_text = main_text.replace('\u3000','')

結論、置換しまくったわけです。

import sclsというのが、ドメインと対象のクラスを管理しているpythonファイルです。

最初はsclsという変数名で、ドメインと対象のクラスだけしか管理していなかったのですが、途中で文字コードがshift-jisの古のサイトが出てきたので、急遽schaという変数を作成し、ドメインと文字コードも管理し始めました。

ポイントはmain-contentみたいなクラス名のdiv内全部のソースを取得し、とにかく置換しまくったという点です。(大事なことなので2回)

main_text = re.sub(r'<[^>]*?>','',main_text)

htmlタグ(<>で囲まれている文字列)はこれで全削除しました。

main_text = re.sub('(.+?)','', main_text)
main_text = re.sub('\(.+?\)', '', main_text)
main_text = re.sub('「|」','', main_text)

形態素解析で()、()、「」などの括弧が邪魔になりそうだったので全削除しました。()、()はその中の文字列も全削除で、「」は括弧のみ削除しました。

main_text = ''.join(main_text.split())
main_text = main_text.replace('\u3000','')

これで全角、半角スペースの削除をしました。

Sentenceという謎オブジェクト

sumer = LexRankSummarizer() 
sum_text = sumer(document=parser.document, sentences_count=sentences_count)

何も考えずに、このコードでsum_textをprintすると、

(<Sentence: hogehogeほげほげ>, <Sentence: fugafugaふがふが>)というタプル型でアウトプットが出ます。

更に何も考えずに、sum_text.replace(‘<Sentence:’,”)としてみたら、それは出来んぞ!!!というエラーをくらいまくりました。

にしかた
明らかにsumyを理解しきれていない証拠が垣間見えたぜ
解決方法は以下のコードです。
for sentence in sum_text: 
list_text = list_text + '\n\n' + sentence.__str__()

2回改行コード挟んでますが、これはどっちでもよい。

とりあえずsentenceでループさせ、sentence.__str__()で文字型になるようです。

ここでは1文につなげたかったので、list_textに文字を連結させています。

まとめ

にしかた
きちんとsumyの仕様を理解してから使うべきだぜ!