This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/kb_workflow/pipeline/link_parser.py
T
starmen1265 279ae684ea feat(06-02): move pipeline core files to kb_workflow.pipeline
- Move pipeline.py, schemas.py, config.py, state.py, errors.py
- Move retry.py, idempotency.py, link_parser.py
- Create pipeline/__init__.py with updated imports
- Update all internal imports to use kb_workflow.pipeline prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 00:16:56 +08:00

182 lines
5.2 KiB
Python

"""Link Parser module
Provides unified URL parsing for various content platforms.
Supports Bilibili (video, article) and Zhihu (answer, zhuanlan) URLs.
"""
import re
from dataclasses import dataclass, field
from typing import Optional
import requests
from kb_workflow.pipeline.retry import retry_on_network_error
@dataclass
class ParsedLink:
"""Represents a parsed link with platform and content information
Attributes:
platform: Platform name (bilibili, zhihu)
content_type: Content type (video, article, answer, zhuanlan)
content_id: Unique content identifier
original_url: Original URL provided by user
resolved_url: Resolved URL after short link expansion (defaults to original_url)
"""
platform: str
content_type: str
content_id: str
original_url: str
resolved_url: str = field(default="")
def __post_init__(self):
"""Set resolved_url to original_url if not provided"""
if not self.resolved_url:
self.resolved_url = self.original_url
class LinkParser:
"""Unified URL parser for multiple platforms
Parses URLs from Bilibili and Zhihu, extracting platform, content type,
and content ID. Supports short link resolution for b23.tv URLs.
"""
# Bilibili patterns
BILIBILI_VIDEO_PATTERN = re.compile(
r'https?://(?:www\.)?bilibili\.com/video/(BV[a-zA-Z0-9]+)',
re.IGNORECASE
)
BILIBILI_ARTICLE_PATTERN = re.compile(
r'https?://(?:www\.)?bilibili\.com/(?:read|article)/(cv[a-zA-Z0-9]+)',
re.IGNORECASE
)
# Zhihu patterns
ZHIHU_ANSWER_PATTERN = re.compile(
r'https?://(?:www\.)?zhihu\.com/question/\d+/answer/(\d+)',
re.IGNORECASE
)
ZHIHU_ZHUANLAN_PATTERN = re.compile(
r'https?://zhuanlan\.zhihu\.com/p/(\d+)',
re.IGNORECASE
)
# Short link patterns
B23_TV_PATTERN = re.compile(
r'https?://(?:www\.)?b23\.tv/([a-zA-Z0-9]+)',
re.IGNORECASE
)
# Timeout for short link resolution
SHORT_LINK_TIMEOUT = 10
@retry_on_network_error
def _resolve_shortlink(self, url: str) -> Optional[str]:
"""Resolve short link to its final URL
Uses HTTP HEAD request with redirects to get the final URL.
Args:
url: Short link URL to resolve
Returns:
Final URL after redirects, or None if resolution failed
"""
try:
response = requests.head(
url,
allow_redirects=True,
timeout=self.SHORT_LINK_TIMEOUT
)
if response.status_code == 200:
return response.url
return None
except (requests.RequestException, OSError):
return None
def parse(self, url: str) -> Optional[ParsedLink]:
"""Parse a URL and extract platform, content type, and ID
Args:
url: URL to parse
Returns:
ParsedLink with extracted information, or None if URL is not recognized
"""
if not url or not isinstance(url, str):
return None
url = url.strip()
# Try b23.tv short link first
if self.B23_TV_PATTERN.match(url):
resolved_url = self._resolve_shortlink(url)
if resolved_url:
# Recursively parse the resolved URL
result = self._parse_standard_url(resolved_url)
if result:
result.original_url = url
result.resolved_url = resolved_url
return result
return None
return self._parse_standard_url(url)
def _parse_standard_url(self, url: str) -> Optional[ParsedLink]:
"""Parse a standard (non-short) URL
Args:
url: URL to parse
Returns:
ParsedLink with extracted information, or None if URL is not recognized
"""
# Try Bilibili video
match = self.BILIBILI_VIDEO_PATTERN.match(url)
if match:
return ParsedLink(
platform="bilibili",
content_type="video",
content_id=match.group(1),
original_url=url,
resolved_url=url
)
# Try Bilibili article
match = self.BILIBILI_ARTICLE_PATTERN.match(url)
if match:
return ParsedLink(
platform="bilibili",
content_type="article",
content_id=match.group(1),
original_url=url,
resolved_url=url
)
# Try Zhihu answer
match = self.ZHIHU_ANSWER_PATTERN.match(url)
if match:
return ParsedLink(
platform="zhihu",
content_type="answer",
content_id=match.group(1),
original_url=url,
resolved_url=url
)
# Try Zhihu zhuanlan
match = self.ZHIHU_ZHUANLAN_PATTERN.match(url)
if match:
return ParsedLink(
platform="zhihu",
content_type="zhuanlan",
content_id=match.group(1),
original_url=url,
resolved_url=url
)
# URL not recognized
return None