介绍

跨时代的作品,一个轻量的虚拟机,让程序员不再纠结于环境部署,更多地集中于代码编写

作用

1
2
3
打包:就是把你软件运行所需的所有东西打包到一起
分发:你可以把你打包好的“安装包"上传到一个镜像仓库,任何人可以拿来即用
部署:拿着“安装包"就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境

下载安装

1
hub.docker.com

首先在虚拟机设置里的处理器那个地方打开虚拟化功能

控制面板-程序-启用或关闭Windows功能,打开hyper-v

然后管理员ps里运行

1
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

然后在应用商店安装wsl,也就是Ubuntu子系统

打开docker之后右上角设置,找到engine,添加国内源

docker部署命令

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run --name <xxx> -d <xxx>
--name 给容器起名字
-d 后台运行并返回id
-p 指定端口,比如9999:1234即开放9999端口供本地连接,连接到docker的1234端口
以安装redis镜像为例,可以指定redis版本
docker run --name myredis -d -p 6379:6379 redis:5.0.14-alpine
这里的redis:镜像的仓库名(repository),表示使用官方提供的 Redis 镜像。
5.0.14-alpine:镜像的标签(tag),用于指定具体的 Redis 版本和底层操作系统。
如果只写 redis,默认会拉取 redis:latest,即最新版本的 Redis。
redis:5.0.14-alpine 不是一个特殊的参数,而是 Docker 运行容器时指定的镜像名,格式为:
<仓库名>:<标签>
然后会在软件中看到镜像
-d参数是后台运行,如果跑起来的时候莫名其妙exit(0)了,可以试试去掉-d参数

docker部署爬虫项目

爬虫项目基于python的,需要docker安装python镜像,这里不能直接pull,需要在ps里开启守护进程,不然镜像启动不了

1
docker run -t -d python:3.8.20-slim-bullseye

比如现在写了一个爬虫,添加requirements.txt

1
requests==2.26.0

然后添加Dockerfile,这里先说一下语法

1
2
3
4
5
6
7
8
9
10
11
12
#FROM 指定新建的image镜像,是基于什么镜像上的
FROM python:3.8.20-slim-bullseye
#MAINTAINER 指定作者
MAINTAINER test
#ADD 添加文件 ADD 参数1 参数2 参数1是本地的文件夹 参数2是docker镜像的文件夹,ADD目录相当于把本地的文件夹复制到docker文件夹下
ADD ./ /code
#WORKDIR 指定docker启动之后,在那个目录启动,这里直接指定在代码所在目录下启动
WORKDIR /code
#RUN 运行命令,这里通过RUN在启动docker的时候就安装依赖,免得手动安装
RUN pip install -r requirements.txt
#CMD 指定开机运行的命令,可以自动跑python代码,但是运行完就直接退出了,需要在log中查看结果
CMD ["python","./main.py"]

制作镜像

1
2
#在docker文件的目录下执行
docker build -t <制作的docker文件名> <制作的镜像生成在那个目录下,用.即可>

pycharm远程同步docker

使用本地pycharm把文件上传到docker容器里面,而且docker容器中的文件也能同步到本地,pycharm中使用的是docker中的解释器

首先下载一个完整的python镜像,i而非精简版这样linux系统才会有基础的功能,也就是挑大的下

1
2
3
4
5
6
7
8
docker run -p 8022:22 -it -d --name pycharm-py37 python:3.7
-it是给Linux系统创建可交互的控制台
apt-get update
apt-get install openssh-server vim
vim /etc/ssh/sshd_config PermitRootLogin yes
passwd root
service ssh restart
使用docker port查看docker端口开放情况

pycharm中打开工具,选择部署,选择配置,选SFTP,然后配SSH

右侧有mappings选项,是做目录映射的,本地路径就是当前项目的路径,部署路径是docker中的路径

然后配置解释器,添加ssh解释器,不要选下面已经存在的ssh,重新输入账号密码,python解释器路径在/usr/local/bin/python,或者用which python去查,然后配置目录同步

后续可以在tools->development中浏览远程主机、文件上传下载等功能

