豆瓣爬虫实例

本文提供 3 个例子来说明如何利用 Python 爬取豆瓣的数据。

用 requests 获取页面

三个实例都要用到 request 库来获取页面资源。

1
2
3
4
5
6
7
8
9
10
11
def getHTMLText(url): # 获取网页的HTML源码
try:
# 设置响应等待时间为30秒
r = requests.get(url, timeout=30)
# 检查状态码是否为200,否则抛出异常。
r.raise_for_status()
# 返回请求的相应内容。
return r.text
except:
# 抛出异常则重试
return getHTMLText(url)

实例一:豆瓣国产电视剧爬虫

观察所需爬取的页面

所需爬取的页面为按时间排序的国产电视剧评分等信息

查看其源代码发现电视剧列表内容是由 js 加载的。

获取电视剧豆瓣地址列表

打开控制台,观察一下 network 中往返的数据。


观察到网页通过查询 https://movie.douban.com/j/search_subjects?type=tv&tag=国产剧&sort=time&page_limit=20&page_start=0
其中 type 是分类(movie,tv);tag 是小分类(热门,美剧,英剧,韩剧,日剧,国产剧,港剧,日本动画,综艺);sort 是排序(recommend,time,rank);page_limit 是每页展示的电视剧数量(不可修改);page_start 是起始页面,为 20 的倍数(例如,第一页是 0,第二页是 20 等等)。
返回的是 json 数据,直接读取其 url 字段即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def getTVdramalist(url, Numofpage):
# 电视剧豆瓣地址列表
TVdramalist = []
# Numofpage为爬取页面数
for i in range(int(Numofpage)):
# 获取第i页网页的资源,20为每页的电视剧记录数
text = getHTMLText(url+str(i*20))
# 由于此url返回json对象,所以将获取到的资源转换为json格式
json_text = json.loads(text)
# 获取json中的url字段,存入TVdramalist列表
for j in range(20):
dic = json_text['subjects'][j]
TVdramalist.append(dic['url'])
return TVdramalist

moivelist_url = 'https://movie.douban.com/j/search_subjects?type=tv&tag=国产剧&sort=time&page_limit=20&page_start='
Numofpage = 5
# 获取最新5*20条电视剧的详情页链接
moivelist = getTVdramalist(moivelist_url, Numofpage)

分析电视剧详情页

对于上面获取的每一个 url 对应一个电视剧详情页。

查看其源代码发现电视剧详情内容是由 HTML 加载的。

则用 beautifulsoup 库结合正则表达式 re 库分析并获取所需字段。

1
2
3
4
5
6
7
8
9
10
def getTVdramainfo(url, writer):
text = getHTMLText(url)
# 将获取到的资源转换为soup对象
soup = BeautifulSoup(text)
# 找到所需字段
name = soup.find(property="v:itemreviewed").string
rating = float(soup.find(property="v:average").string)
votes = int(soup.find(property="v:votes").string)
episode = int(re.search(u'集数:</span> (\d+)<br/>', text).group(1))
duration = int(re.search(u'单集片长:</span> (\d+)分钟<br/>', text).group(1))

从property为v:itemreviewed的标签可以得到电视剧名。
从property为v:average的标签可以得到评分。
从property为v:votes的标签可以得到评分人数。
<span class="pl">集数:</span> 12<br/><span class="pl">单集片长:</span> 55分钟<br/> 的标签属性和其他标签的属性都一样,这里直接用正则表达式进行查找反而更方便。

存储所爬信息到数据库

这里使用 sqlite3 库实现

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
def dbtablecreate():
# 执行一条SQL语句,创建ChineseTVdrama表
try:
cursor.execute('CREATE TABLE ChineseTVdrama (name TEXT PRIMARY KEY, rating REAL, votes INT, episode INT, duration INT)')
print('Table created successfully')
except Exception as e:
print(e)

def dbinsertorupdate(name, rating, votes, episode, duration):
# 执行一条SQL语句,插入一条记录
try:
cursor.execute('INSERT INTO ChineseTVdrama (name, rating, votes, episode, duration) VALUES (?, ?, ?, ?, ?)', (name, rating, votes, episode, duration))
print('data insert successfully')
except:
# 如果该数据已经存在,则插入异常,改用更新数据
try:
cursor.execute('UPDATE ChineseTVdrama SET rating = ?, votes = ?, episode = ?, duration = ? WHERE name = ?', (rating, votes, episode, duration, name))
print('data update successfully')
except Exception as e:
print('update: ' + str(e))

def dbquery(name):
# 以电视剧名称为关键词查询数据库
try:
cursor.execute('SELECT name, rating, votes, episode, duration FROM ChineseTVdrama WHERE name = ?', (name))
for row in cursor:
return row
except Exception as e:
print('query: ' + str(e))

