使用Scrapy抓取大量网站

4 投票
1 回答
2621 浏览
提问于 2025-04-18 01:58

我想分析一些互相关联的网站的链接结构和文本内容(比如关于科幻小说的网站)。我有一份大约300个授权网站的列表,想要从中提取数据。一旦我把抓取到的页面存入数据库,我会用其他工具来分析这些数据。

看起来Scrapy是执行这种任务的最佳工具之一,但我在定义一个能满足我需求的爬虫时遇到了困难。我需要以下功能:

  • 只抓取特定域名(这个列表保存在一个外部文本文件中,可能会改变)
  • 限制递归深度到一个给定的值(例如3)。
  • 对于每个页面,保存标题、HTML内容和链接到一个SQLite数据库中。
  • 使用缓存来避免频繁请求同样的页面。缓存应该有一个过期时间(例如1周)。过期后,页面需要重新抓取。
  • 我想手动运行爬虫(目前不需要定时抓取)。

为了实现这个目标,我开始这样定义爬虫:

# http://doc.scrapy.org/en/latest/intro/tutorial.html

from scrapy.spider import Spider
from scrapy import log
from scrapy.http.request import Request
from scrapy.contrib.spiders import CrawlSpider,Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import HtmlXPathSelector
from scrapy.selector import Selector
from ..items import PageItem

class PageSpider(CrawlSpider):
    name = "page"   

    rules = (Rule(SgmlLinkExtractor(allow=(),), callback='parse_item', follow=True),)   
    #restrict_xpaths=('//body',)), 

    def parse_item(self, response):
        log.msg( "PageSpider.parse" )
        log.msg( response.url )
        #sel = Selector(response)
        links = sel.xpath('//body//a/@href').extract()
        #log.msg("links")
        #log.msg(links)
        item = PageItem()
        item['url'] = response.url
        item['content'] = response.body
        item['links'] = "\n".join( links )
        return item

我该如何将允许的网站列表加载到爬虫的allow中?为了存储抓取到的内容,我使用了一个管道,似乎工作得还不错(虽然还没有时间逻辑,但它能把数据存储到本地数据库中):

# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy import log
import sqlite3
import time
#from items import PageItem

class MyProjectPipeline(object):

    _db_conn = None;

    def process_item(self, item, spider):
        log.msg( "process item" )
        if not self.url_exists(item['url']):
            # insert element
            c = MyProjectPipeline._db_conn.cursor()
            items = [( item['url'], item['content'], item['links'], time.time() )]
            c.executemany('INSERT INTO pages_dump VALUES (?,?,?,?)', items)
            MyProjectPipeline._db_conn.commit()
        return item

    def open_spider(self, spider):
        # https://docs.python.org/2/library/sqlite3.html
        log.msg( "open sql lite DB" )
        MyProjectPipeline._db_conn = sqlite3.connect('consp_crawl_pages.db')
        c = MyProjectPipeline._db_conn.cursor()
        # create table
        c.execute('''create table if not exists pages_dump ( p_url PRIMARY KEY, p_content, p_links, p_ts )''')
        MyProjectPipeline._db_conn.commit()

    def close_spider(self, spider):
        log.msg( "closing sql lite DB" )
        MyProjectPipeline._db_conn.close()

    def url_exists(self, url):
        c = MyProjectPipeline._db_conn.cursor()
        c.execute("SELECT p_url FROM pages_dump WHERE p_url = ?", (url,))
        data=c.fetchone()
        if data is None:
            return False
        return True

我该如何阻止爬虫请求一个已经在数据库中的网址?

我这样做是否合理,还是在Scrapy中有更自然的方法来实现这些功能?我的Python水平不太好,所以也欢迎任何编码建议 :-)

感谢任何评论,
Mulone

1 个回答

2

我知道这个回答来得有点晚,但我还是想试着回答你的问题:

1) 如果你想从一个文本文件中抓取域名,你只需要在 __init__ 方法里设置一个叫 allowed_domains 的属性就可以了:

class PageSpider(CrawlSpider):
    name = "page"
    def __init__(self, *args, **kwargs):
        self.allowed_domains = open('YOUR_FILE').read().splitlines()
        super(PageSpider, self).__init__(*args, **kwargs)

2) 如果你想限制抓取的深度,只需要设置 DEPTH_LIMIT 的设置

3) 如果你想把数据保存到数据库里,使用管道(pipeline)是个好方法——你做得对。=)

4) Scrapy 默认情况下已经避免在同一次抓取中重复请求,但如果你想避免在之前的抓取中重复请求,你需要选择一种机制来外部存储之前的请求,并在 爬虫中间件 中进行过滤,就像Talvalin在评论中提到的那个链接:https://stackoverflow.com/a/22968884/149872

撰写回答