mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-31 21:06:18 +08:00
174 lines
5.1 KiB
Python
174 lines
5.1 KiB
Python
import re
|
|
import random
|
|
from bs4 import BeautifulSoup
|
|
|
|
from . import safe_fetch
|
|
|
|
DEFAULT_BRIEF_LEN = 4096
|
|
MAX_BRIEF_CHARS = 100_000
|
|
MAX_TITLE_CHARS = 500
|
|
|
|
|
|
def clamp_brief_len(value) -> int:
|
|
"""Keep the caller-supplied output length inside a sane range."""
|
|
try:
|
|
length = int(value)
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_BRIEF_LEN
|
|
return max(1, min(length, MAX_BRIEF_CHARS))
|
|
|
|
|
|
__site_adapters__ = []
|
|
"""网站适配器列表
|
|
[
|
|
{
|
|
"regexp": [
|
|
"网址正则表达式0",
|
|
"网址正则表达式1",
|
|
],
|
|
"cls": <SiteAdapterBase>
|
|
}
|
|
]
|
|
"""
|
|
|
|
def site(regexp: list[str]):
|
|
"""网站适配器装饰器
|
|
"""
|
|
def wrapper(cls):
|
|
__site_adapters__.append({
|
|
"regexp": regexp,
|
|
"cls": cls
|
|
})
|
|
return cls
|
|
return wrapper
|
|
|
|
|
|
user_agents = [
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0',
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1.2 Safari/537.36',
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1 Safari/537.36',
|
|
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
|
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'
|
|
]
|
|
|
|
|
|
class SiteAdapterBase:
|
|
"""网站内容适配器的基类"""
|
|
|
|
@classmethod
|
|
def feed(cls, url: str) -> bool:
|
|
"""判断是否适配此网址"""
|
|
return True
|
|
|
|
@classmethod
|
|
def get_html(cls, url: str, timeout: float = safe_fetch.TOTAL_TIMEOUT, **kwargs) -> (int, str):
|
|
"""获取网页的HTML内容
|
|
|
|
反反爬策略应该在此应用
|
|
|
|
目标校验、重定向逐跳校验、字节上限与总截止时间都在
|
|
``safe_fetch`` 中实施;这里再限制交给解析器的字符数。
|
|
"""
|
|
document = safe_fetch.fetch_document(
|
|
url,
|
|
timeout=timeout,
|
|
headers={'User-Agent': random.choice(user_agents)},
|
|
**kwargs,
|
|
)
|
|
|
|
return document.status_code, document.text[:safe_fetch.MAX_PARSE_CHARS]
|
|
|
|
@classmethod
|
|
def extra_plain(cls, raw_html: str) -> str:
|
|
"""提取网页的纯文本内容"""
|
|
soup = BeautifulSoup(raw_html, 'html.parser')
|
|
raw = soup.get_text()
|
|
|
|
# 删除所有空行或只有空格的行
|
|
raw = re.sub(r'\n\s*\n', '\n', raw)
|
|
|
|
return raw
|
|
|
|
@classmethod
|
|
def extra_title_element(cls, raw_html: str) -> str:
|
|
"""提取网页的标题元素的纯文字"""
|
|
soup = BeautifulSoup(raw_html, 'html.parser')
|
|
|
|
element = soup.title
|
|
if element is None:
|
|
return ''
|
|
|
|
title = element.string
|
|
if title is None:
|
|
title = element.get_text()
|
|
|
|
return title.strip()[:MAX_TITLE_CHARS]
|
|
|
|
@classmethod
|
|
def regexp_brief(cls, raw_html: str, key: str, regexp: str, briefs: list[str]) -> list[str]:
|
|
"""从网页内容中提取信息,加入到briefs列表中"""
|
|
|
|
# 多行匹配
|
|
r = re.compile(regexp, re.DOTALL)
|
|
|
|
value = r.search(raw_html)
|
|
|
|
# value = re.search(regexp, raw_html)
|
|
if value:
|
|
briefs.append(key+": "+value.group(1).strip())
|
|
return briefs
|
|
|
|
@classmethod
|
|
def regexp_delete(cls, raw_text: str, regexp: str) -> str:
|
|
return re.sub(regexp, '', raw_text)
|
|
|
|
@classmethod
|
|
def make_ret(
|
|
cls,
|
|
status_code: int=200,
|
|
message: str='ok',
|
|
title: str='',
|
|
briefs: list[str] | None = None
|
|
):
|
|
"""生成返回的字典"""
|
|
return {
|
|
"status": status_code,
|
|
"message": message,
|
|
"content": {
|
|
"title": title,
|
|
"briefs": [] if briefs is None else briefs
|
|
}
|
|
}
|
|
|
|
@classmethod
|
|
def process(cls, url: str, brief_len: int, **kwargs) -> dict:
|
|
"""处理网页内容"""
|
|
brief_len = clamp_brief_len(brief_len)
|
|
|
|
status_code, raw_html = cls.get_html(url, **kwargs)
|
|
if status_code != 200:
|
|
return {
|
|
"status": status_code,
|
|
"message": "error status code: "+str(status_code)
|
|
}
|
|
|
|
plain = cls.extra_plain(raw_html)
|
|
title = cls.extra_title_element(raw_html)
|
|
|
|
if len(plain) >= brief_len:
|
|
plain = plain[:brief_len]
|
|
|
|
return {
|
|
"status": status_code,
|
|
"message": "ok",
|
|
"content": {
|
|
"title": title,
|
|
"briefs": [plain]
|
|
}
|
|
}
|