编程目的:

对up主的所有视频数据进行爬取,得到【链接、标题、观看量、点赞量、投币量、标签、封面图】等信息用于分析与制作可视化

编程思路:

0.前置函数

# 函数部分
def print_stage_start(stage, message):
    sys.stdout.write(Fore.YELLOW)
    print("══════════════════════════════════════════════════════════════════════════════")
    print(f"[提示] 阶段{stage}:{message}")
    print("══════════════════════════════════════════════════════════════════════════════")
    logging.info(f"[提示] 阶段{stage}:{message}")
    sys.stdout.write(Style.RESET_ALL)


def print_stage_result(stage, total, success, error):
    sys.stdout.write(Fore.YELLOW)
    print("══════════════════════════════════════════════════════════════════════════════")
    print(f"[结果] 阶段{stage}:一共 {total} 条数据, 成功 {success}, 失败 {error}")
    print("══════════════════════════════════════════════════════════════════════════════")
    logging.info(f"[结果] 阶段{stage}:一共 {total} 条数据, 成功 {success}, 失败 {error}")
    sys.stdout.write(Style.RESET_ALL)

1. 程序初始化与准备工作

  • 导入库
    使用 DrissionPage(浏览器驱动)爬取网页,csv 做数据存储,logging 做日志记录,colorama 做彩色终端输出。

  • 初始化变量

    • v_info_list1:存放阶段一结果(视频基本信息:ID、标题、BVID、封面图)。

    • v_info_list2:存放阶段二结果(视频详细信息:观看量、点赞量、投币量、标签)。

    • existing_bvid_list:存放 CSV 中已有的 BVID,用于去重。

    • 计数器(成功/失败次数)、标志位(是否遇到重复 BVID)等。

  • 日志和终端提示
    程序运行时,会实时输出阶段提示、运行结果、报错信息。

# python库导入部分
from DrissionPage import WebPage, ChromiumOptions
from colorama import init, Fore, Style
import sys, time, csv, os, logging

# 初始化部分
v_info_list1 = [["ID", "视频标题", "BVID", "封面图片"],]    # 阶段一数据存储列表
v_info_list2 = [["观看量", "点赞量", "投币量", "标签"],]     # 阶段二数据存储列表
existing_bvid_list = []     # CSV中存在的BVID列表
id = 0                      # 数据ID
num = 0                     # 获取数据计数器
error_num1 = 0              # 阶段一报错计数器
success_num1 = 0            # 阶段一成功计数器
error_num2 = 0              # 阶段二报错计数器
success_num2 = 0            # 阶段二成功计数器
stop_flag = True            # 是否遇到重复BVID的标志

# 配置日志,添加 encoding 参数指定编码为 utf-8
logging.basicConfig(
    level=logging.DEBUG,  # 设置日志级别为 DEBUG,这意味着会记录所有级别(DEBUG、INFO、WARNING、ERROR、CRITICAL)的日志
    format='%(asctime)s - %(levelname)s - %(message)s',  # 定义日志的输出格式
    filename='app.log',  # 指定日志输出到的文件
    filemode='w',  # 以写入模式打开文件,每次运行程序都会覆盖之前的日志文件
    encoding='utf-8'  # 指定文件编码为 utf-8,确保中文能正常显示
)

2. 确认工作路径和目标 UP

  • 获取并确认当前工作路径,用于保存 CSV 文件。

  • 输入目标 UP 主的 UID,确认爬取对象。

  • 访问 UP 的空间主页,抓取 UP 主名称,用于生成 CSV 文件名。

# 获取并确认当前工作路径
current_dir = os.getcwd()
print("────────────────────────────────────────────────────────────────────────")
print(f"📂 当前工作路径:{current_dir}")
print("────────────────────────────────────────────────────────────────────────")
while 1:
    queren_current_dir = input("\n请确认工作路径(Y/n):")
    if queren_current_dir in ["Y","y"]:
        logging.info(f"工作路径:{current_dir}")
        break
    elif queren_current_dir in ["N","n"]:
        print(f"{Fore.YELLOW}[警告] 正在退出程序!请更换工作路径再运行此程序!{Style.RESET_ALL}")
        time.sleep(2)
        sys.exit(0)
    else:
        print("请输入Y/n")

#访问up投稿视频主页
while 1:
    up_uid = input("请输入UP的uid:")
    try:
        up_uid = str(int(up_uid))
        logging.info(f"UP_UID:{up_uid}")
        break
    except: 
        print("输入错误,类型需为数字!")   

# 程序主要部分
start_time = time.time()  # 程序开始计时

# 配置无头模式和浏览器指纹
co = ChromiumOptions()
co.headless(True)
co.set_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")

# 初始化浏览器 发送访问请求 获取up名称 关闭页面
wp = WebPage(chromium_options=co)
wp.listen.start('x/space/wbi/acc/info')
wp.get(f"https://space.bilibili.com/{up_uid}/upload/video?tid=0&pn=4&keyword=&order=pubdate")
packet = wp.listen.wait()
up_name = packet.response.body['data']['name']
wp.quit()

