ニュースサイトのスクレイピングと分析

NewsAPIやスクレイピングでニュース記事を収集し、トレンド分析を実装します。

この記事で学べること

  • NewsAPIでニュース収集
  • BeautifulSoupでWebページ解析
  • テキストマイニングとキーワード抽出
  • トレンドの可視化

1. 準備:必要なライブラリ

# インストール
pip install newsapi-python
pip install beautifulsoup4
pip install requests
pip install pandas
pip install matplotlib
pip install wordcloud
pip install janome
pip install python-dotenv

requirements.txt

newsapi-python==0.2.7
beautifulsoup4==4.12.2
requests==2.31.0
pandas==2.1.0
matplotlib==3.8.0
wordcloud==1.9.2
janome==0.5.0
python-dotenv==1.0.0

2. NewsAPI キーの取得

アカウント作成手順

1. NewsAPI にアクセス
   https://newsapi.org/

2. 「Get API Key」をクリック
   - メールアドレス登録
   - パスワード設定

3. APIキー確認
   ダッシュボードでキーをコピー

4. 無料プラン制限
   - 1日: 100リクエスト
   - 過去1ヶ月のニュースのみ
   - 商用利用不可

.envファイルに保存

# .env
NEWS_API_KEY=your_api_key_here

3. NewsAPIでニュース取得

news_scraper.py

#!/usr/bin/env python3
"""ニューススクレイパー(NewsAPI)"""

import os
from newsapi import NewsApiClient
from dotenv import load_dotenv
import pandas as pd
from datetime import datetime, timedelta

load_dotenv()

class NewsScraper:
    def __init__(self):
        api_key = os.getenv('NEWS_API_KEY')
        self.newsapi = NewsApiClient(api_key=api_key)
    
    def get_top_headlines(self, country='jp', category=None, q=None):
        """トップヘッドラインを取得
        
        Parameters:
        -----------
        country : str
            国コード ('jp', 'us', 'gb' など)
        category : str
            カテゴリ ('business', 'entertainment', 'general',
                     'health', 'science', 'sports', 'technology')
        q : str
            検索キーワード
        """
        try:
            response = self.newsapi.get_top_headlines(
                country=country,
                category=category,
                q=q,
                page_size=100
            )
            
            if response['status'] != 'ok':
                print(f"❌ エラー: {response.get('message', '不明')}")
                return None
            
            articles = self._parse_articles(response['articles'])
            return pd.DataFrame(articles)
            
        except Exception as e:
            print(f"❌ エラー: {e}")
            return None
    
    def search_news(self, q, language='jp', sort_by='publishedAt', days=7):
        """ニュースを検索
        
        Parameters:
        -----------
        q : str
            検索キーワード
        language : str
            言語コード ('jp', 'en' など)
        sort_by : str
            ソート順 ('relevancy', 'popularity', 'publishedAt')
        days : int
            遡る日数
        """
        try:
            from_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
            
            response = self.newsapi.get_everything(
                q=q,
                language=language,
                sort_by=sort_by,
                from_param=from_date,
                page_size=100
            )
            
            if response['status'] != 'ok':
                print(f"❌ エラー: {response.get('message', '不明')}")
                return None
            
            articles = self._parse_articles(response['articles'])
            return pd.DataFrame(articles)
            
        except Exception as e:
            print(f"❌ エラー: {e}")
            return None
    
    def get_sources(self, country='jp', category=None, language='jp'):
        """ニュースソース一覧を取得"""
        try:
            response = self.newsapi.get_sources(
                country=country,
                category=category,
                language=language
            )
            
            sources = []
            for source in response['sources']:
                sources.append({
                    'id': source['id'],
                    'name': source['name'],
                    'description': source.get('description', ''),
                    'url': source['url'],
                    'category': source['category']
                })
            
            return pd.DataFrame(sources)
            
        except Exception as e:
            print(f"❌ エラー: {e}")
            return None
    
    def _parse_articles(self, articles):
        """記事データをパース"""
        parsed = []
        
        for article in articles:
            parsed.append({
                'title': article.get('title', ''),
                'description': article.get('description', ''),
                'content': article.get('content', ''),
                'author': article.get('author', '不明'),
                'source': article['source']['name'],
                'url': article.get('url', ''),
                'published_at': article.get('publishedAt', ''),
                'image_url': article.get('urlToImage', '')
            })
        
        return parsed