moivelist_url = 'https://movie.douban.com/j/search_subjects?type=tv&tag=国产剧&sort=time&page_limit=20&page_start='
Numofpage = 5
# 获取最新5*20条电视剧的详情页链接
moivelist = getTVdramalist(moivelist_url, Numofpage)
# 连接到SQLite数据库
# 如果文件不存在,会自动在当前目录创建
doubandatabase = sqlite3.connect('豆瓣.db')
# 创建一个Cursor
cursor = doubandatabase.cursor()
# 创建一个数据库表
dbtablecreate()
# 从每个电视剧的详情页中获取所需字段存入数据库
for i in moivelist:
try:
getTVdramainfo(i, cursor)
except Exception as e:
print(e)
continue
# 关闭Cursor
cursor.close()
# 提交事务
doubandatabase.commit()
# 关闭Connection
doubandatabase.close()

conn = sqlite3.connect('豆瓣.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
for i in cursor.fetchall():
tablename = i[0]
cursor.execute("SELECT * FROM '{}'".format(tablename))
with open(tablename + '.csv', 'w', newline='', encoding='utf_8_sig') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow([j[0] for j in cursor.description])
csv_writer.writerows(cursor)

可以用SQLiteSpy.exe查看数据库。

实例二:豆瓣正在上映电影爬虫

观察所需爬取的页面

所需爬取的页面为正在上映电影

查看其源代码。

获取电影信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def getnowshowing(url, city):
text = getHTMLText(url + city)
# 将获取到的资源转换为soup对象
soup = BeautifulSoup(text)
# 找到所有样式为lists的标签
lists = soup.find(class_="lists")
# 遍历其儿子节点
for eachmoive in lists.children:
if type(eachmoive) == bs4.element.Tag:
# 找到所需字段
name = eachmoive['data-title']
rating = float(eachmoive['data-score'])
votes = int(eachmoive['data-votecount'])
if eachmoive['data-duration'].encode('utf-8') == '':
duration = 0
else:
duration = int(str(eachmoive['data-duration'].encode('utf-8')).split('\\')[0].split('\'')[1])

从源码发现电影列表内容分为正在上映和即将上映两块,soup.find(class_="lists") 只返回第一个符合条件的标签。
其每一个孩子节点包含每一个电影信息。所有信息都存在孩子节点的 <li> 标签内,所以只取 Tag 对象,Tag 对象的属性访问与字典一样。
注意 data-duration 的值有可能为空,所以要单独判断。
eachmoive ['data-duration'] 返回 'xxx 分钟 ' 的 Unicode 对象,.encode('utf-8') 将 Unicode 对象转换成 string 对象。filter(str.isdigit, str) 提取 str 中数字。

存储所爬信息到数据库

这里使用 sqlite3 库实现

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
def dbtablecreate():
# 执行一条SQL语句,创建NowShowingMoive表
try:
cursor.execute('CREATE TABLE NowShowingMoive (name TEXT PRIMARY KEY, rating REAL, votes INT, duration INT)')
print('Table created successfully')
except Exception as e:
print(e)

def dbinsert(name, rating, votes, duration):
# 执行一条SQL语句,插入一条记录
try:
cursor.execute('INSERT INTO NowShowingMoive (name, rating, votes, duration) VALUES (?, ?, ?, ?)', (name, rating, votes, duration))
print('data insert successfully')
except Exception as e:
print('insert: ' + str(e))

def dbquery(name):
# 以电影名称为关键词查询数据库
try:
cursor.execute('SELECT name, rating, votes, duration FROM NowShowingMoive WHERE name = ?', (name))
for row in cursor:
return row
except Exception as e:
print('query: ' + str(e))

# 连接到SQLite数据库
# 如果文件不存在,会自动在当前目录创建
doubandatabase = sqlite3.connect('豆瓣.db')
# 创建一个Cursor
cursor = doubandatabase.cursor()
# 如果数据库表已经存在,则删除
cursor.execute('DROP TABLE IF EXISTS NowShowingMoive')
# 创建一个数据库表
dbtablecreate()
nowshowing_url = 'https://movie.douban.com/cinema/nowplaying/'
city = 'wuhan'
nowshowing = getnowshowing(nowshowing_url, city)
# 关闭Cursor
cursor.close()
# 提交事务
doubandatabase.commit()
# 关闭Connection
doubandatabase.close()

conn = sqlite3.connect('豆瓣.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
for i in cursor.fetchall():
tablename = i[0]
cursor.execute("SELECT * FROM '{}'".format(tablename))
with open(tablename + '.csv', 'w', newline='', encoding='utf_8_sig') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow([j[0] for j in cursor.description])
csv_writer.writerows(cursor)

可以用SQLiteSpy.exe查看数据库。

实例三:豆瓣电影 TOP250 爬虫

观察所需爬取的页面

所需爬取的页面为豆瓣电影 Top250

查看其源代码。

获取电影信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def getTop250moive(url, Numofpage):
# Numofpage为爬取页面数
for i in range(Numofpage):
# 获取第i页网页的资源,25为每页的电影记录数
text = getHTMLText(url+str(i*25))
# 将获取到的资源转换为soup对象
soup = BeautifulSoup(text)
# 找到所有样式为info的标签
lists = soup.find_all(class_="info")
# 找到所需字段
for eachmoive in lists:
name = eachmoive.find(class_="title").string
year = int(eachmoive.find('p').get_text().split('\n')[2].strip()[0:4])
country = eachmoive.find('p').get_text().split('\n')[2].split('/')[1].strip()
rating = float(eachmoive.find(property="v:average").string)
# 两种方法都可以
votes = int(str(eachmoive.find(property="v:best").next_sibling.next_sibling.string.encode('utf-8')).split('\\')[0].split('\'')[1])
votes = int(str(eachmoive.find(text=re.compile(u"人评价")).encode('utf-8')).split('\\')[0].split('\'')[1])
if eachmoive.find(class_="inq") == None:
comment = ''
else:
comment = eachmoive.find(class_="inq").string

从网址发现,Top250 的电影分别展示在 10 个页面,每个页面展示 25 条电影信息,所以 url 的 start 参数为 25 的倍数,值为 0-9。
从源码发现 soup.find(class_="info") 返回每一个电影的信息所在的标签如图。
从样式为 title 的标签可以得到电影名。
从第一个 p 标签可以返回导演、主演、上映年份、国家、类型等信息,并且是分列 2 行,
.split('\n')[2] 得到第 2 行的字符串,.strip()[0:4]) 去掉空格并取前 4 个字符作为年份。
.split('/')[1].strip() 以斜线分隔符分割字符串并取出第二段字符串并去掉前后空格,即国家信息。
从 property 为 v:average 的标签可以得到评分。
由于评分人数从 <span>878960人评价</span> 标签中获取,但是 span 标签没有任何属性。
所以有两种方法:
1. 通过 find(property="v:best") 找到上一个平行节点,然后通过.next_sibling.next_sibling.string 访问。
2. 通过 eachmoive.find(text=re.compile(u"人评价")) 直接用正则表达式查找相应的字符串。
最后通过 filter(str.isdigit, str) 提取 str 中数字。
从样式为 inq 的标签可以得到电影评论,注意电影可能没有评论,所以要单独判断。

存储所爬信息到数据库

这里使用 sqlite3 库实现

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
def dbtablecreate():
# 执行一条SQL语句,创建Top250movie表
try:
cursor.execute('CREATE TABLE Top250movie (name TEXT PRIMARY KEY, year INT, country TEXT, rating REAL, votes INT, comment TEXT)')
print('Table created successfully')
except Exception as e:
print(e)

def dbinsert(name, year, country, rating, votes, comment):
# 执行一条SQL语句,插入一条记录
try:
cursor.execute('INSERT INTO Top250movie (name, year, country, rating, votes, comment) VALUES (?, ?, ?, ?, ?, ?)', (name, year, country, rating, votes, comment))
print('data insert successfully')
except Exception as e:
print('insert: ' + str(e))

def dbquery(name):
# 以电影名称为关键词查询数据库
try:
cursor.execute('SELECT name, year, country, rating, votes, comment FROM Top250movie WHERE name = ?', (name))
for row in cursor:
return row
except Exception as e:
print('query: ' + str(e))

# 连接到SQLite数据库
# 如果文件不存在,会自动在当前目录创建
doubandatabase = sqlite3.connect('豆瓣.db')
# 创建一个Cursor
cursor = doubandatabase.cursor()
# 如果数据库表已经存在,则删除
cursor.execute('DROP TABLE IF EXISTS Top250movie')
# 创建一个数据库表
dbtablecreate()
moivelist_url = 'https://movie.douban.com/top250?start='
Numofpage = 10
# 获取最新10*25条电影信息
getTop250moive(moivelist_url, Numofpage)
# 关闭Cursor
cursor.close()
# 提交事务
doubandatabase.commit()
# 关闭Connection
doubandatabase.close()

conn = sqlite3.connect('豆瓣.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
for i in cursor.fetchall():
tablename = i[0]
cursor.execute("SELECT * FROM '{}'".format(tablename))
with open(tablename + '.csv', 'w', newline='', encoding='utf_8_sig') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow([j[0] for j in cursor.description])
csv_writer.writerows(cursor)

可以用SQLiteSpy.exe查看数据库。

其他

实例一源码
实例二源码
实例三源码
SQLiteSpy 数据库查看器下载
豆瓣爬虫结果下载