docker目录挂载

制作一个自己的镜像之后,如果我后边持续修改了代码怎么办?重新build和run一个虚拟机吗?

当然不是,只需要目录挂载就可以了

1
-v 本地绝对路径:虚拟机路径

之后修改的时候会自动同步

1
docker run --name testtt -it -d -v 本地项目路径:/tmp <docker名称>

docker虚拟机联通

开多个不同的虚拟机,虚拟机之间相互依赖,多个虚拟机之间需要通信

1
2
3
4
5
6
创建网络
docker network create mynet
网络中开启redis,--network-alias是指定网络别名,代码访问这个网络可以使用别名访问
docker run -d --name redis --network mynet --network-alias redis redis:latest
网络中开启爬虫项目
docker run --name mountpy --network mynet -it -d -v D:\Pycodesicrawl2022\docker容器:/code mountpy

两个容器都会在同一个网络下,爬虫代码可以直接访问

1
2
3
4
5
6
import redis
pool = redis.ConnectionPool(host='redis'port=6379)
r =redis.Redis(connection_pool=pool)
#操作
r.set("xjb",66)
print(r.get("xjb"))

pycharm调用镜像解释器

就是可以不通过镜像创建容器,容器是跑起来的镜像,直接调用镜像文件中的python环境

pycharm的配置解释器的时候选docker,选择镜像

发布自己的镜像包

注册账号之后,创建仓库

1
2
3
4
docker login -u xxx
# mountpy是镜像名,latest是版本,test是创建的仓库名,前面John是用户名
docker tag mountpy:latest John/test
docker push John/test

docker-compose多容器打包

一个项目会依赖多个docker容器

docker-compose用于把多个容器进行打包执行

1
2
3
4
5
6
7
8
9
docker-compose.yml
version:版本
services:下边列出容器
image:使用的镜像
build:使用的dockerfile所在的路径
volumns:挂载的目录
networks:网络
links:多容器连接别名,一般和networks一起出现
tty:保持后台运行不退出

以把redis的镜像和python代码的镜像两个镜像打包为例,上面是通过network来连接的,这次来打包成一个,Dockerfile不变,docker-compose.yml和Dockerfile在同路径下,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version:"2"

services:
redis:
image: redis:latest
networks:
- mynet

mycrawl:
build:
volumes:
- ./:/code
networks:
- mynet
links:
- redis
tty: true

networks:
mynet:
driver: bridge

然后运行docker-compose up -d

docker修改镜像安装目录

默认安装在C:\Users\Administrator\AppData\Local\Docker\wsl

1
2
3
4
5
6
7
8
wsl --shutdown
wsl --export docker-desktop-data D:\docker\docker-desktop-data.tar
wsl --export docker-desktop D:\docker\docker-desktop.tar
wsl --unregister docker-desktop-data
wsl --unregister docker-desktop
wsl --import docker-desktop-data D:\docker\image D:\docker\docker-desktop-data.tar
wsl --import docker-desktop D:\docker\bin D:\docker\docker-desktop.tar
重启docker,再次wsl -l -v正常显示则已经迁移成功

分布式爬取某东商品

对于商品评论比较多的情况,如果是单线程,就会非常慢,特别是商品种类还多的话,那就更慢

爬取商品页

这里评论数和商品id挂钩,而且不在网页url返回的源码里,是动态加载的,需要找到js接口,F12找,很容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"""
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=3&s=56&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=5&s=116&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=7&s=176&click=1
"""
import requests
from bs4 import BeautifulSoup

class JD:
def __init__(self):
self.headers={
'referer': 'https://www.jd.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36'
}

def get_comment_num(self,sku):
url='https://club.jd.com/comment/productCommentSummaries.action?referenceIds='+str(sku)
html=requests.get(url,self.headers)
if html.status_code==200:
CommentCountStr=html.json()['CommentsCount'][0]['CommentCountStr']
return CommentCountStr
return None