# 使用例
if __name__ == '__main__':
    scraper = NewsScraper()
    
    # 日本のトップヘッドライン
    print("=== 日本のトップニュース ===")
    df = scraper.get_top_headlines(country='jp')
    
    if df is not None:
        print(f"取得記事数: {len(df)}\n")
        print(df[['title', 'source']].head(10))
    
    # キーワード検索
    print("\n=== 'AI' 関連ニュース ===")
    df_ai = scraper.search_news('AI OR 人工知能', days=7)
    
    if df_ai is not None:
        print(f"取得記事数: {len(df_ai)}\n")
        print(df_ai[['title', 'published_at']].head(5))

実行結果

=== 日本のトップニュース ===
取得記事数: 87

                                     title         source
0  日本経済、3四半期連続でプラス成長                    日本経済新聞
1  新型コロナワクチン、追加接種開始                 NHK NEWS WEB
2  東京株式市場、日経平均が年初来高値更新              Bloomberg
3  政府、デジタル化推進で新予算案発表                  朝日新聞
4  スポーツ界、来年の国際大会開催地決定              スポーツ報知

=== 'AI' 関連ニュース ===
取得記事数: 42

                                     title            published_at
0  ChatGPT新機能、マルチモーダル対応へ        2025-10-26T10:30:00Z
1  Google、AI検索機能を大幅強化            2025-10-26T08:15:00Z
2  日本企業、生成AI導入が加速              2025-10-25T14:22:00Z

4. BeautifulSoupで個別サイトをスクレイピング

web_scraper.py

#!/usr/bin/env python3
"""Webスクレイパー(BeautifulSoup)"""

import requests
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime
import time

class WebNewsScraper:
    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
    
    def scrape_yahoo_news(self, max_articles=20):
        """Yahoo!ニューストピックスをスクレイピング
        
        注意: 実際の使用前に利用規約を確認すること
        """
        try:
            url = 'https://news.yahoo.co.jp/topics/top-picks'
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.content, 'html.parser')
            
            articles = []
            # 記事要素を探す(実際の構造に合わせて調整必要)
            article_elements = soup.find_all('article', limit=max_articles)
            
            for article in article_elements:
                # タイトル取得
                title_elem = article.find('a')
                if title_elem:
                    title = title_elem.get_text(strip=True)
                    link = title_elem.get('href', '')
                    
                    articles.append({
                        'title': title,
                        'url': link,
                        'source': 'Yahoo!ニュース',
                        'scraped_at': datetime.now()
                    })
            
            return pd.DataFrame(articles)
            
        except Exception as e:
            print(f"❌ スクレイピングエラー: {e}")
            return None
    
    def scrape_nhk_news(self, max_articles=20):
        """NHKニュースをスクレイピング
        
        注意: robots.txtとサービス利用規約を遵守
        """
        try:
            url = 'https://www3.nhk.or.jp/news/'
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.content, 'html.parser')
            
            articles = []
            # 記事リンクを探す
            links = soup.find_all('a', class_='content--summary', limit=max_articles)
            
            for link in links:
                title = link.get_text(strip=True)
                href = link.get('href', '')
                
                if href.startswith('/'):
                    href = 'https://www3.nhk.or.jp' + href
                
                articles.append({
                    'title': title,
                    'url': href,
                    'source': 'NHK NEWS WEB',
                    'scraped_at': datetime.now()
                })
                
                # 負荷軽減のため待機
                time.sleep(0.5)
            
            return pd.DataFrame(articles)
            
        except Exception as e:
            print(f"❌ スクレイピングエラー: {e}")
            return None
    
    def scrape_article_content(self, url):
        """記事本文を取得"""
        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.content, 'html.parser')
            
            # 本文を抽出(サイトによって構造が異なる)
            # article, mainタグやclass="content"などを探す
            content = ''
            
            # 一般的なパターン
            for tag in soup.find_all(['article', 'main']):
                paragraphs = tag.find_all('p')
                content = '\n'.join([p.get_text(strip=True) for p in paragraphs])
                if content:
                    break
            
            return content
            
        except Exception as e:
            print(f"❌ 本文取得エラー: {e}")
            return None