print("\n────────────────────────────────────────────────────────────────────────")
print(f"up名称:{up_name}")
print("────────────────────────────────────────────────────────────────────────\n")
logging.info(f"up名称:{up_name}")

time.sleep(1)

# 定义文件名
file_name = f"B站UP-{up_name}视频数据.csv"
file_path = os.path.join(current_dir, file_name)

3. 历史数据去重

  • 检测是否存在同名 CSV 文件:

    • 如果存在,读取文件中已有的 BVID 存入 existing_bvid_list

    • 如果不存在,则从头开始爬取。

  • 这样可以保证“已经爬过的视频不会重复写入”。

# 判断是否有同名文件(重复文件)读取是否含有数据
if os.path.exists(file_path):
    print(f"[提示] 检测到已有数据文件:“{file_name}”  准备读取已有BVID用于去重处理。")
    logging.info(f"[提示] 检测到已有数据文件:“{file_name}”。")
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            reader = csv.reader(f)
            header = next(reader, None)  # 读取表头
            data_rows = list(reader)     # 读取剩余行
            
            if not data_rows:
                print(f"{Fore.YELLOW}[警告] 数据文件仅有表头,没有实际数据,视为无历史数据。{Style.RESET_ALL}")
                logging.warning(f"[警告] 数据文件仅有表头,没有实际数据,视为无历史数据。")
            else:
                for row in data_rows:
                    if len(row) >= 3:  # 第三列是 BVID
                        csv_bvid = row[2].strip()
                        if csv_bvid:
                            existing_bvid_list.append(csv_bvid)
                Li_Shi_Shu_Ju_num = len(existing_bvid_list)
                print(f"{Fore.GREEN}[完成] 已加载 {Li_Shi_Shu_Ju_num} 条历史 BVID !\n{Style.RESET_ALL}")
                logging.info(f"[完成] 已加载 {Li_Shi_Shu_Ju_num} 条历史 BVID !")

    except Exception as e:
        print(f"{Fore.RED}[错误] 读取文件时发生错误:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 读取文件时发生错误:{e}")
        existing_bvid_list = []  # 防止程序中断
else:
    print(f"[提示] 未检测到同名UP主的数据文件,将从头开始爬取。\n")
    logging.info(f"[提示] 未检测到同名UP主的数据文件,将从头开始爬取。")

4. 阶段一:爬取 UP 投稿视频列表

  • 访问 UP 视频列表页面,通过监听网络请求接口 /space/wbi/arc/search 获取 JSON 数据。

  • 遍历视频数据

    • 提取 标题封面图BVID

    • 如果 BVID 不在 existing_bvid_list 中,说明是新视频 → 存入 v_info_list1

    • 如果 BVID 已存在,提示“已存在”,跳过。

  • 翻页逻辑:如果存在“下一页”按钮,则点击继续获取,直到没有更多视频。

  • 结果写入 CSV
    将阶段一获取的结果(新视频)追加写入 CSV 文件的第 1~4 列。

########################################################################################################################################
# 阶段一:爬取up主页投稿视频

##################################################################################################
# 阶段一开始                                                                                      #
print_stage_start("一", "开始爬取视频数据:视频标题, BVID, 封面图片")                               #
##################################################################################################

# 初始化浏览器 发送访问请求 获取up视频列表
wp = WebPage(chromium_options=co)
wp.listen.start('/space/wbi/arc/search')
wp.get(f"https://space.bilibili.com/{up_uid}/upload/video?tid=0&pn=4&keyword=&order=pubdate")

while stop_flag:
    packet = wp.listen.wait()
    vlist1 = packet.response.body['data']['list']['vlist']
    try:
        for v in vlist1:
            title = v['title']
            pic = v['pic']
            bvid = v['bvid']
            if bvid not in existing_bvid_list:
                id += 1
                v_info1 = [id,title,bvid,pic]
                print(f"{Fore.GREEN}[√]添加{v_info1}{Style.RESET_ALL}")
                v_info_list1.append(v_info1)
                success_num1 += 1
            else:
                print(f"[提示] {bvid}已经存在")
        else:
            try:
                xyy = wp.ele('text=下一页')
                xyy.click()
                time.sleep(1)
            except:
                break

    except Exception as e:
        print(f"{Fore.RED}[错误] 报错:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 阶段一报错:{e}")
        error_num1 += 1

##################################################################################################
# 阶段一结果                                                                                      #
print_stage_result("一", len(v_info_list1)-1, success_num1, error_num1)                          #
##################################################################################################

