Python + Selenium 实战:从零构建知乎内容提取系统(含GUI与数据库)

一、前言

        知乎作为国内领先的知识分享社区,汇聚了数亿用户的高质量内容,涵盖科技、文化、生活、教育等各个领域。这些内容不仅具有很高的实时性和参考价值,也构成了一个巨大的结构化信息宝库。然而,知乎官方并未提供批量内容导出功能,对于需要系统研究、数据分析或内容归档的用户来说,手动整理效率极低。

        本次实训项目采用 Python 技术栈开发了一套知乎话题内容智能提取系统。通过该项目,我不仅掌握了爬虫核心技术,还实现了从数据采集、解析到存储、展示的全流程开发,真正体验了一个完整项目的开发过程。

1.1 项目亮点

  • 全流程自动化:登录(一次登录后续无需再登录)→搜索话题→提取→存储→展示

  • 反爬策略应对:模拟真实用户行为,绕过常见检测

  • 结构化存储:Oracle数据库保存,便于后续分析

  • 友好界面:GUI操作,无需编程基础即可使用

  • 模块化设计:代码清晰,易于维护和扩展

1.2 技术栈

        Python、Selenium、XPath(网页内容定位)、Oracle数据库、Tkinter(GUI界面)、多线程技术(提升用户体验)。

二、项目整体架构设计

2.1 系统功能模块

        本项目采用模块化设计,将复杂系统分解为三个核心模块。

2.2 技术选型

1.  Selenium

        知乎作为大型平台,采用了多种反爬措施,Selenium 能够模拟真实浏览器的完整行为,包括 JavaScript 执行、Cookie 管理、页面交互等,是应对这类动态网站的利器。

        推荐的Selenium教程:小白爬虫——selenium入门超详细教程-优快云博客

2. Oracle 数据库

        该数据库具备企业级稳定性,专为处理结构化数据设计,提供完善的事务支持以确保数据一致性。其CLOD类型特性特别适合存储长文本内容,具有显著的学习价值,可帮助掌握企业常用数据库技术。

        数据库的连接和建立“问题”和“回答”表:

