Python爬虫学习笔记

今天开始学习写爬虫吧~

Python对XML,JSON,HTML的解析

xml

我们知道一般的无结构文本的组织格式常见的有CSV, XML, JSON, HTML这些. 其中比较相近的是XML和HTML这两个. 他们之间最大的不同点应该就是XML中没有自闭和标签这一点了, 例如:在HTML中经常会看到<br/>这样的自己就把自己闭合的标签, 而在XML中, 这个就是非法的字符.

废话不说了, 直接看一个XML的解析实例:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<bookstore name="The First Bookstore">
<book name="HarryPotter">
<amount>23</amount>
<price>100</price>
</book>
<book name="Little Prince">
<amount>10</amount>
<price>88</price>
</book>
</bookstore>

这是一个典型的XML文件 现在我们去用Python搞它:

1
2
3
4
5
6
7
8
9
10
11
12
from xml.dom import minidom

doc = minidom.parse('book.xml')
root = doc.documentElement
print(root.nodeName)
print(root.getAttribute('name'))
books = root.getElementsByTagName("book")
for book in books:
amounts = book.getElementsByTagName("amount")
prices = book.getElementsByTagName("price")
print("The book " + book.getAttribute("name") + " have " + amounts[0].childNodes[0].nodeValue + " left.")
print("And the price of it is " + prices[0].childNodes[0].nodeValue)

其中, 我们使用DOM的方式进行解析, 从代码中也可以看出来, 这里是把整个XML文件解析结束之后(也就是装载进内存之后)才可以进行处理. 显然这种方式的有点在于他可以快速的定位到任意一个节点上, 而缺点自然就是速度慢和资源占用大了.

输出结果就像这样:

1
2
3
4
5
6
bookstore
The First Bookstore
The book HarryPotter have 23 left.
And the price of it is 100
The book Little Prince have 10 left.
And the price of it is 88

与DOM不一样的, 另一种解析方式, 叫做SAX. 他的特点就是通过Handler的方式对这个XML文件进行解析, 读到哪里处理哪里.

代码就像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from xml.parsers.expat import ParserCreate

class DefaultSaxHandler:
def start_element(self, name, attrs):
self.name = name
print('element %s, attrs: %s' % (name, str(attrs)))

def end_element(self, name):
print("element %s end." % name)

def char_data(self, val):
if val.strip():
print("And %s's value is %s" % (self.name, val))

handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data

with open("book.xml", 'r') as f:
parser.Parse(f.read())

我们说过Python是一个鸭子类型的语言, 就是说只要你实现了这些功能, 那么我就可以说你是什么类型的. 这里, 我们实现一个SAX的解析器, 通过三个方法, 来读取他们的节点名称, 属性列表, 节点的值, 以及最后的读取到结束的标签的动作.

这样通过对handler赋值, 我们就可以进行文档的解析了, 输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
element bookstore, attrs: {'name': 'The First Bookstore'}
element book, attrs: {'name': 'HarryPotter'}
element amount, attrs: {}
And amount's value is 23
element amount end.
element price, attrs: {}
And price's value is 100
element price end.
element book end.
element book, attrs: {'name': 'Little Prince'}
element amount, attrs: {}
And amount's value is 10
element amount end.
element price, attrs: {}
And price's value is 88
element price end.
element book end.
element bookstore end.

十分简单吧.

JSON

我们之前其实说过了Python对JSON的支持的, 虽然篇幅不多, 但是这个模块真的不是很复杂,(起码使用起来不难).

比如这样的JSON文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"value": [{
"book": "HarryPort",
"price": "101",
"amount": "199"
},

{
"book": "Little Prince",
"price": "18",
"amount": "123"
}
]
}

python对JSON的api设计是, 如果文件操作, 那么就是loaddump. 而如果是对字符串对象的操作的话, 就是loadsdumps了. 所以针对上面的那个文件, 我们这样来解析:

1
2
3
4
5
import json

f = open("book.json", encoding="utf-8")
output = json.load(f)
print(output)

就完了. 很简单. 这里输出的output变量其实是个字典. 当然了, 我也可以从字典得到JSON字符串, 综合起来就是这样的

1
2
3
4
5
6
7
8
9
import json

f = open("book.json", encoding="utf-8")
output = json.load(f)
print(type(output))
print(output['value'])
json_str = json.dumps(output)
print(type(json_str))
print(json_str)

上面代码的输出是:

1
2
3
4
<class 'dict'>
[{'book': 'HarryPort', 'price': '101', 'amount': '199'}, {'book': 'Little Prince', 'price': '18', 'amount': '123'}]
<class 'str'>
{"value": [{"book": "HarryPort", "price": "101", "amount": "199"}, {"book": "Little Prince", "price": "18", "amount": "123"}]}

最后我们在来说一下HTML的解析, 关于这个, 我打算直接用个例子就带过去, 原因有两个:

  • 原生的HTML Parser在实际情况下不会得到使用, 第三方库的使用概率要大的多
  • HTML Parser模块很简单, 他和前面的SAX解析XML几乎差不多.

HTML

首先一个Web页面:

1
2
3
4
5
6
7
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr/><center>nginx/1.4.6 (Ubuntu)</center>
</body>
</html>

( 嘿嘿, 特意挑了一个403页面 )

这里我们能看到除了存在和XML一样的的前后闭合标签, 还有一个烦人的<hr/>. 现在我们来实现一下我们的Parser:

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
#!/usr/bin/env python3
from html.parser import HTMLParser


