5507b1d986
- kb-link-parser: Multi-platform URL parser (Bilibili, YouTube, Zhihu) - kb-bilibili-metadata: Bilibili video/article/up主 metadata extractor - kb-zhihu-metadata: Zhihu question/answer/user metadata extractor - kb-bilibili-content: Bilibili video download and content extraction - kb-ocr-processor: OCR text extraction from video frames - kb-deep-analyzer: AI-powered deep content analysis - kb-workflow: Workflow orchestration (inbox, archive, subscriptions, report) Also add: - ARCHITECTURE_FLOWCHART.md: System architecture diagrams - DETAILED_DESIGN.md: Complete design documentation
388 lines
11 KiB
Python
388 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Link Parser - Extract and resolve IDs from various platform URLs.
|
|
|
|
Supports:
|
|
- Bilibili (video, article, up主, collection, series)
|
|
- YouTube (video, channel, playlist)
|
|
- Zhihu (question, answer, user, collection)
|
|
- Short links (b23.tv, etc.)
|
|
|
|
Usage:
|
|
python link-parser.py --url "https://www.bilibili.com/video/BV1xx411c7mD"
|
|
python link-parser.py --batch urls.txt --output results.json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from typing import Optional, Dict, Any
|
|
from urllib.parse import urlparse
|
|
|
|
from link_parser_shortlink import ShortLinkResolver
|
|
|
|
|
|
class LinkParser:
|
|
"""Main link parser class"""
|
|
|
|
# Bilibili patterns
|
|
BILIBILI_VIDEO_PATTERN = re.compile(r'https?://(?:www\.)?bilibili\.com/video/(BV[a-zA-Z0-9]+)')
|
|
BILIBILI_VIDEO_SHORT_PATTERN = re.compile(r'https?://b23\.tv/([a-zA-Z0-9]+)')
|
|
BILIBILI_ARTICLE_PATTERN = re.compile(r'https?://(?:www\.)?bilibili\.com/article/([a-zA-Z0-9]+)')
|
|
BILIBILI_SPACE_PATTERN = re.compile(r'https?://(?:space\.)?bilibili\.com/(\d+)')
|
|
BILIBILI_FAVEPATTERN = re.compile(r'https?://(?:www\.)?bilibili\.com/opus/(\d+)')
|
|
BILIBILI_SERIES_PATTERN = re.compile(r'https?://(?:www\.)?bilibili\.com/series/([a-zA-Z0-9]+)')
|
|
|
|
# YouTube patterns
|
|
YOUTUBE_VIDEO_PATTERN = re.compile(r'(?:https?://)?(?:www\.)?(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})')
|
|
YOUTUBE_CHANNEL_PATTERN = re.compile(r'(?:https?://)?(?:www\.)?youtube\.com/channel/([a-zA-Z0-9_-]+)')
|
|
YOUTUBE_USER_PATTERN = re.compile(r'(?:https?://)?(?:www\.)?youtube\.com/user/([a-zA-Z0-9_-]+)')
|
|
YOUTUBE_PLAYLIST_PATTERN = re.compile(r'(?:https?://)?(?:www\.)?youtube\.com/playlist\?list=([a-zA-Z0-9_-]+)')
|
|
|
|
# Zhihu patterns
|
|
ZHIHAU_QUESTION_PATTERN = re.compile(r'https?://(?:www\.)?zhihu\.com/question/(\d+)')
|
|
ZHIHAU_ANSWER_PATTERN = re.compile(r'https?://(?:www\.)?zhihu\.com/question/\d+/answer/(\d+)')
|
|
ZHIHAU_USER_PATTERN = re.compile(r'https?://(?:www\.)?zhihu\.com/people/([a-zA-Z0-9_-]+)')
|
|
ZHIHAU_COLUMN_PATTERN = re.compile(r'https?://(?:www\.)?zhihu\.com/column/([a-zA-Z0-9_-]+)')
|
|
ZHIHAU_COLLECTION_PATTERN = re.compile(r'https?://(?:www\.)?zhihu\.com/collection/(\d+)')
|
|
|
|
def __init__(self):
|
|
self.shortlink_resolver = ShortLinkResolver()
|
|
|
|
def resolve_shortlink(self, url: str) -> Optional[str]:
|
|
"""
|
|
Resolve short link to get final URL.
|
|
|
|
Args:
|
|
url: Short URL to resolve
|
|
|
|
Returns:
|
|
Final URL after resolution, or None if not resolvable
|
|
"""
|
|
# Check if it's a known short link pattern
|
|
if self.shortlink_resolver.is_bilibili_short_link(url):
|
|
# For b23.tv, we need Playwright to resolve
|
|
# Return the short URL as-is with a note
|
|
return url
|
|
|
|
# Return original URL for other cases
|
|
return url
|
|
|
|
def extract_bilibili_id(self, url: str) -> Optional[Dict[str, str]]:
|
|
"""
|
|
Extract Bilibili ID from URL.
|
|
|
|
Args:
|
|
url: Bilibili URL
|
|
|
|
Returns:
|
|
Dict with platform, type, and id fields, or None if not matched
|
|
"""
|
|
# Try video
|
|
match = self.BILIBILI_VIDEO_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "bilibili",
|
|
"type": "video",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try article
|
|
match = self.BILIBILI_ARTICLE_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "bilibili",
|
|
"type": "article",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try space (UP主)
|
|
match = self.BILIBILI_SPACE_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "bilibili",
|
|
"type": "up主",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try collection
|
|
match = self.BILIBILI_FAVEPATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "bilibili",
|
|
"type": "collection",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try series
|
|
match = self.BILIBILI_SERIES_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "bilibili",
|
|
"type": "series",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
return None
|
|
|
|
def extract_youtube_id(self, url: str) -> Optional[Dict[str, str]]:
|
|
"""
|
|
Extract YouTube ID from URL.
|
|
|
|
Args:
|
|
url: YouTube URL
|
|
|
|
Returns:
|
|
Dict with platform, type, and id fields, or None if not matched
|
|
"""
|
|
# Try video
|
|
match = self.YOUTUBE_VIDEO_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "youtube",
|
|
"type": "video",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try channel
|
|
match = self.YOUTUBE_CHANNEL_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "youtube",
|
|
"type": "channel",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try user
|
|
match = self.YOUTUBE_USER_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "youtube",
|
|
"type": "user",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try playlist
|
|
match = self.YOUTUBE_PLAYLIST_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "youtube",
|
|
"type": "playlist",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
return None
|
|
|
|
def extract_zhihu_id(self, url: str) -> Optional[Dict[str, str]]:
|
|
"""
|
|
Extract Zhihu ID from URL.
|
|
|
|
Args:
|
|
url: Zhihu URL
|
|
|
|
Returns:
|
|
Dict with platform, type, and id fields, or None if not matched
|
|
"""
|
|
# Try question
|
|
match = self.ZHIHAU_QUESTION_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "zhihu",
|
|
"type": "question",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try answer
|
|
match = self.ZHIHAU_ANSWER_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "zhihu",
|
|
"type": "answer",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try user
|
|
match = self.ZHIHAU_USER_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "zhihu",
|
|
"type": "user",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try column
|
|
match = self.ZHIHAU_COLUMN_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "zhihu",
|
|
"type": "column",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
# Try collection
|
|
match = self.ZHIHAU_COLLECTION_PATTERN.match(url)
|
|
if match:
|
|
return {
|
|
"platform": "zhihu",
|
|
"type": "collection",
|
|
"id": match.group(1)
|
|
}
|
|
|
|
return None
|
|
|
|
def parse(self, url: str) -> Dict[str, Any]:
|
|
"""
|
|
Parse URL and extract platform-specific ID.
|
|
|
|
Args:
|
|
url: URL to parse
|
|
|
|
Returns:
|
|
Dict with platform, type, id, and original_url fields
|
|
"""
|
|
result = {
|
|
"platform": None,
|
|
"type": None,
|
|
"id": None,
|
|
"original_url": url,
|
|
"resolved_url": None,
|
|
"success": False,
|
|
"error": None
|
|
}
|
|
|
|
# First, resolve short links
|
|
resolved = self.resolve_shortlink(url)
|
|
if resolved:
|
|
result["resolved_url"] = resolved
|
|
url = resolved
|
|
|
|
# Try each platform
|
|
bilibili_result = self.extract_bilibili_id(url)
|
|
if bilibili_result:
|
|
result.update(bilibili_result)
|
|
result["success"] = True
|
|
return result
|
|
|
|
youtube_result = self.extract_youtube_id(url)
|
|
if youtube_result:
|
|
result.update(youtube_result)
|
|
result["success"] = True
|
|
return result
|
|
|
|
zhihu_result = self.extract_zhihu_id(url)
|
|
if zhihu_result:
|
|
result.update(zhihu_result)
|
|
result["success"] = True
|
|
return result
|
|
|
|
# Check for short links that need resolution
|
|
short_links = self.shortlink_resolver.detect_short_links(url)
|
|
if short_links:
|
|
result["error"] = "Short link detected. Please provide the full URL."
|
|
return result
|
|
|
|
result["error"] = "Unsupported URL format"
|
|
return result
|
|
|
|
|
|
def format_output(data: Dict[str, Any], output_format: str = "json") -> str:
|
|
"""Format output based on specified format."""
|
|
if output_format == "json":
|
|
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
elif output_format == "compact":
|
|
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
|
elif output_format == "text":
|
|
lines = []
|
|
for key, value in data.items():
|
|
if value is not None:
|
|
lines.append(f"{key}: {value}")
|
|
return "\n".join(lines)
|
|
else:
|
|
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Parse and extract IDs from various platform URLs",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
%(prog)s --url "https://www.bilibili.com/video/BV1xx411c7mD"
|
|
%(prog)s --url "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
%(prog)s --url "https://www.zhihu.com/question/266309957"
|
|
%(prog)s --batch urls.txt --output results.json
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--url", "-u",
|
|
help="Single URL to parse"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--batch", "-b",
|
|
help="File containing URLs to parse (one per line)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output", "-o",
|
|
help="Output file path (for batch mode)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--format", "-f",
|
|
choices=["json", "compact", "text"],
|
|
default="json",
|
|
help="Output format (default: json)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Check if we're in shortlink help mode
|
|
if len(sys.argv) == 2 and sys.argv[1] in ['--help', '-h']:
|
|
parser.print_help()
|
|
return 0
|
|
|
|
if not args.url and not args.batch:
|
|
parser.print_help()
|
|
print("\nError: Please provide --url or --batch")
|
|
return 1
|
|
|
|
link_parser = LinkParser()
|
|
|
|
if args.url:
|
|
# Single URL mode
|
|
result = link_parser.parse(args.url)
|
|
print(format_output(result, args.format))
|
|
return 0 if result["success"] else 1
|
|
|
|
elif args.batch:
|
|
try:
|
|
with open(args.batch, 'r', encoding='utf-8') as f:
|
|
urls = [line.strip() for line in f if line.strip()]
|
|
except Exception as e:
|
|
print(f"Error reading file: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
results = []
|
|
for url in urls:
|
|
result = link_parser.parse(url)
|
|
results.append(result)
|
|
|
|
if args.output:
|
|
with open(args.output, 'w', encoding='utf-8') as f:
|
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
|
print(f"Results written to {args.output}")
|
|
else:
|
|
for result in results:
|
|
print(format_output(result, args.format))
|
|
print("-" * 40)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|