def start(self):
for i in range(3):
index_url=f'https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={str(i)}'
print(f'正在爬取第{i+1}页')
html=requests.get(index_url,headers=self.headers)
#print(html.text)
if html.status_code==200:
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('ul.gl-warp.clearfix li')
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')
if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
product_comment=self.get_comment_num(sku)

print("商品名:",product_name,product_price)
print('商品价格:',product_price)
print('商品评论数:',product_comment)
print('商品图片:',product_img)
print('商品店铺:',product_shopname)

if __name__ == '__main__':
jd=JD()
jd.start()

京东商品的页面,随着滚轮往下滑动,会加载出下半页的商品,动态加载的,是ajax,下半页的商品信息的id是根据上半页的商品id作为请求参数访问某个接口,然后返回下半页的商品id的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=3&s=56&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=5&s=116&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=7&s=176&click=1
"""
import requests
from bs4 import BeautifulSoup

class JD:
def __init__(self):
self.headers={
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36'
}

def get_comment_num(self,sku):
url='https://club.jd.com/comment/productCommentSummaries.action?referenceIds='+str(sku)
html=requests.get(url,self.headers)
if html.status_code==200:
CommentCountStr=html.json()['CommentsCount'][0]['CommentCountStr']
return CommentCountStr
return None

def get_another_product(self,skus,page,index_url):
sku_str=''
for num,s in enumerate(skus):
if num==len(skus)-1:
sku_str+=s
break
sku_str+=s+','

url=f'https://search.jd.com/s_new.php?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={page}&show_items={sku_str}'
headers=self.headers
headers['referer']=index_url
html=requests.get(url,headers=headers)
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('li')
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')
if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
print("商品名:",product_name,product_price)
print('商品价格:',product_price)
print('商品评论数:',product_comment)
print('商品图片:',product_img)
print('商品店铺:',product_shopname)

def start(self):
for i in range(3):
index_url=f'https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={str(i)}'
print(f'正在爬取第{i+1}页')
headers=self.headers
headers['referer']='https://www.jd.com/'
html=requests.get(index_url,headers=headers)
#print(html.text)
if html.status_code==200:
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('ul.gl-warp.clearfix li')
skus=[]
#爬取上半页
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')

if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
print("商品名:",product_name,product_price)
print('商品价格:',product_price)
print('商品评论数:',product_comment)
print('商品图片:',product_img)
print('商品店铺:',product_shopname)

#爬取下半页
self.get_another_product(skus,i+1,index_url)

if __name__ == '__main__':
jd=JD()
jd.start()

评论爬取

爬取评论的话,也就是找到商品详情的url,然后去抓评论,这里获取评论也是ajax请求获取的,评论页数的话根据ajax请求返回的数据字段是否为空来判断页数是否已经迭代完了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=3&s=56&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=5&s=116&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=7&s=176&click=1
"""
import requests
from bs4 import BeautifulSoup
import json

class JD:
def __init__(self):
self.headers={
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763'
}

def get_comment_num(self,sku):
url='https://club.jd.com/comment/productCommentSummaries.action?referenceIds='+str(sku)
html=requests.get(url,self.headers)
if html.status_code==200:
CommentCountStr=html.json()['CommentsCount'][0]['CommentCountStr']
return CommentCountStr
return None

def get_comment_data(self,url):
#https://item.jd.com/34608882177.html
if url.startswith('//'):
url='https:'+url
product_id=url.split('/')[-1].split('.')[0]

for i in range(0,100):
url='https://club.jd.com/comment/productPageComments.action'
params={
'productId': product_id,
'score': '0',
'sortType': '5',
'page': i,
'pageSize': '10',
'isShadowSku': '0',
'fold': '1'
}
try:
html=requests.get(url,params=params,headers=self.headers)
print(html.text)
if html.json()['comments']:
coms=html.json()['comments']
for c in coms:
print("com:",c)
except json.decoder.JSONDecodeError:
pass

exit(0)


def get_another_product(self,skus,page,index_url):
sku_str=''
for num,s in enumerate(skus):
if num==len(skus)-1:
sku_str+=s
break
sku_str+=s+','

