网络爬虫48H快速入门

这是一篇货真价实的48H快速入门爬虫+实战的博文☣今年笔者的大学软件课设题目就是做一个豆瓣电影数据爬虫,所以我将以一个网络爬虫小白快速入门后的视角来讲解

开始前准备

当然小白指的还是有一定基础的,在开始前你需要:

  • 具有基础的编程能力和思维,如:循环、条件判断、编码转换等
  • 基本的数据结构和算法能力:队列(FIFO)、哈希表等
  • 掌握一门你的工具编程语言,爬虫一般推荐Python(优美好调试and菜鸟教程传送门
  • 有HTTP、URL等WEB传输的基本概念(比如404、200和500是否能让你想起什么?)
  • 能看懂HTML文件的格式(附菜鸟教程传送门
  • 有数据库的概念

什么是爬虫?

概况成一句话:爬虫就是按照你指定的规律不断循环提取页面里你需要的信息(包括下一步要爬取的网页链接)。因为互联网就是一个通信“网”🕸,所以我们的数据寻找程序就像是一只在“网”上的各个“节点”(有URL的页面)间“爬来爬去”(跳转),这就是爬虫名字的由来。下图是爬虫的工作原理:

爬虫原理简图

比如说,这次(笔者的软件课设)要爬取豆瓣电影网的电影数据。让我们先缕一缕要求:

  • 爬虫入口:豆瓣Top250页面
  • 以广度优先的方法在网络上爬开
  • 提取电影的数据
    • 名字
    • 年份
    • 类型
    • 国家
    • 导演
    • 编剧
    • 主要演员
    • 上映日期
    • 其他名字
    • 电影页面链接
  • 把提取到的电影数据存储进格式化(CSV)文件
  • 提取到的电影数据存入图数据库(Neo4j)并建立适当的节点关系
  • 提取当前页面中推荐的其他电影作为爬虫爬开的方式
  • 使用代理服务器加速爬虫运行速度和安全性

开始动手吧

在这个简单的豆瓣爬虫中,笔者使用Python作为主语言,因为Py做开发时可用的库比较多且调用起来比较方便。接下来将要用到的库有:

  • urllib3:功能强大的HTTP客户端库;用来访问网站并且下载HTML页面数据,支持使用代理服务器
  • BeautifulSoup:解析HTML文件的库;对滴🎈分析网页就是用这个为主
  • time:提供系统时间服务的库;比如用来做延时、获取当前系统时间等
  • re:正则表达式库;分析网页的时候用来解决BeautifulSoup无法查找的数据
  • deque:双向队列;对其重新封装定义就是本次咱们要使用的广义遍历URL管理器啦
  • json:功能和其包名字一样,用来解析和转码JSON字符串的

知道要用什么工具包,那接下来就让笔者带大家开始快速入门+动手开发吧🚀

笔者使用的Python开发工具为:
Anaconda🐍(Python环境管理器)+Spyder🕷(数据分析强推的IDE)

抓取(下载)第一个页面内容

首先尝试爬取的页面是豆瓣电影top250主页面,也就是下面这个:

豆瓣top250

使用urllib3工具包即可完成对网页的访问和下载,代码实例如下:

1
2
3
4
5
6
7
8
9
10
11
import urllib3  # 导入工具包

downloader = urllib3.PoolManager(); # 实例化下载器

urlNow = "https://movie.douban.com/top250?start=0"; # 要下载的页面的URL,这里用一个变量来方便表示

res = downloader.request('GET',urlNow) # 下载指定url的数据

res.status # 查看HTTP响应状态码;接收到服务器的正常响应是200

res.data # 查看响应的页面数据⚠可能会跳出很多很多内容噢

如果在笔者的电脑上运行上代码如下:

urllib3

如果你的状态码和笔者一样是200,那么就代表你下载成功啦!😘很简单对吧

查找HTML中的指定对象

让我们用浏览器再次打开上面的豆瓣电影top250主页面,并且打开“检查”功能查看网页结构数据。如下:

网页检查

在使用工具前,我们先人工analyse一下我们需要的内容数据,从上图中对网页的源代码检查中我们可发现:该静态网页的主电影内容在标签页ol中,具有class = "grid"的属性;ol下有10个电影的li标签,让我们再打开看看细节:

li细节

这时候所有数据的存储都很显然了吧!(博客的日期为2021年1月,可能你阅读时会有些许不同)

  • 电影的海报存储在<div class="pic">之内
  • 电影的数据主体为<div class="info">
    • 电影的名字存储在<div class="hd">
    • 导演、评价等数据存储在<div class="bd">

接下来让我们对其进行分析,代码如下:

1
2
3
4
5
from bs4 import BeautifulSoup # 导入“靓汤”分析包

wholeHTML_bsObj = BeautifulSoup(res.data,"html.parser",from_encoding="utf-8"); # 使用HTML数据实例化“靓汤”对象

mainGrid = wholeHTML_bsObj.find("ol",class_ = "grid_view"); # 提取上面我们发现的ol主对象

你可以打印一下mainGrid对象看看结果,让我们继续吧

提取出了主要的数据块,就可以用同样类似find的操作继续分析查找需要的数据

1
2
movieBrief_list = mainGrid.find_all("li");  # 提取出该块中所有的单个电影,返回的是一个list对象
movieBrief_list[0]

让我们看看这时候提取到的电影数据list的第一个对象内容吧,如下:

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
<li>
<div class="item">
<div class="pic">
<em class="">1</em>
<a href="https://movie.douban.com/subject/1292052/">
<img alt="肖申克的救赎" class="" src="https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.jpg" width="100"/>
</a>
</div>
<div class="info">
<div class="hd">
<a class="" href="https://movie.douban.com/subject/1292052/">
<span class="title">肖申克的救赎</span>
<span class="title"> / The Shawshank Redemption</span>
<span class="other"> / 月黑高飞(港) / 刺激1995(台)</span>
</a>
<span class="playable">[可播放]</span>
</div>
<div class="bd">
<p class="">
导演: 弗兰克·德拉邦特 Frank Darabont 主演: 蒂姆·罗宾斯 Tim Robbins /...<br/>
1994 / 美国 / 犯罪 剧情
</p>
<div class="star">
<span class="rating5-t"></span>
<span class="rating_num" property="v:average">9.7</span>
<span content="10.0" property="v:best"></span>
<span>2241363人评价</span>
</div>
<p class="quote">
<span class="inq">希望让人自由。</span>
</p>
</div>
</div>
</div>
</li>

是不是和上面我们“检查”网页时候的第一个电影的内容完全相符呢!😜

存储数据

其实会了上面的下载数据和分析数据,基本你就拥有了爬虫的两大主要功能了,存储数据算是最后的环节了。对于存储,本次的豆瓣电影由于对象间存在着联系,所以笔者选用Neo4j图数据库进行存储电影、导演等对象节点,再建立节点间的边来表示关系。对于一般爬虫,直接存储数据就可了,所以笔者也使用了csv来存储每个电影的属性

图数据库(Neo4J)存储

Neo4J数据库的学习和入门,笔者推荐看W3Cschool的教程。最多2个小时就能学会了

在使用数据库前,我们先对数据的组织结构进行设计,总共设计了5个对象和

  1. 电影内容对象(movie)

    属性名称(键) 参数类型(值) 说明
    name str字符串 电影名字
    originalName str 电影原名
    otherName str 其他名字
    link str 详细内容链接
    country str 国家
    year int正整数 上映年份
    date int 上映时间,如0120
    length int 电影时长
    score float 电影评分
  2. 导演对象(director)

    属性名称(键) 参数类型(值) 说明
    name str 导演名字
    originalName str 原名
    link str 导演页面链接
  3. 演员对象(actor)

    属性名称(键) 参数类型(值) 说明
    name str 演员名字
    originalName str 原名
    link str 演员页面链接
  4. 编剧对象(scriptwriter)

    属性名称(键) 参数类型(值) 说明
    name str 编剧名字
    originalName str 原名
  5. 电影类型对象(movieType)

    属性名称(键) 参数类型(值) 说明
    name str 电影类型
  6. 节点间关系

    源对象 关系 目标对象 说明
    director 导演 movie
    movie 电影类型属于 movieType
    actor 参演 movie
    actor 合作过 director 双向关系
    movie 喜欢本电影也喜欢 movie 双向关系

对Neo4J的操作实在是太简单了,将在neo4j客户端操作的控制指令以字符串的形式传给GraphDatabase.driver.session()创建的任务对象运行write_transaction方法就可以啦!笔者设计的数据库内容,封装为一个类后如下:

如果你连最最最最基础的Python面向对象都不晓得的话,下面的内容也许会将你的阅读兴趣直接清零(虽然看起来很多,但是其实大部分都是注释hhh)

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# -*- coding: utf-8 -*-
"""
Created on Fri Dec 11 16:27:43 2020

@author: singularity
"""

import neo4j
from neo4j import (GraphDatabase, WRITE_ACCESS, unit_of_work)

class myNeo4j:
def __init__(self,
url = "neo4j://localhost:7687",
userName = "neo4j",
password = "1234567890"):
'''
创建一个neo4j图数据库操作对象

Parameters
----------
url : str
数据库路径,默认为本地的7687端口路径
userName : str
登陆用户名
password : str
登陆密码

Returns
-------
None.

'''
self.driver = GraphDatabase.driver(url, auth=(userName,password))

def __del__(self):
'''
对象析构函数

Returns
-------
None.

'''
self.driver.close()

'''
neo4j库进行增删改查时使用的内部session
禁止外部调用!
BEGIN
'''
def __search_Movie(self,tx,movieName):
'''
功能:电影查找
说明:根据电影主名字查找
'''
res = tx.run('''MATCH (
a:movie {name: $name}) RETURN (a) AS mv''',
name=movieName
)
resList = []
for i in res:
resList.append(i["mv"])
return resList

def __add_Movie(self,tx,movieName,originalName,otherName,
link,country,year,date,length,score):
return tx.run('''
MERGE (
m:movie {
name : $movieName,
originalName : $originalName,
otherName : $otherName,
link : $link,
country : $country,
year : $year,
date : $date,
length : $length,
score : $score})
RETURN (m)
''',
movieName = movieName,
originalName = originalName,
otherName = otherName,
link = link,
country = country,
year = year,date = date,
length = length,score = score).single().value()



def __establishRelation(self,tx,
srcType,srcName,
tarType,tarName,
relation):
return tx.run('''
MATCH (a:'''+srcType+''' {name:$srcN}),
(b:'''+tarType+''' {name : $tarN})
CREATE (a) -[r:'''+relation+''']-> (b)
RETURN a,b,r
''',
srcN = srcName,
tarN = tarName).single().value()
'''
neo4j内部session定义
END
'''

def add_Movie(self,
movieName,
originalName = '', otherName = '',
infPageLink = '',
country = '',
year = 0,date = 0,
length = 120,score = 0):
'''


Parameters
----------
movieName : str
电影名字.
originalName : str
电影原名.
otherName : str
其他名字.
infPageLink : str
电影内容页面链接

Returns
-------
TYPE
DESCRIPTION.

'''

with self.driver.session() as session:
return session.write_transaction(self.__add_Movie,
movieName,
originalName, otherName,
infPageLink,
country,
year,date,
length,score
);

def add_director(self,name,link = ''):
'''
添加导演

Parameters
----------
name : STR
导演名字.
link : STR
导演页面链接.

Returns
-------
TYPE
DESCRIPTION.

'''
def __add_director(self,tx,name,link):
return tx.run('''
MERGE (d:director {
name : $directorName,
link : $directorLink})
RETURN (d)
''',
directorName = name,
directorLink = link).single().value()
with self.driver.session() as session:
return session.write_transaction(__add_director,
name,link);

def add_scriptwriter(self,name,link = ''):
'''
添加编剧人物对象

Parameters
----------
name : STR
编剧名字
link : STR
编剧的个人页面

Returns
-------
None.

'''
def __add_scriptwriter(tx,name,link):
return tx.run('''
MERGE (d:scriptwriter {
name : $directorName,
link : $directorLink})
RETURN (d)
''',
directorName = name,
directorLink = link).single().value()

with self.driver.session() as session:
return session.write_transaction(__add_scriptwriter,name,link);

def add_actor(self,
name,
originalName,
sex = True,
birthday = '1997-14-2',
country = '',
link = ''):
'''
添加演员

Parameters
----------
name : STR
演员名字.
originalName : STR
原名.
sex : bool
性别,男生true,女生false.
birthday : int
生日时间戳,例如:20200819.
country : STR
演员所在国家.
link : STR
演员页面链接.

Returns
-------
TYPE
DESCRIPTION.

'''
def __add_actor(self,tx,name,originalName,sex,birthday,country,link):

return tx.run('''
MERGE (a:actor {
name : $actorName,
originalName : $actorOrgName,
sex : $actorSex,
birthday : $actorBirthday,
country : $actorCountry,
link : $actorLink})
RETURN (a)
''',
actorName = name,
actorOrgName = originalName,
actorSex = sex,
actorBirthday = birthday,
actorCountry = country,
actorLink = link).single().value()
with self.driver.session() as session:
return session.write_transaction(__add_actor,
name,originalName,sex,
birthday,country,link);

def add_movieType(self,mType):
'''
添加电影类型

Parameters
----------
mType : STR
电影类型名,例如:喜剧、动作.

Returns
-------
TYPE
DESCRIPTION.

'''

def __add_movieType(self,tx,mType):
return tx.run('''
MERGE (t:movieType {
name : $mtype})
RETURN t
''',
mtype = mType).single().value()

with self.driver.session() as session:
return session.write_transaction(self.__add_movieType,mType);

def establishRelation_director2movie(self,
directorSrcName,movieTargName,
relation = "导演"):
'''
建立导演和电影的关系

Parameters
----------
directorSrcName : STR
导演名字.
movieTargName : STR
电影名字.
relation : STR
关系.

Returns
-------
TYPE
DESCRIPTION.

'''
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"director",directorSrcName,
"movie",movieTargName,
relation);

def establishRelation_srcriptWriter2movie(self,srcName,targName,relation = "编剧"):
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"scriptwriter",srcName,
"movie",targName,
relation);

