Skip to content

Commit

Permalink
v0.7
Browse files Browse the repository at this point in the history
  • Loading branch information
theeluwin committed Sep 7, 2020
1 parent c03f53e commit ad59e76
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 147 deletions.
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ RUN mkdir -p /workspace
WORKDIR /workspace

# install packages
RUN pip3 install setuptools networkx nose nose-exclude flake8 coverage coveralls
RUN pip install -U pip
RUN pip install setuptools networkx nose nose-exclude flake8 coverage coveralls requests

# run
# install this package
ADD . /workspace/
RUN python setup.py build && \
python setup.py install

# run test
ENTRYPOINT []
CMD ["nosetests", "--config=.noserc"]
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2019 Jamie J. Seol
Copyright (c) 2020 Jamie J. Seol

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
64 changes: 45 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,73 @@
# TextRank for Korean
# textrankr

[![Build Status](https://travis-ci.org/theeluwin/textrankr.svg?branch=master)](https://travis-ci.org/theeluwin/textrankr)
[![Coverage Status](https://coveralls.io/repos/github/theeluwin/textrankr/badge.svg?branch=master)](https://coveralls.io/github/theeluwin/textrankr?branch=master)
[![PyPI version](https://badge.fury.io/py/textrankr.svg)](https://badge.fury.io/py/textrankr)

Reorder sentences using [TextRank](http://digital.library.unt.edu/ark:/67531/metadc30962/) algorithm.
Click [here](http://konlpy.org/en/latest/install/) to see how to install [KoNLPy](http://konlpy.org/) properly (you'll need some java stuff).
Reorder sentences using the [TextRank](http://digital.library.unt.edu/ark:/67531/metadc30962/) algorithm.

Check out [lexrankr](https://github.com/theeluwin/lexrankr), which is another awesome summarizer!

Not available for Python 2 anymore (if necessary, use version 0.3).
* Mostly designed for Korean, but not limited to.
* Check out [lexrankr](https://github.com/theeluwin/lexrankr), which is an another awesome summarizer!
* Not available for Python 2 anymore (if necessary, use version 0.3).

## Installation

```bash
pip install textrankr
```

## Usage
## Tokenizers

Tokenizers are not included. You have to implement it by yourself.

Example:

```python
from textrankr import TextRank
from typing import List

textrank = TextRank(your_text_here)
print(textrank.summarize()) # gives you some text
print(textrank.summarize(3, verbose=False)) # up to 3 sentences, returned as list
class MyTokenizer:
def __call__(self, text: str) -> List[str]:
tokens: List[str] = text.split()
return tokens
```

## Test
한국어의 경우 [KoNLPy](http://konlpy.org)를 사용하는 방법이 있습니다. 아래 예시처럼 `phrases`를 쓰게되면 엄밀히는 토크나이저가 아니지만 이게 더 좋은 결과를 주는것 같습니다.

Testing requires some additional packages (`flake8` is optional, though).
```python
from typing import List
from konlpy.tag import Okt

```bash
pip install nose nose-exclude flake8 coverage
class OktTokenizer:
okt: Okt = Okt()

def __call__(self, text: str) -> List[str]:
tokens: List[str] = self.okt.phrases(text)
return tokens
```

Test with [nose](https://nose.readthedocs.io/).
## Usage

```bash
nosetests --config=.noserc
```python
from typing import List
from textrankr import TextRank

mytokenizer: MyTokenizer = MyTokenizer()
textrank: TextRank = TextRank(mytokenizer)

k: int = 3 # num sentences in the resulting summary

summarized: str = textrank.summarize(your_text_here, k)
print(summarized) # gives you some text

# if verbose=False, it returns a list
summaries: List[str] = textrank.summarize(your_text_here, k, verbose=False)
for summary in summaries:
print(summary)
```

Or, you can use docker.
## Test

Use docker.

```bash
docker build -t textrankr -f Dockerfile .
Expand Down
15 changes: 4 additions & 11 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
# -*- coding: utf-8 -*-

import os
import warnings
from typing import List

from setuptools import setup
from setuptools import find_packages


requirements = [
requirements: List[str] = [
'setuptools',
'networkx',
'konlpy',
]

if os.name == 'nt':
warnings.warn("See http://konlpy.org/en/latest/install/#id2 to properly install KoNLPy.", RuntimeWarning)


setup(
name='textrankr',
version='0.6',
version='0.7',
license='MIT',
author='Jamie Seol',
author_email='theeluwin@gmail.com',
url='https://github.com/theeluwin/textrankr',
description='TextRank for Korean',
description='TextRank implemented in Python',
packages=find_packages(),
include_package_data=True,
install_requires=requirements,
Expand Down
29 changes: 15 additions & 14 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@

import unittest

from typing import List

from textrankr import TextRank

from .tokenizers import OktTokenizer

class TestTextrankr(unittest.TestCase):

def setUp(self):
self.text = "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\". 트위터가 수많은 트윗을 분석해 정보를 판매하는 서비스를 미국 정보당국에는 제공하지 않기로 했다. 월스트리트저널은 미국 정보당국 관계자 등을 인용해 데이터마이너(Dataminer)가 정보당국에 대한 서비스는 중단하기로 했다고 9일(현지시간) 보도했다. 트위터가 5% 지분을 가진 데이터마이너는 소셜미디어상 자료를 분석해 고객이 의사결정을 하도록 정보를 제공하는 기업이다. 트위터에 올라오는 트윗에 실시간으로 접근해 분석한 자료를 고객에게 팔 수 있는 독점권을 갖고 있다. 정보당국은 이 회사로부터 구매한 자료로 테러나 정치적 불안정 등과 관련된 정보를 획득했다. 이 회사가 정보당국에 서비스를 판매하지 않기로 한 것은 트위터의 결정인 것으로 알려졌다. 데이터마이너 경영진은 최근 “트위터가 정보당국에 서비스하는 것을 원치 않는다”고 밝혔다고 이 신문은 전했다. 트위터도 성명을 내고 “정보당국 감시용으로 데이터를 팔지 않는 것은 트위터의 오래된 정책”이라며 “트위터 자료는 대체로 공개적이고 미국 정부도 다른 사용자처럼 공개된 어카운트를 살펴볼 수 있다”고 해명했다. 그러나 이는 이 회사가 2년 동안 정보당국에 서비스를 제공해 온 데 대해서는 타당한 설명이 되지 않는다. 트위터의 이번 결정은 미국의 정보기술(IT)기업과 정보당국 간 갈등의 연장 선상에서 이뤄진 것으로 여겨지고 있다. IT기업은 이용자 프라이버시에 무게 중심을 두는 데 비해 정보당국은 공공안전을 우선시해 차이가 있었다. 특히 애플은 캘리포니아 주 샌버너디노 총격범의 아이폰에 저장된 정보를 보겠다며 데이터 잠금장치 해제를 요구하는 미 연방수사국(FBI)과 소송까지 진행했다. 정보당국 고위 관계자도 “트위터가 정보당국과 너무 가까워 보이는 것을 우려하는 것 같다”고 말했다. 데이터마이너는 금융기관이나, 언론사 등 정보당국을 제외한 고객에 대한 서비스는 계속할 계획이다. ."
self.textrank = TextRank(self.text)
class TestTextRank(unittest.TestCase):

def test_ranked(self):
results = self.textrank.summarize(3, verbose=False)
self.assertEqual(len(results), 3)
self.assertEqual(results[0], "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\".")
def setUp(self) -> None:
self.text: str = "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\". 트위터가 수많은 트윗을 분석해 정보를 판매하는 서비스를 미국 정보당국에는 제공하지 않기로 했다. 월스트리트저널은 미국 정보당국 관계자 등을 인용해 데이터마이너(Dataminer)가 정보당국에 대한 서비스는 중단하기로 했다고 9일(현지시간) 보도했다. 트위터가 5% 지분을 가진 데이터마이너는 소셜미디어상 자료를 분석해 고객이 의사결정을 하도록 정보를 제공하는 기업이다. 트위터에 올라오는 트윗에 실시간으로 접근해 분석한 자료를 고객에게 팔 수 있는 독점권을 갖고 있다. 정보당국은 이 회사로부터 구매한 자료로 테러나 정치적 불안정 등과 관련된 정보를 획득했다. 이 회사가 정보당국에 서비스를 판매하지 않기로 한 것은 트위터의 결정인 것으로 알려졌다. 데이터마이너 경영진은 최근 “트위터가 정보당국에 서비스하는 것을 원치 않는다”고 밝혔다고 이 신문은 전했다. 트위터도 성명을 내고 “정보당국 감시용으로 데이터를 팔지 않는 것은 트위터의 오래된 정책”이라며 “트위터 자료는 대체로 공개적이고 미국 정부도 다른 사용자처럼 공개된 어카운트를 살펴볼 수 있다”고 해명했다. 그러나 이는 이 회사가 2년 동안 정보당국에 서비스를 제공해 온 데 대해서는 타당한 설명이 되지 않는다. 트위터의 이번 결정은 미국의 정보기술(IT)기업과 정보당국 간 갈등의 연장 선상에서 이뤄진 것으로 여겨지고 있다. IT기업은 이용자 프라이버시에 무게 중심을 두는 데 비해 정보당국은 공공안전을 우선시해 차이가 있었다. 특히 애플은 캘리포니아 주 샌버너디노 총격범의 아이폰에 저장된 정보를 보겠다며 데이터 잠금장치 해제를 요구하는 미 연방수사국(FBI)과 소송까지 진행했다. 정보당국 고위 관계자도 “트위터가 정보당국과 너무 가까워 보이는 것을 우려하는 것 같다”고 말했다. 데이터마이너는 금융기관이나, 언론사 등 정보당국을 제외한 고객에 대한 서비스는 계속할 계획이다. ."
self.tokenizer: OktTokenizer = OktTokenizer()
self.textrank: TextRank = TextRank(self.tokenizer)

def test_verbose(self):
result = self.textrank.summarize(1, verbose=True)
self.assertEqual(result, "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\".")
def test_ranked(self) -> None:
summaries: List[str] = self.textrank.summarize(self.text, 3, verbose=False)
self.assertEqual(len(summaries), 3)
self.assertEqual(summaries[0], "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\"")

def test_sentence(self):
sent = self.textrank.sentences[0]
self.assertEqual(str(sent), "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\".")
def test_verbose(self) -> None:
summaries: str = self.textrank.summarize(self.text, 1, verbose=True)
self.assertEqual(summaries, "트위터, \"정보당국에 데이터 분석자료 팔지 않겠다\"")


if __name__ == '__main__':
Expand Down
44 changes: 44 additions & 0 deletions tests/tokenizers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-

import json
import requests

from typing import List

from konlpy.tag import Okt
from requests.models import Response


class OktTokenizer:
"""
A POS-tagger based tokenizer functor. Note that these are just an examples. Using phrases function rather than a mere POS tokenizer seems better.
Example:
tokenizer: OktTokenizer = OktTokenizer()
tokens: List[str] = tokenizer(your_text_here)
"""

okt: Okt = Okt()

def __call__(self, text: str) -> List[str]:
tokens: List[str] = self.okt.phrases(text)
return tokens


class ApiTokenizer:
"""
An API based tokenizer functor, assuming that the response body is a jsonifyable string with content of list of str tokens.
Example:
tokenizer: ApiTokenizer = ApiTokenizer()
tokens: List[str] = tokenizer(your_text_here)
"""

def __init__(self, endpoint: str) -> None:
self.endpoint: str = endpoint

def __call__(self, text: str) -> List[str]:
body: bytes = text.encode('utf-8')
res: Response = requests.post(self.endpoint, data=body)
tokens: List[str] = json.loads(res.text)
return tokens
5 changes: 1 addition & 4 deletions textrankr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
# -*- coding: utf-8 -*-

from .sentence import Sentence
from .textrankr import TextRank
from .textrank import TextRank
23 changes: 0 additions & 23 deletions textrankr/phraser.py

This file was deleted.

25 changes: 15 additions & 10 deletions textrankr/sentence.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-

from collections import Counter


class Sentence(object):
class Sentence:
"""
The purpose of this class is as follows:
1. In order to use the 'pagerank' function in the networkx library, you need a hashable object.
2. Summaries should keep the sentence order from its original text to improve the verbosity.
Note that the 'bow' stands for 'bag-of-words'.
"""

def __init__(self, phraser, text, index=0):
self.index = index
self.text = text.strip()
self.tokens = phraser(self.text)
self.bow = Counter(self.tokens)
def __init__(self, index: int, text: str, bow: Counter) -> None:
self.index: int = index
self.text: str = text
self.bow: Counter = bow

def __str__(self):
def __str__(self) -> str:
return self.text

def __hash__(self):
def __hash__(self) -> int:
return self.index
60 changes: 60 additions & 0 deletions textrankr/textrank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import List
from typing import Dict
from typing import Callable

from networkx import Graph
from networkx import pagerank

from .sentence import Sentence

from .utils import parse_text_into_sentences
from .utils import build_sentence_graph


class TextRank:
"""
Args:
tokenizer: a function or a functor of Callable[[str], List[str]] type.
tolerance: a threshold for omitting edge weights.
Example:
tokenizer: YourTokenizer = YourTokenizer()
textrank: TextRank = TextRank(tokenzier)
summaries: str = textrank.summarize(your_text_here)
print(summaries)
"""

def __init__(self, tokenizer: Callable[[str], List[str]], tolerance: float = 0.05) -> None:
self.tokenizer: Callable[[str], List[str]] = tokenizer
self.tolerance: float = tolerance

def summarize(self, text: str, num_sentences: int = 3, verbose: bool = True):
"""
Summarizes the given text, using the textrank algorithm.
Args:
text: a raw text to be summarized.
num_sentences: number of sentences in the summarization results.
verbose: if True, it will return a summarized raw text, otherwise it will return a list of sentence texts.
"""

# parse text
sentences: List[Sentence] = parse_text_into_sentences(text, self.tokenizer)

# build graph
graph: Graph = build_sentence_graph(sentences, tolerance=self.tolerance)

# run pagerank
pageranks: Dict[Sentence, float] = pagerank(graph, weight='weight')

# get top-k sentences
sentences = sorted(pageranks, key=pageranks.get, reverse=True)
sentences = sentences[:num_sentences]
sentences = sorted(sentences, key=lambda sentence: sentence.index)

# return summaries
summaries = [sentence.text for sentence in sentences]
if verbose:
return '\n'.join(summaries)
else:
return summaries

0 comments on commit ad59e76

Please sign in to comment.