class DefaultParser(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag == 'html':
print('Start parsing.')
if tag == 'title':
print("Output title:")
if tag == 'h1':
print("Output h1:")
if attrs:
print("%s has attr: %s" % (tag, attrs))

def handle_endtag(self, tag):
if tag == 'html':
print("End parsing")

def handle_startendtag(self, tag, attrs):
if tag == 'hr':
print('Found hr')

def handle_data(self, data):
if data:
print(data)


parser = DefaultParser()
with open("index.html", encoding='utf-8') as f:
parser.feed(f.read())
parser.close()

这样执行之后获得的结果是这样的:

1
2
3
4
5
6
7
8
9
Start parsing.
Output title:
403 Forbidden
body has attr: [('bgcolor', 'white')]
Output h1:
403 Forbidden
Found hr
nginx/1.4.6 (Ubuntu)
End parsing

其实, Python除了这个HTMLParser, 还有其他的用来解析HTML文档的模块, 而且事实上, 他们要更加好用. 不过也就到这里了. 现在我们来看看这一次爬虫使用到的第一个库! — request.

Request库的使用

Request是一个超级好用的Python网络库, 封装大量的urllib3中的API, 并提供一个更好用的接口给开发者. 最简单的一个请求操作就是这样的:

1
2
3
4
5
6
7
8
9
import requests

url = "http://59.68.29.126"

res = requests.get(url)
print(res)
print(res.status_code)
print(res.headers)
print(res.text)

你会发现, get不就是我们的HTTP方法中的一个嘛, 事实也是如此, request的HTTP方法就是直接用的其方法名来命名的.

这样的请求输出的结果是这样的:

1
2
3
4
5
6
7
8
9
10
<Response [403]>
403
{'Server': 'nginx/1.4.6 (Ubuntu)', 'Date': 'Sun, 05 Nov 2017 12:07:26 GMT', 'Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Content-Encoding': 'gzip'}
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.4.6 (Ubuntu)</center>
</body>
</html>

怎么样, get输出的response对象中携带了大量你所需要的值和结果.

GET请求就是这样的, 那么接下来如果我想要携带参数去请求呢:

请看下面的代码

1
2
3
4
5
6
import requests

url = 'http://httpbin.org/get'
params = {'k1': 'v1', 'k2': None, 'k3': [1, 2, 3]}
res = requests.get(url, params=params)
print(res.url)

输出的结果是:

1
http://httpbin.org/get?k1=v1&k3=1&k3=2&k3=3

由于k2没有参数值, 所以直接就略去了. 而k3是个数组, 所以出现了多次.

关于 httpbin.org这个网站, 它提供了很多很方便的功能, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  ~ curl http://httpbin.org/user-agent
{
"user-agent": "curl/7.54.0"
}
➜ ~ curl http://httpbin.org/headers
{
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.54.0"
}
}

他的主页上有更多说明.

这样就可以携带参数的请求了.

除了文本请求, 有时候我们可能需要去爬一些图片什么的二进制数据, 这怎么办呢? 其实这个时候和我们的Requests的滚阿西就不是很大了, 只不过是需要response对象中的一个content属性罢了, 以图片为例, 来看下面的一个实例:

1
2
3
4
5
6
7
8
import requests
from PIL import Image
from io import BytesIO

url = "https://www.baidu.com/img/bd_logo1.png"
res = requests.get(url)
image = Image.open(BytesIO(res.content))
image.save("baidu_logo.png")

这样, 我们就可以进行图片的抓取了. 有的时候, 一张图有可能会很大, 这个时候, 我们可以把它当做原始数据 一个chunk一个chunk的读取, 也就是流式读取. 来看改进之后的:

1
2
3
4
5
6
7
8
9
10
import requests
from PIL import Image
from io import BytesIO


url = "https://www.baidu.com/img/bd_logo1.png"
res = requests.get(url, stream=True)
with open("baidu_logo_stream.png", 'wb+') as f:
for trunk in res.iter_content(512):
f.write(trunk)

GET的用法其实就是这些了, 接着我们说一下POST的用法, 最常用的场景就是提交表单了. 这个时候其实和GET携带参数差不多的用法:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
from PIL import Image
from io import BytesIO


url = "http://httpbin.org/post"
form = {"username": 'Justin', "password": "ielts666"}
res = requests.post(url, data=form)
print(json.loads(res.text)['form'])
res = requests.post(url, data=json.dumps(form))
print(json.loads(res.text)['form'])
print(json.loads(res.text)['data'])

得到的结果如下:

1
2
3
{'password': 'ielts666', 'username': 'Justin'}
{}
{"username": "Justin", "password": "ielts666"}

也就是说, 当我们post传入的data是个字段对象的时候, 数据就会填充在表单中,而如果是纯文本, 就会丢在data项目中.

说道了提交表单, 我们还要说一下有关Cookie的一些, 首先先用必应做个实验: (因为它的Cookie比较多 - -)

1
2
3
4
5
import requests

url = "https://bing.com"
res = requests.get(url)
print(res.cookies.items())

结果是这样的:

1
[('MUID', '270542D09F6B6A1B05D349E29EB76BFB'), ('SRCHD', 'AF=NOFORM'), ('SRCHUID', 'V=2&GUID=0FC639A2A7F7477EAD9CC949860A8CBD&dmnchg=1'), ('SRCHUSR', 'DOB=20171106'), ('_EDGE_S', 'F=1&SID=0E4DEFBCD35565913449E48ED28964EC'), ('_EDGE_V', '1'), ('_SS', 'SID=0E4DEFBCD35565913449E48ED28964EC'), ('MUIDB', '270542D09F6B6A1B05D349E29EB76BFB')]

当然我们也可以进行遍历, 进行进一步的操作.