def establishRelation_movie2type(self,srcName,targName,relation = "电影类型"):
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"movie",srcName,
"movieType",targName,
relation);

def establishRelation_actor2movie(self,srcName,targName,relation = "参演"):
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"actor",srcName,
"movie",targName,
relation);

def establishRelation_actor4director(self,srcName,targName,relation = "合作关系"):
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"actor",srcName,
"director",targName,
relation);

def establishRelation_movie4movie(self,srcName,targName,relation = "喜欢该电影的也喜欢"):
with self.driver.session() as session:
return session.write_transaction(self.__establishRelation,
"movie",srcName,
"movie",targName,
relation);


def cleanDatabase(self):
'''
清空数据库里面的所有数据

Returns
-------
None.

'''
def __cleanRelation(tx):
return tx.run('''MATCH (n)-[r]->(m) DELETE r''')

def __cleanNode(tx):
return tx.run('''MATCH (n) DELETE n''')

# def __cleanAll(tx):
# return tx.run('''MATCH (n)-[r]->(m) DELETE r,n,m''')

with self.driver.session() as session:
session.write_transaction(__cleanRelation);
session.write_transaction(__cleanNode);

def search_Movie(self,movieName):
'''
查找电影资料

Parameters
----------
movieName : STR
查找的电影名称.

Returns
-------
None.

'''
with self.driver.session() as session:
return session.read_transaction(self.__search_Movie,movieName);