def setup_database(self):     # 连接 Oracle+初始化
    database_servers = [
        "localhost:1521/ORCL",
    ]
    for ds in database_servers:
        try:
            self.db_connection = cx_Oracle.connect(
                user="你的数据库名称",
                password="你的数据库密码",
                dsn=ds,
                mode=cx_Oracle.SYSDBA
            )
            print(f"数据库连接成功: {ds}")
            self.create_tables()
            return
        except Exception as e:
            print(f"连接失败 {ds}: {e}")

    try:
        cursor = self.db_connection.cursor()

        # 删除已存在的表
        tables = ['ZHIHU_ANSWERS', 'ZHIHU_QUESTIONS']
        for table in tables:
            try:
                cursor.execute(f"DROP TABLE {table} CASCADE CONSTRAINTS")  # 不用加分号
            except:
                pass

        # 创建序列
        try:
            cursor.execute("DROP SEQUENCE answer_id_seq")  # 不用加分号
        except:
            pass

        cursor.execute("CREATE SEQUENCE answer_id_seq START WITH 1 INCREMENT BY 1")  # 不用加分号

        # 创建问题表
        cursor.execute("""
            CREATE TABLE ZHIHU_QUESTIONS (
                question_id VARCHAR2(100) PRIMARY KEY,
                title VARCHAR2(500),
                url VARCHAR2(500),
                crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)  # 多行字符串不需要分号

        # 创建回答表
        cursor.execute("""
            CREATE TABLE ZHIHU_ANSWERS (
                answer_id NUMBER PRIMARY KEY,
                question_id VARCHAR2(100),
                author VARCHAR2(100),
                publish_time VARCHAR2(100),
                content CLOB,
                crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (question_id) REFERENCES ZHIHU_QUESTIONS(question_id)
            )
        """)

        self.db_connection.commit()
        cursor.close()
        print("数据表创建成功")
    except Exception as e:
        print(f"数据表创建失败: {e}")
        import traceback
        traceback.print_exc()  # 打印错误信息
3. Tkinter 

        利用Tkinter制作GUI界面的好处在于 该内置库无需额外安装,具有出色的跨平台兼容性,特别适合开发中小型桌面应用程序。

三、核心模块实现详解

3.1 登录模块

        知乎的登录验证相对严格,直接使用 requests 发送登录请求容易被识别为机器人,我们这里选择的是模拟真实用户的完整登录流程

1. 模拟人类操作模拟
# 人类随机延迟
def human_like_delay(self, min_time=1, max_time=3):
    time.sleep(random.uniform(min_time, max_time))
2. 多维度登录状态检测
# 检查登录状态
def check_login_status(self):
    try:
        self.browser.get('https://www.zhihu.com/')
        self.human_like_delay(2, 4)

        # 多种方式检查登录状态
        selectors = [
            '//div[contains(@class, "AppHeader-profile")]',
            '//a[contains(@href, "/people/")]',
            '//div[contains(text(), "写回答")]',
            '//span[contains(text(), "我的")]',
            '//button[contains(text(), "提问")]'
        ]

        for selector in selectors:
            try:
                element = self.browser.find_element(By.XPATH, selector)
                print("已登录")
                return True
            except:
                continue

        print("未登录")
        return False

    except Exception as e:
        print(f"检查登录状态出错: {e}")
        return False
3. 用户交互设计

考虑到验证码和动态验证的可能,系统设计了友好的手动登录流程:

  • 弹出新浏览器窗口供用户登录

  • 登录完成后点击确认按钮

  • 自动保存登录状态到配置文件

3.2 内容提取模块

1. XPath定位回答内容

        XPath 的绝对路径页面结构稍有变动就会失效,本实训采用相对路径的方法,另外采用无限滚动加载,模拟用户滚动行为。

# 获取最新的 10 个回答
def get_answers(self):
    answers = []

    try:
        # 1.尝试点击时间排序
        try:
            sort_btn = WebDriverWait(self.browser, 3).until(
                EC.element_to_be_clickable((By.XPATH, '//button[contains(text(), "时间排序")]'))
            )
            sort_btn.click()
            time.sleep(2)
        except:
            print("")

        # 2.多次滚动触发加载
        for _ in range(8):
            self.browser.execute_script("window.scrollBy(0, 1000)")
            time.sleep(0.8)

        # 3. 使用 XPath
        answer_cards = self.browser.find_elements(
            By.XPATH,
            '//div[contains(@class, "AnswerItem")]'
        )

        print(f"找到 {len(answer_cards)} 个回答")

        if not answer_cards:
            return []

        # 取前5个
        answer_cards = answer_cards[:5]

        # 4. 处理每个回答
        for i, card in enumerate(answer_cards, start=1):
            print(f"\n正在处理第 {i} 个回答...")

            # 展开阅读全文
            try:
                expand_btn = card.find_element(By.XPATH, './/button[contains(text(), "展开")]')
                self.browser.execute_script("arguments[0].click();", expand_btn)
                time.sleep(1)
            except:
                pass

            author_name = self.extract_author(card, i)
            publish_time = self.extract_publish_time(card, i)
            content = self.extract_content(card, i)
            comments = self.get_comments(card)

            content = self.extract_content(card, i)
            # 如果没有正文,跳过这个卡片(通常是图片卡片、广告、空白卡片)
            if not content or len(content.strip()) < 5:
                print(f"跳过无内容回答")
                continue

            answers.append({
                "author": author_name,
                "publish_time": publish_time,
                "content": content,
                "comments": comments
            })

            print(f"第 {i} 个回答处理完成: {author_name}")

    except Exception as e:
        print("获取回答列表失败:", e)

    print(f"总共成功提取 {len(answers)} 个回答")
    return answers
 2. 获取时间

        利用XPath获取时间,并且把时间格式标准化(统一为 YYYY-MM-DD):

# 使用 XPath 提取发布时间(只保留年月日)
def extract_publish_time(self, card, index):
    publish_time = "未知时间"

    try:
        xpaths = (
            './/meta[@itemprop="dateCreated"] | '
            './/meta[@itemprop="dateModified"] | '
            './/span[contains(text(), "发布于")] | '
            './/span[contains(text(), "编辑于")] | '
            './/a[contains(@class, "ContentItem-time")]'
        )

        elems = card.find_elements(By.XPATH, xpaths)

        for el in elems:
            time_text = (el.get_attribute("content") or el.text or "").strip()
            if not time_text:
                continue

            # 处理 ISO 格式时间
            if "T" in time_text and "Z" in time_text:
                # 2022-05-26T01:53:59.000Z → 2022-05-26
                publish_time = time_text.split("T")[0]
                print(f"第{index}个回答时间: {publish_time}")
                return publish_time

            # 普通文本格式
            if "发布于" in time_text or "编辑于" in time_text:
                # 发布于 2022-05-26
                # 编辑于 2022-05-26
                # 只保留后面的 yyyy-mm-dd
                for fmt in ["发布于", "编辑于"]:
                    if fmt in time_text:
                        publish_time = time_text.replace(fmt, "").strip()
                        print(f"第{index}个回答时间: {publish_time}")
                        return publish_time

    except Exception as e:
        print(f"第{index}个回答提取时间失败: {e}")

    return publish_time
3. 内容展开逻辑

        部分回答有"展开阅读全文"按钮,需要先点击展开:

expand_selectors = [
    './/button[contains(text(), "展开阅读全文")]',
    './/button[contains(text(), "显示全部")]',
    './/button[contains(text(), "展开")]'
]

3.3 数据库设计

1. 问题表(ZHIHU_QUESTIONS)
  • question_id: 问题唯一标识(主键)

  • title: 问题标题

  • url: 问题链接

  • crawl_time: 抓取时间

# 创建问题表
cursor.execute("""
    CREATE TABLE ZHIHU_QUESTIONS (
        question_id VARCHAR2(100) PRIMARY KEY,
        title VARCHAR2(500),
        url VARCHAR2(500),
        crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")  
2. 回答表(ZHIHU_ANSWERS)
  • answer_id: 回答ID(序列生成)

  • question_id: 关联问题ID(外键)

  • author: 回答作者

  • publish_time: 发布时间

  • content: 回答内容(CLOB类型)

  • crawl_time: 抓取时间

# 创建回答表
cursor.execute("""
    CREATE TABLE ZHIHU_ANSWERS (
        answer_id NUMBER PRIMARY KEY,
        question_id VARCHAR2(100),
        author VARCHAR2(100),
        publish_time VARCHAR2(100),
        content CLOB,
        crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (question_id) REFERENCES ZHIHU_QUESTIONS(question_id)
    )
""")

3.4 GUI界面

        GUI界面布局设计:

四、项目运行效果展示

4.1 主界面与搜索功能

        系统启动后,首先检查登录状态。如果未登录,会提示用户进行手动登录。登录成功后,状态栏显示 "就绪 - 已登录"。

        在搜索框中输入关键词(如"人工智能"),点击搜索按钮或按回车键,系统开始搜索相关话题,搜索过程中,状态栏实时更新进度。

4.2 结果展示与详情查看

        搜索结果以列表形式展示,每条结果显示序号和问题标题。点击任意条目,系统加载该话题的详细内容。详情页面分为几个部分:

        · 问题标题:醒目显示,可点击跳转到对应的知乎原网页

        · 问题链接:蓝色可点击链接

        · 回答列表:每个回答包含作者、发布时间、详细内容

4.3 数据存储效果

        所有查看过详情的话题内容会自动保存到 Oracle 数据库中,通过数据库客户端可以查看存储的数据:

五、开发中遇到的问题与解决方案

5.1 登录验证问题

解决方案:

  1. 改为手动登录引导,降低风控触发率;

  2. 使用已有的用户数据目录(--user-data-dir);

  3. 添加人性化延迟和随机行为;

  4. 保存登录状态,避免重复登录。

5.2 页面结构变化

解决方案:

  1. 使用相对路径和属性contains定位,而非绝对路径;

  2. 采用多套XPath选择器,逐一尝试;

5.3 动态加载内容

解决方案:

  1. 模拟用户滚动行为触发加载

  2. 设置合理的滚动次数和等待时间

  3. 实施分批次处理,先获取前几个回答

5.4 数据库连接问题

解决方案:

  1. 编写连接测试和重试机制

  2. 详细的错误日志记录

  3. 使用SYSDBA模式连接简化权限问题

六、项目总结与未来展望

6.1 技术收获

        通过本次实训,我系统掌握了多项核心技能:在Selenium高级应用中,学会了浏览器伪装、交互模拟与反爬策略;在XPath精确定位方面,掌握了路径定位、动态内容处理与数据清洗;在全栈开发上,实践了爬虫逻辑、GUI界面、数据库操作与多线程编程;并完整经历了项目开发流程,从需求分析到设计实现、测试调试与文档撰写的全过程,全面提升了工程实践能力。

6.2 可优化方向

从技术演进和项目完善的角度来看,本项目仍有多个可优化方向:

        1. 性能优化:可引入异步IO以提升并发能力,实现增量爬取避免重复数据,并添加缓存机制以减少网络请求;

        2. 功能扩展:能够支持豆瓣、微博等多平台采集,集成词云生成、情感分析等数据分析模块,并实现定时任务与自动化调度;

        3. 用户体验:可设计更美观的现代化界面、增加进度条与预估时间显示,并开发数据导出至Excel、PDF等格式的功能。

        这个项目既让我掌握了爬虫技术,也培养了解决复杂问题的系统思维,从需求分析到功能实现,从技术选型到问题调试,每一个环节都是宝贵的学习经历。

项目源码已上传至 GitHub:18June96/zhihu-crawler: 知乎内容爬取系统代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值