基于Scrapy与BFS策略的中文维基百科定向爬取与结构化解析
1. 为什么需要定向爬取中文维基百科
中文维基百科作为最大的开放式中文知识库,包含了数百万个高质量词条。但在实际应用中,我们往往只需要特定领域的数据。比如做计算机领域的研究时,你可能只需要"人工智能"或"算法"相关的词条,而不需要"红楼梦"或"世界杯"的内容。
传统的数据获取方式主要有两种:一是直接下载维基百科的全量数据包(dumps),二是使用通用爬虫无差别抓取。这两种方法都存在明显缺陷:
- 全量数据包体积庞大(中文维基压缩包约2GB),解压后超过10GB,处理起来非常耗时
- 通用爬虫会抓取大量无关页面,既浪费带宽又增加后续筛选的工作量
- 两种方式都无法保证数据的领域相关性
我在实际项目中就遇到过这样的问题:想构建一个计算机术语知识库,结果下载的全量数据中90%都是无关内容,光是筛选就花了整整两周时间。后来改用定向爬取策略后,效率提升了近10倍。
2. Scrapy框架与BFS策略的优势组合
2.1 为什么选择Scrapy
Scrapy是Python生态中最成熟的爬虫框架之一,相比自己从头写爬虫,它有三大优势:
- 高性能:基于Twisted异步网络库,单机就能实现高并发
- 可扩展:通过中间件机制可以灵活添加各种功能
- 生态完善:有丰富的插件支持,比如自动限速、代理轮换等
我在多个项目中测试过,同样的爬取任务,用Scrapy比用requests+BeautifulSoup快3-5倍。特别是在处理维基百科这种超链接密集的网站时,Scrapy的链接自动发现机制能省去大量重复代码。
2.2 BFS策略的独特价值
广度优先搜索(BFS)特别适合维基百科这种层级明确的知识库结构。与深度优先(DFS)相比,BFS有两大优势:
- 可控性强:可以按层级逐步扩展,避免陷入无关分支
- 内存友好:不需要像DFS那样维护很深的调用栈
举个例子:当从"计算机科学"分类开始时,BFS会先抓取所有一级子分类(如"算法"、"编程语言"),然后再抓取二级子分类(如"排序算法"、"Python语言")。这种策略能确保我们始终在目标领域内爬取,不会突然跳到"电子游戏"这类无关分类。
3. 实战:构建定向爬虫
3.1 环境准备
首先确保安装以下Python包:
pip install scrapy numpy tqdm lxml langconv
langconv用于处理中文简繁转换,可以从GitHub获取:
# 简繁转换工具
from langconv import Converter
def Traditional2Simplified(sentence):
return Converter('zh-hans').convert(sentence)
3.2 核心队列实现
BFS的核心是维护两个队列:
class Queue():
def __init__(self):
self.candidates = [] # 待爬取队列
self.has_viewd = [] # 已爬取队列
self.save_every = 100 # 每100次保存一次进度
def add_candidate(self, url):
if url not in self.candidates and url not in self.has_viewd:
self.candidates.append(url)
def add_has_viewd(self, url):
if url not in self.has_viewd:
self.has_viewd.append(url)
self.save_progress() # 定期保存进度
实际项目中,我还会添加URL去重、断点续爬等功能。比如使用Bloom Filter来高效判断URL是否已爬取,这在处理百万级URL时特别有用。
3.3 Scrapy爬虫实现
创建基础爬虫类:
import scrapy
from scrapy.selector import Selector
class WikiSpider(scrapy.Spider):
name = 'wiki_spider'
allowed_domains = ['zh.wikipedia.org']
custom_settings = {
'CONCURRENT_REQUESTS': 16, # 并发数
'DOWNLOAD_DELAY': 0.5, # 下载间隔
}
def __init__(self):
self.queue = Queue()
self.queue.add_candidate('https://zh.wikipedia.org/wiki/Category:计算机科学')
def parse(self, response):
if 'Category:' in response.url:
yield from self.parse_category(response)
else:
yield from self.parse_content(response)
分类页面解析示例:
def parse_category(self, response):
sel = Selector(response)
links = sel.xpath("//div[@class='mw-category-generated']//a/@href").extract()
# 过滤无关链接
valid_links = [
f"https://zh.wikipedia.org{link}"
for link in links
if not self._should_filter(link)
]
# 添加到队列
self.queue.add_candidates(valid_links)
# 继续处理队列中的URL
for url in self.queue.get_candidates(10): # 每次取10个
yield scrapy.Request(url, callback=self.parse)
4. 数据解析与结构化处理
4.1 信息框(infobox)提取
维基百科的信息框包含最核心的结构化数据:
def extract_infobox(html):
tree = etree.HTML(html)
infobox = tree.xpath("//table[contains(@class,'infobox')]//tr")
result = {}
for row in infobox:
key = row.xpath(".//th//text()")
value = row.xpath(".//td//text()")
if key and value:
result[key[0].strip()] = ' '.join(v.strip() for v in value)
return result
例如"Python"词条的信息框会包含:
{
"设计者": "Guido van Rossum",
"实现者": "Python软件基金会",
"发行时间": "1991年"
}
4.2 导航框(navbox)处理
导航框揭示了实体间的关系:
def extract_navbox(html):
tree = etree.HTML(html)
navboxes = tree.xpath("//table[contains(@class,'navbox')]")
relations = []
for box in navboxes:
category = box.xpath(".//th[@class='navbox-title']//text()")
items = box.xpath(".//td[@class='navbox-list']//a/@title")
if category and items:
relations.append({
"category": category[0],
"related_items": items
})
return relations
比如"排序算法"的导航框会列出所有相关算法,这是构建知识图谱的优质数据源。
4.3 段落文本处理
对正文内容进行结构化提取:
def extract_paragraphs(html):
tree = etree.HTML(html)
sections = tree.xpath("//div[@id='mw-content-text']//h2|//div[@id='mw-content-text']//p")
current_section = "摘要"
result = {current_section: []}
for elem in sections:
if elem.tag == 'h2':
current_section = elem.xpath(".//text()")[0].replace("[编辑]", "")
result[current_section] = []
else:
text = ' '.join(elem.xpath(".//text()")).strip()
if text:
result[current_section].append(text)
return result
这种结构非常适合后续的文本分析任务,比如可以直接用"历史"章节的内容训练摘要模型。
5. 避坑指南与性能优化
5.1 常见问题解决方案
问题1:爬取到无关内容
- 现象:明明从"计算机科学"开始,却抓到了"电子游戏"内容
- 原因:维基百科的分类存在交叉引用
- 解决:添加关键词过滤列表
filter_words = ['游戏', '电影', '体育']
问题2:被封禁IP
- 现象:爬着爬着突然返回403错误
- 解决:
- 设置合理的DOWNLOAD_DELAY(建议0.5-1秒)
- 使用Rotating User Agent中间件
- 必要时使用代理池
问题3:编码问题
- 现象:抓取的中文显示为乱码
- 解决:
# 在settings.py中添加
FEED_EXPORT_ENCODING = 'utf-8'
5.2 性能优化技巧
- 增量爬取:记录已爬取的URL,下次运行时跳过
class SeenURLFilter:
def __init__(self):
self.seen = set()
def add(self, url):
self.seen.add(url)
def __contains__(self, url):
return url in self.seen
- 分布式扩展:使用Scrapy-Redis实现分布式爬取
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
- 内存优化:处理大页面时使用流式解析
from lxml import etree
context = etree.iterparse(response.body, events=('end',), tag='div')
for event, elem in context:
if elem.get('class') == 'mw-parser-output':
process_content(elem)
elem.clear()
6. 数据处理与应用场景
6.1 数据后处理
爬取后的数据通常需要进一步清洗:
def clean_text(text):
# 去除编辑标记
text = re.sub(r'\[\d+\]', '', text)
# 转换繁体
text = Traditional2Simplified(text)
# 去除多余空白
text = ' '.join(text.split())
return text
6.2 典型应用场景
-
知识图谱构建
- infobox直接转化为三元组
- navbox生成实体关系
- 示例:将"算法"分类下的所有实体及其关系导入Neo4j
-
NLP预训练
- 使用段落文本训练领域专用语言模型
- 示例:用计算机领域文本继续训练BERT
-
问答系统
- 将"常见问题"章节转化为QA对
- 示例:从编程语言词条构建技术问答库
-
学术研究
- 分析概念演变历史
- 示例:通过"历史"章节研究人工智能发展脉络
我在实际项目中用这套方法构建了计算机术语知识库,包含约5万个实体和20万条关系,相比全量处理方案,节省了90%以上的数据处理时间。
更多推荐



所有评论(0)