diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..8a777bd Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index cd919e2..87a8b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ ./venv/ .idea/ -data/ \ No newline at end of file +data/ +*/.DS_Store \ No newline at end of file diff --git a/main.py b/main.py index 8cd0036..ef03b92 100644 --- a/main.py +++ b/main.py @@ -78,20 +78,19 @@ class BiliWebCrawler: # 获取视频最高分辨率(基于dimension对象) max_width = 0 max_height = 0 - for format_info in video_data.get('formats', []): - dimension = format_info.get('dimension', {}) - width = dimension.get('width', 0) - height = dimension.get('height', 0) - rotate = dimension.get('rotate', 0) + dimension = video_data.get('dimension', {}) + width = dimension.get('width', 0) + height = dimension.get('height', 0) + rotate = dimension.get('rotate', 0) - # 处理视频旋转(当rotate=1时宽高互换) - if rotate == 1: - width, height = height, width + # 处理视频旋转(当rotate=1时宽高互换) + if rotate == 1: + width, height = height, width - # 通过像素总量比较分辨率 - if (width * height) > (max_width * max_height): - max_width = width - max_height = height + # 通过像素总量比较分辨率 + if (width * height) > (max_width * max_height): + max_width = width + max_height = height # 将分辨率格式化为 "宽x高" 的字符串 resolution_str = f"{max_width}x{max_height}" if max_width and max_height else "未知" @@ -105,23 +104,96 @@ class BiliWebCrawler: tag_data = [tag['tag_name'] for tag in tag_json.get('data', [])] info = { + 'BV号': self.bvid, 'title': video_data.get('title', ''), - 'up主': video_data.get('owner', {}).get('name', ''), + 'up主名称': video_data.get('owner', {}).get('name', ''), # 新增字段 + 'up主UID': video_data.get('owner', {}).get('mid', ''), # 新增UID字段 '播放量': video_data.get('stat', {}).get('view', 0), '弹幕量': video_data.get('stat', {}).get('danmaku', 0), '点赞量': video_data.get('stat', {}).get('like', 0), '投币量': video_data.get('stat', {}).get('coin', 0), '收藏量': video_data.get('stat', {}).get('favorite', 0), + '分享量': video_data.get('stat', {}).get('share', 0), + '评论量': video_data.get('stat', {}).get('reply', 0), + '发布时间的timestamp': video_data.get('pubdate', 0), '发布时间': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(video_data.get('pubdate', 0))), '分区': video_data.get('tname', ''), '标签': tag_data, + '视频方向': self._get_video_orientation(video_data.get('dimension', {})), '视频最高分辨率': resolution_str, - '视频类型': video_data.get('copyright', 0), - '视频分p数': len(video_data.get('pages', [])) + '视频类型': ["","自制", "转载"][video_data.get('copyright', 0)], + '视频分p数': len(video_data.get('pages', [])), + '视频总时长': self.get_video_length(video_data.get('pages', [])), + '简介': video_data.get('desc', '').replace('\n', '\\n'), } return info + def get_video_length(self,pages): + """获取视频总时长""" + length = 0 + for page in pages: + length += page.get('duration', 0) + return length + + def _get_video_orientation(self, dimension): + """判断视频方向(横屏/竖屏)""" + width = dimension.get('width', 0) + height = dimension.get('height', 0) + rotate = dimension.get('rotate', 0) + + # 处理视频旋转(90度或270度旋转时需要交换宽高) + if rotate in [1, 3]: + width, height = height, width + + return "横屏" if width >= height else "竖屏" + + # 在类方法中添加以下新方法(建议放在 get_video_info 方法之后) + def get_up_info(self, mid): + """获取UP主详细信息""" + if not mid: + return None + + url = f"https://api.bilibili.com/x/web-interface/card?mid={mid}&photo=false" + resp = self._safe_request(url) + if not resp: + return None + + try: + data = resp.json().get('data', {}) + card = data.get('card') + up_info = { + 'uid': mid, + '昵称': card['name'], + '性别': card['sex'], + '头像': card['face'], + '等级': card['level_info']['current_level'], + '粉丝数': card['fans'], + '稿件数': data['archive_count'], + '获赞数': data['like_num'], + } + except Exception as e: + print(f"解析UP主数据失败: {str(e)}") + return None + try: + # 获取投稿列表 + archive_url = f'https://api.bilibili.com/x/space/arc/search?mid={mid}&ps=30' + archive_resp = self._safe_request(archive_url) + if archive_resp and archive_resp.status_code == 200: + archive_data = archive_resp.json() + print(archive_data) + videos = archive_data.get('data', {}).get('list', {}).get('vlist', []) + + # 计算30天前的时间戳 + month_ago = time.time() - 30 * 86400 + # 统计符合时间条件的视频 + recent_count = sum(1 for v in videos if v.get('created') > month_ago) + up_info['近一个月投稿数'] = recent_count + except Exception as e: + print(f"获取投稿数据失败: {str(e)}") + + return up_info + def get_danmaku(self): """获取弹幕数据""" if not self.bvid: @@ -277,6 +349,13 @@ class BiliWebCrawler: comments_filename = os.path.join(video_dir, f'{self.bvid}_{len(comments)}_comments.csv') self.save_to_csv(comments, comments_filename) + # 新增UP主信息记录 + print("正在获取UP主信息...") + up_info = self.get_up_info(video_info.get('up主UID')) + up_info['BV号'] = self.bvid + up_csv_path = os.path.join(base_dir, 'up_info.csv') + self.save_to_csv([up_info], up_csv_path, mode='a') + print(f"抓取完成!结果已保存到 {video_dir}/") else: print("未获取到视频信息,无法进行抓取。") diff --git a/指标体系构建.md b/指标体系构建.md new file mode 100644 index 0000000..9f660c2 --- /dev/null +++ b/指标体系构建.md @@ -0,0 +1,56 @@ +## 外在属性指标 +### 1.基础流量指标 +需要视频BV号。 +- [x] 播放量 +- [x] 点赞量 +- [x] 投币量 +- [x] 收藏量 +- [x] 分享量 +- [x] 评论数 +- [x] 弹幕数 +### 2.up画像指标 +需要up主UID。 +- [x] 粉丝数 +- [x] 总获赞数 +- [x] 投稿数 +- [ ] 近一个月投稿数 +### 3.衍生指标 +- [x] 点赞率=点赞量/播放量 +- [x] 互动率=(点赞量+投币量+收藏量+分享量+评论数+弹幕数)/播放量 +- [x] 外溢系数=分享量/收藏量(反映内容外溢性) +## 内在属性指标 +### 1.内容属性指标 +- [x] 时长 +- [x] 发布时间 +- [x] 标题 +- [x] 分区 +- [x] 标签 +- [x] 最高清晰度 +### 2.内容结构指标 +- [ ] 是否分章节 +- [x] 是否分P +- [ ] 是否有字幕 +## 特殊指标(可能无法直接爬取,需特殊处理) +同时需要人工智能和能工智人。 +### 1.较易处理 +- [x] 是否为联合投稿(Sy:这个可以直接获取,没这么麻烦) +- [ ] 是否为系列作品(标题关键词分析) +- [x] 原创or搬运(只能投一个币为搬运)(Sy:这个也可以直接获取,没这么麻烦) +- [x] 横屏or竖屏(Sy:这个还是可以直接获取,没这么麻烦) +- [ ] 是否进入热门(作为机器学习的预测目标) +### 2.较难处理 +产生较大数据量。 +- [ ] 标题原创性(见DS代码) +- [ ] 标签区分度(见DS代码) +- [ ] 弹幕情感倾向得分(单独处理) +- [ ] 封面(图片分析可以单独写一章了) + +## 展望(即做不了的) +鉴于标题吸引力、剪辑质量、音频质量、封面设计、BGM体验等因素涉及主观判断, +平均播放时长这种B站几年没做外显(没法算完播率)无法直接获取, +领域垂直度等指标很难算, +点击率等指标爬不到, +我们希望后来者能克服这些困难进一步分析(如利用问卷等), +也可以考虑时间序列等因素构建更复杂的模型。 + +(总之我们不做) \ No newline at end of file