同样, 我们也可以携带Cookie进行请求, 就像这样:

1
2
3
4
url = "http://httpbin.org/cookies"
cookie = {"name": 'Justin'}
res = requests.get(url, cookies=cookie)
print(res.text)

我们的Cookie确实送过去了:

1
2
3
4
5
{
"cookies": {
"name": "Justin"
}
}

Requests同样可以做到重定向以及重定向的历史:

1
2
3
4
5
url = "http://yaoxuannn.com"
res = requests.get(url, allow_redirects=True)
print(res.url)
print(res.status_code)
print(res.history[0].status_code)

输出结果是:

1
2
3
https://yaoxuannn.com//
200
302

最后关于Requests的基本功能, 我们再说一个使用代理:

1
2
3
url = "http://httpbin.org"
proxies = {"http": "假装有个代理服务器", "https": "假装这里有个代理服务器"}
res = requests.get(url, proxies=proxies)

这样就差不多了.

BeautifulSoup 4的使用

我们提供一个示例文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample File</title>
</head>
<body>
<!--body_start-->
<p class="para">This is a sample file for beautiful soup4</p>
<a href="https://www.baidu.com">Click Here</a><p>And you will see the homepage of baidu
<em id="em">BAIDU</em>
</p>
<p>last para</p>

</html>

可以很清楚的看出来, 它的排版并不是很好看(甚至还有错误[body没有闭合]). 接下来我们就可以使用BS来做一下处理:

1
2
3
4
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("bs_sample.html"), "html.parser")
print(soup.prettify())

输出的文本就是按照层级结构排过版的了 同时, body标签也被自动的补全了.

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>
Sample File
</title>
</head>
<body>
<!--body_start-->
<p class="para">
This is a sample file for beautiful soup4
</p>
<a href="https://www.baidu.com">
Click Here
</a>
<p>
And you will see the homepage of baidu
<em id="em">
BAIDU
</em>
</p>
<p>
last para
</p>
</body>
</html>

接下来就是BS的强大的地方了, 我们可以非常方便的读取文档, 就像下面这样:

1
2
3
4
5
6
7
8
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("bs_sample.html"), "html.parser")
# print(soup.prettify())

print(type(soup.title))
print(soup.title.text)
print(soup.p)

输出的结果是这个样子的:

1
2
3
<class 'bs4.element.Tag'>
Sample File
<p class="para">This is a sample file for beautiful soup4</p>

但是 问题出来了, 这里抓取到的元素都是第一个, 后面的元素好像没法直接获得. 所以这个时候, 我们就需要进行遍历了:

1
2
3
4
5
from bs4 import BeautifulSoup

soup = BeautifulSoup(open("bs_sample.html"), "html.parser")
for item in soup.body.contents:
print(item.name)

输出的结果有点奇怪:

1
2
3
4
5
6
7
8
9
10
None
None
None
p
None
a
p
None
p
None

这是因为papp这四个元素是body下的直接子元素.

接下来我们看一下BS的CSS查询:

1
print(soup.select(".para"))

这样我就得到了:

1
[<p class="para">This is a sample file for beautiful soup4</p>]

这样一个元素列表.

当然id查询也是可以的, 就不演示了. 当然也是支持这样的层级查找:

1
print(soup.select("body > p.para"))

很简单, 更多使用到时候在实战中使用再说. 现在我们来看一下Python > sqlite的使用

sqlite

Python方便的地方就是他自带了sqlite, 这很方便, 而且SQL语句的使用也是很标准的, 所以如果你使用过MySQL, 那么使用sqlite模块也是手到拈来的事情了:

1
2
3
4
5
6
7
8
9
10
11
12
import sqlite3
conn = sqlite3.connect("test.db")
create_sql = "create table if not exists test(id int primary key not null, name varchar(20) not null)"
conn.execute(create_sql)
insert_sql = "insert into test values(?, ?)"
conn.execute(insert_sql, (1, "test"))
conn.execute(insert_sql, (2, "test"))
get_sql = "select * from test where name = ?"
res = conn.execute(get_sql, ("test",))
for item in res.fetchall():
print(item[1])
conn.close()

这样输出的结果就是:

1
2
test
test

好了, 准备的额差不多了, 接下来我就来试着模拟登陆一下好了.

土家购模拟登录

不管写什么爬虫, 我们都需要先观察对方网站是怎么处理的请求, 怎么发送的表单, 土家购的登录十分简单, 直接使用浏览器的开发者工具就可以了: ( 别问我土家购是什么 - - )

tujiagou_login.png

十分好看.

接下来我就要来模拟一下登陆过程了, 直接上个代码:

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
import requests
from bs4 import BeautifulSoup

s = requests.Session()

login_url = "http://tjg.hangowa.com/member/?act=login&op=login&inajax=1"

headers = {"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36",
"Referer":
"http://tjg.hangowa.com/member/?act=login&op=index&ref_url=%2Fmember \
%2F%3Fact%3Dmember_information%26op%3Dindex"}

formdata = {
"formhash": "gjn1eYx_4djUfHKPpQAhjX0eYT0fEDP",
"form_submit": "ok",
"nchash": None,
"user_name": "user",
"password": "不告诉你",
"ref_url": "/member/?act=member_information&op=index"
}

res = s.post(login_url, data=formdata, headers=headers)
home = s.get("http://tjg.hangowa.com/shop/index.php?act=cart")
soup = BeautifulSoup(home.content, "html5lib")
goods = soup.select("td > a > img")
for good in goods:
print(good.get("alt"))

s.close()

这样就可以获得我的购物车的商品了, 结果如下:

