scrapy
命令行工具
创建项目
scrapy startproject myproject [project_dir]
创建爬虫
scrapy genspider mydomain mydomain.com
开始爬虫
scrapy crawl spider
列出当前项目中所有可用的spider
scrapy list
#(venv) xiaogang@Mac all_tutorial % scrapy list
#quotes
#scrapyorg
在浏览器中打开特定的url,检查爬虫看见了什么
scrapy view <url>
使用命令行的模式对特定的url尝试解析,解析的代码可以直接放到parse中使用,确保正确性
scrapy shell url
运行单独的爬虫文件
scrapy runspider spider_name
Scrapy.spider
基础的爬虫
import scrapy
# the basic spider class
class Simple_Demo(scrapy.Spider):
name = 'example' # 爬虫的唯一标识
allowed_domains = ['example.com'] # 允许爬取的域名列表
start_urls = ['http://example.com'] # 爬虫开始爬取的URL列表
def start_requests(self):
urls = ['http://example.com/page1', 'http://example.com/page2']
for url in urls:
yield scrapy.Request(url=url, callback=self.parse, headers={'X-Custom': 'value'}) # 不写默认是start_urls
custom_settings = {
'DOWNLOAD_DELAY': 1,
'USER_AGENT': 'MyBot/1.0'
} # 爬虫特定的设置会覆盖项目全局设置
def parse(self, response): # 默认的处理response方法,通常在call_back中指定
self.logger.info('正在处理: %s', response.url)
yield {
'title': response.css('title::text').get(),
'url': response.url
}
def closed(self, reason):
self.logger.info('爬虫结束运行,原因: %s', reason)
定义规则的爬虫
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class ScrapyorgSpider(CrawlSpider):
name = "scrapyorg"
allowed_domains = ["scrapy.org"]
start_urls = ["https://scrapy.org"]
rules = (Rule(LinkExtractor(allow=r"Items/",# 匹配包含"Items/"的URL
deny= r"shop/"),# 排除包含"shop/"的URL
callback="parse_item", # 处理相应的回调方法
follow=True),)# 是否继续跟踪从这些页面中提取的链接
def parse_item(self, response):
item = {}
#item["domain_id"] = response.xpath('//input[@id="sid"]/@value').get()
#item["name"] = response.xpath('//div[@id="name"]').get()
#item["description"] = response.xpath('//div[@id="description"]').get()
return item
XmlFeedSpider
class scrapy.spiders.XMLFeedSpide
XMLFeedSpider是为解析XML提要而设计的,它通过使用特定的节点名对这些提要进行迭代。
from scrapy.spiders import XMLFeedSpider
from myproject.items import TestItem
class MySpider(XMLFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.xml']
iterator = 'iternodes' # This is actually unnecessary, since it's the default value
itertag = 'item'
def parse_node(self, response, node):
self.logger.info('Hi, this is a <%s> node!: %s', self.itertag, ''.join(node.getall()))
item = TestItem()
item['id'] = node.xpath('@id').get()
item['name'] = node.xpath('name').get()
item['description'] = node.xpath('description').get()
return item
CSVFeedSpider
这个spider与xmlFeedSpider非常相似,只是它迭代行,而不是节点
from scrapy.spiders import CSVFeedSpider
from myproject.items import TestItem
class MySpider(CSVFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.csv']
delimiter = ';'
quotechar = "'"
headers = ['id', 'name', 'description']
def parse_row(self, response, row):
self.logger.info('Hi, this is a row!: %r', row)
item = TestItem()
item['id'] = row['id']
item['name'] = row['name']
item['description'] = row['description']
return item
xpath css
https://www.osgeo.cn/scrapy/topics/selectors.html
xpath
基本表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
nodename |
选择所有名为nodename的节点 | div |
/ |
从根节点选择 | /html |
// |
从当前节点选择匹配的节点(不考虑位置) | //div |
. |
当前节点 | ./p |
.. |
父节点 | ../div |
@ |
选择属性 | @class |
//div # 选择所有div元素
/html/body # 选择body元素(必须是html的子元素)
//div[@class] # 选择所有带有class属性的div
//a/@href # 选择所有a标签的href属性值
//text() # 选择文本节点
from lxml import html
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>示例网页</title>
</head>
<body>
<div id="header" class="page-header">
<h1>欢迎来到我的网站</h1>
<p class="subtitle">探索精彩内容</p>
</div>
<div id="content">
<div class="article">
<h2 class="article-title">第一篇文章</h2>
<p class="article-meta">作者: 张三 | 日期: 2023-05-15</p>
<div class="article-content">
<p>这是第一篇文章的内容...</p>
<p>继续阅读更多<a href="/read-more" class="read-more">详细内容</a></p>
</div>
</div>
<div class="article" id="special-article">
<h2 class="article-title">特别推荐文章</h2>
<p class="article-meta">作者: 李四 | 日期: 2023-05-10</p>
<div class="article-content">
<p>这篇是特别推荐的内容!</p>
<ul>
<li>要点1</li>
<li>要点2</li>
<li>要点3</li>
</ul>
</div>
</div>
</div>
<div id="footer" class="page-footer">
<p>© 2023 我的网站. 保留所有权利.</p>
</div>
</body>
</html>
"""
from lxml import html
# 解析HTML
html_content = """上面提供的HTML代码"""
tree = html.fromstring(html_content)
# 1. 提取主标题
title = tree.xpath('//h1/text()')[0]
print(f"主标题: {title}")
# 2. 提取所有文章标题
article_titles = tree.xpath('//h2[@class="article-title"]/text()')
print("\n文章标题:")
for idx, title in enumerate(article_titles, 1):
print(f"{idx}. {title}")
# 3. 提取特别推荐文章的要点
special_points = tree.xpath('//div[@id="special-article"]//li/text()')
print("\n特别推荐文章的要点:")
for point in special_points:
print(f"- {point}")
# 4. 提取页脚版权信息
copyright = tree.xpath('//div[@class="page-footer"]/p/text()')[0]
print(f"\n版权信息: {copyright}")
//div[contains(@class, 'article')] # 选择class包含"article"的div
(//div[@class='article'])[2] # 选择第二个article div
//div[.//a[@class='read-more']] # 选择包含read-more链接的div
//p[@class='article-meta' and contains(text(), '李四')] # 选择作者是李四的meta信息
处理动态生成的class:
//div[contains(concat(' ', normalize-space(@class), ' article ')] # 精确匹配class单词
处理多层嵌套:
//div[@id='content']/div[1]/div[@class='article-content']/p[1]/text()
处理空白文本:
normalize-space(//p[@class='subtitle']/text()) # 去除多余空格
css
from parsel import Selector
# 解析HTML
html_content = """上面提供的HTML代码"""
sel = Selector(text=html_content)
# 1. 提取主标题
title = sel.css('h1::text').get()
print(f"主标题: {title}")
# 2. 提取所有文章标题
article_titles = sel.css('h2.article-title::text').getall()
print("\n文章标题:")
for idx, title in enumerate(article_titles, 1):
print(f"{idx}. {title}")
# 3. 提取特别推荐文章的要点
special_points = sel.css('#special-article li::text').getall()
print("\n特别推荐文章的要点:")
for point in special_points:
print(f"- {point}")
# 4. 提取页脚版权信息
copyright = sel.css('div.page-footer p::text').get()
print(f"\n版权信息: {copyright}")
# 5. 提取链接和链接文本
link = sel.css('a.read-more::attr(href)').get()
link_text = sel.css('a.read-more::text').get()
print(f"\n链接: {link}, 文本: {link_text}")
部分匹配class
div[class*="article"] /* 选择class包含"article"的div */
选择第n个元素
div.article:nth-of-type(2) /* 选择第二个article div */
选择有特定子元素的元素
div:has(a.read-more) /* 选择包含read-more链接的div (注意: 非CSS标准,但在jQuery和Scrapy中可用) */
多条件选择
p.article-meta:contains("李四") /* 选择作者是李四的meta信息 */
处理动态生成的class:
div[class*="article"] /* class包含"article" */
处理多层嵌套:
#content > div.article:first-child > div.article-content > p:first-child
提取属性值:
a::attr(href) /* Scrapy特有语法 */
items
Items 是用于结构化爬取数据的核心组件,它们定义了爬虫将要提取的数据的固定格式和结构。
import scrapy
class Product(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
stock = scrapy.Field()
tags = scrapy.Field()
last_updated = scrapy.Field(serializer=str)
class ProductItem(scrapy.Item):
name = scrapy.Field(serializer=str)
price = scrapy.Field(serializer=float, required=True)
这个部分和Django中的models中定义数据库字段很相似,不过这里的定义会简单很多,大部分都是scrapy.Field()
item loaders
from scrapy.loader import ItemLoader
from myproject.items import Product
def parse(self, response):
l = ItemLoader(item=Product(), response=response)
l.add_xpath('name', '//div[@class="product_name"]')
l.add_xpath('name', '//div[@class="product_title"]')
l.add_xpath('price', '//p[@id="price"]')
l.add_css('stock', 'p#stock')
l.add_value('last_updated', 'today') # you can also use literal values
return l.load_item()
pipeline
Pipeline(管道)是Scrapy框架中用于处理爬虫抓取到的数据的组件,它构成了Scrapy的数据处理流水线
激活pipeline
ITEM_PIPELINES = {
'myproject.pipelines.PricePipeline': 300,
'myproject.pipelines.JsonWriterPipeline': 800,
}
Pipeline的核心作用
数据清洗:验证、过滤和清理爬取的数据
from scrapy.exceptions import DropItem
class ValidationPipeline(object):
def process_item(self, item, spider):
if not item.get('price'):
raise DropItem("Missing price in %s" % item)
return item
数据存储:将数据保存到数据库或文件中
import pymongo
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db[spider.name].insert_one(dict(item))
return item
去重处理:确保不存储重复数据
from scrapy.exceptions import DropItem
class DuplicatesPipeline(object):
def __init__(self):
self.ids_seen = set()
def process_item(self, item, spider):
if item['id'] in self.ids_seen:
raise DropItem("Duplicate item found: %s" % item)
self.ids_seen.add(item['id'])
return item
后处理:对数据进行额外处理或转换
自定义逻辑:实现项目特定的数据处理需求
import pymongo
from itemadapter import ItemAdapter
class MongoPipeline:
collection_name = 'scrapy_items'
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db[self.collection_name].insert_one(ItemAdapter(item).asdict())
return item
feed export
在Scrapy项目中同时配置了数据库Pipeline和Feed Export时,数据会同时保存到两个地方- 既会保存到配置的数据库中,也会保存到Feed Export指定的输出文件中
Feed Export的核心功能
- 多格式支持:JSON, JSON Lines, CSV, XML等
- 多种存储方式:本地文件系统、FTP、S3、标准输出等
- 灵活配置:通过设置或命令行参数控制导出行为
- 分批处理:支持分批存储大型数据集
- 数据后处理:支持对导出数据进行编码、格式化等处理
基本使用方法
通过命令行参数使用
```bash scrapy crawl myspider -o items.json scrapy crawl myspider -o items.jl # JSON Lines格式 scrapy crawl myspider -o items.csv
#### 通过settings.py配置
```bash
FEED_URI = 'file:///path/to/export/%(name)s_%(time)s.json'
FEED_FORMAT = 'json'
FEED_EXPORT_ENCODING = 'utf-8'
FEED_EXPORT_FIELDS = ['field1', 'field2', 'field3']