url=f'https://search.jd.com/s_new.php?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={page}&show_items={sku_str}'
headers=self.headers
headers['referer']=index_url
html=requests.get(url,headers=headers)
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('li')
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
#内部URL
product_inner_url=li.select_one('.gl-i-wrap .p-name a')['href']
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')
if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
# print("商品名:",product_name,product_price)
# print('商品价格:',product_price)
# print('商品评论数:',product_comment)
# print('商品图片:',product_img)
# print('商品店铺:',product_shopname)

def start(self):
for i in range(0,1):
index_url=f'https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={str(i)}'
print(f'正在爬取第{i+1}页')
headers=self.headers
headers['referer']='https://www.jd.com/'
html=requests.get(index_url,headers=headers)
#print(html.text)
if html.status_code==200:
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('ul.gl-warp.clearfix li')
skus=[]
#爬取上半页
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')

if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
print("商品名:",product_name,product_price)
print('商品价格:',product_price)
print('商品评论数:',product_comment)
print('商品图片:',product_img)
print('商品店铺:',product_shopname)

#内部URL
product_inner_url=li.select_one('.gl-i-wrap .p-name a')['href']
self.get_comment_data(product_inner_url)

#爬取下半页
self.get_another_product(skus,i+1,index_url)

if __name__ == '__main__':
jd=JD()
jd.start()

分布式实现

什么是分布式?把大的任务进行分割,然后分配给多个计算机进行处理,最后把这些计算结果综合起来得到最终的结果

1
2
3
4
5
6
7
8
1.分布式意味着大量
2.分布式中会有多台运行的PC端或者容器
3.分布式需要把任务进行分派
4.爬虫分布式结构中要素
提交任务
分派任务
执行任务
存储结果

任务队列和消息队列

1
2
3
消息队列:kafka用于快速消费队列中的消息,要有处理海量数据的能力

任务队列:celery用于执行耗时任务,通常无需迅速返回结果

这里使用的是任务队列

1
2
3
4
5
6
基于Docker容器进行分布式爬虫
使用任务队列celery执行耗时的评论爬取任务
一台Docker虚拟机用于爬取列表页,提交任务
一台Docker虚拟机用于中间的任务提交使用redis存储
一台Docker虚拟机用于后端的任务结果使用mongodb存储
多台Docker虚拟机用于执行耗时任务

也就是说,redis存储的是任务,每个docker去redis中取任务,如果能获取到返回值,就存储数据,如果获取不到返回值,就把任务交还给redis,等待一段时间之后再去轮询

启动任务队列代码myapp.py

1
2
3
4
5
6
7
8
from celery import Celery
# 第一个参数是项目名,第二个参数是任务的py文件名
app = Celery('project', include=['task'])
# 指定配置文件
app.config_from_object('config')

if __name__ == '__main__':
app.start()

config.py

1
2
3
BROKER_URL = 'redis://127.0.0.1' # 使用Redis作为消息代理

CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 # 任务过期时间

task.py,执行任务,如果返回空就交还任务给redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import time

from myapp import app
import requests,json

import pymongo
client=pymongo.MongoClient("mongodb://127.0.0.1:27017")
db=client['lry']
col=db['test']

@app.task
def get_comment_data(url,tim=1):
#https://item.jd.com/34608882177.html
if tim:
time.sleep(tim)
if url.startswith('//'):
url='https:'+url
product_id=url.split('/')[-1].split('.')[0]
headers={
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763'
}
res=[]
for i in range(0,2):
url='https://club.jd.com/comment/productPageComments.action'
params={
'productId': product_id,
'score': '0',
'sortType': '5',
'page': i,
'pageSize': '10',
'isShadowSku': '0',
'fold': '1'
}
try:
html=requests.get(url,params=params,headers=headers)
if html.json()['comments']:
coms=html.json()['comments']
for c in coms:
print("com:",c)
res.append(c)
except json.decoder.JSONDecodeError:
pass

if res==[]:
app.send_task('task.get_comment_data', args=(url,100,))
return None
col.insert_one({'comment':res})
return res

