BeautifulSoup 抓取 Needolin Dialogue 时为何获取了 Location 内容?
结论
需要同时修正标题匹配和章节范围:先定位包含完整“Needolin Dialogue”的标题,再提取该章节结束之前的内容。
现有代码只要求标题包含“Dialogue”。find_next_sibling("ul") 会查找后续兄弟节点中第一个符合条件的 <ul>,但不会判断列表属于哪个章节,因此可能越过后面的标题,取到 Location 等其他章节中的列表。
输入没有提供目标敌人页面的 HTML,无法确认页面的实际结构。下面的修改方式适用于列表型内容。
为什么会取到错误内容
问题出在这两行:
if "Dialogue" in header.get_text(strip=True):
ul = header.find_next_sibling("ul")
第一行匹配得太宽。注释写的是“Needolin Dialogue”,实际判断却只要求标题包含“Dialogue”,因此也可能选中其他对话章节。
第二行没有限制章节边界。例如,遇到下面的 HTML:
<h2>Needolin Dialogue</h2>
<div>目标对话内容</div>
<h2>Location</h2>
<ul>
<li>地点信息</li>
</ul>
从第一个 <h2> 调用 find_next_sibling("ul"),会跳过 <div> 和第二个 <h2>,找到 Location 下的列表。这可以解释所描述的现象,但不能据此确定目标页面的实际 HTML 结构。
原代码还只搜索 <h2>:如果目标标题使用 <h3>,就匹配不到;如果标题被包在 <div> 中,目标列表也可能与标题不处于同一层级。
替换章节提取方法
下面的方法会搜索各级标题,从目标标题之后按文档顺序遍历,遇到同级或更高级标题时停止。由于遍历按文档顺序进行,也可以处理标题外有包装元素的情况。
示例有两个前提:页面返回的 HTML 包含目标章节,对话以 <li> 列表项呈现。如果对话使用 <p>、表格或其他结构,需要根据实际 HTML 调整提取部分。
将类中的 scrape_enemy_dialogue 方法替换为:
def scrape_enemy_dialogue(self, url):
soup = self.fetch_page(url)
content = soup.select_one(".mw-parser-output")
if content is None:
content = soup.select_one("#mw-content-text")
if content is None:
print(f"Content container not found: {url}")
return []
heading_names = ("h1", "h2", "h3", "h4", "h5", "h6")
target = None
for header in content.find_all(heading_names):
title = " ".join(
header.get_text(" ", strip=True).split()
).casefold()
if "needolin dialogue" in title:
target = header
break
if target is None:
print(f"Needolin Dialogue heading not found: {url}")
return []
target_level = int(target.name[1])
dialogue = []
for node in target.next_elements:
# Skip text nodes and process HTML elements only.
if getattr(node, "name", None) is None:
continue
# Stay inside the selected content container.
if content not in node.parents:
break
# A heading at the same or higher level ends the section.
if node.name in heading_names:
if int(node.name[1]) <= target_level:
break
# Parent list items already include nested list text.
if node.name == "li" and node.find_parent("li") is None:
text = node.get_text(" ", strip=True)
if text:
dialogue.append(text)
print("collected", dialogue)
return dialogue
get_text(" ", strip=True) 会在不同文本节点之间加入空格,减少内联标签造成的单词粘连。过滤嵌套 <li> 是为了避免重复加入父列表项已经提取过的文本。
如果目标是 <h2>,代码会保留其下属 <h3> 中的列表内容,直到遇到下一个 <h2> 或 <h1>。如果目标是 <h3>,则在下一个 <h3>、<h2> 或 <h1> 处停止。
先检查一个具体页面
批量运行前,先选一个在浏览器中确认存在 Needolin Dialogue 的敌人页面,调用 scrape_enemy_dialogue 并检查打印结果。
如果返回空列表,可以临时在 soup = self.fetch_page(url) 后面加入:
for header in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
print(header.name, repr(header.get_text(" ", strip=True)))
这样可以确认 requests 实际取得的 HTML 中有哪些标题。如果找到目标标题,再检查附近的 HTML,确认对话是否放在 <li> 中。如果找不到,就核对实际标题文字,以及返回内容是否为预期页面。浏览器显示的内容与 requests 收到的 HTML 不一定完全相同。
不建议直接把原来的调用改成 header.find_next("ul")。这个方法会扩大搜索范围,但同样没有章节结束条件,仍可能读取后续章节的列表。
顺手修正链接去重
原代码还有一个独立问题:
if full not in links:
links.append((name, full))
full 是字符串,links 保存的是 (name, full) 元组,所以这个判断不能按 URL 正确去重。可以在循环外创建集合:
links = []
seen = set()
然后将去重部分改为:
full = urljoin(self.base_url, href)
if full not in seen:
seen.add(full)
links.append((name, full))
另外,/w/ 路径和“不含冒号”的筛选条件只能筛出部分站内链接,不能保证每个链接都是敌人页面。确认单页提取正确后,还应检查 get_enemy_links() 返回的名称和 URL 是否符合预期。
提供的代码没有显示 Python 缩进。如果本地文件也是这样,需要先恢复类、方法、循环和条件语句的缩进。程序既然已经能够运行,也可能只是粘贴时丢失了格式。
备注:内容仅供参考。