if __name__ == "__main__":
neoObj = myNeo4j();

本地CSV文件存储

笔者在使用pip官方的CSV操作API时,发现对爬虫爬取到的韩文支持并不好。看报错应该是官方CSV存储时默认将数据转码为GBK格式,没有韩文的编码内容。因为CSV文件的格式化准则特别简单,所以笔者索性直接自己写一个简易的CSV操作对象代码如下:

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
# -*- coding: utf-8 -*-
"""
Created on Thu Dec 10 19:37:47 2020

@author: singularity
"""


class myCSV:
'''
这是一个非标准库的csv写入包
'''
def __init__(self,fileAddr,fileOperateType = "wb",encoding = "utf-8"):
'''
创建一个CSV文件写入对象

Parameters
----------
fileAddr : str
文件路径字符串
fileOperateType : str
文件打开方式
encoding : str
存储时的编码方式

Returns
-------
None.

'''
self.__csvFile = open(fileAddr,fileOperateType)
self.__csvName = fileAddr
self.__encodingMethod = encoding


def writerow(self,rowData):
'''
向csv文件写入一行数据

Parameters
----------
rowData : list of string
要写入的字符串列表

Returns
-------
None.

'''
writeData = str()
for now in rowData:
writeData += str(now)
writeData += ','
writeData = writeData[:-1]+'\n'
self.__csvFile.write(writeData.encode(self.__encodingMethod))

