简单的百度图片爬虫

前几天涵哥找人帮她写一个图片爬虫,碰巧我以前曾经有过一点点爬虫经验,于是用python写了一个非常简单的百度图片爬虫。

1.分析图片地址

最简单的爬虫就是分析网页的HTML代码,从代码中的<a>或者<img>标签中找图片地址,然后用urllib或者request库函数下载,所以我也是这样做的。

首先在chrome中打开百度图片搜索的结果页面,然后审查元素,然后找图片标签,结果是这样的:

搜索结果审查元素

这里有几个值得注意的地方:

  1. 图片是以无序列表<ul>呈现的,每个图为一个<li>子项,搭配各种css
  2. data-objurl(对应图中1)是图片的真实地址。
  3. <a>(对应图中2)是点击图片后的页面地址。
  4. src(对应图中3)是页面展示出来的小图地址。

其中data-objurl是我想要的地址。

所以大致思路出来了,先在搜索结果页面中遍历所有同类型的<li>标签,然后在其中找data-objurl,得到图片真实地址,匹配方法可以用BeautifulSoup或者正则表达式

于是我先下载了一个搜索页面,测试匹配和如何遍历所有图片的真实地址,这里有经验的伙伴肯定就已经猜到了会出问题。没错,问题出现了,在我用如下代码下载的搜索页面HTML代码中,完全找不到所谓的<li>标签,跟别提data-objurl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#coding:utf-8
import urllib2

url='''http://image.baidu.com/i?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1428730266003_R&pv=&ic=0&nc=1&z=&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&word=%E6%B1%BD%E8%BD%A6'''

headers = {'Accept':'image/webp',
'User-Agent':'''Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6''',}
req = urllib2.Request(url,headers=headers)
try:
content = urllib2.urlopen(req).read()
except urllib2.URLError,e:
print e
f = open('a.txt','w+')
f.write(content)
f.close()

网页结果

这里问题的元凶就是AJAX了,通常这种有规律的列表类数据都会通过JavaScript异步请求服务器获得,服务器通常返回嵌入的HTML代码或者XML代码或者JSON代码,这样的设计能够让结果多次返回,有利于加强用户体验。

前面的方法虽然不能够得到结果了,但是也并不是没有收获,我们知道了在搜索结果页面加载后,图片的真实地址一定会返回到前端浏览器,只是返回的地址和格式不清楚罢了。

2.分析网络请求

打开chrome审查元素的network标签,重新加载搜索页面或者在页面下拉,可以看到浏览器收到了N多请求:

chrome请求

经过过滤,得到了JSON数据的链接:

JSON请求

打开之后是这样的:

JSON

观察和调试URL参数,我们发现了三个有价值的参数,tn,rn,word,分别代表起始图片序号,返回图片数量以及关键字,改变这三个参数我们就可以得到任意想要的图片结果。

再看返回的JSON数据,objURL代表的是原始地址,就是我们想要的结果,可以用python的JSON库函数或者正则表达式将其读出。

密文

但是细心的伙伴们又发现问题了,这个真实地址竟然是密文!!!

3.解密真实URL

百度也真是让人不省心,竟然给我们一堆密文。但是细一想明明图1中的data-objurl是明文,所以说解密的方法一定就在前端代码里了。咨询了组里的前端大神后,这个问题得到了完美解决(各种js设置断点,各种调试,找到了解密方法)。

原来的密文是这样的:

1
ippr_z2C$qAzdH3FAzdH3Ft42d_z&e3B4pt4j_z&e3BvgAzdH3F7rAzdH3F8dabAzdH3Fdn88dabAzdH3FcdmDl88c-DBdb-9Aal-laCd-nnnFB889mCdE_caa_z&e3B3r2

解密后是这样的:

1
http://img2.mtime.cn/up/1208/2311208/526D9115-DB28-4A09-90C2-333FB1146C2E_500.jpg

其实就是一个简单的映射,映射方法是:

1
2
3
4
5
"0": "7",    "1": "d",    "2": "g",    "3": "j",    "4": "m",    "5": "o",    "6": "r",    "7": "u",    "8": "1",
"9": "4", "AzdH3F": "/", "_z2C$q": ":", "_z&e3B": ".", "a": "0", "b": "8", "c": "5",
"d": "2", "e": "v", "f": "s", "g": "n", "h": "k", "i": "h", "j": "e", "k": "b", "l": "9",
"m": "6", "n": "3", "o": "w", "p": "t", "q": "q", "r": "p", "s": "l", "t": "i", "u": "f",
"v": "c", "w": "a"

额,百度真是用心良苦啊!!!

这里还有一个小插曲,在我写这篇文章的时候百度前端的代码已经跟我当时写爬虫时不一样了,本来页面中是没有真实地址的,地址解析是在onclick事件之后,当时的代码是这样的