1
2
3
4
5
6
骑龙烘青9号50g绿茶
恩施鹤峰骑龙绿茶250g袋装
恩施鹤峰青翠源黑茶400g
斑鹤黄金白茶50g*2一盒二袋
恩施鹤峰斑鹤250g绿茶
%name%

是不是很简单呢, 当然. 这也是因为土家购对登录没有做什么验证啥的, 所以很轻松的就登陆进去了.

接着我们可以使用开发者工具把自己登陆之后的Cookie复制下来, 只要对方没有设置其他的干扰项, 我们就可以使用你的Cookie进行免密码的登陆进去.

接着来看这两个小栗子:

Bilibili排行榜和动态通知

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import json

headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36",
"Referer": "https://www.bilibili.com/ranking"
}

url = "https://www.bilibili.com/index/rank/all-3-0.json"

rank_res = requests.get(url, headers=headers)
rank_json = json.loads(rank_res.text)
location = 1
for item in rank_json['rank']['list']:
print("第%s名: %s Up:%s\n播放数:%s 硬币:%s, 弹幕:%s" % (str(location), item['title'], item['author'], item['play'], item['coins'], item['video_review']))
location += 1;
print("-" * 20)

运行结果就像这样:

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
第1名: 【凉风】不正经解说柯南剧场版M21-唐红的恋歌 Up:凉风有性胖次君
播放数:484836 硬币:30721, 弹幕:5670
--------------------
第2名: 诸葛亮的红色蜀国 Up:画画峰
播放数:822549 硬币:60046, 弹幕:7214
--------------------
第3名: 【马云】王妃:我的baby,我要霸占你的钱! Up:深海色带鱼
播放数:521766 硬币:41325, 弹幕:3235
--------------------
第4名: 【C菌】首款赛博朋克风恐怖神作!【>Observer_ (观察者)】实况连载, 更新第五集 Up:渗透之C君
播放数:398717 硬币:21198, 弹幕:11739
--------------------
第5名: 【兄贵】病名为爱♂ Up:小可儿
播放数:465863 硬币:55400, 弹幕:7637
--------------------
第6名: 死亡?蜘蛛侠:仅剩之日 Up:努力的Lorre
播放数:557282 硬币:60271, 弹幕:8679
--------------------
第7名: 正气凛然!对美少女的丝袜诱惑说不!【二次元观察室04】 Up:吃素的狮子
播放数:220998 硬币:31966, 弹幕:10283
--------------------
第8名: 《守望先锋》动画短片:《荣耀》 Up:网易暴雪游戏视频
播放数:536341 硬币:22895, 弹幕:5294
--------------------
第9名: 【原作党很严格】为什么动画都喜欢魔改剧情?泛式带你走进魔改党背后的真相 Up:泛式
播放数:237893 硬币:23400, 弹幕:3576
...(omitted)

很有意思吧, 另外, 使用你登录之后的Cookie就可以直接进行动态的抓取了, 代码如下(Cookie需要替换):

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
import requests
from datetime import datetime
import json
import re

url = "https://api.bilibili.com/x/web-feed/feed?callback=jQuery172020613184013553032_1510060069102&jsonp=jsonp&ps=10&type=0&_=1510060069292"

cookie = {
'Cookie': "你的Cookie"
}

header = {
'Referer': "https://www.bilibili.com/account/dynamic",
'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36"
}

res = requests.get(url, cookies=cookie, headers=header)
result = res.text

pattern = "jQuery[0-9_]*\((.*)\)"

dynamic = json.loads(re.sub(pattern, r'\1', result))

for data in dynamic['data']:
archive = data['archive']
bangumi = data['bangumi']
print("--" * 10)
if archive:
print("发布时间: %s Up: %s 标题: %s\n%s" % (datetime.fromtimestamp(float(data['pubdate'])), archive['owner']['name'], archive['title'], archive['desc']))
if bangumi:
print("**我关注的番剧更新了!**\n**%s**\n**第%s话: %s**" % (bangumi['title'], bangumi['new_ep']['index'], bangumi['new_ep']['index_title']))

运行的结果就像这样:

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
--------------------
发布时间: 2017-11-07 20:40:25 Up: 刘哔电影 标题: 【刘哔】我不扶墙就服这支广告的设计
京东今年大牌云集,还让外卖小哥抢了戏?我不扶墙就服这支广告的设计,会引起的某些欲望,观看要谨慎,京东11.11 全球好物节,千万别忘了我们的约定!
--------------------
发布时间: 2017-11-06 18:20:23 Up: 刘哔电影 标题: 【刘哔】烂片吐槽之《青春期3》:你确定你过的是青年期 不是更年期吗?
明明是更年期!
--------------------
**我关注的番剧更新了!**
**宝石之国**
**第5话: 归来**
--------------------
发布时间: 2017-11-04 15:53:19 Up: 泛式 标题: 【原作党很严格】为什么动画都喜欢魔改剧情?泛式带你走进魔改党背后的真相
视频类型: 动漫杂谈
相关题材: 欢迎来到实力至上主义的教室,血界战线,电玩咖,中二病也要谈恋爱,噬魂师,火影忍者,甘城光辉游乐园,银魂,食灵,钢之炼金术师,Clannad,旋风管家
简介: 近来魔改一词出现得越来越频繁,为什么动画组总是喜欢教原作党做人?今天就来和大家聊一聊动画化时的剧情魔改现象吧。 (所有论点系业余言论,仅供娱乐)

