Python爬虫 + 大模型:自动抓取行业资讯并生成摘要

0 阅读11分钟

手把手教你用Python爬虫抓取行业网站新闻,结合大模型API自动生成摘要,打造你的AI资讯助手。完整代码+详细注释,小白也能学会!

前言

做技术和商业的人都知道,信息差就是金钱。

每天花大量时间刷各种行业网站、公众号、新闻客户端,找有价值的内容——这个过程既耗时又累人。

如果有一个工具,能自动帮你:

  1. 定时抓取你关心的行业资讯
  2. 自动过滤重复内容
  3. 用大模型生成简洁摘要
  4. 推送到你的邮箱或微信

今天,这个工具我们可以自己动手做。

一、项目整体架构

┌─────────────────────────────────────────────────────────────────┐
│                        AI资讯助手架构图                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│   │   新闻网站    │    │   公众号     │    │   行业论坛   │      │
│   │  RSS/HTML/API │    │   第三方API  │    │    网页抓取  │      │
│   └──────┬───────┘    └──────┬───────┘    └──────┬───────┘      │
│          │                   │                   │              │
│          └───────────────────┼───────────────────┘              │
│                              ↓                                   │
│                   ┌─────────────────┐                            │
│                   │   数据采集层    │                            │
│                   │   Python爬虫    │                            │
│                   └────────┬────────┘                            │
│                            ↓                                      │
│                   ┌─────────────────┐                            │
│                   │   数据处理层    │                            │
│                   │ 去重 / 清洗 / 分类 │                          │
│                   └────────┬────────┘                            │
│                            ↓                                      │
│                   ┌─────────────────┐                            │
│                   │   AI摘要层      │                            │
│                   │  大模型API调用  │                            │
│                   └────────┬────────┘                            │
│                            ↓                                      │
│                   ┌─────────────────┐                            │
│                   │   通知推送层    │                            │
│                   │  邮件/微信/钉钉 │                            │
│                   └─────────────────┘                            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

二、环境准备

2.1 依赖安装

# 创建虚拟环境
python -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate  # Windows

# 安装依赖
pip install requests beautifulsoup4 lxml feedparser
pip install python-dotenv langchain-openai
pip install schedule aioschedule  # 定时任务
pip install apscheduler  # 更强大的定时任务

2.2 配置文件

# .env 文件
# 大模型API配置(可根据需要换成通义、文心等)
OPENAI_API_KEY=sk-your-api-key
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini

# 邮件配置(可选)
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password

# 订阅源配置
RSS_SOURCES=techcrunch,36kr,ithome

三、数据采集层:多源爬虫实现

3.1 RSS订阅源抓取(最简单)

"""
rss_crawler.py - RSS订阅源抓取
支持:36氪、爱范儿、IT之家、少数派等主流科技媒体
"""

import feedparser
import hashlib
from datetime import datetime
from typing import List, Dict
import requests
from bs4 import BeautifulSoup