main.py,提交任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=3&s=56&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=5&s=116&click=0
https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page=7&s=176&click=1
"""
import requests
from bs4 import BeautifulSoup
import json
from task import get_comment_data
from myapp import app

class JD:
def __init__(self):
self.headers={
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763'
}

def get_comment_num(self,sku):
url='https://club.jd.com/comment/productCommentSummaries.action?referenceIds='+str(sku)
html=requests.get(url,self.headers)
if html.status_code==200:
CommentCountStr=html.json()['CommentsCount'][0]['CommentCountStr']
return CommentCountStr
return None

def get_another_product(self,skus,page,index_url):
sku_str=''
for num,s in enumerate(skus):
if num==len(skus)-1:
sku_str+=s
break
sku_str+=s+','

url=f'https://search.jd.com/s_new.php?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={page}&show_items={sku_str}'
headers=self.headers
headers['referer']=index_url
html=requests.get(url,headers=headers)
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('li')
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
#内部URL
product_inner_url=li.select_one('.gl-i-wrap .p-name a')['href']
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')
if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
# print("商品名:",product_name,product_price)
# print('商品价格:',product_price)
# print('商品评论数:',product_comment)
# print('商品图片:',product_img)
# print('商品店铺:',product_shopname)

def start(self):
for i in range(0,2):
index_url=f'https://search.jd.com/Search?keyword=python&wq=python&pvid=13e1b05c465b41c6a91ab45c1386f90e&page={str(i)}'
print(f'正在爬取第{i+1}页')
headers=self.headers
headers['referer']='https://www.jd.com/'
html=requests.get(index_url,headers=headers)
#print(html.text)
if html.status_code==200:
soup=BeautifulSoup(html.text,'lxml')
lis=soup.select('ul.gl-warp.clearfix li')
skus=[]
#爬取上半页
for li in lis:
product_name=li.select_one('.gl-i-wrap .p-name a').text.strip().replace('\n','')
product_img='https:'+li.select_one('.gl-i-wrap .p-img a img')['data-lazy-img']
product_price=li.select_one('.gl-i-wrap .p-price strong i').text
product_shopname=li.select_one('.gl-i-wrap .p-shopnum a')

if product_shopname:
product_shopname=product_shopname.text

sku=li['data-sku']
skus.append(sku)
product_comment=self.get_comment_num(sku)
print("商品名:",product_name,product_price)
print('商品价格:',product_price)
print('商品评论数:',product_comment)
print('商品图片:',product_img)
print('商品店铺:',product_shopname)

#内部URL
product_inner_url=li.select_one('.gl-i-wrap .p-name a')['href']

app.send_task('task.get_comment_data', args=(product_inner_url,0,))

#爬取下半页
self.get_another_product(skus,i+1,index_url)

if __name__ == '__main__':
jd=JD()
jd.start()

执行任务靠的celery库自带的,这个不用管,-c指定多少个worker,这里的worker相当于取任务并执行任务的docker

1
celery -A myapp.py worker -P eventlet -c 100 -l info

然后运行main.py,提交任务,相当于main.py负责爬商品页,redis任务负责爬评论页

docker-compose分布式项目

docker-compose.yml,build指定Dockerfile的文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
version: "2"

services:
redis:
image: redis:latest
networks:
- mynet

mongo:
image: mongo:latest
networks:
- mynet

celery1:
build: .
volumes:
- ./:/code
networks:
- mynet
links:
- redis
- mongo
tty: true

celery2:
build: .
volumes:
- ./:/code
networks:
- mynet
links:
- redis
- mongo
tty: true

celery3:
build: .
volumes:
- ./:/code
networks:
- mynet
links:
- redis
- mongo
tty: true

spider:
build: .
volumes:
- ./:/code
networks:
- mynet
links:
- redis
- mongo
tty: true

networks:
mynet:
driver: bridge

Dockerfile

1
2
3
4
5
FROM python:3.7
MAINTAINER lry
ADD ./ /code
WORKDIR /code
RUN pip install -r requirements.txt

这里弄了三个celery docker,每个可以开多个worker