数据采集综合实践

课程 2025数据采集与融合技术 (https://edu.cnblogs.com/campus/fzu/2025DataCollectionandFusiontechnology)

项目整体

组名、项目简介

组名:基米大哈气

项目背景:针对B站视频评论信息量大、内容杂乱的问题,提供智能化的筛选与分类方案,帮助用户快速了解视频评论风向。

项目目标:开发一个支持评论爬取、智能分类、违禁词管理及可视化分析的综合系统,实现对评论内容的精准筛选与多维度展示。

技术路线:前端采用 React + React Router 实现组件化开发;后端使用 Flask + MySQL 管理数据与接口;核心算法基于本地部署的 Qwen2.5 大模型,并应用 LoRA 微调与 4 位量化技术优化性能;系统最终部署于华为云平台。

团队成员学号

102302113(王光诚)、102302115(方朴)、102302119(庄靖轩)、102302120(刘熠黄)、102302121(许友钿)、102302122(许志安)、102302123(许洋)、102302147(傅乐宜)

项目目标

  1. 智能分类:结合视频类型(如游戏、二次元),将评论自动归类为正常、争论、广告、@某人、无意义五大类。
  2. 数据可视化:提供评论统计、分类分布、高频词云及评论变化曲线图,直观展示数据特征。
  3. 违禁词管理:支持实时增删查改违禁词库,保障过滤机制的高效性。
  4. 自动化爬取:用户只需输入B站链接,系统即可自动抓取评论并进行智能处理,爬取过程中支持播放背景音乐。

其他参考文献

[1]Joshua Ainslie, James Lee-Thorp, Michiel de Jong, Yury Zemlyanskiy, Federico Lebr´on, and SumitSanghai. GQA: Training generalized multi-query Transformer models from multi-head checkpoints. InEMNLP, pp. 4895–4901. Association for Computational Linguistics, 2023.

码云链接(由于git上上传不了大于1GB的文件,所以我们将所有源码都放到了github上,小组成员间底代码不分开)

项目代码(GitHub):https://github.com/liuliuliuliu617-maker/-/tree/master <br> 项目演示网址: http://1.94.247.8/(31号前可以查看,之后代金券应该过期了)

分工部分

在本项目中,我主要负责 B 站数据采集模块的设计与实现,目标是稳定、高质量地获取可用于模型训练的视频评论数据集。核心任务包括数据来源确定、采集流程设计、反爬策略控制以及数据结构规范化。

码云链接:https://gitee.com/miu-a/2025_crawl_project.git

获取Cookies部分

通过 Selenium 手动登录 B 站并导出浏览器 Cookies(cookies_raw.json),爬虫程序直接复用该 Cookies 进行接口访问。

Cookies
 from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import json

def save_cookies():
    # 启动 Chrome
    options = webdriver.ChromeOptions()
    options.add_argument("--start-maximized")

    # 避免被识别为自动化
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)

    driver = webdriver.Chrome(
        service=Service(ChromeDriverManager().install()),
        options=options
    )

    print("正在打开 Bilibili 登录页面,请手动扫码登录……")
    driver.get("https://passport.bilibili.com/login")

    # 等待完成登录
    input("登录成功后,按 Enter 键继续:")

    print("正在保存 Cookies……")

    selenium_cookies = driver.get_cookies()

    with open("cookies_raw.json", "w", encoding="utf-8") as f:
        json.dump(selenium_cookies, f, indent=4, ensure_ascii=False)

    print("Cookies 已保存:cookies_raw.json")

    driver.quit()


if __name__ == "__main__":
    save_cookies()

爬取评论代码部分

Scraper
 import requests
import json
import time
import os
import re
import random
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def make_session():
    session = requests.Session()

    retries = Retry(
        total=6,
        backoff_factor=1,
        status_forcelist=[500, 502, 503, 504, 429],
        allowed_methods=["GET"]
    )

    adapter = HTTPAdapter(max_retries=retries, pool_connections=10, pool_maxsize=10)
    session.mount("https://", adapter)
    session.mount("http://", adapter)

    return session

session = make_session()

# 配置参数
DOMAIN_SLEEP = 8
REQUEST_TIMEOUT = 30
RETRY_DELAY = 5

# 1. 加载 cookies
def load_cookies():
    with open("cookies_raw.json", "r", encoding="utf-8") as f:
        cookie_list = json.load(f)
    return "; ".join(f"{c['name']}={c['value']}" for c in cookie_list)


def make_headers(cookie_str):
    return {
        "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",
        "Referer": "https://www.bilibili.com/",
        "Cookie": cookie_str,
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
    }