class RSSNewsCollector:
    """RSS新闻采集器"""
    
    # 预设的RSS订阅源
    RSS_SOURCES = {
        "36氪": "https://36kr.com/feed",
        "爱范儿": "https://www.ifanr.com/feed",
        "IT之家": "https://www.ithome.com/rss/",
        "少数派": "https://sspai.com/feed",
        "科技媒体": "https://www.techcrunch.com/feed/",
        "量子位": "https://www.qbitai.com/feed",
        "机器之心": "https://arrings.machinethat.net/feed",
    }
    
    def __init__(self):
        self.seen_hashes = set()  # 用于去重
    
    def get_content_hash(self, title: str, link: str) -> str:
        """生成内容唯一标识"""
        content = f"{title}{link}"
        return hashlib.md5(content.encode()).hexdigest()
    
    def fetch_rss(self, source_name: str, url: str) -> List[Dict]:
        """抓取单个RSS源"""
        articles = []
        
        try:
            feed = feedparser.parse(url)
            print(f"[{source_name}] 抓取到 {len(feed.entries)} 条内容")
            
            for entry in feed.entries:
                # 生成唯一标识
                article_hash = self.get_content_hash(entry.title, entry.link)
                
                # 去重检查
                if article_hash in self.seen_hashes:
                    continue
                self.seen_hashes.add(article_hash)
                
                article = {
                    "title": entry.title,
                    "link": entry.link,
                    "source": source_name,
                    "published": entry.get("published", datetime.now().isoformat()),
                    "summary": entry.get("summary", ""),
                    "hash": article_hash
                }
                articles.append(article)
                
        except Exception as e:
            print(f"[{source_name}] 抓取失败: {e}")
        
        return articles
    
    def collect_all(self) -> List[Dict]:
        """采集所有订阅源"""
        all_articles = []
        
        for source_name, url in self.RSS_SOURCES.items():
            articles = self.fetch_rss(source_name, url)
            all_articles.extend(articles)
        
        print(f"\n总计采集到 {len(all_articles)} 条新闻(去重后)")
        return all_articles

# 使用示例
if __name__ == "__main__":
    collector = RSSNewsCollector()
    news = collector.collect_all()
    
    for item in news[:3]:
        print(f"- {item['source']}: {item['title']}")

3.2 网页爬虫:抓取无RSS的网站

"""
web_crawler.py - 通用网页爬虫
适用于没有RSS的网站,使用BeautifulSoup解析
"""

import requests
from bs4 import BeautifulSoup
from datetime import datetime
from typing import List, Dict, Optional
import time
import random
from urllib.parse import urljoin

class WebNewsCrawler:
    """网页新闻爬虫"""
    
    # 常用请求头,模拟浏览器
    HEADERS = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    }
    
    def __init__(self, timeout: int = 10):
        self.session = requests.Session()
        self.session.headers.update(self.HEADERS)
        self.timeout = timeout
    
    def fetch_page(self, url: str) -> Optional[str]:
        """获取网页内容"""
        try:
            # 随机延时,避免请求过快
            time.sleep(random.uniform(1, 3))
            
            response = self.session.get(url, timeout=self.timeout)
            response.raise_for_status()
            response.encoding = response.apparent_encoding
            return response.text
            
        except Exception as e:
            print(f"获取页面失败 {url}: {e}")
            return None
    
    def parse_article(self, html: str, base_url: str) -> List[Dict]:
        """解析新闻列表页"""
        soup = BeautifulSoup(html, "lxml")
        articles = []
        
        # 常见的文章列表选择器(根据网站调整)
        selectors = [
            "article",
            ".article-item",
            ".news-item",
            ".post-item",
            "div[class*='article']",
            "div[class*='news']",
        ]
        
        for selector in selectors:
            items = soup.select(selector)
            if items:
                for item in items[:20]:  # 限制每页抓取数量
                    title_elem = item.select_one("h2,h3,h4,a")
                    link_elem = item.select_one("a[href]")
                    date_elem = item.select_one("time,.date,.time")
                    
                    if title_elem and link_elem:
                        title = title_elem.get_text(strip=True)
                        href = link_elem.get("href", "")
                        
                        # 补全相对链接
                        full_url = urljoin(base_url, href)
                        
                        # 过滤非新闻链接
                        if any(x in href for x in [".pdf", ".jpg", ".png", "javascript"]):
                            continue
                        
                        article = {
                            "title": title,
                            "link": full_url,
                            "source": base_url,
                            "published": date_elem.get_text(strip=True) if date_elem else "",
                            "fetched_at": datetime.now().isoformat()
                        }
                        articles.append(article)
                break
        
        return articles
    
    def crawl_site(self, url: str, max_pages: int = 3) -> List[Dict]:
        """抓取整个网站"""
        all_articles = []
        
        for page in range(1, max_pages + 1):
            page_url = url if page == 1 else f"{url}?page={page}"
            html = self.fetch_page(page_url)
            
            if html:
                articles = self.parse_article(html, url)
                all_articles.extend(articles)
                print(f"第{page}页: 抓取到 {len(articles)} 条")
        
        return all_articles

