1. 为什么需要定向爬取中文维基百科

中文维基百科作为最大的开放式中文知识库,包含了数百万个高质量词条。但在实际应用中,我们往往只需要特定领域的数据。比如做计算机领域的研究时,你可能只需要"人工智能"或"算法"相关的词条,而不需要"红楼梦"或"世界杯"的内容。

传统的数据获取方式主要有两种:一是直接下载维基百科的全量数据包(dumps),二是使用通用爬虫无差别抓取。这两种方法都存在明显缺陷:

  • 全量数据包体积庞大(中文维基压缩包约2GB),解压后超过10GB,处理起来非常耗时
  • 通用爬虫会抓取大量无关页面,既浪费带宽又增加后续筛选的工作量
  • 两种方式都无法保证数据的领域相关性

我在实际项目中就遇到过这样的问题:想构建一个计算机术语知识库,结果下载的全量数据中90%都是无关内容,光是筛选就花了整整两周时间。后来改用定向爬取策略后,效率提升了近10倍。

2. Scrapy框架与BFS策略的优势组合

2.1 为什么选择Scrapy

Scrapy是Python生态中最成熟的爬虫框架之一,相比自己从头写爬虫,它有三大优势:

  1. 高性能:基于Twisted异步网络库,单机就能实现高并发
  2. 可扩展:通过中间件机制可以灵活添加各种功能
  3. 生态完善:有丰富的插件支持,比如自动限速、代理轮换等

我在多个项目中测试过,同样的爬取任务,用Scrapy比用requests+BeautifulSoup快3-5倍。特别是在处理维基百科这种超链接密集的网站时,Scrapy的链接自动发现机制能省去大量重复代码。

2.2 BFS策略的独特价值

广度优先搜索(BFS)特别适合维基百科这种层级明确的知识库结构。与深度优先(DFS)相比,BFS有两大优势:

  1. 可控性强:可以按层级逐步扩展,避免陷入无关分支
  2. 内存友好:不需要像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 性能优化技巧

  1. 增量爬取:记录已爬取的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
  1. 分布式扩展:使用Scrapy-Redis实现分布式爬取
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
  1. 内存优化:处理大页面时使用流式解析
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 典型应用场景

  1. 知识图谱构建

    • infobox直接转化为三元组
    • navbox生成实体关系
    • 示例:将"算法"分类下的所有实体及其关系导入Neo4j
  2. NLP预训练

    • 使用段落文本训练领域专用语言模型
    • 示例:用计算机领域文本继续训练BERT
  3. 问答系统

    • 将"常见问题"章节转化为QA对
    • 示例:从编程语言词条构建技术问答库
  4. 学术研究

    • 分析概念演变历史
    • 示例:通过"历史"章节研究人工智能发展脉络

我在实际项目中用这套方法构建了计算机术语知识库,包含约5万个实体和20万条关系,相比全量处理方案,节省了90%以上的数据处理时间。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