更新爬虫方案文档,增加摘要提取模块以生成文档摘要;优化基础爬虫类的标题提取逻辑,支持多个选择器,调整内容处理逻辑以去除重复标题。

This commit is contained in:
oy2020
2026-01-31 16:34:13 +08:00
parent 3c625d1c3a
commit c707704d80
5 changed files with 355 additions and 31 deletions

View File

@@ -18,6 +18,7 @@ from abc import ABC, abstractmethod
from .config import BASE_URL, HEADERS, REQUEST_DELAY, OUTPUT_DIR
from .utils import ensure_dir, download_image, safe_filename, make_absolute_url
from .extract_abstract import generate_abstract
class BaseCrawler(ABC):
@@ -128,14 +129,28 @@ class BaseCrawler(ABC):
selector = self.config.get("title_selector", "h1")
index = self.config.get("title_index", 0)
# 支持多个选择器,用逗号分隔(类似 extract_content 的处理方式)
selectors = [s.strip() for s in selector.split(',')]
# 收集所有匹配的标签
all_tags = []
for sel in selectors:
# 对于简单的标签名(如 "h1", "h2"),直接查找
if sel in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'title']:
found_tags = soup.find_all(sel)
all_tags.extend(found_tags)
else:
# 对于其他选择器,尝试查找
found_tags = soup.find_all(sel)
all_tags.extend(found_tags)
# 优先从配置的选择器提取
tags = soup.find_all(selector)
if tags and len(tags) > index:
title = tags[index].get_text(strip=True)
if all_tags and len(all_tags) > index:
title = all_tags[index].get_text(strip=True)
if title:
return title
elif tags:
title = tags[0].get_text(strip=True)
elif all_tags:
title = all_tags[0].get_text(strip=True)
if title:
return title
@@ -328,19 +343,52 @@ class BaseCrawler(ABC):
return images_info
def content_to_markdown(self, content: BeautifulSoup) -> str:
def content_to_markdown(self, content: BeautifulSoup, page_title: str = None) -> str:
"""
将内容转换为 Markdown
Args:
content: 内容区域
page_title: 页面标题如果提供会移除内容中与标题重复的第一个h1/h2标签
Returns:
Markdown 文本
"""
# 如果提供了页面标题,检查并移除内容中与标题重复的标签
if page_title:
# 创建内容的副本,避免修改原始内容
content_copy = BeautifulSoup(str(content), 'html.parser')
# 移除与标题完全相同的第一个h1
first_h1 = content_copy.find('h1')
if first_h1:
h1_text = first_h1.get_text(strip=True)
if h1_text == page_title:
first_h1.decompose()
# 移除与标题完全相同的第一个h2
first_h2 = content_copy.find('h2')
if first_h2:
h2_text = first_h2.get_text(strip=True)
if h2_text == page_title:
first_h2.decompose()
# 检查标题是否包含"型号:"前缀如果是也移除内容中只包含产品名称的h2
# 例如:标题是"型号eCoder11",内容中有"eCoder11"的h2
if '型号:' in page_title or '型号:' in page_title:
product_name = page_title.replace('型号:', '').replace('型号:', '').strip()
if product_name:
# 查找第一个只包含产品名称的h2
for h2 in content_copy.find_all('h2'):
h2_text = h2.get_text(strip=True)
if h2_text == product_name:
h2.decompose()
break # 只移除第一个匹配的
return markdownify.markdownify(str(content_copy), heading_style="ATX")
return markdownify.markdownify(str(content), heading_style="ATX")
def add_content_to_docx(self, doc: Document, content: BeautifulSoup, output_dir: str):
def add_content_to_docx(self, doc: Document, content: BeautifulSoup, output_dir: str, page_title: str = None):
"""
将内容添加到 Word 文档
@@ -348,7 +396,17 @@ class BaseCrawler(ABC):
doc: Document 对象
content: 内容区域
output_dir: 输出目录(用于解析图片路径)
page_title: 页面标题如果提供会跳过内容中与标题重复的第一个h1标签
"""
# 如果提供了页面标题创建内容副本并移除重复的h1
if page_title:
content = BeautifulSoup(str(content), 'html.parser')
first_h1 = content.find('h1')
if first_h1:
h1_text = first_h1.get_text(strip=True)
if h1_text == page_title:
first_h1.decompose() # 移除该标签
# 按文档顺序处理元素,保持列表的连续性
for element in content.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'li', 'ul', 'ol', 'table']):
if element.name == 'img':
@@ -444,8 +502,8 @@ class BaseCrawler(ABC):
# 处理图片
images = self.process_images(content, url)
# 转换为 Markdown
markdown = self.content_to_markdown(content)
# 转换为 Markdown传入标题用于去除重复的h1标签
markdown = self.content_to_markdown(content, title)
return {
"url": url,
@@ -481,7 +539,7 @@ class BaseCrawler(ABC):
p = doc.add_paragraph()
p.add_run(f"原文链接: {page_data['url']}").italic = True
self.add_content_to_docx(doc, page_data["content"], self.output_dir)
self.add_content_to_docx(doc, page_data["content"], self.output_dir, title)
doc.save(docx_path)
def save_combined_documents(self, all_pages: list[dict]):
@@ -505,14 +563,37 @@ class BaseCrawler(ABC):
# === 处理 Markdown ===
existing_urls = set()
existing_content = ""
existing_pages = [] # 存储已存在的页面信息(用于重新生成摘要)
# 如果文件已存在读取现有内容并提取已存在的URL
# 如果文件已存在读取现有内容并提取已存在的URL和页面信息
if os.path.exists(md_path):
with open(md_path, "r", encoding="utf-8") as f:
existing_content = f.read()
# 提取已存在的URL用于去重
url_pattern = r'\*\*原文链接\*\*: (https?://[^\s\n]+)'
existing_urls = set(re.findall(url_pattern, existing_content))
# 提取已存在的页面信息标题、URL和部分内容用于重新生成摘要
# 匹配格式:## 标题\n\n**原文链接**: URL\n\n内容...
# 使用更复杂的正则来匹配每个页面的完整内容块
page_blocks = re.split(r'\n\n---\n\n', existing_content)
for block in page_blocks:
# 匹配页面标题和URL
title_match = re.search(r'^##\s+([^\n]+)', block, re.MULTILINE)
url_match = re.search(r'\*\*原文链接\*\*:\s+(https?://[^\s\n]+)', block)
if title_match and url_match:
title = title_match.group(1).strip()
url = url_match.group(1).strip()
# 提取内容部分跳过标题和URL行
content_start = url_match.end()
markdown_content = block[content_start:].strip()
# 只取前500字符作为预览
markdown_preview = markdown_content[:500] if len(markdown_content) > 500 else markdown_content
existing_pages.append({
'title': title,
'url': url,
'markdown': markdown_preview
})
# 过滤掉已存在的页面基于URL去重
new_pages = [page for page in all_pages if page['url'] not in existing_urls]
@@ -529,14 +610,63 @@ class BaseCrawler(ABC):
new_md_content += page["markdown"]
new_md_content += "\n\n---\n\n"
# 合并所有页面(已存在的 + 新添加的),用于生成摘要
all_pages_for_abstract = existing_pages + all_pages
# 生成摘要新建文档时生成追加新内容时也重新生成确保包含所有URL
abstract = None
if not existing_content:
# 新建文档:使用当前爬取的页面生成摘要
print(f" 正在生成文档摘要...")
abstract = generate_abstract(all_pages, output_dir_name)
else:
# 追加模式:重新生成摘要,包含所有页面(已存在的 + 新添加的)
print(f" 正在重新生成文档摘要(包含所有 {len(all_pages_for_abstract)} 篇)...")
abstract = generate_abstract(all_pages_for_abstract, output_dir_name)
# 追加或创建文件
if existing_content:
# 追加模式:在现有内容后追加新内容
combined_md = existing_content.rstrip() + "\n\n" + new_md_content
print(f" 追加 {len(new_pages)} 篇新内容到现有文档")
# 追加模式:更新摘要部分,然后在现有内容后追加新内容
# 使用正则表达式替换摘要部分(从标题后到第一个"---"分隔符之间的内容)
# 匹配模式:标题行 + 摘要内容 + 分隔符
title_pattern = r'^#\s+.*?全集\s*\n\n'
separator_pattern = r'\n\n---\n\n'
# 查找标题后的第一个分隔符位置
title_match = re.search(title_pattern, existing_content, re.MULTILINE)
if title_match:
title_end = title_match.end()
# 查找第一个分隔符
separator_match = re.search(separator_pattern, existing_content[title_end:])
if separator_match:
# 替换摘要部分
separator_start = title_end + separator_match.start()
separator_end = title_end + separator_match.end()
# 保留标题和分隔符,替换中间的内容
combined_md = existing_content[:title_end]
if abstract:
combined_md += abstract
combined_md += existing_content[separator_end:]
# 追加新内容
combined_md = combined_md.rstrip() + "\n\n" + new_md_content
else:
# 如果没有找到分隔符,说明可能没有摘要,直接添加摘要和新内容
combined_md = existing_content[:title_end]
if abstract:
combined_md += abstract + "\n\n---\n\n"
combined_md += existing_content[title_end:].lstrip()
combined_md = combined_md.rstrip() + "\n\n" + new_md_content
else:
# 如果没有找到标题,说明格式异常,直接追加
combined_md = existing_content.rstrip() + "\n\n" + new_md_content
print(f" 追加 {len(new_pages)} 篇新内容到现有文档,并更新摘要")
else:
# 新建模式:创建新文档
combined_md = f"# {output_dir_name}全集\n\n" + new_md_content
# 构建文档内容:标题 + 摘要 + 正文
combined_md = f"# {output_dir_name}全集\n\n"
if abstract:
combined_md += f"{abstract}\n\n---\n\n"
combined_md += new_md_content
with open(md_path, "w", encoding="utf-8") as f:
f.write(combined_md)
@@ -563,7 +693,7 @@ class BaseCrawler(ABC):
doc.add_heading(page["title"], level=1)
p = doc.add_paragraph()
p.add_run(f"原文链接: {page['url']}").italic = True
self.add_content_to_docx(doc, page["content"], self.output_dir)
self.add_content_to_docx(doc, page["content"], self.output_dir, page["title"])
doc.add_page_break()
doc.save(docx_path)
print(f" 追加 {len(new_pages_for_doc)} 篇新内容到 Word 文档")
@@ -574,11 +704,28 @@ class BaseCrawler(ABC):
doc = Document()
doc.add_heading(f'{output_dir_name}全集', level=1)
# 添加摘要只在新建时生成复用Markdown部分生成的摘要
if not existing_content and abstract:
# 将Markdown格式的摘要转换为Word格式
# 处理Markdown链接将 [文本](URL) 转换为 "文本 (URL)" 格式
abstract_text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1 (\2)', abstract)
# 移除Markdown加粗标记
abstract_text = abstract_text.replace('**', '')
# 添加摘要段落
for line in abstract_text.split('\n'):
if line.strip():
doc.add_paragraph(line.strip())
else:
doc.add_paragraph() # 空行
doc.add_paragraph() # 空行
doc.add_paragraph("" * 50) # 分隔线
doc.add_paragraph() # 空行
for page in all_pages:
doc.add_heading(page["title"], level=1)
p = doc.add_paragraph()
p.add_run(f"原文链接: {page['url']}").italic = True
self.add_content_to_docx(doc, page["content"], self.output_dir)
self.add_content_to_docx(doc, page["content"], self.output_dir, page["title"])
doc.add_page_break()
doc.save(docx_path)