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
297 lines
8.3 KiB
Python
297 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Short link resolver for link-parser skill.
|
|
|
|
Provides utilities for detecting and resolving short links, particularly
|
|
Bilibili short links (b23.tv) which may be blocked by anti-scraping measures.
|
|
|
|
Supported short link services:
|
|
- Bilibili: b23.tv
|
|
- Generic URL shorteners (detection only)
|
|
"""
|
|
|
|
import re
|
|
from typing import Optional, Dict, List, Tuple
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
class ShortLinkResolver:
|
|
"""Short link detection and resolution handler"""
|
|
|
|
# Pattern for Bilibili short links
|
|
BILIBILI_SHORT_PATTERN = re.compile(r'https?://b23\.tv/([a-zA-Z0-9]+)')
|
|
|
|
# Pattern for other short link services
|
|
OTHER_SHORT_PATTERNS = {
|
|
'bit.ly': re.compile(r'https?://bit\.ly/\w+'),
|
|
't.cn': re.compile(r'https?://t\.cn/\w+'),
|
|
'dwz.cn': re.compile(r'https?://dwz\.cn/\w+'),
|
|
'goo.gl': re.compile(r'https?://goo\.gl/\w+'),
|
|
'tinyurl.com': re.compile(r'https?://tinyurl\.com/\w+'),
|
|
}
|
|
|
|
@staticmethod
|
|
def detect_short_links(text: str) -> List[Dict[str, str]]:
|
|
"""
|
|
Detect short links in text.
|
|
|
|
Args:
|
|
text: Input text to search
|
|
|
|
Returns:
|
|
List of detected short links with metadata:
|
|
[
|
|
{
|
|
'url': 'https://b23.tv/xxxxx',
|
|
'platform': 'bilibili',
|
|
'is_short': True
|
|
},
|
|
...
|
|
]
|
|
"""
|
|
detected = []
|
|
|
|
# Check for Bilibili short links
|
|
for match in ShortLinkResolver.BILIBILI_SHORT_PATTERN.finditer(text):
|
|
detected.append({
|
|
'url': match.group(0),
|
|
'code': match.group(1),
|
|
'platform': 'bilibili',
|
|
'is_short': True
|
|
})
|
|
|
|
# Check for other short link services
|
|
for platform, pattern in ShortLinkResolver.OTHER_SHORT_PATTERNS.items():
|
|
for match in pattern.finditer(text):
|
|
detected.append({
|
|
'url': match.group(0),
|
|
'platform': platform,
|
|
'is_short': True
|
|
})
|
|
|
|
return detected
|
|
|
|
@staticmethod
|
|
def is_bilibili_short_link(url: str) -> bool:
|
|
"""
|
|
Check if URL is a Bilibili short link.
|
|
|
|
Args:
|
|
url: URL to check
|
|
|
|
Returns:
|
|
True if URL is a b23.tv short link
|
|
"""
|
|
return bool(ShortLinkResolver.BILIBILI_SHORT_PATTERN.match(url))
|
|
|
|
@staticmethod
|
|
def format_short_link_help(short_link: Optional[str] = None) -> str:
|
|
"""
|
|
Format help message for short links.
|
|
|
|
Args:
|
|
short_link: Optional short link URL to mention
|
|
|
|
Returns:
|
|
Help message
|
|
"""
|
|
lines = [
|
|
"Detection: Short link",
|
|
""
|
|
]
|
|
|
|
if short_link:
|
|
lines.append(f"Short link: {short_link}")
|
|
lines.append("")
|
|
|
|
lines.extend([
|
|
"Due to anti-scraping protection, short links cannot be automatically resolved.",
|
|
"Please provide the full URL or the content ID (e.g., BV号 for Bilibili).",
|
|
"",
|
|
"Full URL format: https://www.bilibili.com/video/BVxxxxx"
|
|
])
|
|
|
|
return "\n".join(lines)
|
|
|
|
@staticmethod
|
|
def format_short_link_warning(short_links: List[Dict[str, str]]) -> str:
|
|
"""
|
|
Format warning message for multiple short links.
|
|
|
|
Args:
|
|
short_links: List of short link metadata
|
|
|
|
Returns:
|
|
Warning message
|
|
"""
|
|
if not short_links:
|
|
return ""
|
|
|
|
lines = [
|
|
"Short links detected:",
|
|
""
|
|
]
|
|
|
|
for i, link in enumerate(short_links, 1):
|
|
platform = link.get('platform', 'unknown')
|
|
url = link['url']
|
|
|
|
if platform == 'bilibili':
|
|
lines.append(f" [{i}] Bilibili short link: {url}")
|
|
lines.append(" Full URL or BV ID required")
|
|
else:
|
|
lines.append(f" [{i}] {platform}: {url}")
|
|
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
class PlaywrightResolver:
|
|
"""
|
|
Browser-based short link resolver using Playwright.
|
|
|
|
This is an optional implementation for environments where Playwright
|
|
is available. It can bypass anti-scraping measures by simulating
|
|
a real browser.
|
|
|
|
Requirements:
|
|
pip install playwright
|
|
playwright install chromium
|
|
"""
|
|
|
|
def __init__(self, headless: bool = True, timeout: int = 10000):
|
|
"""
|
|
Initialize Playwright resolver.
|
|
|
|
Args:
|
|
headless: Whether to run browser in headless mode
|
|
timeout: Page navigation timeout in milliseconds
|
|
"""
|
|
self.headless = headless
|
|
self.timeout = timeout
|
|
self._playwright = None
|
|
|
|
def _check_playwright_available(self) -> bool:
|
|
"""Check if Playwright is installed and available"""
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
def resolve(self, short_url: str) -> Optional[str]:
|
|
"""
|
|
Resolve short link using Playwright browser automation.
|
|
|
|
Args:
|
|
short_url: Short URL to resolve
|
|
|
|
Returns:
|
|
Final URL after redirects, or None if resolution fails
|
|
"""
|
|
if not self._check_playwright_available():
|
|
raise ImportError(
|
|
"Playwright not installed. Install with: pip install playwright && playwright install chromium"
|
|
)
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
try:
|
|
with sync_playwright() as p:
|
|
# Launch browser
|
|
browser = p.chromium.launch(headless=self.headless)
|
|
context = browser.new_context(
|
|
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',
|
|
viewport={'width': 1920, 'height': 1080}
|
|
)
|
|
page = context.new_page()
|
|
|
|
# Navigate to short URL
|
|
page.goto(short_url, timeout=self.timeout, wait_until='networkidle')
|
|
|
|
# Get final URL
|
|
final_url = page.url
|
|
|
|
# Close browser
|
|
browser.close()
|
|
|
|
return final_url
|
|
|
|
except Exception as e:
|
|
print(f"Playwright resolution failed: {e}")
|
|
return None
|
|
|
|
def resolve_bilibili_short(self, short_url: str) -> Optional[str]:
|
|
"""
|
|
Resolve Bilibili short link and extract BV ID.
|
|
|
|
Args:
|
|
short_url: Bilibili short URL (e.g., https://b23.tv/xxxxx)
|
|
|
|
Returns:
|
|
Full Bilibili URL with BV ID, or None if resolution fails
|
|
"""
|
|
final_url = self.resolve(short_url)
|
|
if not final_url:
|
|
return None
|
|
|
|
# Check if final URL contains BV ID
|
|
if 'bilibili.com/video/' in final_url:
|
|
return final_url
|
|
|
|
# Extract BV ID if present in URL
|
|
bv_pattern = re.compile(r'(BV[a-zA-Z0-9]{10})')
|
|
match = bv_pattern.search(final_url)
|
|
if match:
|
|
return f"https://www.bilibili.com/video/{match.group(1)}"
|
|
|
|
return None
|
|
|
|
|
|
def get_resolver() -> ShortLinkResolver:
|
|
"""
|
|
Get the default resolver instance.
|
|
|
|
Returns:
|
|
ShortLinkResolver instance
|
|
"""
|
|
return ShortLinkResolver()
|
|
|
|
|
|
def detect_short_links(text: str) -> List[Dict[str, str]]:
|
|
"""
|
|
Detect short links in text.
|
|
|
|
Args:
|
|
text: Input text to search
|
|
|
|
Returns:
|
|
List of detected short links with metadata
|
|
"""
|
|
resolver = ShortLinkResolver()
|
|
return resolver.detect_short_links(text)
|
|
|
|
|
|
# CLI interface for testing
|
|
def main():
|
|
"""CLI for testing the short link resolver."""
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 link_parser_shortlink.py 'text to test'")
|
|
print("\nExamples:")
|
|
print(' python3 link_parser_shortlink.py "Check this: https://b23.tv/oLBZZfK"')
|
|
print(' python3 link_parser_shortlink.py "https://bit.ly/xxxxx"')
|
|
sys.exit(1)
|
|
|
|
text = ' '.join(sys.argv[1:])
|
|
short_links = detect_short_links(text)
|
|
|
|
import json
|
|
print(json.dumps(short_links, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|