Kota Takahashi commited on
Commit
a08962e
1 Parent(s): c311dc0

ファーストコミット

Browse files
.gitignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Caches and logs
7
+ *.log
8
+ logs/
9
+ *.cache/
10
+
11
+ # Environment variables
12
+ .env
13
+
14
+ # Static files (usually collected by Django's collectstatic)
15
+ /static/
16
+
17
+ # Media files
18
+ /media/
19
+
20
+ # Database
21
+ *.sqlite3
22
+
23
+ # IDE specific files
24
+ .idea/
25
+ .vscode/
26
+
27
+ # Dependency directories
28
+ venv/
29
+ env/
30
+
31
+ # Compiled Python files
32
+ *.pyc
33
+ *.pyo
34
+ *.pyd
35
+
36
+ # macOS
37
+ .DS_Store
app.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ from news_scraper import YahooNewsScraper
4
+ from tidif_calclator import JapaneseTextVectorizer
5
+ from cosine_similarity_calculator import CosineSimilarityCalculator
6
+ from summerizer import TextSummarizer
7
+
8
+ st.title("ニュース検索アプリ")
9
+
10
+ # 初期化
11
+ best_article_text = None
12
+ best_article_url = None
13
+ best_max_word = None
14
+ max_word = None
15
+ best_max_value = -1 # cos類似度は0以上なので、初期値を-1に設定
16
+ num_news = 5
17
+
18
+ # セッションステートの初期化
19
+ if 'news_fetched' not in st.session_state:
20
+ st.session_state['news_fetched'] = False
21
+ st.session_state['article_text_list'] = []
22
+ st.session_state['article_url_list'] = []
23
+
24
+ if st.button('最新ニュース取得'):
25
+ with st.spinner('ニュースを取得中...'):
26
+ # yahooニュースをスクレイピング
27
+ scraper = YahooNewsScraper()
28
+ article_text_list = []
29
+ article_url_list = []
30
+ for i in range(num_news):
31
+ article_text, detail_url = scraper.scrape_article(i)
32
+ article_text_list.append(article_text)
33
+ article_url_list.append(detail_url)
34
+ st.session_state['news_fetched'] = True # 処理完了フラグを設定
35
+ st.session_state['article_text_list'] = article_text_list # セッションステートに保存
36
+ st.session_state['article_url_list'] = article_url_list
37
+ st.write("取得完了しました")
38
+
39
+ if st.session_state['news_fetched']:
40
+ search_word = st.text_input('名詞', placeholder='名詞を入力してください', max_chars=10, help='10文字以内の名詞')
41
+ if st.button('要約作成'):
42
+ article_text_list = st.session_state['article_text_list']
43
+ article_url_list = st.session_state['article_url_list']
44
+ for temp_article_text, temp_article_url in zip(article_text_list, article_url_list):
45
+ # TD-IDF値を計算
46
+ vectorizer = JapaneseTextVectorizer()
47
+ tfidf_dict = vectorizer.fit_transform(temp_article_text)
48
+
49
+ # cos類似度を計算
50
+ word_similarity = CosineSimilarityCalculator()
51
+ article_keyword_list = list(tfidf_dict.keys())
52
+ result_word_similarity = word_similarity.calculate_similarity(search_word, article_keyword_list)
53
+
54
+ # None でない値のみを抽出
55
+ filtered_data = {k: v for k, v in result_word_similarity.items() if v is not None}
56
+
57
+ # 最大値を持つキーとその値を取得
58
+ if filtered_data: # filtered_dataが空でないことを確認
59
+ max_word = max(filtered_data, key=filtered_data.get)
60
+ max_value = filtered_data[max_word]
61
+ # 最大値がこれまでの最大値より大きければ更新
62
+ if max_value > best_max_value:
63
+ best_max_value = max_value
64
+ best_max_word = max_word
65
+ best_article_text = temp_article_text
66
+ best_article_url = temp_article_url
67
+
68
+ # テキストを要約
69
+ summarizer = TextSummarizer()
70
+ summary_text = summarizer.summarize(best_article_text, max_length=30, min_length=20)
71
+ st.write(f'最も類似度が高いワードは「{best_max_word}」でした')
72
+ st.write(f'url:{best_article_url}')
73
+ st.text_area("要約:", summary_text, height=20)
cosine_similarity_calculator.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gensim
2
+ from sklearn.metrics.pairwise import cosine_similarity
3
+
4
+
5
+ class CosineSimilarityCalculator:
6
+ model_path = 'ja/ja.bin'
7
+
8
+ def __init__(self):
9
+ """
10
+ CosineSimilarityCalculatorクラスを初期化し、
11
+ 事前トレーニング済みのWord2Vecモデルをロード
12
+
13
+ Parameters:
14
+ - なし。
15
+
16
+ Returns:
17
+ - なし。
18
+ """
19
+ self.model = gensim.models.Word2Vec.load(CosineSimilarityCalculator.model_path)
20
+
21
+ def _convert_to_2d_array(self, vector):
22
+ """
23
+ 埋め込みベクトルを2次元配列に変換
24
+
25
+ Parameters:
26
+ - vector (numpy.ndarray): 変換する1次元配列のベクトル
27
+
28
+ Returns:
29
+ - vector_2d (numpy.ndarray): 変換後の2次元配列のベクトル
30
+ """
31
+ return vector.reshape(1, -1)
32
+
33
+ def _calculate_cosine_similarity(self, embedding1, embedding2):
34
+ """
35
+ コサイン類似度を計算
36
+
37
+ Parameters:
38
+ - embedding1 (numpy.ndarray): 1つ目の埋め込みベクトル(2次元配列)
39
+ - embedding2 (numpy.ndarray): 2つ目の埋め込みベクトル(2次元配列)
40
+
41
+ Returns:
42
+ - similarity (numpy.ndarray): コサイン類似度
43
+ """
44
+ return cosine_similarity(embedding1, embedding2)
45
+
46
+ def calculate_similarity(self, search_word, article_keyword_list):
47
+ """
48
+ 指定された検索ワードと記事のキーワードリストの間のコサイン類似度を計算
49
+
50
+ Parameters:
51
+ - search_word (str): 検索ワード
52
+ - article_keyword_list (list): 記事のキーワードリスト
53
+ Returns:
54
+ - similarities (dict): 記事キーワードとそれぞれの検索ワードのコサイン類似度を含む辞書を作成し、モデルにない単語の場合はNoneを返す
55
+ """
56
+ # 検索ワードの埋め込みベクトルを取得
57
+ if search_word in self.model.wv:
58
+ search_embedding = self.model.wv[search_word]
59
+ else:
60
+ print(f"{search_word} は本モデルの語彙にありません。")
61
+ return None
62
+
63
+ similarities = {}
64
+ # 記事キーワードの埋め込みベクトルを取得し、コサイン類似度を計算
65
+ for keyword in article_keyword_list:
66
+ if keyword in self.model.wv:
67
+ keyword_embedding = self.model.wv[keyword]
68
+ search_embedding_2d = self._convert_to_2d_array(search_embedding)
69
+ keyword_embedding_2d = self._convert_to_2d_array(keyword_embedding)
70
+ similarity = self._calculate_cosine_similarity(search_embedding_2d, keyword_embedding_2d)
71
+ similarities[keyword] = similarity[0][0]
72
+ else:
73
+ similarities[keyword] = None
74
+ return similarities
ja/ja.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0b7f970b78b76dd1785c5e665af83c63e0c0c6129d27fcbbb39025eaf3d48a64
3
+ size 4187227
ja/ja.bin.syn0.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6a336b13ed39aba8ea4846d14b2140b2db5444d2a8c96c91387f077c2786be1d
3
+ size 60129680
ja/ja.bin.syn1neg.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d13900fbeee5e3dd84ac6cc64de3e18c27c61f8c2f2eba7fb3c364213f53799e
3
+ size 60129680
news_scraper.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ import re
4
+ from time import sleep
5
+
6
+
7
+ class Scraper:
8
+ def __init__(self):
9
+ """
10
+ Scraperクラスを初期化し、requestsセッションを作成する。
11
+ """
12
+ self.session = requests.Session()
13
+
14
+ def _fetch_content(self, url):
15
+ """
16
+ 指定されたURLのコンテンツを取得する。
17
+
18
+ Parameters:
19
+ - url (str): 取得するウェブページのURL。
20
+
21
+ Returns:
22
+ - content (bytes): 取得したコンテンツのバイトデータ。
23
+ """
24
+ response = self.session.get(url)
25
+ response.raise_for_status() # HTTPエラーが発生した場合は例外を投げる
26
+ return response.content
27
+
28
+ def _parse_html(self, html):
29
+ """
30
+ HTMLコンテンツをBeautifulSoupでパースする。
31
+
32
+ Parameters:
33
+ - html (bytes): パースするHTMLコンテンツ。
34
+
35
+ Returns:
36
+ - soup (BeautifulSoup): パースされたBeautifulSoupオブジェクト。
37
+ """
38
+ soup = BeautifulSoup(html, 'html.parser')
39
+ return soup
40
+
41
+
42
+ class YahooNewsScraper(Scraper):
43
+ base_url = "https://news.yahoo.co.jp/"
44
+
45
+ def get_news_urls(self):
46
+ """
47
+ Yahooニュースのトップページから最新ニュース記事のURLを取得する
48
+
49
+ Parameters:
50
+ - なし
51
+
52
+ Returns:
53
+ - article_url_list (list): ニュース記事のURLリスト(最大5件)
54
+ """
55
+ content = self._fetch_content(self.base_url)
56
+ soup = self._parse_html(content)
57
+ news_list = soup.select('section.topics a') # 'topics'セクション内のすべての<a>タグを選択
58
+ article_url_list = [tag.get('href') for tag in news_list if tag.get('href')] # href属性を抽出
59
+ return article_url_list[:5] # 最初の5つのURLを返す
60
+
61
+ def get_article_url(self, index=0):
62
+ """
63
+ 指定したインデックスのニュース記事のURLを取得する
64
+
65
+ Parameters:
66
+ - index (int): 取得したい記事のインデックス (デフォルトは0)
67
+
68
+ Returns:
69
+ - article_url (str): 指定されたインデックスの記事のURL
70
+
71
+ Raises:
72
+ - IndexError: インデックスが範囲外の場合に発生
73
+ """
74
+ article_urls = self.get_news_urls()
75
+ if index >= len(article_urls):
76
+ raise IndexError("URLが取得できませんでした") # インデックスが範囲外の場合は例外を投げる
77
+ return article_urls[index]
78
+
79
+ def get_article_detail_url(self, article_url):
80
+ """
81
+ 記事ページから詳細記事のURLを取得する
82
+
83
+ Parameters:
84
+ - article_url (str): ニュース記事のURL
85
+
86
+ Returns:
87
+ - detail_url (str): 記事の詳細ページのURL
88
+
89
+ Raises:
90
+ - ValueError: 詳細ページのURLが見つからない場合に発生
91
+ """
92
+ content = self._fetch_content(article_url)
93
+ soup = self._parse_html(content)
94
+ detail_url_tag = soup.select_one('a:-soup-contains("記事全文を読む")') # "記事全文を読む"を含むリンクを選択
95
+ if detail_url_tag:
96
+ return detail_url_tag.get('href') # タグのhref属性を返す
97
+ else:
98
+ raise ValueError("ニュース記事が見つかりませんでした") # タグが見つからない場合はエラーを出力
99
+
100
+ def get_full_article_text(self, detail_url):
101
+ """
102
+ 詳細記事の全文を取得し、不要な文字を削除する
103
+
104
+ Parameters:
105
+ - detail_url (str): 記事の詳細ページのURL
106
+
107
+ Returns:
108
+ - full_text (str): 記事の全文テキスト
109
+ """
110
+ content = self._fetch_content(detail_url)
111
+ soup = BeautifulSoup(content, 'html.parser')
112
+ paragraphs = soup.select('article div.article_body p') # 記事本文内のすべての<p>タグを選択
113
+ full_text = ''.join([p.text for p in paragraphs]) # すべての段落のテキストを結合
114
+ return re.sub(r"[\u3000\n\r]", "", full_text) # 不要な文字を削除
115
+
116
+ def scrape_article(self, index=0):
117
+ """
118
+ 指定されたインデックスの記事をスクレイプし、全文を取得する
119
+
120
+ Parameters:
121
+ - index (int): スクレイプする記事のインデックス (デフォルトは0)
122
+
123
+ Returns:
124
+ - full_text (str): スクレイプされた記事の全文テキスト
125
+ """
126
+ article_url = self.get_article_url(index)
127
+ sleep(1) # サーバー負荷を避けるために1秒待機
128
+ detail_url = self.get_article_detail_url(article_url)
129
+ sleep(1) # サーバー負荷を避けるためにさらに1秒待機
130
+ article_text = self.get_full_article_text(detail_url)
131
+ return article_text, detail_url
summerizer.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline, T5Tokenizer, T5ForConditionalGeneration
2
+
3
+
4
+ class TextSummarizer:
5
+ model_name = "sonoisa/t5-base-japanese"
6
+ tokenizer_name = "sonoisa/t5-base-japanese"
7
+
8
+ def __init__(self):
9
+ """
10
+ TextSummarizerクラスを初期化し、トークナイザ、モデル、パイプラインを設定
11
+
12
+ Parameters:
13
+ - なし
14
+
15
+ Returns:
16
+ - な。
17
+ """
18
+ # トークナイザを個別に初期化し、legacy=Falseを指定
19
+ self.tokenizer = T5Tokenizer.from_pretrained(self.tokenizer_name, legacy=False)
20
+ # モデルを個別に初期化
21
+ self.model = T5ForConditionalGeneration.from_pretrained(self.model_name)
22
+ # パイプラインを初期化
23
+ self.summarizer = pipeline("summarization", model=self.model, tokenizer=self.tokenizer)
24
+
25
+ def summarize(self, text, max_length=20, min_length=10):
26
+ """
27
+ テキストを要約
28
+
29
+ Parameters:
30
+ - text (str): 要約する対象のテキスト。
31
+ - max_length (int): 要約の最大長 (デフォルトは20)
32
+ - min_length (int): 要約の最小長 (デフォルトは10)
33
+
34
+ Returns:
35
+ - summary_text (str): 要約されたテキスト
36
+ """
37
+ summary = self.summarizer(text, max_length=max_length, min_length=min_length, do_sample=False)
38
+ return summary[0]['summary_text']
tidif_calclator.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import MeCab
2
+ import re
3
+ from sklearn.feature_extraction.text import TfidfVectorizer
4
+
5
+
6
+ class JapaneseTextVectorizer:
7
+ def __init__(self):
8
+ """。
9
+ MeCabのTaggerとTF-IDFベクトライザーを初期化
10
+ """
11
+ self.mecab_tagger = MeCab.Tagger()
12
+ self.tfidf_model = TfidfVectorizer(token_pattern='(?u)\\b\\w+\\b', norm=None)
13
+ self.vocab_list = []
14
+
15
+ def _extract_nouns(self, text):
16
+ """
17
+ テキストから名詞を抽出
18
+
19
+ Parameters:
20
+ - text (str): 名詞を抽出する対象のテキスト
21
+
22
+ Returns:
23
+ - nouns (list): 抽出された名詞リスト
24
+ """
25
+ node = self.mecab_tagger.parseToNode(text)
26
+ nouns = []
27
+ while node:
28
+ word = node.surface
29
+ hinshi = node.feature.split(",")[0]
30
+ if hinshi == "名詞":
31
+ if (not word.isnumeric()) and (not re.match(r'^[\u3040-\u309F]+$', word)):
32
+ # 名詞が数値と平仮名のみの場合は除き、それ以外の名詞を保存
33
+ nouns.append(word)
34
+ node = node.next
35
+ return nouns
36
+
37
+ def fit_transform(self, text):
38
+ """
39
+ テキストをTF-IDF表現に変換
40
+ Parameters:
41
+ - text (str): TF-IDF表現に変換する対象のテキスト
42
+ Returns:
43
+ - tfidf_dict (dict): 単語とそのTF-IDF値を格納した辞書
44
+ """
45
+ nouns = self._extract_nouns(text)
46
+ self.tfidf_model.fit(nouns)
47
+ vocab_text = " ".join(nouns)
48
+ tfidf_vec = self.tfidf_model.transform([vocab_text]).toarray()[0]
49
+ tfidf_dict = dict(zip(self.tfidf_model.get_feature_names_out(), tfidf_vec))
50
+ tfidf_dict = {word: num_val for word, num_val in tfidf_dict.items() if num_val > 0}
51
+ # TF-IDF値で辞書をソートし、上位5つの要素を取得
52
+ top_tfidf = dict(sorted(tfidf_dict.items(), key=lambda x: x[1], reverse=True)[:5])
53
+ return top_tfidf