def __del__(self):
'''
默认析构函数

Returns
-------
None.

'''
self.__csvFile.close()

CSV格式化文件语法概况:每一行数据以换行符\n标志;行内的列数据以英文逗号,标志

URL管理器

本次设计的爬虫搜索逻辑是“广度优先”遍历,所以很容易想到用具有FIFO性质的队列来实现,但是为了提高代码的整体鲁棒性,笔者还是对原生deque库进行了二次封装。那么为什么要用双向队列实现呢?主要是为了支持操作回滚和其他支持,从双向队列和队列的实现原理上知道两者在性能上基本是一样的(理论读写速度一样,但双向占用的存储空间更大一点)

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
# -*- coding: utf-8 -*-

from queue import Queue,deque

class urlCtrl:
def __init__(self):
'''
创建URL管理器对象

Returns
-------
None.

'''
self.__queueCache = deque();

def empty(self):
if len(self.__queueCache) == 0:
return True;
else:
return False;

def add_URL(self,URL,Name = ''):
'''
向管理器中添加URL和对应的名字

Parameters
----------
URL : str
url字符串.
Name : str, optional
网址名称. The default is ''.

Returns
-------
None.

'''
self.__queueCache.append((Name,URL));

def get_URL(self):
'''
从URL管理器中提取一个单位

Returns
-------
TYPE
DESCRIPTION.

'''
return self.__queueCache.popleft();