# 使用例
if __name__ == '__main__':
    scraper = WebNewsScraper()
    
    # Yahoo!ニューススクレイピング
    df = scraper.scrape_yahoo_news(max_articles=10)
    
    if df is not None:
        print("=== Yahoo!ニュース ===")
        print(df[['title', 'source']].head())

5. テキストマイニング・キーワード抽出

text_analyzer.py

#!/usr/bin/env python3
"""ニューステキスト分析"""

import pandas as pd
from janome.tokenizer import Tokenizer
from collections import Counter
import re

class NewsTextAnalyzer:
    def __init__(self):
        self.tokenizer = Tokenizer()
        # ストップワード(除外する一般的な語)
        self.stop_words = set([
            'こと', 'もの', 'ため', 'よう', 'それ', 'ここ',
            'さん', 'これ', 'そこ', 'あれ', 'どこ', 'なる',
            'ある', 'する', 'いる', 'れる', 'られる', 'ない'
        ])
    
    def extract_keywords(self, text, top_n=20):
        """キーワードを抽出"""
        # 形態素解析
        tokens = self.tokenizer.tokenize(text)
        
        # 名詞のみ抽出
        nouns = []
        for token in tokens:
            parts = token.part_of_speech.split(',')
            if parts[0] == '名詞' and parts[1] == '一般':
                surface = token.surface
                # 1文字の単語とストップワードを除外
                if len(surface) > 1 and surface not in self.stop_words:
                    nouns.append(surface)
        
        # 頻度カウント
        counter = Counter(nouns)
        return counter.most_common(top_n)
    
    def analyze_news_trends(self, df, text_column='title'):
        """ニューストレンドを分析"""
        # 全テキストを結合
        all_text = ' '.join(df[text_column].dropna().astype(str))
        
        # キーワード抽出
        keywords = self.extract_keywords(all_text, top_n=30)
        
        # DataFrameに変換
        keywords_df = pd.DataFrame(keywords, columns=['キーワード', '出現回数'])
        
        return keywords_df
    
    def extract_entities(self, text):
        """固有表現抽出(簡易版)"""
        entities = []
        tokens = self.tokenizer.tokenize(text)
        
        for token in tokens:
            parts = token.part_of_speech.split(',')
            # 固有名詞
            if parts[0] == '名詞' and parts[1] == '固有名詞':
                entities.append(token.surface)
        
        return Counter(entities)
    
    def get_sentiment_words(self, text):
        """感情語を抽出(簡易版)"""
        # ポジティブワード
        positive_words = ['成功', '向上', '増加', '達成', '改善', '発展', '期待']
        # ネガティブワード
        negative_words = ['失敗', '減少', '低下', '悪化', '問題', '懸念', '危機']
        
        pos_count = sum(1 for word in positive_words if word in text)
        neg_count = sum(1 for word in negative_words if word in text)
        
        return {
            'positive': pos_count,
            'negative': neg_count,
            'sentiment': 'ポジティブ' if pos_count > neg_count else 
                        'ネガティブ' if neg_count > pos_count else '中立'
        }

# 使用例
from news_scraper import NewsScraper

scraper = NewsScraper()
df = scraper.get_top_headlines(country='jp')

if df is not None:
    analyzer = NewsTextAnalyzer()
    
    # トレンドキーワード
    trends = analyzer.analyze_news_trends(df)
    print("=== トレンドキーワード ===")
    print(trends.head(15))
    
    # 感情分析
    sentiments = []
    for title in df['title']:
        sent = analyzer.get_sentiment_words(str(title))
        sentiments.append(sent['sentiment'])
    
    df['sentiment'] = sentiments
    print("\n=== 感情分布 ===")
    print(df['sentiment'].value_counts())

6. データの可視化

news_visualizer.py

#!/usr/bin/env python3
"""ニュースデータ可視化"""

import matplotlib.pyplot as plt
import seaborn as sns
from wordcloud import WordCloud
import pandas as pd

sns.set_style('whitegrid')
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Arial']
plt.rcParams['axes.unicode_minus'] = False