微博:https://weibo.com/FunShiki
--------------------
发布时间: 2017-11-04 14:12:37 Up: 凉风有性胖次君 标题: 【凉风】不正经解说柯南剧场版M21-唐红的恋歌
视频类型: 动漫杂谈
相关题材: 名侦探柯南
简介: 这里什么都有→微博@凉风有性胖次君 http://weibo.com/10112015
--------------------
发布时间: 2017-11-03 19:00:55 Up: 刘哔电影 标题: 【刘哔】三分钟带你看小品改编电影《兄弟,别闹!》
11.10~带上喜欢的妹子一起去看吧!
--------------------
发布时间: 2017-11-03 16:46:26 Up: 逍遥散人 标题: 【散人】穿越尉迟恭 把妹不轻松 手机网聊版
相关游戏: 尉迟恭是什么鬼
简介补充: 游戏名尉迟恭是什么鬼网聊版
录制出现点问题,所以最开始介绍游戏的话语没录上,直接开场。
玩了玩超级有趣,继续做上来大家看个乐。
--------------------
发布时间: 2017-11-03 03:47:12 Up: 刘哔电影 标题: 温情解说之《追缉炸弹客》:最了解你的不是枕边人,而是你的敌人
最了解你的不是枕边人,而是你的敌人
--------------------
发布时间: 2017-11-02 18:30:57 Up: 还有一天就放假了 标题: 【波澜哥】爱澜澜
小红红:五句啊,五句要是再唱不准你就娶我,口亨!
--------------------
发布时间: 2017-11-02 16:31:24 Up: LexBurner 标题: 【Lex吐槽】刀剑神域序列之争,我永远喜欢亚丝娜!
视频类型: 动漫杂谈
相关题材: 刀剑神域序列之争
简介: 新浪微博@LexBurner

(哎呀, 你们看到我关注的UP了..嘿嘿)

Python爬虫框架 — scrapy

但是, 如果说在生产环境中使用脚本一个页面一个页面的下载, 解析实在是太没有逻辑了, 而且没有效率. 而且实现起来的难度较大, 于是, 框架就这样诞生了. 其中**scrapy**就是这些框架中一个十分有名的一个.

来看一下他的架构:
Scrapy

大体上也能搞清楚个大概, Scrapy的工作模式, 接下来我们就来说说这个架构, 最显眼的就是右边的那个下载器了, 他做的事情很单纯, 就是把内容下载过来交给Spider组件, 只要有请求过来, 下载器就会自动的去下载. 而下载的东西去哪里了呢? Spider组件将下载器给他的资源统一输出到输出管道上, 这个管道的另一端是谁呢? 不一定了, 有可能是文件, 有可能是数据库等等. 总之就是你最后期望得到的输出目标. 那么下载器的请求来源是哪里? 自然就是上面的那个调度器了, 由Spiders组件将需要请求的连接地址发送给调度器, 接着根据当前爬虫的状态动态的调度请求, 这就是Scheduler的工作.

关于scrapy本身, 我们就先说道这里, 接着我们来看一个小例子: 抓取七月在线的课程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#! /usr/bin/env python3

import scrapy


class TestSpider(scrapy.Spider):
name = "Test"
start_urls = ["https://www.julyedu.com/category/index"]

def parse(self, response):
for item in response.xpath('//div[@class="course_info_box"]'):
title = item.xpath('a/h4/text()').extract_first()
desc = item.xpath('a/p[@class="course-info-tip"][1]/text()').extract_first()
time = item.xpath('a/p[@class="course-info-tip info-time"]/text()').extract_first()
res = {
"title": title,
"desc": desc,
"time": time
}
yield res

怎么执行呢, 我们可以使用scrapy提供的命令行工具来执行它, 就像是这样

1
crapy runspider scrapy_test.py -o scrapy_test.json

其中-o参数可以将yield出去的结果输出到文件中 要求这个结果必须是基本类型的字典或者None, 以及Scrapy的BaseItem和Request类.

我们现在来看一下上面的那个小例子, 首先我们继承一个爬虫类, 这个类中我们必须要提供的是这个爬虫的名字也就是name类变量和需要爬的urls, 这个是一个列表, 由于上面的例子是个单页测试, 所以就是一个单独的页面. 接着是我们需要我们爬虫抓取的内容如何获取, 如何去解析资源的方法.

现在我们稍微爬去多一点资源, 这一次我们主要是采取URL拼接的方式来爬的: 一个爬取博客园文章和推荐数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BlogSpider(scrapy.Spider):
name = "cnblog"
start_urls = ["https://www.cnblogs.com/pick/#p%s" % p for p in range(2, 11)]

def parse(self, response):
for item in response.xpath('//div[@class="post_item"]'):
title = item.xpath('div[@class="post_item_body"]/h3/a/text()').extract_first()
recommand = item.xpath('div[@class="digg"]/div/span/text()').extract_first()

yield {
"title": title,
"rec_no": recommand
}

注意这里所使用的start_urls, 由于我们可以从URL中发现规律, 这样就可以进行分页的抓取了. 另外还有一种, 就是手动的去请求下一页, 你也可以理解成是重新请求一个新的URL啦, 我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class QuoteSpider(scrapy.Spider):
name = "quoteSpider"
start_urls = ["http://quotes.toscrape.com/tag/life/"]

def parse(self, response):
for item in response.xpath("//div[@class='quote']"):
content = item.xpath("span[@class='text']/text()").extract_first()
author = item.xpath("span[2]/small/text()").extract_first()

yield {
"content": content,
"author": author
}

next_page = response.xpath("//li[@class='next']/a/@href").extract_first()
if next_page:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)

这里我们尝试获取页面上的下一页, 如果存在就继续请求. 由于获取到的href目标是个相对地址, 所以我们还需要进行一次URL的拼接.