# 使用示例
if __name__ == "__main__":
    crawler = WebNewsCrawler()
    
    # 抓取示例网站(请根据需要修改)
    articles = crawler.crawl_site("https://example.com/news")
    print(f"总计: {len(articles)} 条")

四、AI摘要生成:大模型API调用

4.1 摘要生成器实现

"""
summarizer.py - 使用大模型生成新闻摘要
支持:OpenAI GPT、通义千问、文心一言等主流模型
"""

import os
from typing import List, Dict
from dataclasses import dataclass
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

# Pydantic模型定义输出格式
class NewsSummary(BaseModel):
    """新闻摘要数据结构"""
    key_points: List[str] = Field(description="3-5个核心要点,每个不超过20字")
    summary: str = Field(description="50字以内的简要总结")
    category: str = Field(description="分类:技术/商业/政策/人物/其他")
    sentiment: str = Field(description="情感倾向:正面/中性/负面")
    tags: List[str] = Field(description="3-5个标签词")

class NewsSummarizer:
    """AI新闻摘要生成器"""
    
    # 摘要提示词模板
    SUMMARY_PROMPT = """
你是一个专业的科技新闻分析师。请根据以下新闻内容,生成结构化的摘要。

## 新闻标题
{title}

## 新闻链接
{link}

## 新闻内容
{content}

## 要求
1. 提取3-5个核心要点,每个要点不超过20个字
2. 生成50字以内的简要总结
3. 判断内容分类(技术/商业/政策/人物/其他)
4. 判断情感倾向(正面/中性/负面)
5. 提供3-5个标签词

请用中文回答。
"""
    
    def __init__(self, model: str = "gpt-4o-mini"):
        api_key = os.getenv("OPENAI_API_KEY")
        api_base = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1")
        
        self.llm = ChatOpenAI(
            model=model,
            api_key=api_key,
            base_url=api_base,
            temperature=0.3  # 摘要任务用较低的temperature
        )
        
        self.parser = PydanticOutputParser(pydantic_object=NewsSummary)
    
    def summarize_article(self, title: str, link: str, content: str = "") -> Dict:
        """生成单篇文章摘要"""
        
        # 构建提示词
        prompt = ChatPromptTemplate.from_template(self.SUMMARY_PROMPT)
        messages = prompt.format_messages(
            title=title,
            link=link,
            content=content or "(无详细内容)"
        )
        
        try:
            # 调用大模型
            response = self.llm.invoke(messages)
            
            # 解析输出
            result = self.parser.parse(response.content)
            
            return {
                "title": title,
                "link": link,
                "key_points": result.key_points,
                "summary": result.summary,
                "category": result.category,
                "sentiment": result.sentiment,
                "tags": result.tags
            }
            
        except Exception as e:
            print(f"摘要生成失败: {e}")
            return {
                "title": title,
                "link": link,
                "key_points": [],
                "summary": "摘要生成失败",
                "category": "其他",
                "sentiment": "中性",
                "tags": []
            }
    
    def batch_summarize(self, articles: List[Dict], max_concurrent: int = 3) -> List[Dict]:
        """批量生成摘要(带并发控制)"""
        results = []
        
        for i, article in enumerate(articles):
            print(f"[{i+1}/{len(articles)}] 处理: {article['title'][:30]}...")
            
            summary = self.summarize_article(
                title=article.get("title", ""),
                link=article.get("link", ""),
                content=article.get("summary", "")
            )
            results.append(summary)
        
        return results