def rollBack_URL(self):
'''
回滚上一次放进管理器的URL数据

Returns
-------
(Name,URL).
名字+URL
'''
return self.__queueCache.pop();

def clear(self):
'''
清空管理器中所有的数据
'''
self.__queueCache.clear();

def __len__(self):
return len(self.__queueCache)

if __name__ == '__main__':
ctrl = urlCtrl();

Tip:二次封装的好处就是,你可以随意变换URL遍历算法而不用大幅度修改主程序

如果你有性能癖,可自己写一套节省存储空间的可回滚队列。当然也可以用高性能的C++写了之后封装给Py调用

把爬虫逻辑搭起来

至此,爬虫所需要的四大基本功能(下载、分析、存储、URL管理)就都有了,接下来就把他们全部都进行有机拼接吧!也就是本次笔者课设的完整代码,当然在Github开源了仓库大家可直接clone:

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# -*- coding: utf-8 -*-

import urllib3
import bs4
from bs4 import BeautifulSoup
import time
import re

import myCSV
import myNeo4j
import myURLcontroller
import myProxy

'''
用户参数配置
'''
LIMIT_OBEJECT = 100 # 闲置爬取的数据量,单位:个电影

LIMIT_RATE = 3 # 爬虫速率

USE_PROXY = False # 选择是否使用代理服务

CLEAN_ORIGINAL_DATABASE = True # 爬虫开始前删除原始数据库数据

SAVE_HTMLFILE = True # 是否保存爬取过程中的HTML页面内容
SAVE2CSV = True # 是否把爬取的数据存储到本地csv文件中
'''
用户参数配置完毕
'''

# 获取运行时间
RUNNING_TIME = time.strftime('%Y-%m-%d,%H:%m',time.localtime(time.time()))

#下载器
downloader = urllib3.PoolManager();

#URL管理器
urlManager = myURLcontroller.urlCtrl();

#代理获取器
proxyPool = myProxy.myProxy("http://stream.singularity-blog.top",5010);

#图数据库
GDB_movie = myNeo4j.myNeo4j();

#CSV读写器
if SAVE2CSV:
movieBrief_csvWriter = myCSV.myCSV("./csvData/movieBrief"+RUNNING_TIME+".csv","wb","utf-8")

movieBrief_csvWriter.writerow(["电影名字",
"电影原名",
"其他名字",
"详细内容链接",
"导演",
"编剧",
"主演",
"年份",
"电影类型",
"国家/地区",
"语言",
"上映日期",
"片长"
])

if CLEAN_ORIGINAL_DATABASE:
GDB_movie.cleanDatabase()

'''
爬取基础的250个页面数据到url数据器里
'''

print('''-------开始爬取根数据--------''')
urlRoot = "https://movie.douban.com/top250?start=" # 根爬取地址

pageNum = 0 #遍历用的页面号
while pageNum < 10:

urlNow = urlRoot + str(pageNum*25) #当前要爬的路径

r = downloader.request('GET',urlNow) #下载HTML数据

if(r.status != 200): #检测是否请求失败(被豆瓣查水表)
print("get error,status code:",str(r.status))
exit(-1)

pageNum += 1 #迭代

'''
存储网页原HTML数据
用户可配置开启
'''
if SAVE_HTMLFILE:
localSaveFile = open("./sourceHTMl/top250Main/豆瓣电影top250-"+str(pageNum)+".html",mode = "wb")