# 提取 BV
def extract_bv(url):
    m = re.search(r"BV\w+", url)
    return m.group(0) if m else None

# BV → aid
def get_aid_from_bv(bv, headers):
    api = f"https://api.bilibili.com/x/web-interface/view?bvid={bv}"
    try:
        resp = session.get(api, headers=headers, timeout=REQUEST_TIMEOUT, verify=False)
        resp.raise_for_status()
        data = resp.json()
        if data.get("code") == 0:
            return data["data"]["aid"]
        else:
            print(f"❌ 获取AID失败: {data.get('message')}")
            return None
    except requests.exceptions.RequestException as e:
        print(f"❌ 获取AID请求失败: {e}")
        return None


# 提取评论
def extract_clean_comment(reply):
    content = reply.get("content", {})

    # 评论文本
    text = content.get("message", "")

    # 评论图片
    pics = []
    for p in content.get("pictures", []) or []:
        if "img_src" in p:
            pics.append(p["img_src"])

    # 表情包
    emote_pics = []
    emote = content.get("emote", {}) or {}
    for k, v in emote.items():
        if "url" in v:
            emote_pics.append(v["url"])

    return {
        "text": text,
        "pictures": pics,
        "emote_pictures": emote_pics
    }


# 获取评论
def fetch_comments(aid, bv, headers, limit=20):
    page = 1
    collected = []

    while len(collected) < limit:
        api = f"https://api.bilibili.com/x/v2/reply?type=1&oid={aid}&pn={page}&ps=20"
        headers["Referer"] = f"https://www.bilibili.com/video/{bv}/"

        try:
            resp = session.get(api, headers=headers, timeout=REQUEST_TIMEOUT, verify=False)
            resp.raise_for_status()
            
            try:
                data = resp.json()
            except json.JSONDecodeError:
                print("接口返回非JSON,风控或cookies失效")
                print(f"响应内容: {resp.text[:200]}")
                break

            if data["code"] != 0:
                print(f"接口返回错误: {data.get('message')}")
                break

            if not data["data"].get("replies"):
                break

            collected.extend(data["data"]["replies"])
            page += 1
            
            # 添加随机延迟
            time.sleep(random.uniform(1, 2))

        except requests.exceptions.RequestException as e:
            print(f"❌ 获取评论请求失败: {e}")
            break

    return collected[:limit]


# 爬单个视频
def scrape_single_video(bv, domain_name, headers):
    # 请求前添加随机延迟
    time.sleep(random.uniform(1, 3))
    
    aid = get_aid_from_bv(bv, headers)
    if not aid:
        print(f"❌ BV 转 AID 失败:{bv}")
        return False

    print(f"开始爬取视频:{bv} (aid={aid})")

    raw_comments = fetch_comments(aid, bv, headers, limit=20)
    
    if not raw_comments:
        print(f"未获取到评论:{bv}")
        return False

    clean_comments = [extract_clean_comment(c) for c in raw_comments]

    # 输出目录
    save_dir = os.path.join("output", domain_name)
    os.makedirs(save_dir, exist_ok=True)

    out_path = os.path.join(save_dir, f"{bv}.json")

    try:
        with open(out_path, "w", encoding="utf-8") as f:
            json.dump(clean_comments, f, ensure_ascii=False, indent=2)
        print(f"已保存:{out_path} (共{len(clean_comments)}条评论)")
        return True
    except Exception as e:
        print(f"❌ 保存文件失败:{e}")
        return False