到现在为止, 我们都在使用scrapy的单文件, 但是事实上我们一个爬虫都是有一个项目来驱动的, 使用scrapy创建一个project是十分简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  scrapy startproject qqnews
New Scrapy project 'qqnews', using template directory '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/scrapy/templates/project', created in:
/.../qqnews

You can start your first spider with:
cd qqnews
scrapy genspider example example.com
➜ tree qqnews
qqnews
├── qqnews
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│   ├── __init__.py
│   └── __pycache__
└── scrapy.cfg

4 directories, 7 files

而且我们的爬虫就在这个spider目录下创建:

1
➜  vim qqnews/qqnews/spiders/qq_news_spider.py

接着我们就可以去写爬虫了…

脱坑指南 如果你爬取不了(比如说被拒绝, 或者超时, 或者一直没反应), 那么尝试一下将setting.py里面的ROBOTSTXT_OBEY改成False. 这样我们的scrapy就不会遵循robots规则了.

另外, 腾讯新闻的渲染好坑啊, 他竟然是把所有的内容都扔到了前面的一个script里面的__initData变量里面(unicode编码)

12.08的更新: 腾讯新闻居然改版了?? 下面的代码不再适用.

在这个过程中, 我们可以使用一个scrapy提供的调试工具来帮助我们更好的查看错误出在什么地方.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> scrapy shell http://view.inews.qq.com/a/20171125A07ZUK00
...(omitted)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x1041e7550>
[s] item {}
[s] request <GET http://view.inews.qq.com/a/20171125A07ZUK00>
[s] response <200 http://view.inews.qq.com/a/20171125A07ZUK00>
[s] settings <scrapy.settings.Settings object at 0x105233940>
[s] spider <DefaultSpider 'default' at 0x10550f668>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser

接着就来上代码吧:

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
#! /usr/bin/env python3

import re
import scrapy


class NewsSpider(scrapy.Spider):
name = "qqnews"
start_urls = ["http://news.qq.com/world_index.shtml"]
cookie = {
"pgv_pvi": "6074546176",
"RK": "Kbk2ExxeFH",
"pgv_pvid": "6264596920",
"ptcz": "252b68d3ef2d7ecb0281a23b9f3f2e7c69a5f0d7427c7245392836431c89ce58",
"pt2gguin": "o0438425627"
}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Upgrade-Insecure-Requests": "1"
}

def parse(self, response):
hotspot = response.xpath('//div[@id="subHot"]/a/@href').extract_first()
news = response.xpath('//div[@class="list first"]/div/div/div/em/a/@href').extract()
for item in news:
yield scrapy.Request(item, callback=self.news_parse, cookies=self.cookie, headers=self.headers)

def news_parse(self, response):
pattern = r'"cnt_html":"(.*)","content"'
raw_content = response.xpath('/html/body/script[3]/text()').extract_first().strip()
res = re.search(pattern, raw_content).groups()[0].encode().decode('unicode_escape')
print(res)
yield {
'article_html': res
}

好了, 但是到现在 我们都只是蠢蠢的在yield和print对不, 但是对于一个项目来说 我们是需要一个Item来把他们保存下来的. 这就是scrapy的Item, 注意到子安我们的项目目录中中是存在一个叫做item.py的文件的, 我们在这里面写上需要保存的项目:

1
2
class QqnewsItem(scrapy.Item):
article = scrapy.Field()

接着在爬虫中引入它并给他附上值就行了:

1
2
3
4
5
6
7
8
from qqnews.iterms import QqnewsItem
...(omitted)
def news_parse(self, response):
item = QqnewsItem()
pattern = r'"cnt_html":"(.*)","content"'
raw_content = response.xpath('/html/body/script[3]/text()').extract_first().strip()
res = re.search(pattern, raw_content).groups()[0].encode().decode('unicode_escape')
item['article'] = res

接着我们的item还可以继续输送到pipeline中, pipeline正如其名, 将输送进来的数据进行美化加工. 如果有点乱了的话, 不妨看一下前面的架构图

Pipeline常见的应用场景是:

  • 清理HTML数据
  • 抓取关键字
  • 重复性检查
  • 存储到DB中

我们来看一下这个文件:

1
2
3
class QqnewsPipeline(object):
def process_item(self, item, spider): # 这里传进来的item就是刚刚说的那个啦, 我们可以在这里提取他的字段进行修改和再赋值.
return item

return一个item或者字典, 还可以抛出

其实, 我们可以通过覆盖一下的几个方法来继续更多的操作

  • open_crawler 比如我们打开文件, 打开数据库链接都是在这里搞
  • close_crawler close掉链接和文件描述符
  • from_crawler 在这个方法中, 我们可以绑定各种钩子以及访问核心组件 信号和配置等

接下来我打算写一个豆瓣读书的Top250这样的项目, 这一次就会综合上面的各个组件了.

首先启动项目:

1
➜  scrapy startproject douban_book

接着我们先来定义我们需要抓取的信息有哪些, 然后先把他们呢注册一下.

1
2
3
4
5
6
7
8
9
import scrapy


class DoubanBookItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
edition_year = scrapy.Field()
publisher = scrapy.Field()
rating = scrapy.Field()

接着就可以开始定义我们的爬虫了:

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
import scrapy
from douban_book.items import DoubanBookItem


header = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Referer": "https://book.douban.com/"
}

class DoubanSpider(scrapy.Spider):
name = "douban_book_Spider"
allowed_domains = ["douban.com"]
start_urls = ['https://book.douban.com/top250']