localSaveFile.write(r.data) #存储网页数据

localSaveFile.close() #关闭文件

'''
开始分析网页文件
'''
wholeHTML_bsObj = BeautifulSoup(r.data,"html.parser",from_encoding="utf-8")

mainGrid = wholeHTML_bsObj.find("ol",class_ = "grid_view")

movieBrief_list = mainGrid.find_all("li")

for movie in movieBrief_list:

movieName = movie.find_all("span",class_ = "title") #查电影的名字
movieOtherName = movie.find("span",class_ = "other") #查电影的其他名字
movieLink = movie.find('a').attrs['href'] #电影详细内容链接

#防止电影没有第二名字
movieName.append(movieName[0])

# if SAVE2CSV: # 存储数据到CSV文件
# movieBrief_csvWriter.writerow([movieName[0].text.replace(u'\xa0', u' '),
# movieName[1].text.replace(u'\xa0', u' ').replace(" / ",''),
# movieOtherName.text.replace(u'\xa0', u' ').replace(" / ",' '),
# movieLink
# ])

# 电影名字字符格式调整
movieName[0] = movieName[0].text.replace(u'\xa0', u' ');
movieName[1] = movieName[1].text.replace(u'\xa0', u' ').replace(" / ",'');

# 存储数据到知识图谱库
# GDB_movie.add_Movie(movieName[0],
# movieName[1],
# movieOtherName.text.replace(u'\xa0', u' ').replace(" / ",' '),
# movieLink
# )

# 存储页面链接到(广义爬虫)URL管理器中
urlManager.add_URL(movieLink,movieName[0])

'''
防止查水表,适当限速
'''
time.sleep(LIMIT_RATE)

print("爬取了第"+str(pageNum)+"次根数据")

print("top250主页面数据爬取完成")



'''
普通广义爬虫
'''
pageNum = 0

while LIMIT_OBEJECT > pageNum:

'''循环迭代'''
pageNum += 1; # 迭代
print("-----正在爬取第"+str(pageNum)+"次页面数据-----")

if urlManager.empty():
break;
movieNow = urlManager.get_URL() #获取当前爬取页面的URL
urlNow = movieNow[1];
name = movieNow[0];

'''从网站下载数据'''
time.sleep(LIMIT_RATE) # 限速
r = downloader.request('GET',urlNow) #下载HTML数据


'''
开始分析页面
'''
wholeHTML_bsObj = BeautifulSoup(r.data,"html.parser",from_encoding="utf-8")

# 年份数据
year = wholeHTML_bsObj.find("span",class_ = "year").text.replace('(','').replace(')','')

#电影详细数据块提取
infoDiv = wholeHTML_bsObj.find("div",id = "info")

#电影主要人物数据提取:导演、编剧、主演
mainAttrList = infoDiv.find_all("span",class_ = "attrs")
director = mainAttrList[0].text # 导演
directorLink = mainAttrList[0].find('a').attrs['href']; # 导演页面链接
scriptwriter = mainAttrList[1].text # 编剧
leadRoleA = mainAttrList[2].find_all("a") # 主演提取
leadRoleList = []
for i in leadRoleA:
# 主演数据格式转换,转为元组对:(主演名字+主演页面链接)
leadRoleList.append((i.text,i.attrs['href']))

#电影类型提取
genre = infoDiv.find_all("span",property = "v:genre")
genreList = [];
for i in genre: # 格式转换
genreList.append(i.text)
#国家
matchCountry = re.search( r'制片国家/地区:.*\n', infoDiv.text, re.M|re.I)
country = matchCountry.group().replace("制片国家/地区:",'').replace("\n",'').replace(" ",'')
#语言
matchLanguage = re.search( r'语言:.*\n', infoDiv.text, re.M|re.I)
language = matchLanguage.group().replace("语言:",'').replace("\n",'').replace(" ",'')
#上映日期
releaseDataRes = infoDiv.find_all("span",property = "v:initialReleaseDate")
releaseDataList = []
for i in releaseDataRes: # 格式转换
releaseDataList.append(i.text)
#片长
movieLength = infoDiv.find("span",property = "v:runtime").text
# 其他名字

matchOtherName = re.search(r'又名:.*\n', infoDiv.text, re.M|re.I)
if matchOtherName is None:
matchOtherName = '';
otherName = '';
else:
otherName = matchOtherName.group().replace("又名:",'').replace("\n",'').replace(" ",'')