########################################################################################################################################
# 将阶段一结果写入 CSV 文件
if len(v_info_list1) > 1:
    try:
        with open(file_path, 'a', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            # 将列表中的列表写入 CSV 文件
            writer.writerows(v_info_list1)
        print(f"\n{Fore.GREEN}[完成] 文件 '{file_name}' 已成功创建或覆盖!{Style.RESET_ALL}")
        logging.info(f"[完成] 阶段一:文件 '{file_name}' 已成功创建或覆盖!")
    except Exception as e:
        print(f"{Fore.RED}[错误] 写入文件时发生错误:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 阶段一:写入文件时发生错误:{e}")

# 读取 CSV 文件并遍历第三列内容从第二行开始,遇到空值停止
print("[提示] 读取CSV文件加载BVID!\n")
logging.info(f"[提示] 阶段二:读取CSV文件加载BVID!")
try:
    with open(file_path, 'r', encoding='utf-8') as csvfile:
        reader = csv.reader(csvfile)
        # 跳过表头行
        next(reader, None)
        third_column_values = []
        for row in reader:
            if len(row) >= 3:                # 确保行有至少三列
                cell_value = row[2].strip()  # 获取第三列的值并去除首尾空白
                if cell_value:               # 如果不是空值,添加到列表
                    third_column_values.append(cell_value)
                else:  # 如果是空值,停止遍历
                    break
except Exception as e:
    print(f"\n{Fore.RED}[错误] 读取文件时发生错误:{e}{Style.RESET_ALL}\n")
    logging.error(f"[错误] 读取文件时发生错误:{e}")

5. 阶段二:爬取视频详情数据

  • 读取 CSV 文件,获取其中所有 BVID 列表。

  • 逐个访问视频详情页

    1. 方法一:监听接口 x/web-interface/wbi/view/detail,直接获取视频详细数据:观看量、点赞量、投币量、标签。

    2. 方法二(备用方案):如果接口获取失败,则从网页元素中解析:

      • 观看数 → .view-text

      • 点赞数 / 投币数 → video-toolbar-item-text

      • 标签 → .tag-link

      • 如果数据中出现小数点或中文“万”等情况,为了避免不准确,会被过滤。

  • 写入结果
    将阶段二获取的数据存入 v_info_list2

########################################################################################################################################
# 阶段二:爬取up视频数据

##################################################################################################
# 阶段二开始                                                                                      #
print_stage_start("二", "开始爬取视频数据:观看量, 点赞量, 投币量, 标签")                            #
##################################################################################################

for bvid in third_column_values:
    num += 1
    tab = wp.new_tab("https://www.bilibili.com/video/"+bvid)
    tab.listen.start('x/web-interface/wbi/view/detail')
    packet = tab.listen.wait(timeout=3)

    # 方法一
    try:
        vlist2 = packet.response.body['data']['View']['stat']
        participle = packet.response.body['data']['participle']
        participle = '、'.join(participle)
        v_info2 = [vlist2['view'],vlist2['like'],vlist2['coin'],participle]
        print(f"{Fore.GREEN}[√]添加{num}{v_info2}{Style.RESET_ALL}")
        v_info_list2.append(v_info2)
        success_num2 += 1

    # 方法二
    except Exception as e1:
        try:
            tab.listen.start(f'{bvid}')
            tab.get(f"https://www.bilibili.com/video/{bvid}/")
            packet = tab.listen.wait(timeout=2)

            # 观看数
            f2_view = ""
            tags0 = tab.eles('.view-text')
            if tags0:
                view_text = tags0[0].text.strip()
                if '.' not in view_text and view_text.isdigit():
                    f2_view = int(view_text)
            else:
                print("[提示] 未获取到观看量数据")
                logging.warning("[提示] 未获取到观看量数据")

            # 点赞数 + 投币数
            f2_like = f2_coin = ""
            tags1 = tab.eles('.:video-toolbar-item-text')
            if len(tags1) >= 2:
                like_raw = tags1[0].text.strip()
                coin_raw = tags1[1].text.strip()
                if '.' not in like_raw and like_raw.isdigit():
                    f2_like = int(like_raw)
                if '.' not in coin_raw and coin_raw.isdigit():
                    f2_coin = int(coin_raw)
            else:
                print("[提示] 点赞量和投币量未获取到数据")
                logging.warning("[提示] 点赞量和投币量未获取到数据")

            # 标签
            f2_tags = ""
            tags2 = tab.eles('.tag-link')
            if tags2:
                f2_tags = '、'.join(tag.text for tag in tags2)

            # 四项中只要有任意一项非空就添加
            if any([f2_view, f2_like, f2_coin, f2_tags]):
                v_info2 = [f2_view, f2_like, f2_coin, f2_tags]
                print(f"{Fore.GREEN}[√]添加{num}{v_info2}{Style.RESET_ALL}")
                v_info_list2.append(v_info2)
                success_num2 += 1

            else:
                raise ValueError(f"⚠️ 方法二失败\n四项数据均为空或非法: 观看量{view_text}、点赞量{like_raw}、投币量{coin_raw}、标签{f2_tags}")
                
        except Exception as e2:
            Error = f"[错误]{bvid}报错:{e2}"
            logging.error(Error)
            v_info_list2.append([Error])
            error_num2 += 1
            print(f"{Fore.RED}{Error}{Style.RESET_ALL}")

    tab.close()
wp.quit()

##################################################################################################
# 阶段二结果                                                                                      #
print_stage_result(2, num, success_num2, error_num2)                                             #
##################################################################################################

6. 阶段二结果合并写入 CSV

  • 读取已有的 CSV 文件,把阶段二的数据(观看量、点赞量、投币量、标签)写入第 5~8 列。

  • 确保行列对齐:

    • 如果行数不够,自动补空行。

    • 如果列数不够,自动扩展。

########################################################################################################################################
# 将阶段二结果写入 CSV 文件
print(f"\n[提示] 开始写入CSV文件")
logging.info(f"[提示] 阶段二:开始写入CSV文件")
try:
    with open(file_name, 'r', encoding='utf-8') as f:
        rows = list(csv.reader(f))
except FileNotFoundError:
    print(f"{Fore.RED}[错误] 找不到文件: {file_name}{Style.RESET_ALL}")
    logging.error(f"[错误] 找不到文件: {file_name}")
    rows = []
except Exception as e:
    print(f"{Fore.RED}[错误] 读取文件时发生异常: {e}{Style.RESET_ALL}")
    logging.error(f"[错误] 读取文件时发生异常: {e}")
    rows = []

try:
    # 确保行数足够
    if len(rows) < len(v_info_list2):
        rows.extend([[] for _ in range(len(v_info_list2) - len(rows))])

    # 处理每一行数据
    for i in range(len(v_info_list2)):
        if len(rows[i]) < 8:    # 扩展列数(如果不够)
            rows[i].extend([''] * (8 - len(rows[i])))

        if v_info_list2[i]:     # 写入有效数据(非空列表)
            # 处理标签列(如果是列表)
            if len(v_info_list2[i]) > 3 and isinstance(v_info_list2[i][3], list):
                v_info_list2[i][3] = ','.join(v_info_list2[i][3])

            # 填充第5~8列(下标4~7)
            for j in range(min(len(v_info_list2[i]), 4)):
                rows[i][4 + j] = str(v_info_list2[i][j])

except IndexError as e:
    print(f"{Fore.RED}[索引错误] 行或列索引越界:{e}{Style.RESET_ALL}")
    logging.error(f"[索引错误] 行或列索引越界:{e}")
except Exception as e:
    print(f"{Fore.RED}[错误] 处理数据时出错:{e}{Style.RESET_ALL}")
    logging.error(f"[错误] 处理数据时出错:{e}")

try:
    # 写入原始文件(覆盖)
    with open(file_name, 'w', encoding='utf-8', newline='') as f:
        csv.writer(f).writerows(rows)
except Exception as e:
    print(f"{Fore.RED}[错误] 写入文件时发生异常: {e}{Style.RESET_ALL}")
    logging.error(f"[错误] 写入文件时发生异常: {e}")

7. 程序收尾

  • 打印阶段结果(总数、成功数、失败数)。

  • 程序结束时退出浏览器,关闭资源。

  • CSV 文件保存最终结果,便于后续数据分析。

########################################################################################################################
#结果(绿色)                                                                                                           #
sys.stdout.write(Fore.LIGHTGREEN_EX)                                                                                   #
print("\n##########################################################################################")                  #
print(rf"文件位置:{file_path}")                                                                                        #
print("##########################################################################################\n")                  #
logging.info(rf"文件位置:{file_path}")
sys.stdout.write(Style.RESET_ALL)                                                                                      #
########################################################################################################################

end_time = time.time()  # 程序结束计时
elapsed_time = end_time - start_time

print("\n────────────────────────────────────────────────────────────────────────")
print(f"本次程序一共爬取{num}条数据,总运行时长:{elapsed_time:.2f} 秒")
print(f"感谢使用!程序结束运行正在退出!\n")
logging.info(f"本次程序一共爬取{num}条数据,总运行时长:{elapsed_time:.2f} 秒! 程序结束运行! ")
sys.exit(0)

⚡总结:
这个程序分为 两大阶段

  1. 爬取视频基本信息(并去重存储)

  2. 根据 BVID 爬取详细数据并写入 CSV

每个阶段都有 异常处理、日志记录、结果统计,同时支持断点续爬(通过去重机制实现)。

完整代码:

目前程序最新版本为:1.2.0

后续版本会在本人GitHub上更新:https://github.com/VincentCassano/bilibili-crawler

# python库导入部分
from DrissionPage import WebPage, ChromiumOptions
from colorama import init, Fore, Style
import sys, time, csv, os, logging

########################################################################################################################################
# 函数部分
def print_stage_start(stage, message):
    sys.stdout.write(Fore.YELLOW)
    print("══════════════════════════════════════════════════════════════════════════════")
    print(f"[提示] 阶段{stage}:{message}")
    print("══════════════════════════════════════════════════════════════════════════════")
    logging.info(f"[提示] 阶段{stage}:{message}")
    sys.stdout.write(Style.RESET_ALL)


def print_stage_result(stage, total, success, error):
    sys.stdout.write(Fore.YELLOW)
    print("══════════════════════════════════════════════════════════════════════════════")
    print(f"[结果] 阶段{stage}:一共 {total} 条数据, 成功 {success}, 失败 {error}")
    print("══════════════════════════════════════════════════════════════════════════════")
    logging.info(f"[结果] 阶段{stage}:一共 {total} 条数据, 成功 {success}, 失败 {error}")
    sys.stdout.write(Style.RESET_ALL)


########################################################################################################################################
# 初始化部分
init()  # colorama
v_info_list1 = [["ID", "视频标题", "BVID", "封面图片"],]    # 阶段一数据存储列表
v_info_list2 = [["观看量", "点赞量", "投币量", "标签"],]     # 阶段二数据存储列表
existing_bvid_list = []     # CSV中存在的BVID列表
id = 0                      # 数据ID
num = 0                     # 获取数据计数器
error_num1 = 0              # 阶段一报错计数器
success_num1 = 0            # 阶段一成功计数器
error_num2 = 0              # 阶段二报错计数器
success_num2 = 0            # 阶段二成功计数器
stop_flag = True            # 是否遇到重复BVID的标志

# 配置日志,添加 encoding 参数指定编码为 utf-8
logging.basicConfig(
    level=logging.DEBUG,  # 设置日志级别为 DEBUG,这意味着会记录所有级别(DEBUG、INFO、WARNING、ERROR、CRITICAL)的日志
    format='%(asctime)s - %(levelname)s - %(message)s',  # 定义日志的输出格式
    filename='app.log',  # 指定日志输出到的文件
    filemode='w',  # 以写入模式打开文件,每次运行程序都会覆盖之前的日志文件
    encoding='utf-8'  # 指定文件编码为 utf-8,确保中文能正常显示
)

########################################################################################################################################
# 欢迎使用:工具LOGO 介绍 注意事项
sys.stdout.write(Fore.CYAN)
print(r"""
                     //           ________   ___   ___        ___   ________   ___   ___        ___   
          \\        //           |\   __  \ |\  \ |\  \      |\  \ |\   __  \ |\  \ |\  \      |\  \
           \\      //            \ \  \|\ /_\ \  \\ \  \     \ \  \\ \  \|\ /_\ \  \\ \  \     \ \  \
   ##WWWWWWWWWWWWWWWWWWWWWW##     \ \   __  \\ \  \\ \  \     \ \  \\ \   __  \\ \  \\ \  \     \ \  \
   ## WWWWWWWWWWWWWWWWWWWW ##      \ \  \|\  \\ \  \\ \  \____ \ \  \\ \  \|\  \\ \  \\ \  \____ \ \  \
   ## hh                hh ##       \ \_______\\ \__\\ \_______\\ \__\\ \_______\\ \__\\ \_______\\ \__\
   ## hh    //    \\    hh ##        \|_______| \|__| \|_______| \|__| \|_______| \|__| \|_______| \|__| 
   ## hh   //      \\   hh ##
   ## hh                hh ##         ██████╗ ██████╗  █████╗ ██╗    ██╗██╗     ███████╗██████╗ 
   ## hh      wwww      hh ##        ██╔════╝ ██╔══██╗██╔══██╗██║    ██║██║     ██╔════╝██╔══██╗
   ## hh                hh ##        ██║  ███╗██████╔╝███████║██║ █╗ ██║██║     █████╗  ██████╔╝
   ## MMMMMMMMMMMMMMMMMMMM ##        ██║   ██║██╔═══╝ ██╔══██║██║███╗██║██║     ██╔══╝  ██╔═══╝ 
   ##MMMMMMMMMMMMMMMMMMMMMM##        ╚██████╔╝██║     ██║  ██║╚███╔███╔╝███████╗███████╗██║     
        \/            \/              ╚═════╝ ╚═╝     ╚═╝  ╚═╝ ╚══╝╚══╝ ╚══════╝╚══════╝╚═╝    
                                                                             bilibili-crawler v1.2.0
""")

sys.stdout.write(Fore.LIGHTGREEN_EX)
print("\t\t╔═══════════════════════════════════════════════════════════════════╗")
print("\t\t║             欢迎使用 Bilibili-Crawler@v1.2.0 工具                 ║")
print("\t\t║───────────────────────────────────────────────────────────────────║")
print("\t\t║   创作者 :陆炳阳(Vincent Cassano)                              ║")
print("\t\t║  程序描述:初代版本,用于爬取指定B站UP主的视频列表及详细数据      ║")
print("\t\t║            视频标题、BVID、封面图片、播放、点赞、投币、标签       ║")
print("\t\t║  构建时间:2025 年 6 月 15 日  |  发布时间:2025 年 6 月 16 日    ║")
print("\t\t╚═══════════════════════════════════════════════════════════════════╝\n")
print("\t\t📢 本版本更新发布时间:2025 年 6 月 23 日,欢迎提交 issues 与反馈。")
print("\t\t🔗 GitHub: https://github.com/VincentCassano/bilibili-crawler")

sys.stdout.write(Fore.LIGHTRED_EX)
print("\n## ========================================= 这是一条分割线 ========================================= ##\n")
print("📌 注意事项:")
print("   在阶段二过程中,若出现此报错:'bool' object has no attribute 'response'")
print("   ├─ 📍 原因:B站返回了异常格式的数据包,无法匹配常规接口:x/web-interface/wbi/view/detail")
print("   ├─ ✅ 状态:程序会自动切换为备用方案(方法二)尝试获取页面可视数据")
print("   ├─ ⚠️ 说明:为确保数据准确,方法二将过滤包含小数点或“万”等中文单位的数据")
print("   └─ 💡 提示:若部分数据字段显示为空(如点赞量、播放量等),属正常行为,可放心忽略")
print("\n## ========================================= 这是一条分割线 ========================================= ##\n")
sys.stdout.write(Style.RESET_ALL)

########################################################################################################################################
# 获取并确认当前工作路径
current_dir = os.getcwd()
print("────────────────────────────────────────────────────────────────────────")
print(f"📂 当前工作路径:{current_dir}")
print("────────────────────────────────────────────────────────────────────────")
while 1:
    queren_current_dir = input("\n请确认工作路径(Y/n):")
    if queren_current_dir in ["Y","y"]:
        logging.info(f"工作路径:{current_dir}")
        break
    elif queren_current_dir in ["N","n"]:
        print(f"{Fore.YELLOW}[警告] 正在退出程序!请更换工作路径再运行此程序!{Style.RESET_ALL}")
        time.sleep(2)
        sys.exit(0)
    else:
        print("请输入Y/n")

#访问up投稿视频主页
while 1:
    up_uid = input("请输入UP的uid:")
    try:
        up_uid = str(int(up_uid))
        logging.info(f"UP_UID:{up_uid}")
        break
    except: 
        print("输入错误,类型需为数字!")   

########################################################################################################################################
# 程序主要部分
start_time = time.time()  # 程序开始计时

# 配置无头模式和浏览器指纹
co = ChromiumOptions()
co.headless(True)
co.set_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")

# 初始化浏览器 发送访问请求 获取up名称 关闭页面
wp = WebPage(chromium_options=co)
wp.listen.start('x/space/wbi/acc/info')
wp.get(f"https://space.bilibili.com/{up_uid}/upload/video?tid=0&pn=4&keyword=&order=pubdate")
packet = wp.listen.wait()
up_name = packet.response.body['data']['name']
wp.quit()

print("\n────────────────────────────────────────────────────────────────────────")
print(f"up名称:{up_name}")
print("────────────────────────────────────────────────────────────────────────\n")
logging.info(f"up名称:{up_name}")

time.sleep(1)

# 定义文件名
file_name = f"B站UP-{up_name}视频数据.csv"
file_path = os.path.join(current_dir, file_name)

# 判断是否有同名文件(重复文件)读取是否含有数据
if os.path.exists(file_path):
    print(f"[提示] 检测到已有数据文件:“{file_name}”  准备读取已有BVID用于去重处理。")
    logging.info(f"[提示] 检测到已有数据文件:“{file_name}”。")
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            reader = csv.reader(f)
            header = next(reader, None)  # 读取表头
            data_rows = list(reader)     # 读取剩余行
            
            if not data_rows:
                print(f"{Fore.YELLOW}[警告] 数据文件仅有表头,没有实际数据,视为无历史数据。{Style.RESET_ALL}")
                logging.warning(f"[警告] 数据文件仅有表头,没有实际数据,视为无历史数据。")
            else:
                for row in data_rows:
                    if len(row) >= 3:  # 第三列是 BVID
                        csv_bvid = row[2].strip()
                        if csv_bvid:
                            existing_bvid_list.append(csv_bvid)
                Li_Shi_Shu_Ju_num = len(existing_bvid_list)
                print(f"{Fore.GREEN}[完成] 已加载 {Li_Shi_Shu_Ju_num} 条历史 BVID !\n{Style.RESET_ALL}")
                logging.info(f"[完成] 已加载 {Li_Shi_Shu_Ju_num} 条历史 BVID !")

    except Exception as e:
        print(f"{Fore.RED}[错误] 读取文件时发生错误:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 读取文件时发生错误:{e}")
        existing_bvid_list = []  # 防止程序中断
else:
    print(f"[提示] 未检测到同名UP主的数据文件,将从头开始爬取。\n")
    logging.info(f"[提示] 未检测到同名UP主的数据文件,将从头开始爬取。")

########################################################################################################################################
# 阶段一:爬取up主页投稿视频

##################################################################################################
# 阶段一开始                                                                                      #
print_stage_start("一", "开始爬取视频数据:视频标题, BVID, 封面图片")                               #
##################################################################################################

# 初始化浏览器 发送访问请求 获取up视频列表
wp = WebPage(chromium_options=co)
wp.listen.start('/space/wbi/arc/search')
wp.get(f"https://space.bilibili.com/{up_uid}/upload/video?tid=0&pn=4&keyword=&order=pubdate")

while stop_flag:
    packet = wp.listen.wait()
    vlist1 = packet.response.body['data']['list']['vlist']
    try:
        for v in vlist1:
            title = v['title']
            pic = v['pic']
            bvid = v['bvid']
            if bvid not in existing_bvid_list:
                id += 1
                v_info1 = [id,title,bvid,pic]
                print(f"{Fore.GREEN}[√]添加{v_info1}{Style.RESET_ALL}")
                v_info_list1.append(v_info1)
                success_num1 += 1
            else:
                print(f"[提示] {bvid}已经存在")
        else:
            try:
                xyy = wp.ele('text=下一页')
                xyy.click()
                time.sleep(1)
            except:
                break

    except Exception as e:
        print(f"{Fore.RED}[错误] 报错:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 阶段一报错:{e}")
        error_num1 += 1

##################################################################################################
# 阶段一结果                                                                                      #
print_stage_result("一", len(v_info_list1)-1, success_num1, error_num1)                          #
##################################################################################################

########################################################################################################################################
# 将阶段一结果写入 CSV 文件
if len(v_info_list1) > 1:
    try:
        with open(file_path, 'a', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            # 将列表中的列表写入 CSV 文件
            writer.writerows(v_info_list1)
        print(f"\n{Fore.GREEN}[完成] 文件 '{file_name}' 已成功创建或覆盖!{Style.RESET_ALL}")
        logging.info(f"[完成] 阶段一:文件 '{file_name}' 已成功创建或覆盖!")
    except Exception as e:
        print(f"{Fore.RED}[错误] 写入文件时发生错误:{e}{Style.RESET_ALL}")
        logging.error(f"[错误] 阶段一:写入文件时发生错误:{e}")

# 读取 CSV 文件并遍历第三列内容从第二行开始,遇到空值停止
print("[提示] 读取CSV文件加载BVID!\n")
logging.info(f"[提示] 阶段二:读取CSV文件加载BVID!")
try:
    with open(file_path, 'r', encoding='utf-8') as csvfile:
        reader = csv.reader(csvfile)
        # 跳过表头行
        next(reader, None)
        third_column_values = []
        for row in reader:
            if len(row) >= 3:                # 确保行有至少三列
                cell_value = row[2].strip()  # 获取第三列的值并去除首尾空白
                if cell_value:               # 如果不是空值,添加到列表
                    third_column_values.append(cell_value)
                else:  # 如果是空值,停止遍历
                    break
except Exception as e:
    print(f"\n{Fore.RED}[错误] 读取文件时发生错误:{e}{Style.RESET_ALL}\n")
    logging.error(f"[错误] 读取文件时发生错误:{e}")
########################################################################################################################################
# 阶段二:爬取up视频数据

##################################################################################################
# 阶段二开始                                                                                      #
print_stage_start("二", "开始爬取视频数据:观看量, 点赞量, 投币量, 标签")                            #
##################################################################################################

for bvid in third_column_values:
    num += 1
    tab = wp.new_tab("https://www.bilibili.com/video/"+bvid)
    tab.listen.start('x/web-interface/wbi/view/detail')
    packet = tab.listen.wait(timeout=3)

    # 方法一
    try:
        vlist2 = packet.response.body['data']['View']['stat']
        participle = packet.response.body['data']['participle']
        participle = '、'.join(participle)
        v_info2 = [vlist2['view'],vlist2['like'],vlist2['coin'],participle]
        print(f"{Fore.GREEN}[√]添加{num}{v_info2}{Style.RESET_ALL}")
        v_info_list2.append(v_info2)
        success_num2 += 1

    # 方法二
    except Exception as e1:
        try:
            tab.listen.start(f'{bvid}')
            tab.get(f"https://www.bilibili.com/video/{bvid}/")
            packet = tab.listen.wait(timeout=2)

            # 观看数
            f2_view = ""
            tags0 = tab.eles('.view-text')
            if tags0:
                view_text = tags0[0].text.strip()
                if '.' not in view_text and view_text.isdigit():
                    f2_view = int(view_text)
            else:
                print("[提示] 未获取到观看量数据")
                logging.warning("[提示] 未获取到观看量数据")

            # 点赞数 + 投币数
            f2_like = f2_coin = ""
            tags1 = tab.eles('.:video-toolbar-item-text')
            if len(tags1) >= 2:
                like_raw = tags1[0].text.strip()
                coin_raw = tags1[1].text.strip()
                if '.' not in like_raw and like_raw.isdigit():
                    f2_like = int(like_raw)
                if '.' not in coin_raw and coin_raw.isdigit():
                    f2_coin = int(coin_raw)
            else:
                print("[提示] 点赞量和投币量未获取到数据")
                logging.warning("[提示] 点赞量和投币量未获取到数据")

            # 标签
            f2_tags = ""
            tags2 = tab.eles('.tag-link')
            if tags2:
                f2_tags = '、'.join(tag.text for tag in tags2)

            # 四项中只要有任意一项非空就添加
            if any([f2_view, f2_like, f2_coin, f2_tags]):
                v_info2 = [f2_view, f2_like, f2_coin, f2_tags]
                print(f"{Fore.GREEN}[√]添加{num}{v_info2}{Style.RESET_ALL}")
                v_info_list2.append(v_info2)
                success_num2 += 1

            else:
                raise ValueError(f"⚠️ 方法二失败\n四项数据均为空或非法: 观看量{view_text}、点赞量{like_raw}、投币量{coin_raw}、标签{f2_tags}")
                
        except Exception as e2:
            Error = f"[错误]{bvid}报错:{e2}"
            logging.error(Error)
            v_info_list2.append([Error])
            error_num2 += 1
            print(f"{Fore.RED}{Error}{Style.RESET_ALL}")

    tab.close()
wp.quit()

##################################################################################################
# 阶段二结果                                                                                      #
print_stage_result(2, num, success_num2, error_num2)                                             #
##################################################################################################

########################################################################################################################################
# 将阶段二结果写入 CSV 文件
print(f"\n[提示] 开始写入CSV文件")
logging.info(f"[提示] 阶段二:开始写入CSV文件")
try:
    with open(file_name, 'r', encoding='utf-8') as f:
        rows = list(csv.reader(f))
except FileNotFoundError:
    print(f"{Fore.RED}[错误] 找不到文件: {file_name}{Style.RESET_ALL}")
    logging.error(f"[错误] 找不到文件: {file_name}")
    rows = []
except Exception as e:
    print(f"{Fore.RED}[错误] 读取文件时发生异常: {e}{Style.RESET_ALL}")
    logging.error(f"[错误] 读取文件时发生异常: {e}")
    rows = []

try:
    # 确保行数足够
    if len(rows) < len(v_info_list2):
        rows.extend([[] for _ in range(len(v_info_list2) - len(rows))])

    # 处理每一行数据
    for i in range(len(v_info_list2)):
        if len(rows[i]) < 8:    # 扩展列数(如果不够)
            rows[i].extend([''] * (8 - len(rows[i])))

        if v_info_list2[i]:     # 写入有效数据(非空列表)
            # 处理标签列(如果是列表)
            if len(v_info_list2[i]) > 3 and isinstance(v_info_list2[i][3], list):
                v_info_list2[i][3] = ','.join(v_info_list2[i][3])

            # 填充第5~8列(下标4~7)
            for j in range(min(len(v_info_list2[i]), 4)):
                rows[i][4 + j] = str(v_info_list2[i][j])

except IndexError as e:
    print(f"{Fore.RED}[索引错误] 行或列索引越界:{e}{Style.RESET_ALL}")
    logging.error(f"[索引错误] 行或列索引越界:{e}")
except Exception as e:
    print(f"{Fore.RED}[错误] 处理数据时出错:{e}{Style.RESET_ALL}")
    logging.error(f"[错误] 处理数据时出错:{e}")

try:
    # 写入原始文件(覆盖)
    with open(file_name, 'w', encoding='utf-8', newline='') as f:
        csv.writer(f).writerows(rows)
except Exception as e:
    print(f"{Fore.RED}[错误] 写入文件时发生异常: {e}{Style.RESET_ALL}")
    logging.error(f"[错误] 写入文件时发生异常: {e}")

########################################################################################################################################
# 打印前十行数据预览
print(f"{Fore.GREEN}[完成] 已成功写入文件!{Style.RESET_ALL}")
logging.info(f"[完成] 已成功写入文件!")

try:
    with open(file_name, 'r', encoding='utf-8') as f:
        reader = list(csv.reader(f))
        print("\n────────────────────────────────────────────────────────────────────────")
        print("📄 CSV 文件前 10 行预览(含表头):")
        print("────────────────────────────────────────────────────────────────────────\n")
        for i, row in enumerate(reader[:10]):
            print(f"第{i}行:{row}")
except Exception as e:
    print(f"{Fore.RED}[错误] 无法读取或打印 CSV 文件内容:{e}{Style.RESET_ALL}")
    logging.error(f"[错误] 无法读取或打印 CSV 文件内容:{e}")

########################################################################################################################
#结果(绿色)                                                                                                           #
sys.stdout.write(Fore.LIGHTGREEN_EX)                                                                                   #
print("\n##########################################################################################")                  #
print(rf"文件位置:{file_path}")                                                                                        #
print("##########################################################################################\n")                  #
logging.info(rf"文件位置:{file_path}")
sys.stdout.write(Style.RESET_ALL)                                                                                      #
########################################################################################################################

end_time = time.time()  # 程序结束计时
elapsed_time = end_time - start_time

print("\n────────────────────────────────────────────────────────────────────────")
print(f"本次程序一共爬取{num}条数据,总运行时长:{elapsed_time:.2f} 秒")
print(f"感谢使用!程序结束运行正在退出!\n")
logging.info(f"本次程序一共爬取{num}条数据,总运行时长:{elapsed_time:.2f} 秒! 程序结束运行! ")
sys.exit(0)