class NewsVisualizer:
    def __init__(self, df):
        self.df = df.copy()
    
    def plot_keyword_ranking(self, keywords_df, top_n=20):
        """キーワードランキンググラフ"""
        top_keywords = keywords_df.head(top_n)
        
        plt.figure(figsize=(12, 8))
        bars = plt.barh(
            range(len(top_keywords)),
            top_keywords['出現回数'],
            color='#2196F3'
        )
        
        plt.yticks(
            range(len(top_keywords)),
            top_keywords['キーワード']
        )
        plt.xlabel('出現回数', fontsize=12)
        plt.title(f'ニュース トップ{top_n}キーワード', fontsize=14, fontweight='bold')
        plt.gca().invert_yaxis()
        
        # 値を表示
        for i, (bar, count) in enumerate(zip(bars, top_keywords['出現回数'])):
            plt.text(
                count,
                i,
                f' {count}',
                va='center',
                fontsize=10
            )
        
        plt.tight_layout()
        plt.savefig('news_keywords.png', dpi=300, bbox_inches='tight')
        plt.show()
    
    def create_wordcloud(self, text_column='title'):
        """ワードクラウド作成"""
        from janome.tokenizer import Tokenizer
        
        # テキスト結合
        all_text = ' '.join(self.df[text_column].dropna().astype(str))
        
        # 形態素解析
        tokenizer = Tokenizer()
        tokens = tokenizer.tokenize(all_text, wakati=True)
        wakati_text = ' '.join(tokens)
        
        # ワードクラウド生成
        wordcloud = WordCloud(
            width=1200,
            height=600,
            background_color='white',
            font_path='C:/Windows/Fonts/msgothic.ttc',  # 日本語フォント
            colormap='viridis',
            max_words=100,
            relative_scaling=0.5
        ).generate(wakati_text)
        
        plt.figure(figsize=(15, 8))
        plt.imshow(wordcloud, interpolation='bilinear')
        plt.axis('off')
        plt.title('ニュース ワードクラウド', fontsize=16, fontweight='bold', pad=20)
        plt.tight_layout()
        plt.savefig('news_wordcloud.png', dpi=300, bbox_inches='tight')
        plt.show()
    
    def plot_sources_distribution(self):
        """ニュースソース別分布"""
        source_counts = self.df['source'].value_counts().head(15)
        
        plt.figure(figsize=(12, 6))
        bars = plt.bar(
            range(len(source_counts)),
            source_counts.values,
            color='#FF9800'
        )
        
        plt.xticks(
            range(len(source_counts)),
            source_counts.index,
            rotation=45,
            ha='right'
        )
        plt.ylabel('記事数', fontsize=12)
        plt.title('ニュースソース別記事数', fontsize=14, fontweight='bold')
        
        # 値を表示
        for bar in bars:
            height = bar.get_height()
            plt.text(
                bar.get_x() + bar.get_width()/2.,
                height,
                f'{int(height)}',
                ha='center',
                va='bottom',
                fontsize=10
            )
        
        plt.tight_layout()
        plt.savefig('news_sources.png', dpi=300, bbox_inches='tight')
        plt.show()
    
    def plot_timeline(self):
        """時系列グラフ"""
        if 'published_at' not in self.df.columns:
            print("published_at カラムがありません")
            return
        
        # 日時に変換
        self.df['published_date'] = pd.to_datetime(
            self.df['published_at'],
            errors='coerce'
        )
        
        # 日別集計
        daily_counts = self.df.groupby(
            self.df['published_date'].dt.date
        ).size()
        
        plt.figure(figsize=(14, 6))
        plt.plot(
            daily_counts.index,
            daily_counts.values,
            marker='o',
            linewidth=2,
            markersize=8,
            color='#4CAF50'
        )
        
        plt.xlabel('日付', fontsize=12)
        plt.ylabel('記事数', fontsize=12)
        plt.title('日別ニュース記事数', fontsize=14, fontweight='bold')
        plt.grid(True, alpha=0.3)
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.savefig('news_timeline.png', dpi=300, bbox_inches='tight')
        plt.show()

7. 完全なニュース分析システム

#!/usr/bin/env python3
"""完全なニュース分析システム"""

from news_scraper import NewsScraper
from text_analyzer import NewsTextAnalyzer
from news_visualizer import NewsVisualizer
import pandas as pd