# 按领域批量爬
def main():
    cookie_str = load_cookies()
    headers = make_headers(cookie_str)

    # 手动选择 txt 文件
    print("当前 video_list 目录下的文件:")
    video_list_dir = "video_list"
    txt_files = [f for f in os.listdir(video_list_dir) if f.endswith(".txt")]

    for f in txt_files:
        print(" -", f)

    target_txt = input("\n请输入你要爬取的 txt 文件名(例如 APEX.txt):").strip()

    if target_txt not in txt_files:
        print("文件不存在!")
        return

    domain_name = target_txt.replace(".txt", "")
    txt_path = os.path.join(video_list_dir, target_txt)

    print(f"\n===== 爬取领域:{domain_name} =====\n")

    # 读取 txt 的 URL
    with open(txt_path, "r", encoding="utf-8") as f:
        urls = [line.strip() for line in f if line.strip()]

    # 批处理参数
    BATCH_SIZE = 10
    BATCH_SLEEP = 300
    
    success_count = 0
    fail_count = 0
    failed_bvs = []

    print(f"总共需要爬取 {len(urls)} 个视频\n")

    # 逐个爬取
    for i, url in enumerate(urls, 1):
        bv = extract_bv(url)
        if not bv:
            print(f"❌ 无法提取 BV:{url}")
            fail_count += 1
            continue

        print(f"\n[{i}/{len(urls)}] ", end="")
        success = scrape_single_video(bv, domain_name, headers)
        
        if success:
            success_count += 1
        else:
            fail_count += 1
            failed_bvs.append(bv)

        # 批次休息
        if i % BATCH_SIZE == 0 and i < len(urls):
            print(f"\n⏸️ 已爬取 {i} 个视频,休息 {BATCH_SLEEP} 秒避免反爬...")
            time.sleep(BATCH_SLEEP)

    # 保存失败的任务
    if failed_bvs:
        failed_file = os.path.join("output", domain_name, "failed_tasks.txt")
        with open(failed_file, "w", encoding="utf-8") as f:
            for bv in failed_bvs:
                f.write(f"{bv}\n")
        print(f"\n⚠ 有 {fail_count} 个任务失败,已保存到: {failed_file}")

    print(f"\n🎉 领域 {domain_name} 的爬取完成!")
    print(f"✅ 成功: {success_count}, ❌ 失败: {fail_count}")


if __name__ == "__main__":
    main()

一、程序启动与网络环境准备

程序启动后创建一个全局 HTTP 会话对象,并配置连接池与自动重试机制,用于提高网络请求的稳定性。当请求出现超时、服务器错误或访问频率限制时,程序会自动重试,从而减少因网络波动导致的任务失败。

二、身份认证与请求头构造

在访问 B 站接口前,从本地文件读取浏览器 Cookies,并拼接为标准的 Cookie 字符串。随后统一构造请求头,模拟正常浏览器访问行为,以确保接口能够正常响应并降低被风控拦截的概率。

三、任务选择

扫描本地存放视频列表的目录,列出所有可用的文本文件,手动选择需要爬取的文件。每个文件代表一个领域或主题,文件中每一行对应一个链接,作为本次爬取任务的输入数据。

四、批量任务初始化

在正式爬取前,初始化统计变量,用于记录成功任务数、失败任务数及失败的视频编号,并在任务结束时输出统计结果与失败记录。同时设置批量爬取参数,在处理一定数量的视频后进行较长时间的休眠,以降低访问频率,减少触发反爬机制的风险。

五、单个视频处理流程

按顺序遍历视频列表,对每个视频执行相同的处理流程。首先从视频链接中提取 BV 号,并在请求前随机等待一小段时间,用于模拟真实用户访问行为,避免连续高速请求。

六、视频信息解析与评论获取

在获取评论前,先将 BV 号转换为内部使用的视频 ID。随后通过评论接口按页请求评论数据,直到达到预设数量或接口不再返回数据为止。分页请求过程中会加入短暂的随机延迟,以进一步降低请求频率。

七、评论数据清洗与整理

对每条原始评论进行清洗,提取评论文本、图片链接和表情包图片链接,使数据结构更加简洁,便于后续存储与分析。

八、数据存储与任务结果返回

清洗后的评论数据会按视频所属领域保存为 JSON 文件。如果在任意步骤中出现错误,将该视频标记为失败任务,并继续处理下一个视频。

九、批次控制与反爬策略

在批量爬取过程中,采用分批处理加休眠的策略,在处理完固定数量的视频后进行长时间休眠,从而有效降低整体访问压力,减少账号被风控或接口受限的风险。

十、失败任务记录与统计输出

任务结束后,检查是否存在失败的视频任务,并将其统一保存到文本文件中,便于后续补爬或排查问题。最后输出本次爬取任务的统计结果,包括成功数量、失败数量及对应的爬取领域。

总结

这次实践通过不断地尝试和试错,让我有了一定的感悟。通过使用 Cookies 维持登录状态,实现对目标数据的稳定获取,避免了频繁验证带来的中断问题。在这一过程中,我逐步理解了真实项目中数据获取并非爬到就行,而是需要兼顾数据完整性与可复现性。在爬取评论时也意识到原始数据中会有冗余字段和无关信息,数据的清洗也极为重要。采集阶段也要考虑各种潜在问题,不能追求速度而丢失质量,稳定性比效率更重要。失败任务的记录和管理也十分重要,对后续的补爬和数据完善很有帮助。这次实践让我对数据采集在整个模型训练流程中的基础性作用有了更直观的认识,数据采集绝不仅是将数据提取出来就行,更重要的是数据的质量以及数据结构的统一,以便于后续模型的训练。

posted @ 2025-12-30 16:36  缪阿  阅读(0)  评论(0)    收藏  举报