def parse(self, response):
print(response)
yield scrapy.Request(response.url, callback=self.page_parse, headers=header)

for link in response.xpath('//div[@class="paginator"]/a/@href').extract():
print(link)
yield scrapy.Request(link, callback=self.page_parse, headers=header)

def page_parse(self, response):
for item in response.xpath('//tr[@class="item"]'):
book = DoubanBookItem()
book['name'] = item.xpath('td[2]/div[1]/a/@title').extract_first()
book['rating'] = item.xpath('td[2]/div[2]/span[2]/text()').extract_first()
info = item.xpath('td[2]/p[1]/text()').extract_first().split(" / ")
book['author'] = info[0]
book['price'] = info[-1 ]
book['edition_year'] = info[-2]
book['publisher'] = info[-3]
yield book

到此, 我们就完成了一个包含item的爬虫. 但是这个地方要说明一下, 第29-33行的工作其实严格上应该放在pipeline中来处理, 而不是在我们的爬虫中进行处理. 优化后这就是一个较为完整的爬虫项目了 虽然他很微小.

接下来我们来说一下如何抓取图片, 这是一个很大的需求, 所以scrapy早就为我们想好了, 它提供了一个ImagesPipeline类, 只要将一个item传过去, 并且携带特定的字段就可以继续操作, 包括基本的去重, 缩略图生成, 图片大小过滤. 当然这是需要PIL库的支持的.

大体说来, 我们的图片爬取步骤几乎是固定的, 就是这样的:

  • 爬取一个item, 并且将图片的URL加入到其image_urls字段中
  • 从Spider中返回的item送到pipeline中, 图片在这里会被下载而item被锁定直到下载完成, 最后剩余的一些信息(路径, 校验和等)会被传递到item的相应字段中.

一个图片item起码要包含:

1
2
3
4
5
class PictureItem(scrapy.Item):
images = scrapy.Field()
image_urls = scrapy.Field()
image_paths = scrapy.Field()

imagesimage_urls这两个属性.

另外, 写好pipeline之后, 我们需要在setting.py文件中开启该pipeline, 并且制定一些图片下载的全局总控参数, 例如:

1
2
3
4
5
6
7
8
ITEM_PIPELINES = {
'photo_crawler.pipelines.PhotoCrawlerPipeline': 300,
}

IMAGES_STORE = '/tmp/pics'
IMAGES_EXPIRES = 90
IMAGES_MIN_HEIGHT = 480
IMAGES_MIN_WIDTH = 920

尺寸大小小于IMAGES_MIN_HEIGHT*IMAGES_MIN_WIDTH的图片会被忽略.

接着在spider中, 我们需要通过解析把图片的url都丢到item中, 传给pipeline.

最重要的部分来了, 我们需要在imagepipeline的继承类中个对下面的两个方法进行重载, get_media_requests(item, info)item_completed(results, items, info)

其中, 前者返回一个Request对象, 交给后者来处理, 后者处理之后会返回一个元组, 包含是否成功的标识以及图片信息或者报错信息.

接下来就来试试吧. 先把最基本的爬虫写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import scrapy
from photo_crawler.items import PictureItem

class PicSpider(scrapy.Spider):
name = 'Douban_crawler'
start_urls = ['https://www.douban.com/photos/album/1655441250/']

def parse(self, response):
pic_list = response.xpath('//div[@class="photolst clearfix"]/div/a/@href').extract()
print(pic_list)
for page in pic_list:
page = page + "large/"
yield scrapy.Request(url=page, callback=self.parse_large)

def parse_large(self, response):
picture = PictureItem()
ori_pic_url = response.xpath('//*[@id="pic-viewer"]/a/img/@src').extract()
picture['image_urls'] = ori_pic_url
yield picture

这里我们的示例来自豆瓣的相册.

上面的内容大体在干什么基本都是我上面说过的, 主要是来看看pipeline做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-

from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
from scrapy import Request

class PhotoCrawlerPipeline(ImagesPipeline):

headers = {
"cookie":"bid=MsOyVJ-WczQ",
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
}

def get_media_requests(self, item, info):
yield Request(item['image_urls'][0], headers=self.headers, meta=item)

def item_completed(self, results, item, info):
print(results)
path = [img['path'] for ok, img in results if ok]
if not path:
raise DropItem('No path')
item['image_paths'] = path

这里我们简单的写了一个头部, 然后就重载了上面说的那两个方法.

这样执行之后, 我们就可以看到在SETTING.py文件中定义的存储路径下的图片了, 但是这个时候图片都是SHA-1算法的摘要值, 显然不方便辨认, 所以这就需要我们进行重新命名, 可以在pipeline中加上这个方法:

1
2
3
4
5
6
def file_path(self, request, response=None, info=None):
ext = request.url.split('/')[-1].split('.')[-1]
item_name = request.meta['images'][0]
path = 'full/{}.{}'.format(item_name, ext)
print(path)
return path

返回的路径就是文件的位置, 当然也包括了他自己的文件名和后缀. 而且在这里使用到了上面的get_media_requests中meta参数传递的item.

并发编程, 多并发爬虫

在前面的Scrapy使用中 我们发现他爬取的速度非常快, 这是因为它使用了并发和异步的爬取模式.

我曾经在一开始学习Python的时候稍微了解过一些关于多线程, 多进程的并发编程. 但现在看来, 那个时候理解的显然不够深入, 因此我们现在再来把Python的并发编程复习一遍吧.

先来写几个小例子吧, 就当是复习了:

1
2
3
4
5
6
7
8
9
# fork only work on Unix/Linux
import os

pid = os.fork()
print("Forking...")
if pid == 0:
print("This is child process %s" % os.getpid())
else:
print("parent process %s" % os.getpid())

但是我们知道这个fork仅仅工作在Unix/Linux平台上, 如果是为了更好的可移植性, 就应该使用一个Python为我们封装好的多进程库multiprocessing.

1
2
3
4
5
6
7
8
9
10
11
12
import os
import multiprocessing

def run_process():
print("Running. (%s)" % os.getpid())

if __name__ == "__main__":
print("parent process %s" % os.getpid())
p = multiprocessing.Process(target=run_process)
p.start()
p.join()
print("End")

对于多线程的支持, Python提供连个模块: _threadthreading. 推荐使用后者, 因为其封装的更高层, 这样我就可以更加专注于业务逻辑的编写上.

说道多线程就一定会提到一个问题, 那就是资源争用. 下面的实验可以的话尽量使用Python2来做测试.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time, threading

balance = 0

def change_it(n):
global balance
balance = balance + n
balance = balance - n

def run_thread(n):
for i in range(100000):
change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

执行之后就会发现balance的值发生了变化.

那么怎么办呢, 加个锁就行了:

1
2
3
4
5
6
7
8
9
10
11
import time, threading

lock = threading.Lock()

def run_thread(n):
for i in range(100000):
lock.acquire()
try:
change_it(n)
finally:
lock.release()

速度测试:

1
2
3
4
5
6
7
8
9
➜  time python test.py
52
python test.py 0.06s user 0.04s system 117% cpu 0.081 total
➜ time python test.py
50
python test.py 0.06s user 0.04s system 115% cpu 0.081 total
➜ time python test.py
10
python test.py 0.06s user 0.04s system 117% cpu 0.085 total

在加了锁之后:

1
2
3
4
5
6
7
8
9
➜  time python test.py
0
python test.py 0.16s user 0.16s system 133% cpu 0.237 total
➜ time python test.py
0
python test.py 0.16s user 0.15s system 130% cpu 0.234 total
➜ time python test.py
0
python test.py 0.16s user 0.16s system 131% cpu 0.245 total

接着我们再说一下线程安全的(当然也有对多进程的)队列, Python提供了先进先出的, 后进先出的, 以及优先队列三种. 当队列中没有元素的时候获取就会发生阻塞, 同样, 当队列满了的时候加入元素也会造成阻塞.

一个消费者/生产者模型:

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
import threading
import queue
import time
from random import randint

q = queue.Queue()

def debug(func):
def wrapper(*args):
print("Current thread: %s started." % threading.current_thread().name)
return func(*args)
return wrapper

@debug
def push():
while True:
time.sleep(randint(0,2))
q.put(0)

@debug
def pull():
while True:
q.get()
print("Get one element. Current size: %d" % q.qsize())

threads = [ threading.Thread(target=push, name="Push_thread"), threading.Thread(target=pull, name="Pull_thread")]
for thread in threads:
thread.start()

样例输出:

1
2
3
4
5
6
7
8
9
➜  python3 queueTest.py
Current thread: Push_thread started.
Current thread: Pull_thread started.
Get one element. Current size: 0
Get one element. Current size: 0
Get one element. Current size: 3
Get one element. Current size: 2
Get one element. Current size: 1
Get one element. Current size: 0

我们在之前也提到过一个叫threadLocal的玩意, 这个东西其实你可以理解成是为了方便我们的变量获取而封装的一个东西.

由于我们创建和销毁进程是需要成本的, 所以使用线程池就可以充分利用CPU以及降低切换成本. 例子就略啦.

接下来就来开始这一部分的正式内容吧, 先来说说协程. 使用select进行一步IO, 缺点在于不能实现消息循环和状态控制.

来一段代码来做演示:

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

async def wget(host):
print("wget %s..." % host)
connect = asyncio.open_connection(host, 80)
reader, writer = await connect
header = 'GET / HTTP/1.1\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
await writer.drain()
while True:
line = await reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8')))
writer.close()

loop = asyncio.get_event_loop()
hosts = ['www.google.com', 'www.baidu.com', 'www.sina.com', 'www.douban.com']
tasks = [wget(host) for host in hosts]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

这里的asyncawait是Python3.5以后的版本中新增的关键字, 如果是以前的版本, 应该使用@asyncio.coroutine装饰器和yield from关键字.

尽管google没法访问到, 但是我们依然可以不受到干扰的得到其他的头部信息.

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
#! /usr/bin/env python3
#######################################################################
# File Name: pic_crawler.py
# Author:Justin
# mail:justin13wyx@gmail.com
# Created Time: Tue Dec 12 20:53:47 2017
# ==============================================================

import threading
import requests
import json
from os import path
from datetime import datetime

def download(url, name=None, path="/tmp/"):
data = requests.get(url).content
if not name:
name = url.split("/")[-1]
path = path.join([name])
print("downloading %s" % path)
with open(path, 'wb') as pic:
pic.write(data)

def parse(content):
data = json.loads(content, encoding="utf-8")
imgs = []
for item in data["list"]:
sub_item = item["arr"]
if "image" in sub_item["type"]:
imgs.extend("http://litten.me/ins/%s.jpg" % img for img in sub_item["link"])
for thread in [ threading.Thread(target=download, name="download_thread", args=(img,)) for img in imgs ]:
thread.start()

def crawl(url=None):
if not url:
stamp = int(datetime.now().timestamp() * 1000)
target = "http://litten.me/photos/ins.json?t=%s" % str(stamp)
else:
target = url
content = requests.get(target).text
parse(content)

if __name__ == "__main__":
crawl()