# 使用示例
if __name__ == "__main__":
    summarizer = NewsSummarizer()
    
    test_article = {
        "title": "OpenAI发布GPT-5,性能提升10倍",
        "link": "https://example.com/gpt5",
        "summary": "OpenAI在今日发布了最新的GPT-5模型..."
    }
    
    result = summarizer.summarize_article(**test_article)
    print(result)

4.2 兼容通义千问/文心一言

"""
multi_model.py - 多模型兼容封装
可以轻松切换不同的AI服务提供商
"""

from enum import Enum
from typing import Dict, List
from langchain_openai import ChatOpenAI
from langchain_community.chat_models import Tongyi
import os

class AIModel(Enum):
    """支持的AI模型"""
    GPT4 = "gpt-4o"
    GPT4_MINI = "gpt-4o-mini"
    QWEN = "qwen-plus"
    WENXIN = "ernie-4.0-8k"

class AISummarizerFactory:
    """AI摘要生成器工厂"""
    
    @staticmethod
    def create(model_type: AIModel, **kwargs):
        """创建指定类型的AI摘要生成器"""
        
        if model_type == AIModel.GPT4 or model_type == AIModel.GPT4_MINI:
            return ChatOpenAI(
                model=model_type.value,
                api_key=os.getenv("OPENAI_API_KEY"),
                base_url=os.getenv("OPENAI_API_BASE"),
                temperature=kwargs.get("temperature", 0.3)
            )
        
        elif model_type == AIModel.QWEN:
            # 通义千问
            return ChatOpenAI(
                model="qwen-plus",
                api_key=os.getenv("DASHSCOPE_API_KEY"),  # 通义API Key
                base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
                temperature=kwargs.get("temperature", 0.3)
            )
        
        elif model_type == AIModel.WENXIN:
            # 文心一言(需要使用百度云SDK)
            from langchain_community.chat_models import ErnieBotChat
            return ErnieBotChat(
                model_name="ernie-4.0-8k",
                ernie_api_key=os.getenv("ERNIE_API_KEY"),
                secret_key=os.getenv("ERNIE_SECRET_KEY")
            )
        
        raise ValueError(f"不支持的模型类型: {model_type}")

五、定时任务与通知推送

5.1 定时任务调度器

"""
scheduler.py - 定时任务调度器
每天定时执行资讯采集和摘要生成
"""

import schedule
import time
import logging
from datetime import datetime
from rss_crawler import RSSNewsCollector
from summarizer import NewsSummarizer
from notifier import NewsNotifier

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='news_daily.log'
)

def daily_task():
    """每日任务:采集 + 摘要 + 推送"""
    
    print(f"\n{'='*50}")
    print(f"开始执行每日资讯任务 - {datetime.now()}")
    print('='*50)
    
    try:
        # 1. 采集新闻
        print("\n[步骤1] 采集新闻...")
        collector = RSSNewsCollector()
        articles = collector.collect_all()
        
        if not articles:
            print("没有采集到新内容")
            return
        
        # 2. 生成摘要
        print("\n[步骤2] 生成AI摘要...")
        summarizer = NewsSummarizer()
        summaries = summarizer.batch_summarize(articles)
        
        # 3. 推送通知
        print("\n[步骤3] 推送通知...")
        notifier = NewsNotifier()
        notifier.send_daily_digest(summaries)
        
        print(f"\n✅ 任务完成!处理了 {len(summaries)} 条资讯")
        
    except Exception as e:
        logging.error(f"任务执行失败: {e}", exc_info=True)
        print(f"❌ 任务执行失败: {e}")

# 调度配置
def setup_schedule():
    """配置定时任务"""
    
    # 每天早上8点执行
    schedule.every().day.at("08:00").do(daily_task)
    
    # 工作日早上8点30分执行(可选)
    # schedule.every().mondy.at("08:30").do(daily_task)
    # schedule.every().tuesday.at("08:30").do(daily_task)
    # ...
    
    print("📅 定时任务已配置:")
    print("   - 每天 08:00 执行一次")
    
    # 启动调度器
    print("\n🚀 启动调度器...")
    while True:
        schedule.run_pending()
        time.sleep(60)