def full_news_analysis(keyword=None, country='jp', days=7):
    """完全なニュース分析"""
    print(f"\n{'='*60}")
    print(f"  ニュース分析レポート")
    if keyword:
        print(f"  キーワード: {keyword}")
    print(f"{'='*60}\n")
    
    scraper = NewsScraper()
    
    # 1. データ収集
    print("[1/5] ニュースデータ収集中...")
    if keyword:
        df = scraper.search_news(keyword, days=days)
    else:
        df = scraper.get_top_headlines(country=country)
    
    if df is None or len(df) == 0:
        print("❌ データ取得失敗")
        return None
    
    print(f"✅ {len(df)}件のニュース記事を取得\n")
    
    # 2. 基本統計
    print("[2/5] 基本統計...")
    print(f"期間: {df['published_at'].min()} ~ {df['published_at'].max()}")
    print(f"ユニークソース数: {df['source'].nunique()}")
    print(f"\nトップソース:")
    print(df['source'].value_counts().head(5))
    print()
    
    # 3. テキスト分析
    print("[3/5] テキスト分析中...")
    analyzer = NewsTextAnalyzer()
    keywords_df = analyzer.analyze_news_trends(df)
    print("\n=== トップキーワード ===")
    print(keywords_df.head(10))
    print()
    
    # 4. 可視化
    print("[4/5] グラフ作成中...")
    visualizer = NewsVisualizer(df)
    visualizer.plot_keyword_ranking(keywords_df, top_n=20)
    visualizer.create_wordcloud()
    visualizer.plot_sources_distribution()
    visualizer.plot_timeline()
    print("✅ グラフを保存しました\n")
    
    # 5. CSV保存
    print("[5/5] データ保存中...")
    from datetime import datetime
    filename = f'news_analysis_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
    df.to_csv(filename, index=False, encoding='utf-8-sig')
    print(f"✅ データ保存: {filename}\n")
    
    print(f"{'='*60}")
    print("  分析完了")
    print(f"{'='*60}\n")
    
    return df, keywords_df

if __name__ == '__main__':
    # 日本のニュース分析
    df, keywords = full_news_analysis(country='jp')
    
    # または特定キーワードで分析
    # df, keywords = full_news_analysis(keyword='AI OR 人工知能', days=7)

8. 定期実行とアラート

import schedule
import time
from datetime import datetime

def monitor_news_keywords(keywords, interval_minutes=60):
    """特定キーワードのニュースを監視"""
    scraper = NewsScraper()
    
    def check_keywords():
        print(f"\n[{datetime.now().strftime('%H:%M:%S')}] ニュース監視実行")
        
        for keyword in keywords:
            df = scraper.search_news(keyword, days=1)
            
            if df is not None and len(df) > 0:
                new_articles = len(df)
                print(f"📰 '{keyword}': {new_articles}件の新着記事")
                
                # アラート条件
                if new_articles >= 10:
                    print(f"  🚨 注目: 多数の記事が検出されました!")
                    # メール通知などを実装可能
    
    # 初回実行
    check_keywords()
    
    # 定期実行
    schedule.every(interval_minutes).minutes.do(check_keywords)
    
    while True:
        schedule.run_pending()
        time.sleep(60)

# 使用例
monitor_keywords = ['AI', '株価', '気候変動']
# monitor_news_keywords(monitor_keywords, interval_minutes=30)

まとめ

NewsAPIとスクレイピングでニュースを収集・分析できます!

重要ポイント

  • ✅ NewsAPIは無料で簡単
  • ✅ BeautifulSoupでカスタムスクレイピング
  • ✅ Janomeで日本語テキスト分析
  • ✅ トレンドキーワードを可視化
  • ✅ 定期監視でリアルタイム追跡

応用例

  • メディアモニタリング
  • 競合分析・市場調査
  • 危機管理(炎上検知)
  • 投資判断支援
  • コンテンツ企画

注意事項

  • ⚠️ 必ず利用規約とrobots.txtを確認
  • ⚠️ スクレイピング頻度に注意
  • ⚠️ APIレート制限を遵守
  • ⚠️ 著作権に配慮
  • ⚠️ 個人情報の取り扱いに注意