我想写一个爬热点新闻的爬虫项目,最好能满足
我是用conda创建的虚拟环境,这个自己去配去
创建环境conda create -n envName python=3.10
进入环境conda activate envName
安装scrapypip install scrapy
创建项目scrapy startproject newsCrawler
newsCrawler是我的项目名
跟着他的提示
cd newsCrawler
scrapy genspider example example.com
这个example是一个你的Spider类,他会作为example.py在spiders文件夹里,这个类设置的爬取的网站是example.com,这个可以自己再改的,所以无脑用就行
爬取的是百度热搜
import scrapy
class NewsItem(scrapy.Item):
title = scrapy.Field()
url = scrapy.Field()
time = scrapy.Field()
exampleSpider.py
为baiduSpider.py
,修改允许爬取的域名和起始URL# 部分代码
class BaiduSpider(scrapy.Spider):
name = 'baidu'
allowed_domains = ['top.baidu.com']
start_urls = ['https://top.baidu.com/board?tab=realtime']
# 这个代码和上面的是一块的,这个parse就是BaiduSpider的类方法
def parse(self, response):
news_list = response.xpath('//*[@id="sanRoot"]/main/div[2]/div/div[2]/div[position()>=1]')
for news in news_list:
item = NewsItem()
item['url'] = news.xpath('./div[2]/a/@href').get()
item['title'] = news.xpath('./div[2]/a/div[1]/text()').get()
yield item
settings.py
中配置以UTF-8
导出,不然中文会以unicode
字符显示。(似乎也可以在Spider中写编码或者用Pipeline中编码并导出,但我就是用的命令导出的,所以没考虑那两个)# 导出数据时以UTF-8编码导出
FEED_EXPORT_ENCODING='UTF-8'
main.py
from scrapy.cmdline import execute
if __name__ == '__main__':
# 同在cmd中输入 scrapy crawl baidu -o baidu_news.json
# 注意第一个baidu是被执行的spider的name
# 如果不想打印日志,可以再加个'--nolog'
execute(['scrapy', 'crawl', 'baidu','-o','baidu_news.json'])
Linux下Docker安装几种NoSQL和MQ
https://news.qq.com/
在控制台中发现数据是从接口中获得的
请求网址:https://i.news.qq.com/trpc.qqnews_web.kv_srv.kv_srv_http_proxy/list?sub_srv_id=24hours&srv_id=pc&offset=0&limit=20&strategy=1&ext={%22pool%22:[%22top%22],%22is_filter%22:7,%22check_type%22:true}
请求方法:GET
携带数据:
貌似没有加密参数,即没有采取反爬,于是不准备模拟浏览器,而是直接爬取请求
import json
from urllib.parse import urlencode
import scrapy
from scrapy import Request
from newsCrawler.items import NewsItem
class TencentSpider(scrapy.Spider):
name = 'tencent'
allowed_domains = ['news.qq.com/']
base_url = 'https://i.news.qq.com/trpc.qqnews_web.kv_srv.kv_srv_http_proxy/list'
def start_requests(self):
query = {
'sub_srv_id': '24hours',
'srv_id': 'pc',
'offset': 0,
'limit': 20,
'strategy': 1,
'ext': json.dumps({
'pool': ['top'],
'is_filter': 7,
'check_type': True
})
}
str_query = urlencode(query)
url = self.base_url + "?" + str_query
yield Request(url=url,method='GET')
def parse(self, response):
json_data = json.loads(response.text)
news_list = json_data.get('data').get('list')
for news in news_list:
item = NewsItem()
item['title'] = news.get('title')
item['url'] = news.get('url')
yield item
突然看到一个汇总了各个新闻网链接的网站
http://www.hao123.com/newswangzhi
结果爬不动,发现robots协议是这样的:
User-agent: Baiduspider
Allow: /
User-agent: Baiduspider-image
Allow: /
User-agent: Baiduspider-news
Allow: /
User-agent: Googlebot
Allow: /
...
User-agent: *
Disallow: /
意思就是除了上面这些,其他的都不能爬,所以要禁用robots协议
在settings.py中将该参数由True修改为False
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
这个的代码就不放了,没啥含金量
robots协议是让搜索引擎判断这个页面是否允许被抓取的,所以我们自己的爬虫练习还是可以把他关掉的
https://news.sina.com.cn/roll/#pageid=153&lid=2509&k=&num=50&page=1
这个新浪的滚动网站
通过观察发现也是一个用接口获得数据的,其实这个从他每一分钟就异步刷新一次就知道
但是下面这三个参数发现是随时间变化的,因为还没到能逆向的程度,所以直接选用模拟浏览器的操作进行爬取,选用的是Playwright
安装Playwright
注意第二行是为我们安装浏览器及驱动及配置
pip3 install playwright
playwright install
不得不说这个网站也太离谱了,返回结果居然不是一个json,花了半天弄出格式化的json字符串切片
下面只是一段测试代码,未接入scrapy
import json
from playwright.sync_api import sync_playwright
def on_response(response):
# 找到这个接口
if '/api/roll/get' in response.url and response.status == 200:
print(f'Statue {response.status}:{response.url}')
# 强行截取格式化的json
json_str = response.text()[46:-14]
# 字符串转json,注意字符串是loads,文件时load
json_data = json.loads(json_str)
news_list = json_data.get('result').get('data')
for news in news_list:
url = news.get('url')
title = news.get('title')
print(f'{url} {title}')
with sync_playwright() as p:
for browser_type in [p.chromium]:
# 以无头模式打开谷歌浏览器
browser = browser_type.launch(headless=True)
page = browser.new_page()
# 绑定监听response事件
page.on('response',on_response)
page.goto('https://news.sina.com.cn/roll/#pageid=153&lid=2509&k=&num=50&page=1')
# 等待网络请求结束、空闲下来
page.wait_for_load_state(state='networkidle')
browser.close()
我们直接导入崔庆才大大的Gerapy Playwright的包,这个包整合了Scrapy和Playwright
三行代码,轻松实现 Scrapy 对接新兴爬虫神器 Playwright!
GIthub项目地址
但是会有多个问题
This package does not work on Windows
,所以不能在windows上运行,会报NotImplementedError
,我索性在虚拟机上安装了anaconda并运行项目,步骤如下:
scrapy报错twisted.internet.error.ReactorAlreadyInstalledError: reactor already installed,报错见标题,方法见博客
出现gzip.BadGzipFile: Not a gzipped file (b’
仓库Issues里有人给了解决方案:去掉一个中间件,即
然后项目就能起来了
import scrapy
from gerapy_playwright import PlaywrightRequest
from newsCrawler.items import NewsItem
class SinaSpider(scrapy.Spider):
name = 'sina'
allowed_domains = ['news.sina.com.cn']
base_url = 'https://news.sina.com.cn/roll/#pageid=153&lid=2509&k=&num=50&page=1'
def start_requests(self):
# 大佬的包,解决了爬取页面的问题
yield PlaywrightRequest(
self.base_url,
wait_until='domcontentloaded',
callback=self.parse,
# 注意这里要设个等待时间,等ajax数据显示在网页上
sleep=0.5
)
def parse(self, response):
# 第一层是一些ul
news_1_list = response.xpath('/html/body/div[1]/div[1]/div[2]/div[3]/div[2]/div/ul[position()>=1]')
for news_1 in news_1_list:
# 第二层是一些li
news_2_list = news_1.xpath('./li[position()>=1]')
print(len(news_2_list))
# 每个li都是一个新闻
for news in news_2_list:
item = NewsItem()
item['title'] = news.xpath('./span[2]/a/text()').get()
item['url'] = news.xpath('./span[2]/a/@href').get()
yield item
怎么说呢,崔大大讲的大部分思想都能看懂,但是代码就看不懂了。。。
所以直接拿来用了!
仓库地址
安装并运行过程:(Docker-Compose版)
git clone https://github.com/Python3WebSpider/ProxyPool.git
cd ProxyPool
docker-compose up -d
http://IP:5555/random
这章的内容是真的多,一看吓死人,再一看稍微好一点
主要多了几个东西
ItemLoader
class NewsItem(scrapy.Item):
title = scrapy.Field()
url = scrapy.Field()
# 新闻媒体
media = scrapy.Field()
我们进入详情页,注意是详情页!!!from scrapy.loader import ItemLoader
from itemloaders.processors import TakeFirst,Join,Compose
class NewsItemLoader(ItemLoader):
# 默认类中全部变量都只是该页面第一次匹配的结点的数据,且去除左右空格
default_output_processor = Compose(TakeFirst(),str.strip)
# 也可以如下
# title_out = Compose(TakeFirst(),str.strip)
# url_out = Compose(TakeFirst(), str.strip)
Identity
,匹配到的结果的第一个非空值TakeFirst
,将结果通过某种分隔符拼接Join
,组合多个函数Compose
、处理jsonSelectJmes
。还有一些东西可以看看关于Scrapy ItemLoader、MapCompose、Compose、input_processor与output_processor的一些理解CrawlSpider
Spider
类的子类,你暂时可以理解他比Spider多了个rules
元组,里面放了很多Rule
对象,他们包含了在列表页面找到新闻链接和翻页按钮的规则、找到链接后爬取完详情页html后的回调函数等内容。就是说我们现在是在进行新闻列表的解析,即在列表页面获取想要的新闻的链接,因为我们是要通过这些链接获取详情页html的嘛CrawlSpider
也是可以通过genspider
进行生成的,他有几个模板,默认模板的其实就是我们之前用的那个。我们这次选择scrapy genspider -t crawl speaker www.71.cn
然后我们再稍微改下speaker.pyfrom scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from newsCrawler.items import NewsItem
from newsCrawler.loaders import NewsItemLoader
class SpeakerSpider(CrawlSpider):
name = 'speaker'
allowed_domains = ['www.71.cn']
start_urls = ['http://www.71.cn/']
rules = (
Rule(LinkExtractor(restrict_xpaths='/html/body/div[8]/div[1]/div[2]/div[2]/div/ul/li[position()>=1]/a',attrs='href'), callback='parse_detail'),
# 因为只是简短demo我就没找有分页的网站了,分页就是如下,只会进行跳转而不会调用回调函数
# Rule(LinkExtractor(restrict_css='.next'))
)
def parse_detail(self, response):
# 包装item
loader = NewsItemLoader(item=NewsItem(),response=response)
loader.add_value('url',response.url)
loader.add_xpath('title','//*[@id="main"]/div/div[2]/div[1]/div[1]/h1/text()')
loader.add_xpath('media','//*[@id="main"]/div/div[2]/div[1]/div[1]/div[1]/span[2]/text()')
yield loader.load_item()
运行可出结果,但是我大意了,有些详情页面结构不一样的,不过无伤大雅为啥上面弄了什么loader、Rule这些东西啊,仔细看下,他们把爬取列表上的链接、爬取结点的选择器、结点的in-out规则都分开了,且都是对象或字符串的形式,而不是用extract_first之类的方法进行操作,我们完全可以把他们放进配置文件里头啊!我们定义一个通用Spider,它会获取要调用哪个配置文件,再使用这些配置进行爬取,这样就可以大大提高项目的可维护性了
具体代码我就不写了,因为我项目暂时不太大,然后有些地方不是单纯用配置文件抽取变量就能解决的,比如scrapy-playwrigh
t那边,所以只留个思想在这吧
这个只涉及ItemPipeline
我这里也只演示存储进redis
pip3 install redis
REDIS_HOST = '192.168.192.129'
REDIS_PORT = 6379
REDIS_DB_INDEX = 0
REDIS_PASSWORD ="root"
ITEM_PIPELINES = {
'scrapyRedisDemo.pipelines.RedisPipeline': 300,
}
from redis.client import StrictRedis
from redis.connection import ConnectionPool
class RedisPipeline(object):
def open_spider(self, spider):
# 第一个参数是settings.py里的属性,第二个参数是获取不到值的时候的替代值
host = spider.settings.get("REDIS_HOST")
port = spider.settings.get("REDIS_PORT")
db_index = spider.settings.get("REDIS_DB_INDEX")
db_psd = spider.settings.get("REDIS_PASSWORD")
# 连接数据库
pool = ConnectionPool(host=host, port=port, db=db_index, password=db_psd)
self.db_conn = StrictRedis(connection_pool=pool)
def process_item(self, item, spider):
self.db_conn.rpush("news", item['title'])
return item
def close_spider(self, spider):
# 关闭连接
self.db_conn.connection_pool.disconnect()
这里使用的是scrapy-redis
包。当然也可以用消息队列,但我没用。
其实单机scrapy就内置了一个队列存放Request,并由调度器拿取Request,同时他还内置了去重、中断时记录上下文等功能。
但是实现分布式的话,肯定不能用内置的这些队列和功能,这些逻辑应该放到分布式中间件上
首先内置了三种集合:队列、栈、有序集合
然后实现了去重,即将item的hash值作为指纹,同时指纹用set去重存储,每次存入item前先查看是否存入指纹成功,成功则存入item,否则不存入
然后也实现了中断时记录上下文
爬取的是4399最新小游戏
#发现一个东西,记录一下
# 注意这里的操作,可以提取一个大标签下所有标签内部的文本,在爬内容或者正文时很重要
temp = response.xpath('........')
item['content'] = temp.xpath('string(.)').extract()[0]
# BT.py
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyRedisDemo.items import FilmItem
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyRedisDemo.items import GameItem
class Spider4399(CrawlSpider):
name = '4399'
allowed_domains = ['www.4399.com']
start_urls = ['https://www.4399.com/flash/']
rules = (
Rule(LinkExtractor(restrict_xpaths='/html/body/div[8]/ul/li[position()>=1]/a',attrs='href'), callback='parse_detail'),
)
def parse_detail(self, response):
item = GameItem()
item['title'] = response.xpath('/html/body/div[7]/div[1]/div[1]/div[2]/div[1]/h1/a/text()').get()
item['content'] = response.xpath('/html/body/div[7]/div[1]/div[1]/div[2]/div[4]/div/font/text()').get()
item['category'] = response.xpath('/html/body/div[7]/div[1]/div[1]/div[2]/div[2]/a/text()').get()
item['url'] = response.url
yield item
# items.py
import scrapy
class GameItem(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
category = scrapy.Field()
url = scrapy.Field()
重要的来了!!!!!配置Scrapy-Redis
settings.py中添加
# Redis连接参数
REDIS_HOST = '192.168.192.129'
REDIS_PORT = 6379
REDIS_PARAMS = {
'password': 'root'
}
# 将调度器的类和去重的类替换为Scrapy-Redis提供的
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
DUPEFILTER_CLASS='scrapy_redis.dupefilter.RFPDupeFilter'
# 调度队列(三选一,默认为优先队列)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
# 这里配置不持久化,即爬取完后不保存爬取队列和去重指纹集合,方便调试一些
SCHEDULER_PERSIST = False
# 配置itempipeline(将数据存入redis,很耗内存)
ITEM_PIPELINES = {'scrapy_redis.pipelines.RedisPipeline':300}
这里就不详细讲布隆过滤器了,但还是简单提一句:
知道位图bitmap不?比如五个人,每个人有及格和不及格两种情况,就可以用一个五位二进制数,如01100表示第二个和第三个人及格了,但是每多一个人就要多加一位,相对费内存,在数据量大时不太好
于是有一种方法:指定多个哈希函数,对要存入的数进行哈希,然后把每个哈希值对应的位的值变为1,这样容易冲突、误判,但是确实降低了内存消耗,而且原则是能查到的可能是误判,但是查不到的一定不存在
直接用了崔大大的包了
pip install scrapy-redis-bloomfilter
然后在settings.py中增加或修改如下配置
DUPEFILTER_CLASS = 'scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter'
BLOOMFILTER_BIT = 20
此时发现redis中的指纹集合的后缀就是bloomfilter了
多机同时更新改动肯定是很麻烦的事,所以我们需要一个管理平台
提供了管理各Scrapy项目的命令
pip3 install scrapyd-cient
相比于单纯的Scrapyd提供了一系列更方便的API
然后我们就要对项目进行一些配置的修改了
对于项目里的scrapy.cfg,修改url为被部署的主机的url,让scrapyd能访问到,同时为该主机起个别名spyder-2
[deploy:spyder-1]
url = http://192.168.192.129:6800/
在项目里执行scrapyd-deploy vm-1
部署
看到Gerapy我就知道崔大大又来推广自己的项目了(笑
这是一个基于Scrapyd、Django、Vue.js的分布式爬虫管理框架,提供图形化管理服务
pip3 install gerapy
# 在当前目录生成gerapy文件夹
gerapy init
# 初始化数据库
gerapy migrate
# 生成管理员账号
gerapy initadmin
# 默认在8000端口上启动服务
gerapy runserver
http://localhost:8000
admin
pip install pipreqs
pipreqs ./ --encoding utf-8
# 使用了Docker基础镜像之python3.10
FROM python:3.10
# 指定工作目录
WORKDIR /spyder
# 把依赖文件复制到工作目录下,即/spyder
COPY requirements.txt .
# 安装包
RUN pip install -r requirements.txt
# 这里是故意把整个项目的复制放到后面的,具体原因太长了我就不写了
COPY . .
# 容器启动时执行的命令
CMD ["scrapy","crawl","4399"]
# REDIS_URL = 'redis://user:pass@hostname:9001'
REDIS_URL = os.getenv('REDIS_URL')
invalid reference format: repository name must be lowercase
docker build -t 项目名 .
用docker images
查看镜像是否创建成功.env
文件,针对你settings.py
中的环境变量编写,比如我的就是REDIS_URL='redis://:[email protected]:6379'
注意不要有空格出现docker run --env-file .env 镜像名
username
是跟你以后仓库地址有关的,比如仓库名叫demo,username叫lyy,那么仓库地址就是lyy/demodocker login
命令也可以登录docker tag 镜像名:版本 想放的仓库地址:版本
推送镜像到Docker Hubshell docker push 想放的仓库地址:版本
docker run --env-file .env 镜像名
这是一个用yaml配置服务的工具,比Docker命令方便多了
docker-compose.yaml
version: "3"
services:
redis:
# 使用已有的镜像进行构建
image: redis:alpine
container_name: redis
ports:
- "6379"
scrapyRedisDemo:
build: "."
image: "truedude/scrapyredisdemo"
environment:
REDIS_URL: 'redis://:[email protected]:6379'
# 等redis起了才起这个容器
depends_on:
- redis
docker-compose build
docker-compose up
docker-compose push
TODO