if __name__ == "__main__":
    # 测试运行(不等待定时)
    # daily_task()
    
    # 启动定时调度
    setup_schedule()

5.2 通知推送模块

"""
notifier.py - 通知推送模块
支持:邮件、微信(企业微信/Server酱)、钉钉机器人
"""

import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import List, Dict
import requests

class NewsNotifier:
    """资讯通知推送器"""
    
    def __init__(self):
        self.smtp_server = os.getenv("SMTP_SERVER")
        self.smtp_port = int(os.getenv("SMTP_PORT", 587))
        self.smtp_user = os.getenv("SMTP_USER")
        self.smtp_password = os.getenv("SMTP_PASSWORD")
        self.recipients = os.getenv("RECIPIENT_EMAILS", "").split(",")
        
        # Server酱配置(微信推送)
        self.serverchan_key = os.getenv("SERVERCHAN_KEY")
        
        # 钉钉机器人
        self.dingtalk_token = os.getenv("DINGTALK_TOKEN")
        self.dingtalk_secret = os.getenv("DINGTALK_SECRET")
    
    def format_daily_digest(self, summaries: List[Dict]) -> str:
        """格式化每日摘要"""
        
        html = """
        <html>
        <head>
            <style>
                body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
                h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
                .article { margin: 20px 0; padding: 15px; border-left: 4px solid #007bff; background: #f8f9fa; }
                .title { font-size: 18px; font-weight: bold; margin-bottom: 10px; }
                .summary { color: #666; margin-bottom: 10px; }
                .tags { color: #007bff; font-size: 12px; }
                .key-points { margin: 10px 0; padding-left: 20px; }
                .key-points li { margin: 5px 0; }
            </style>
        </head>
        <body>
            <h1>📰 AI科技资讯日报</h1>
            <p>今日共为您整理 {count} 条重要资讯</p>
        """.format(count=len(summaries))
        
        for item in summaries[:10]:  # 最多显示10条
            key_points_html = "".join(f"<li>{p}</li>" for p in item.get("key_points", []))
            
            html += f"""
            <div class="article">
                <div class="title">
                    [{item.get('category', '其他')}] {item.get('title', '')}
                </div>
                <div class="summary">{item.get('summary', '')}</div>
                <ul class="key-points">
                    {key_points_html}
                </ul>
                <div class="tags">
                    {' '.join(f'#{tag}' for tag in item.get('tags', []))}
                </div>
                <p><a href="{item.get('link', '')}">阅读原文 →</a></p>
            </div>
            """
        
        html += """
        <hr>
        <p style="color: #999; font-size: 12px;">
            本报告由AI资讯助手自动生成 | 
            <a href="https://yunduancloud.icu">访问博客了解更多</a>
        </p>
        </body>
        </html>
        """
        
        return html
    
    def send_email(self, subject: str, html_content: str):
        """发送邮件"""
        if not self.smtp_user or not self.recipients:
            print("邮件配置不完整,跳过邮件发送")
            return
        
        msg = MIMEMultipart("alternative")
        msg["Subject"] = subject
        msg["From"] = self.smtp_user
        msg["To"] = ", ".join(self.recipients)
        
        msg.attach(MIMEText(html_content, "html"))
        
        try:
            with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
                server.starttls()
                server.login(self.smtp_user, self.smtp_password)
                server.sendmail(self.smtp_user, self.recipients, msg.as_string())
            print("✅ 邮件发送成功")
        except Exception as e:
            print(f"❌ 邮件发送失败: {e}")
    
    def send_serverchan(self, title: str, content: str):
        """通过Server酱发送微信通知"""
        if not self.serverchan_key:
            print("Server酱未配置,跳过微信通知")
            return
        
        url = f"https://sctapi.ftqq.com/{self.serverchan_key}.send"
        data = {
            "title": title,
            "desp": content
        }
        
        try:
            requests.post(url, data=data)
            print("✅ Server酱推送成功")
        except Exception as e:
            print(f"❌ Server酱推送失败: {e}")
    
    def send_daily_digest(self, summaries: List[Dict]):
        """发送每日摘要"""
        
        # 格式化内容
        html = self.format_daily_digest(summaries)
        plain_text = "\n".join([
            f"- [{item.get('category')}]{item.get('title')}: {item.get('summary')}"
            for item in summaries[:10]
        ])
        
        subject = f"📰 AI科技资讯日报 | {len(summaries)}条重要资讯"
        
        # 发送邮件
        self.send_email(subject, html)
        
        # 发送微信(Server酱)
        if summaries:
            first_summary = summaries[0]
            msg = f"📰 今日资讯速递\n\n{first_summary.get('title')}\n{first_summary.get('summary')}\n\n共{len(summaries)}条,点击查看全文 →"
            self.send_serverchan(subject, msg)

