Created
September 18, 2020 08:33
-
-
Save dakeshi19/8892b5fcc55262cb527c202568bfabfd to your computer and use it in GitHub Desktop.
spaCyのMatchのお試し
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import spacy | |
from spacy.matcher import Matcher | |
import re | |
nlp = spacy.load('ja_core_news_md') | |
_text = """ | |
関西国際空港側の居酒屋で、酒を飲みすぎたため、電車で眠りこけてしまったため、寝過ごしてしまった。終点の駅で目が醒めたのだが、周りに何もなくてとても淋しい気持ちになった。ふと夜空を見上げると、八分咲きの美しい桜の花が慰めてくれるようだった。 | |
本当はもう少しこなれた文章をどこかから引用したかったが、著作権的なところの問題を考えたくなかったのと、ルールのベンチマークに使うには都合の良い文章が見つからなかったため、ひとまず創作した。 | |
冒頭のテキストは決して私の美的感覚を反映したものではないことをお断りしたい。もちろん、本来はもっと文才があるということを言いたいわけではない。また、この文章は「クワイン(Quine)」的なものになっているわけでも、なんらかの縦読みなどの暗号的なものや仕掛けが含まれているものではない。深読みしないでください。 | |
ムーンサルトはお断りしたい。 | |
ムーンサルト斬りしたい。 | |
お飾りが綺麗だ。 | |
文章をどこからか引用する。 | |
文章を引用する。 | |
""" | |
doc = nlp(_text.replace(" ", "。").replace(" ", "、")) | |
mc = Matcher(nlp.vocab) | |
def ruletag(regex, op=None): | |
if op: | |
return {"TAG": {"REGEX": regex}, "OP": op} | |
return {"TAG": {"REGEX": regex}} | |
名詞動詞形容詞 = [ruletag("^(名詞|動詞|形容詞|形状詞|限定詞|接辞|接頭辞)(?!.*(非自立可能|助数詞可能|副詞可能)).*$")] | |
名詞 = [ruletag("^名詞")] | |
動詞形容詞 = [ruletag("^(動詞|形容詞|形状詞)")] | |
任意 = [ruletag("^(?!補助記号).+$")] | |
N = 7 | |
for i in range(N): | |
ptnX = 名詞動詞形容詞 + 任意 * i + 名詞 | |
mc.add("RULE_X_"+str(i), None, ptnX) | |
ptnY = 名詞動詞形容詞 + 任意 * i + 動詞形容詞 | |
mc.add("RULE_Y_"+str(i), None, ptnY) | |
def 繋がり担保(token1, token2): | |
""" | |
token1とtoken2の間に(間接的でも良いので)依存関係が成立しているかをチェックする。 | |
※依存関係がない場合は、意味のあるフレーズを切り出すには不向きなであり、これを枝狩りするためのもの。 | |
""" | |
t1 = token1 | |
while t1.dep_ != 'ROOT': | |
if (t1h := t1.head).i == token2.i: | |
return True | |
t1 = t1h | |
return False | |
def 近傍取得(token, XXX, right=True): | |
""" | |
tokenに対して、右向き(right=True)に、今回の仕様でのフレーズ成立の為に必要な語尾のトークンを取得する。 | |
right=Falseは左向きとなる。XXXは語尾に続けていく対象とする依存関係を指定する。 | |
""" | |
INC, LIMIT = (1, len(doc)) if right else (-1, -1) | |
i = token.i | |
while (i := i + INC) != LIMIT: | |
x = doc[i] | |
if x.dep_ in ('punct'): # 句読点にあたった場合はそこで終了 | |
break | |
if right: | |
if x.dep_ not in XXX: | |
break | |
else: | |
if x.dep_ not in XXX and x.head.i != token.i: | |
break | |
return i - INC | |
def 対になる語を見つける(token, p): | |
""" | |
token(動詞、形容詞や形状詞)と対になる目的語などを取得する。 | |
""" | |
# 対とみなす語のうち、より優先する「依存語」の種別(例. 動詞の場合、主語よりも目的語を優先する) | |
depprior1 = ['obj', 'nmod', 'nsubj', 'iobj', 'obl'] | |
depprior2 = ['nsubj', 'iobj', 'nmod', 'obl', 'obj'] | |
depprior = depprior1 if token.tag_.startswith( | |
'動詞') else depprior2 | |
目印 = sorted([t for t in token.children if p <= t.i < token.i], | |
key=lambda x: depprior.index(x.dep_) if x.dep_ in depprior else len(depprior)) | |
if 目印: | |
return 目印[0] | |
return token | |
def 複合語(token): | |
""" | |
「この人は美的感覚が...」→ 「感覚」というトークンからみて複合語を成立させるために「美的」部分を取得する。 | |
""" | |
return 近傍取得(token, ('compound', 'dep'), right=False) | |
def フレーズらしくする(r, p, l): | |
""" | |
「ボールを素早く蹴る」というフレーズが「蹴る」を起点に取得できているとして、 | |
「サッカーボールを蹴る」というホットワード風のフレーズを取得する。 | |
※pは「ボール」のトークン位置、lは蹴るのトークン位置。 | |
(※原文の中では、「黒いサッカーボールを...」というセンテンスとなっているイメージ) | |
""" | |
起点token = doc[l] | |
# (1)動詞の後ろに続く助動詞を取得する 等 | |
# | |
継ぎ足し = 近傍取得(起点token, ('aux', 'advcl'), right=True) | |
if l < 継ぎ足し: | |
活用 = doc[l: 継ぎ足し + 1].text | |
else: | |
tmpA = 起点token.text | |
tmpB = 起点token.lemma_ | |
活用 = tmpA if tmpA[-1] == tmpB[-1] else tmpB | |
# (2)注目しているトークンが複合語であれば、複合語の形にする | |
複合語はじまり = 複合語(起点token) | |
# (3)注目している語と対になる語(動詞に対して目的語...)を取得する | |
相手 = 対になる語を見つける(起点token, p) | |
ここから = 複合語(相手) | |
ここまで = 近傍取得(相手, ('case'), right=True) # 「ボールを蹴る」の「ボール」に対して助詞の「を」取得 | |
# 上記、(1)、(2)、(3)を関連するトークンの並び順を考慮した上で、結合する。 | |
if ここまで < 複合語はじまり: | |
return doc[ここから: ここまで + 1].text + doc[複合語はじまり:l].text + 活用 | |
else: | |
return doc[ここから: ここまで + 1].text + 活用 | |
def 名詞フレーズらしくする(span): | |
# a = span[-1] | |
# if a.text == 'ため' and a.tag_.startswith('名詞'): | |
# return span[:-2].text | |
return span.text | |
def 帳尻合わせ(phrases_text): | |
""" | |
複数のマッチルールに該当したことなどにより、重複した場合は除外する...など | |
""" | |
hiragana = re.compile(r'[\u3041-\u309F]{1,5}') | |
# 同じものが連続で現れる場合は重複除外する。 | |
tmp = [] | |
i = 0 | |
phstr = phrases_text[i] | |
while (i := i + 1) and i < len(phrases_text): | |
n = phrases_text[i] | |
if phstr == n: | |
continue | |
# 4文字以下で全てひらがなの場合はフレーズにふさわしくないとして除外(経験則) | |
if not hiragana.fullmatch(phstr): | |
print(phstr) | |
phstr = n | |
# マッチさせる | |
phrases_text = [] | |
for match_id, s, e in mc(doc): | |
span = doc[s:e] | |
first = span[0] | |
last = span[-1] | |
if 繋がり担保(first, last) or 近傍取得(last, ('compound', 'dep'), right=False) == first.i: | |
rule = nlp.vocab.strings[match_id] | |
if last.tag_.startswith(('動詞', '形容詞', '形状詞')): | |
phrases_text.append(フレーズらしくする(rule, first.i, last.i)) | |
else: | |
phrases_text.append(名詞フレーズらしくする(doc[複合語(first): last.i + 1])) | |
帳尻合わせ(phrases_text) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment