mirror of
https://github.com/NanmiCoder/MediaCrawler.git
synced 2026-07-24 05:10:30 +08:00
- 抖音、B站、快手、微博的帖子/视频/评论ID从BigInteger改为String, 避免PostgreSQL下字符串写入BIGINT报错及未来ID溢出风险 - B站dynamic_id改为String,修复API返回id_str被强转int导致的精度丢失 - 知乎提取器对content_id/question_id显式str()转换 - main.py启动数据库保存模式时自动建表,无需手动--init_db - 同步更新相关老化测试
384 lines
12 KiB
Python
384 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Copyright (c) 2025 relakkes@gmail.com
|
||
#
|
||
# This file is part of MediaCrawler project.
|
||
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/store/bilibili/_store_impl.py
|
||
# GitHub: https://github.com/NanmiCoder
|
||
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
|
||
#
|
||
|
||
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
|
||
# 1. 不得用于任何商业用途。
|
||
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
|
||
# 3. 不得进行大规模爬取或对平台造成运营干扰。
|
||
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
|
||
# 5. 不得用于任何非法或不当的用途。
|
||
#
|
||
# 详细许可条款请参阅项目根目录下的LICENSE文件。
|
||
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。
|
||
|
||
|
||
# -*- coding: utf-8 -*-
|
||
# @Author : persist1@126.com
|
||
# @Time : 2025/9/5 19:34
|
||
# @Desc : Bilibili storage implementation class
|
||
import asyncio
|
||
import csv
|
||
import json
|
||
import os
|
||
import pathlib
|
||
from typing import Dict
|
||
|
||
import aiofiles
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import sessionmaker
|
||
|
||
import config
|
||
from base.base_crawler import AbstractStore
|
||
from database.db_session import get_session
|
||
from database.models import BilibiliVideoComment, BilibiliVideo, BilibiliUpDynamic
|
||
from tools.async_file_writer import AsyncFileWriter
|
||
from tools import utils, words
|
||
from var import crawler_type_var
|
||
from database.mongodb_store_base import MongoDBStoreBase
|
||
|
||
|
||
class BiliCsvStoreImplement(AbstractStore):
|
||
def __init__(self):
|
||
self.file_writer = AsyncFileWriter(
|
||
crawler_type=crawler_type_var.get(),
|
||
platform="bili"
|
||
)
|
||
|
||
async def store_content(self, content_item: Dict):
|
||
"""
|
||
content CSV storage implementation
|
||
Args:
|
||
content_item:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_to_csv(
|
||
item=content_item,
|
||
item_type="videos"
|
||
)
|
||
|
||
async def store_comment(self, comment_item: Dict):
|
||
"""
|
||
comment CSV storage implementation
|
||
Args:
|
||
comment_item:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_to_csv(
|
||
item=comment_item,
|
||
item_type="comments"
|
||
)
|
||
|
||
async def store_creator(self, creator: Dict):
|
||
"""
|
||
creator CSV storage implementation
|
||
Args:
|
||
creator:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_to_csv(
|
||
item=creator,
|
||
item_type="creators"
|
||
)
|
||
|
||
async def store_contact(self, contact_item: Dict):
|
||
"""
|
||
creator contact CSV storage implementation
|
||
Args:
|
||
contact_item: creator's contact item dict
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_to_csv(
|
||
item=contact_item,
|
||
item_type="contacts"
|
||
)
|
||
|
||
async def store_dynamic(self, dynamic_item: Dict):
|
||
"""
|
||
creator dynamic CSV storage implementation
|
||
Args:
|
||
dynamic_item: creator's contact item dict
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_to_csv(
|
||
item=dynamic_item,
|
||
item_type="dynamics"
|
||
)
|
||
|
||
|
||
class BiliDbStoreImplement(AbstractStore):
|
||
async def store_content(self, content_item: Dict):
|
||
"""
|
||
Bilibili content DB storage implementation
|
||
Args:
|
||
content_item: content item dict
|
||
"""
|
||
video_id = content_item.get("video_id")
|
||
content_item["liked_count"] = int(content_item.get("liked_count", 0) or 0)
|
||
content_item["create_time"] = int(content_item.get("create_time", 0) or 0)
|
||
|
||
async with get_session() as session:
|
||
result = await session.execute(select(BilibiliVideo).where(BilibiliVideo.video_id == video_id))
|
||
video_detail = result.scalar_one_or_none()
|
||
|
||
if not video_detail:
|
||
content_item["add_ts"] = utils.get_current_timestamp()
|
||
content_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
new_content = BilibiliVideo(**content_item)
|
||
session.add(new_content)
|
||
else:
|
||
content_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
for key, value in content_item.items():
|
||
setattr(video_detail, key, value)
|
||
await session.commit()
|
||
|
||
async def store_comment(self, comment_item: Dict):
|
||
"""
|
||
Bilibili comment DB storage implementation
|
||
Args:
|
||
comment_item: comment item dict
|
||
"""
|
||
comment_id = comment_item.get("comment_id")
|
||
comment_item["create_time"] = int(comment_item.get("create_time", 0) or 0)
|
||
comment_item["like_count"] = str(comment_item.get("like_count", "0"))
|
||
comment_item["sub_comment_count"] = str(comment_item.get("sub_comment_count", "0"))
|
||
comment_item["parent_comment_id"] = str(comment_item.get("parent_comment_id", "0"))
|
||
|
||
async with get_session() as session:
|
||
result = await session.execute(select(BilibiliVideoComment).where(BilibiliVideoComment.comment_id == comment_id))
|
||
comment_detail = result.scalar_one_or_none()
|
||
|
||
if not comment_detail:
|
||
comment_item["add_ts"] = utils.get_current_timestamp()
|
||
comment_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
new_comment = BilibiliVideoComment(**comment_item)
|
||
session.add(new_comment)
|
||
else:
|
||
comment_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
for key, value in comment_item.items():
|
||
setattr(comment_detail, key, value)
|
||
await session.commit()
|
||
|
||
async def store_creator(self, creator: Dict):
|
||
# 教学版:UP 主个人资料不再落库
|
||
pass
|
||
|
||
async def store_contact(self, contact_item: Dict):
|
||
# 教学版:UP-粉丝关系表已移除,不再存储联系人信息
|
||
pass
|
||
|
||
async def store_dynamic(self, dynamic_item):
|
||
"""
|
||
Bilibili dynamic DB storage implementation
|
||
Args:
|
||
dynamic_item: dynamic item dict
|
||
"""
|
||
dynamic_id = dynamic_item.get("dynamic_id")
|
||
|
||
async with get_session() as session:
|
||
result = await session.execute(select(BilibiliUpDynamic).where(BilibiliUpDynamic.dynamic_id == dynamic_id))
|
||
dynamic_detail = result.scalar_one_or_none()
|
||
|
||
if not dynamic_detail:
|
||
dynamic_item["add_ts"] = utils.get_current_timestamp()
|
||
dynamic_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
new_dynamic = BilibiliUpDynamic(**dynamic_item)
|
||
session.add(new_dynamic)
|
||
else:
|
||
dynamic_item["last_modify_ts"] = utils.get_current_timestamp()
|
||
for key, value in dynamic_item.items():
|
||
setattr(dynamic_detail, key, value)
|
||
await session.commit()
|
||
|
||
|
||
class BiliJsonStoreImplement(AbstractStore):
|
||
def __init__(self):
|
||
self.file_writer = AsyncFileWriter(
|
||
crawler_type=crawler_type_var.get(),
|
||
platform="bili"
|
||
)
|
||
|
||
async def store_content(self, content_item: Dict):
|
||
"""
|
||
content JSON storage implementation
|
||
Args:
|
||
content_item:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_single_item_to_json(
|
||
item=content_item,
|
||
item_type="contents"
|
||
)
|
||
|
||
async def store_comment(self, comment_item: Dict):
|
||
"""
|
||
comment JSON storage implementation
|
||
Args:
|
||
comment_item:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_single_item_to_json(
|
||
item=comment_item,
|
||
item_type="comments"
|
||
)
|
||
|
||
async def store_creator(self, creator: Dict):
|
||
"""
|
||
creator JSON storage implementation
|
||
Args:
|
||
creator:
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_single_item_to_json(
|
||
item=creator,
|
||
item_type="creators"
|
||
)
|
||
|
||
async def store_contact(self, contact_item: Dict):
|
||
"""
|
||
creator contact JSON storage implementation
|
||
Args:
|
||
contact_item: creator's contact item dict
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_single_item_to_json(
|
||
item=contact_item,
|
||
item_type="contacts"
|
||
)
|
||
|
||
async def store_dynamic(self, dynamic_item: Dict):
|
||
"""
|
||
creator dynamic JSON storage implementation
|
||
Args:
|
||
dynamic_item: creator's contact item dict
|
||
|
||
Returns:
|
||
|
||
"""
|
||
await self.file_writer.write_single_item_to_json(
|
||
item=dynamic_item,
|
||
item_type="dynamics"
|
||
)
|
||
|
||
|
||
|
||
class BiliJsonlStoreImplement(AbstractStore):
|
||
def __init__(self):
|
||
self.file_writer = AsyncFileWriter(
|
||
crawler_type=crawler_type_var.get(),
|
||
platform="bili"
|
||
)
|
||
|
||
async def store_content(self, content_item: Dict):
|
||
await self.file_writer.write_to_jsonl(
|
||
item=content_item,
|
||
item_type="contents"
|
||
)
|
||
|
||
async def store_comment(self, comment_item: Dict):
|
||
await self.file_writer.write_to_jsonl(
|
||
item=comment_item,
|
||
item_type="comments"
|
||
)
|
||
|
||
async def store_creator(self, creator: Dict):
|
||
await self.file_writer.write_to_jsonl(
|
||
item=creator,
|
||
item_type="creators"
|
||
)
|
||
|
||
async def store_contact(self, contact_item: Dict):
|
||
await self.file_writer.write_to_jsonl(
|
||
item=contact_item,
|
||
item_type="contacts"
|
||
)
|
||
|
||
async def store_dynamic(self, dynamic_item: Dict):
|
||
await self.file_writer.write_to_jsonl(
|
||
item=dynamic_item,
|
||
item_type="dynamics"
|
||
)
|
||
|
||
|
||
class BiliSqliteStoreImplement(BiliDbStoreImplement):
|
||
pass
|
||
|
||
|
||
class BiliMongoStoreImplement(AbstractStore):
|
||
"""Bilibili MongoDB storage implementation"""
|
||
|
||
def __init__(self):
|
||
self.mongo_store = MongoDBStoreBase(collection_prefix="bilibili")
|
||
|
||
async def store_content(self, content_item: Dict):
|
||
"""
|
||
Store video content to MongoDB
|
||
Args:
|
||
content_item: Video content data
|
||
"""
|
||
video_id = content_item.get("video_id")
|
||
if not video_id:
|
||
return
|
||
|
||
await self.mongo_store.save_or_update(
|
||
collection_suffix="contents",
|
||
query={"video_id": video_id},
|
||
data=content_item
|
||
)
|
||
utils.logger.info(f"[BiliMongoStoreImplement.store_content] Saved video {video_id} to MongoDB")
|
||
|
||
async def store_comment(self, comment_item: Dict):
|
||
"""
|
||
Store comment to MongoDB
|
||
Args:
|
||
comment_item: Comment data
|
||
"""
|
||
comment_id = comment_item.get("comment_id")
|
||
if not comment_id:
|
||
return
|
||
|
||
await self.mongo_store.save_or_update(
|
||
collection_suffix="comments",
|
||
query={"comment_id": comment_id},
|
||
data=comment_item
|
||
)
|
||
utils.logger.info(f"[BiliMongoStoreImplement.store_comment] Saved comment {comment_id} to MongoDB")
|
||
|
||
async def store_creator(self, creator_item: Dict):
|
||
# 教学版:UP 主个人资料不再落库
|
||
pass
|
||
|
||
|
||
class BiliExcelStoreImplement:
|
||
"""Bilibili Excel storage implementation - Global singleton"""
|
||
|
||
def __new__(cls, *args, **kwargs):
|
||
from store.excel_store_base import ExcelStoreBase
|
||
return ExcelStoreBase.get_instance(
|
||
platform="bilibili",
|
||
crawler_type=crawler_type_var.get()
|
||
)
|