# 使用示例
if __name__ == "__main__":
    notifier = NewsNotifier()
    
    # 测试发送
    test_data = [{
        "title": "测试新闻标题",
        "link": "https://example.com",
        "summary": "这是一条测试新闻的摘要内容",
        "category": "技术",
        "tags": ["AI", "新闻"]
    }]
    
    # notifier.send_daily_digest(test_data)

六、完整运行示例

# 1. 配置环境变量
export OPENAI_API_KEY="sk-your-key"
export SMTP_SERVER="smtp.gmail.com"
export SMTP_PORT="587"
export SMTP_USER="your-email@gmail.com"
export SMTP_PASSWORD="your-app-password"

# 2. 运行程序
python main.py

# 3. 或者使用定时模式
python scheduler.py

# 4. 查看日志
tail -f news_daily.log

效果预览:

2026-04-10 08:00:00 - INFO - 开始执行每日资讯任务
2026-04-10 08:00:01 - INFO - [36] 抓取到 30 条内容
2026-04-10 08:00:02 - INFO - [IT之家] 抓取到 25 条内容
2026-04-10 08:00:03 - INFO - ...
2026-04-10 08:00:30 - INFO - 总计采集到 45 条新闻(去重后)
2026-04-10 08:00:31 - INFO - [1/45] 处理: OpenAI发布GPT-5...
2026-04-10 08:00:35 - INFO - [2/45] 处理: 腾讯云新出...
...
2026-04-10 08:15:00 - INFO -  邮件发送成功
2026-04-10 08:15:01 - INFO -  Server酱推送成功

七、进阶优化建议

优化方向实现方式效果
增量抓取记录上次抓取位置避免重复抓取相同内容
分布式爬虫使用Scrapy-Redis提升抓取速度
智能去重用embedding计算相似度更精准的去重
自动分类训练文本分类模型自动打标签
情感分析接入专门的情绪分析API更精准的情感判断
多语言翻译接入翻译API覆盖英文资讯

八、总结

本文完整实现了一个AI资讯助手系统:

  1. 数据采集:RSS订阅源 + 通用网页爬虫
  2. 去重处理:基于内容hash的智能去重
  3. AI摘要:调用大模型API生成结构化摘要
  4. 定时任务:每天自动执行
  5. 多端推送:邮件 + 微信通知

这套方案的成本极低:

  • API调用:GPT-4o-mini 处理50条新闻约 $0.1
  • 爬虫:免费
  • 推送:免费

如果你有更多需求,可以在此基础上添加:

  • 自定义订阅源
  • 关键词过滤
  • 自动生成思维导图
  • 对接更多通知渠道

完整代码已整理好,欢迎访问我的博客获取更多实战内容!


关于作者

长期关注大模型应用落地与云服务器实战,专注技术在企业场景中的落地实践。

个人博客:yunduancloud.icu —— 持续更新云计算、AI大模型实战教程,欢迎访问交流。