'''开始导入数据至图数据库'''
GDB_movie.add_Movie(movieName = name,
originalName = name,
otherName = otherName,
infPageLink = urlNow,
country = country,
year = int(year),
date = str(releaseDataList),
length = movieLength
) # 添加电影至数据库

for i in leadRoleList:
GDB_movie.add_actor(name = i[0],
originalName = i[0],
link = i[1]); # 添加演员
GDB_movie.establishRelation_actor2movie(i[0],name); # 建立关系

for i in genreList:
GDB_movie.add_movieType(i); # 添加电影类型
GDB_movie.establishRelation_movie2type(name,i); # 建立关系

GDB_movie.add_director(director,directorLink); # 添加导演
GDB_movie.establishRelation_director2movie(director,name); # 建立导演关系

GDB_movie.add_scriptwriter(scriptwriter); # 添加编剧
GDB_movie.establishRelation_srcriptWriter2movie(scriptwriter,name); # 添加编剧关系



'''保存数据至本地'''
if SAVE2CSV:
movieBrief_csvWriter.writerow([name, # 电影名字
name, # 电影原名
otherName, # 其他名字
urlNow, # 电影页面链接
director, # 导演
scriptwriter, # 编剧
str(leadRoleList), # 主演
year, # 年份
str(genreList), # 类别
country, # 国家
language, # 语言
str(releaseDataList),# 发行上映日期
str(movieLength) # 电影长度
])

'''更新URL管理器新数据'''
# 电影推荐数据块提取
recmDiv = wholeHTML_bsObj.find("div",id = "recommendations")
recmMovieFind = recmDiv.find_all("dd");
# 提取当前页面可爬的URL
recmMovieList = [];
for i in recmMovieFind:
recmMovieList.append((i.find('a').text,i.find('a').attrs['href']))
#判断找到的新页面是否已经爬过了,如果爬过则跳过
for i in recmMovieList:
if len(GDB_movie.search_Movie(i[0])) == 0: # 查看是否爬过了
urlManager.add_URL(i) # 添加到URL管理器中


print("完成爬取第"+str(pageNum)+"次页面数据")
'''
程序退出前收尾工作
'''
print("爬虫运行完成,共爬取电影数据:"+str(pageNum)+"条。");
print("------------正在退出程序--------------");

if SAVE2CSV: # 关闭CSV读写器
del movieBrief_csvWriter

print("bye~")

设置爬虫的爬取数量为100后,让我们打开Neo4J图数据库,查看所有的节点和关系,看下图的结果还是不错的吧

爬虫效果

这个爬虫其实是个半成品,很明显笔者是使用sleep做延时实现限速的,并且没有使用代理,限速功能使能后爬取速率就是网页间读取时间间隔。读者若有时间可以把代理服务器的功能加上,来大幅度提高访问效率,这边推荐一个还不错的代理服务爬虫项目:Proxy_Pool

代理服务器

首先让我们先晓得为什么要有代理服务器🙃这些大前提是你必须要知道的:

  • 任何服务器资源都是有限的,服务器只不过是一个稳定不易崩的电脑罢了
    • 服务器CPU一般主频还没有PC的高噢
    • 再🐂的服务器,都是通过网卡连接上互联网的
    • 服务器也是要插电的,电费也是网站运营的一大开销
  • 服务器访问的请求多了,网站会直接掉线!
  • 你的爬虫无法对网站运营商带来经济受益(不符合经济学规则的事情都会被人们排斥)
  • 爬虫的访问速率是人类的好几百~几万倍,也就是对服务器来说需要承受不正常的压力大的非人的无受益的访问
  • 换位思考你是运营商,就知道为什么需要代理

所以像豆瓣这样的网站都不希望被爬虫占有太多资源,那么就出现了一系列封杀爬虫的手段。比如:黑名单IP、限制某IP的访问频率等等

如果我们的爬虫不做限速直接访问目标服务器,由于访问频率太高了瞬间就被查水表了。那么,代理服务器的作用就很明显了,我们自己不能访问,那就让别人帮我们访问,然后再把数据传给我们就好了😁

假设豆瓣的IP限制频率为最快2秒访问一次服务器,而我们电脑处理一个页面数据时间为40ms,和代理服务器交互的时间忽略。那么我们需要24个代理服务器,轮流使用(包括本机在内)IP访问目标服务器就可以不被查水表了!🎈

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 RY.J
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信