1
onclick="storeData( 'ippr_z2C$qAzdH3FAzdH3Futsjf_z&e3Bvitgwwjp_z&e3Bv54AzdH3Ft4w2jfAzdH3Fcl0nm0n09dcb8_z&e3B2tu', 2);p(event,0,{ist:'',sr:'0',u:'ippr_z2C$qAzdH3FAzdH3Futsjf_z&e3Bvitgwwjp_z&e3Bv54AzdH3Ft4w2jfAzdH3Fcl0nm0n09dcb8_z&e3B2tu',f:'ippr_z2C$qAzdH3FAzdH3Fooo_z&e3Bvitgwwjp_z&e3Bv54AzdH3Fw6ptvsjAzdH3Ftg1jx_z&e3Bwfrx?t1=8blll',cs:'1559793225,3606235040',t:1, asp:0, rts:'0'});"

而现今这个代码已经消失了,而且图片真实地址出现在了前面提到的data-objurl中,估计是为了提高用户体验,在页面加载的时候提前解密了,大百度果然是精益求精,吊爆了!

4. 爬虫

写了一堆基本原理,终于可以写爬虫了,这个部分是最简单也最没有技术含量的。直接上代码吧。

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
#coding:utf-8
import re
import urllib2
import urllib
import datetime
import socket
import os
def parse_url(s):
#映射的字典
dic = {
"0": "7", "1": "d", "2": "g", "3": "j", "4": "m", "5": "o", "6": "r", "7": "u", "8": "1",
"9": "4", "a": "0", "b": "8", "c": "5", "d": "2", "e": "v", "f": "s", "g": "n", "h": "k",
"i": "h", "j": "e", "k": "b", "l": "9", "m": "6", "n": "3", "o": "w", "p": "t", "q": "q",
"r": "p", "s": "l", "t": "i", "u": "f", "v": "c", "w": "a"
}
s = s.replace("AzdH3F","/")
s = s.replace("_z2C$q", ":")
s = s.replace("_z&e3B", ".")
p = ""
for i in s:
if i in dic.keys():
p += dic[i]
else:
p += i

return p

def baidu_crawler(start,word,ren):
#三个参数,start为起始offset,word为关键字,ren为返回图片个数
json_url = '''http://image.baidu.com/i?tn=resultjson_com&ie=utf-8&pn=%s&word=%s&rn=%s&itg=0&z=0&fr=&width=&height=&lm=-1&ic=0&s=0&st=-1#'''
key_word = []
for item in word:
a = urllib.quote(item)
key_word.append(a)
#关键字转义为ascii字符
key_word_string = '+'.join(key_word)
pre_name = '_'.join(word)

url = json_url%(str(start),key_word_string,str(ren))
#print url
#伪装成浏览器请求,一定程度防止被ban
headers = {'Accept':'image/webp',
'User-Agent':'''Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6''',}
req = urllib2.Request(url,headers=headers)
try:
content = urllib2.urlopen(req).read()
except urllib2.URLError,e:
print e
#正则匹配JSON数据
pattern_url = re.compile(r'\"objURL\":\"(.*?)\",',re.S)
result_pic = pattern_url.findall(content)

i = start
for result in result_pic:
real_url = parse_url(result)
#print real_url
i+=1
pic_name = "%s_%s.jpg" % (pre_name,str(datetime.datetime.now().second)+'_'+str(i))

print 'downloading pic No.%s' % str(i)

if not os.path.exists('pics'):
os.mkdir('pics')
name = 'pics/%s' % pic_name
if os.path.isfile(name):
os.remove(name)
try:
#设置超时
socket.setdefaulttimeout(20)
#下载图片
urllib.urlretrieve(real_url,name)
except urllib.ContentTooShortError,e:
print 'error 1'
print e
os.remove(name)
except socket.timeout,e:
print 'error 2'
print e
os.remove(name)
except IOError,e:
print 'error 3'
print e

def my_crawler(num,word):
i=0
while num/60 !=0:
baidu_crawler(i,word,60)
i+=60
num -=60

baidu_crawler(i,word,num)

if __name__=='__main__':
import sys
args = sys.argv
if len(args) < 3:
print u'参数格式错误'
print u'格式:python baidu_crawler_4.py number word\nnumber:要下载的图片数量\nword:关键字(关键字可以用空格隔开)'
sys.exit(0)
num = int(args[1])
word = args[2:]

my_crawler(num,word)

代码在python2.7下运行,没有任何依赖。
马丹,博客和代码一样丑陋!!

gist: https://gist.github.com/lidawn/7e7e00f04e33bd5f803d

另汽车之家外观爬虫gist: https://gist.github.com/lidawn/61982497121e10720fff