0%

Python

2019 年 01 月 04 日 16:32:17 更新了新的 Chrome 镜像 将 Python 版本升级到了 3.7 Note: 推荐使用结尾提供的 Docker 镜像进行二次打包运行代码 各位小伙伴儿的采集日常是不是被 JavaScript 的各种点击事件折腾的欲仙欲死啊?好不容易找到个 Selenium+Chrome 可以解决问题! 但是另一个 ▄█▀█● 的事实摆在面前,服务器都特么没有 GUI 啊·· 好吧!咱们要知难而上!决不能被这个点小困难打倒······· 然而摆在面前的事实是···· 他丫的各种装不上啊!坑爹啊! 那么我来拯救你们于水火之间了! 服务器如下:

1
2
3
4
5
6
7
8
9
10
11
[root@spider01 ~]# hostnamectl
Static hostname: spider01
Icon name: computer-vm
Chassis: vm
Machine ID: 1c4029c4e7fd42498e25bb75101f85b6
Boot ID: f5a67454b94b454fae3d75ef1ccab69f
Virtualization: kvm
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-514.6.2.el7.x86_64
Architecture: x86-64

安装 Chromeium:

1
2
3
4
## 安装yum源
[root@spider01 ~]# sudo yum install -y epel-release
## 安装Chrome
[root@spider01 ~]# yum install -y chromium

去这个地方:https://sites.google.com/a/chromium.org/chromedriver/downloads 下载 ChromeDriver 驱动放在/usr/bin/目录下: 完成结果如下:

1
2
3
[root@spider01 ~]# ll /usr/bin/ | grep chrom
-rwxrwxrwx. 1 root root 7500280 1129 17:32 chromedriver
lrwxrwxrwx. 1 root root 47 1130 09:35 chromium-browser -> /usr/lib64/chromium-browser/chromium-browser.sh

安装 XVFB:

1
2
[root@spider01 ~]# yum install Xvfb -y
[root@spider01 ~]# yum install xorg-x11-fonts* -y

新建在/usr/bin/ 一个名叫 xvfb-chromium 的文件写入以下内容:

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
[root@spider01 ~]# cat /usr/bin/xvfb-chromium
#!/bin/bash

_kill_procs() {
kill -TERM $chromium
wait $chromium
kill -TERM $xvfb
}

# Setup a trap to catch SIGTERM and relay it to child processes
trap _kill_procs SIGTERM

XVFB_WHD=${XVFB_WHD:-1280x720x16}

# Start Xvfb
Xvfb :99 -ac -screen 0 $XVFB_WHD -nolisten tcp &
xvfb=$!

export DISPLAY=:99

chromium --no-sandbox --disable-gpu$@ &
chromium=$!

wait $chromium
wait $xvfb

更改软连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 更改Chrome启动的软连接
[root@spider01 ~]# ln -s /usr/lib64/chromium-browser/chromium-browser.sh /usr/bin/chromium


[root@spider01 ~]# rm -rf /usr/bin/chromium-browser

[root@spider01 ~]# ln -s /usr/bin/xvfb-chromium /usr/bin/chromium-browser

[root@spider01 ~]# ln -s /usr/bin/xvfb-chromium /usr/bin/google-chrome

[root@spider01 ~]# ll /usr/bin/ | grep chrom*
-rwxrwxrwx. 1 root root 7500280 1129 17:32 chromedriver
lrwxrwxrwx. 1 root root 47 1130 09:47 chromium -> /usr/lib64/chromium-browser/chromium-browser.sh
lrwxrwxrwx. 1 root root 22 1130 09:48 chromium-browser -> /usr/bin/xvfb-chromium
-rwxr-xr-x. 1 root root 73848 127 2016 chronyc
lrwxrwxrwx. 1 root root 22 1130 09:48 google-chrome -> /usr/bin/xvfb-chromium
-rwxrwxrwx. 1 root root 387 1129 18:16 xvfb-chromium

来瞅瞅能不能用哦:

1
2
3
4
5
6
7
8
9
10
11
>>> from selenium import webdriver
>>> options = webdriver.ChromeOptions()
>>> options.add_argument('--headless')
>>> options.add_argument('--no-sandbox')
>>> options.add_argument('--disable-extensions')
>>> options.add_argument('--disable-gpu')
>>> driver = webdriver.Chrome(options=options)
>>> driver.get("http://www.baidu.com")
>>> driver.find_element_by_xpath("./*//input[@id='kw']").send_keys("哎哟卧槽")
>>> driver.find_element_by_xpath("./*//input[@id='su']").click()
>>> driver.page_source

No problem!!!! 好了部署完了!当然 Docker 这么火贼适合懒人了!来来 看这儿 Docker 版的 妥妥滴!

1
docker pull thsheep/python:3.7-debian-chrome

做好了 Python3.7 和 Chrome 集成 需要自己使用 Dockerfile 来重新打包安装你需要的 Python 包。

Note: 请按照以下方式初始化 Webdriver!!!!!!!!

1
2
3
4
5
6
7
8
9
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-extensions')
options.add_argument('--disable-gpu')

driver = webdriver.Chrome(options=options)

否则会出现无法初始化 Webdriver 的情况

顺便一提!!!!这个玩意儿从事 Web 测试工作的小伙伴可以用!!!!!!!!

下面是 Dockerfile 文件:

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 python:3.7-stretch

ENV DBUS_SESSION_BUS_ADDRESS=/dev/null

#============================================
# Google Chrome
#============================================
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \
apt-get update -qqy && \
apt-get -qqy install google-chrome-stable unzip&& \
rm /etc/apt/sources.list.d/google-chrome.list && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*

#==================
# Chrome driver
# CHROME_DRIVER_VERSION 需要根据上面安装的Chrome版本来设置(最好设置成最新版本)
# http://chromedriver.chromium.org/downloads 版本号在这页面上查看
#==================
ARG CHROME_DRIVER_VERSION=2.45
RUN wget -O /tmp/chromedriver.zip https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip && \
rm -rf /opt/selenium/chromedriver && \
unzip /tmp/chromedriver.zip -d /opt/selenium && \
rm /tmp/chromedriver.zip && \
mv /opt/selenium/chromedriver /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION && \
chmod 755 /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION && \
ln -fs /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver

RUN google-chrome-stable --version

Python

微博登录限制了错误次数···加上 Cookie 大批账号被封需要从 Cookie 池中 剔除被封的账号··· 需要使用代理··· 无赖百度了大半天都是特么的啥玩意儿???结果换成了 Google 手到擒来 分分钟解决(那么问题来了?百度除了卖假药还会干啥?) Selenium+Chrome 认证代理不能通过 options 处理。只能换个方法使用扩展解决 原文地址:https://stackoverflow.com/questions/29983106/how-can-i-set-proxy-with-authentication-in-selenium-chrome-web-driver-using-pyth#answer-30953780 (Stack Overflow 这是个好地方啊) 走你!

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
# -*- coding: utf-8 -*-
# @Time : 2017/11/15 9:50
# @Author : 哎哟卧槽
# @Site :
# @File : pubilc.py
# @Software: PyCharm

import string
import zipfile

def create_proxyauth_extension(proxy_host, proxy_port,
proxy_username, proxy_password,
scheme='http', plugin_path=None):
"""代理认证插件

args:
proxy_host (str): 你的代理地址或者域名(str类型)
proxy_port (int): 代理端口号(int类型)
proxy_username (str):用户名(字符串)
proxy_password (str): 密码 (字符串)
kwargs:
scheme (str): 代理方式 默认http
plugin_path (str): 扩展的绝对路径

return str -> plugin_path
"""


if plugin_path is None:
plugin_path = 'vimm_chrome_proxyauth_plugin.zip'

manifest_json = """
{
"version": "1.0.0",
"manifest_version": 2,
"name": "Chrome Proxy",
"permissions": [
"proxy",
"tabs",
"unlimitedStorage",
"storage",
"<all_urls>",
"webRequest",
"webRequestBlocking"
],
"background": {
"scripts": ["background.js"]
},
"minimum_chrome_version":"22.0.0"
}
"""

background_js = string.Template(
"""
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "${scheme}",
host: "${host}",
port: parseInt(${port})
},
bypassList: ["foobar.com"]
}
};

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
return {
authCredentials: {
username: "${username}",
password: "${password}"
}
};
}

chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
);
"""
).substitute(
host=proxy_host,
port=proxy_port,
username=proxy_username,
password=proxy_password,
scheme=scheme,
)
with zipfile.ZipFile(plugin_path, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)

return plugin_path

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from selenium import webdriver
from common.pubilc import create_proxyauth_extension

proxyauth_plugin_path = create_proxyauth_extension(
proxy_host="XXXXX.com",
proxy_port=9020,
proxy_username="XXXXXXX",
proxy_password="XXXXXXX"
)


co = webdriver.ChromeOptions()
# co.add_argument("--start-maximized")
co.add_extension(proxyauth_plugin_path)


driver = webdriver.Chrome(executable_path="C:\chromedriver.exe", chrome_options=co)
driver.get("http://ip138.com/")
print(driver.page_source)

无认证代理:

1
2
3
4
5
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=http://ip:port')
driver = webdriver.Chrome(executable_path="C:\chromedriver.exe", chrome_options=0ptions)
driver.get("http://ip138.com/")
print(driver.page_source)

以上完毕 So Easy

Java

PS:此文章为小白提供,大佬请绕道!!!! 首先特别感谢大才哥给我提供这个平台,未来我希望把java这个版块的内容补全。 今天要讲的是数据类型,最最最基础的内容~ java标识符、数据类型、关键字 开始我们先看下如何注释java代码。 标识符:类名,方法名,变量。 有三种方式分别为 //表示注释一行代码 / 表示注释一行或者多行代码 (从上面到下面都是注释的代码) / 下面还有一种注释方式叫做文档注释。 / 通常这样表示 */ 文档注释一般写在代码开头用来简述你所做程序的具体内容,在这之前我们首先看一下javadoc命令,我先编写一个简答的代码: package com.briup.chap02; / @author Twinkle @version 1.0 It’s a text file / public class PrimitiveType{ public static void main(String[] args){ byte b = 123; byte b1 = 300; } } 我们javadoc -d 生成目录 编译文件 编译成功后,我们打开刚刚生成doc里打开index.html看一下,大概是这样的: 类概要 类: Student 说明: It’s a text file 这样我们就可以看出文档注释的意义了,他可以显示在你编译出来文档的说明里,但有人会发现为啥我们编写出来的author没有出来呀? 因为他的最前面有一个@,我们需要编写的时候把它加上去才能显示出来,现在我们来试一下, —javadoc -d bin/doc-author -version src/PrimitiveType.java,这样作者和版本信息就出来了 一.类名 这边我们要记住一些代码的基本格式: 类名的写法:Student(前面首字母要大写) 方法和变量的写法:genderItem(前面单词小写,后面单词开头要大写) 常量写法:MAX_PAGE(常量大写,中间一般加下划线) 二.关键字 关键字其实就是电脑里面已经定义好的有特殊意义的标识符,像int,for,double什么的都是关键字。具体意思请百度一下~ 三.数据类型 数据类型是这篇文章的重点,我们来看下这些基本的数据类型 类型 二进制位 例 范围 byte 8位 11111111~01111111 -2^7~2^7-1 short 16位 16个二进制代码 -2^15~2^15-1 int 32位 32个二进制代码 -2^31~2^31-1 long 64位 64个二进制代码 -2^63~2^63-1 浮点型: float 32位 32个二进制代码 double 64位 64个二进制代码 布尔型: boolean 只有false和true两种类型。 具体解释一下为什么会有这么多类型呢?而且二进制位为什么还不一样? 类型多的原因是因为有些数值本身就很小,传递给大的数据类型的话,虽然可以进去,但是有些二进制位就空闲了,占用了多余的内存却没有什么作用,所以才会有这么多的类型。 我们知道编程最终的目的是我们把代码传递给硬件,通过硬件来工作,但是呢,硬件只识别二进制代码,所以java会有一个把它的代码转化为二进制代码的过渡,上面的二进制位就是二进制码的数目,我们要想看他的范围有多大,可以这样算,二进制的第一位为标志符,通俗一点讲就是正负号,后面还有n位的话它的范围就是-|2^n|~|2^n-1| 如果我们定义的类型超出这个范围的话(也就是盆子里已经装满了东西如果再加),java就会报错,超出指定的范围,所以当我们定义数据类型的时候要搞清楚各数据类型的范围。 还有一个特殊的数据类型:char (‘字符’) char的具体位数要结合unicode编码。问题又来了,unicode编码又是什么鬼!unicode编码是一个字符集,里面包含了中,日,韩,三种文字,我们可以通过char的方法来打印出字符:char(‘u\unicode编码’),unicode表具体百度一下哈~ 数据类型转换: 显式转换:也就是强制转换 隐式转换:由JVM虚拟机自行转换 数据类型的强制转换:int a = (强制转换类型)b 转换规则:从存储范围大的类型到存储范围小的类型。 具体规则为:double→float→long→int→short(char)→byte byte b =10; byte a = (int) b; 如果我们把int类型的b转换给byte类型的a的话,会出现溢出现象,所以会报错。 所以正确强制转换的方式为~~: byte b = 10; int(或者更大的类型) a =(int) b; java基本的数据类型就讲到这里啦~ *--可能发布的内容有点混乱,我会尽快把前面的补齐~有疑问的话可以到大才哥的群里找我哈~

未分类

对于 Scrapy 处理 Ajax 处理方式当然是同家兄弟 Splash 比较靠谱! 但是 Splash 有个很坑爹的毛病就是负载承受相对较小·· 一不留神就 GG 了·········· 然后也就没有然后了~~! 所以准备给 Splash 做一个负载均衡;后端放一大堆的 Splash 这样总不会 GG 了吧。 就算其中一个 GG 了还有其它的可替代不是? 废话不多少开整·· 环境是基于: CentOS 7.3 Docker 17.06.2-ce Splash 3.0 HAproxy 1.7.9 (CentOS 大家可以将 yum 切换为阿里云的 yum 源 Docker 同理)

阿里 yum 源: http://mirrors.aliyun.com/help/centos 照葫芦画瓢做一遍(你是 CentOS7 啊!!!!不要选成其他版本了)

注意以下只需要在你需要运行 splash 的机器上安装即可

阿里 Docker 源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# step 1: 安装必要的一些系统工具

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

# Step 2: 添加软件源信息

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# Step 3: 更新并安装 Docker-CE

sudo yum makecache fast
sudo yum -y install docker-ce

# Step 4: 开启Docker服务

sudo service docker start

安装 Docker 加速器:

1
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://8050f360.m.daocloud.io

重启 Docker:

1
systemctl restart docker

这样可以极快的速度拉取镜像。 获取 splash 最新的 docker 镜像:

1
docker pull scrapinghub/splash:master

关闭所有机器防火墙 firewalld(网络安全的环境关闭,不安全的环境请放行端口,自行百度):

1
2
3
systemctl disable firewalld

systemctl stop firewalld

创建 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
# 存放过滤规则文件的目录

[root@localhost ~]# mkdir filters

# 存放JavaScript文件目录

[root@localhost ~]# mkdir js-profiles

# 存放lua模块的目录

[root@localhost ~]# mkdir lua_modules

# 存放代理文件的目录

[root@localhost ~]# mkdir proxy-profiles

# 创建完成如下:

[root@localhost ~]# pwd
/root
[root@localhost ~]# ll
total 4
drwxr-xr-x. 2 root root 25 Sep 26 03:00 filters
drwxr-xr-x. 2 root root 6 Sep 25 21:08 js-profiles
drwxr-xr-x. 2 root root 6 Sep 25 21:08 lua_modules
drwxr-xr-x. 2 root root 32 Sep 25 21:08 proxy-profiles
[root@localhost ~]#

启动 Splash:

1
docker run -d -p 8050:8050 --memory=5.0G --restart=always  --name splash       -v /root/proxy-profiles:/etc/splash/proxy-profiles       -v /root/js-profiles:/etc/splash/js-profiles       -v /root/lua_modules:/etc/splash/lua_modules       -v /root/filters:/etc/splash/filters       scrapinghub/splash:master --maxrss 4500

docker run 启动一个容器 -d 后台启动 -p 8050:8050 将容器的 8050 端口和物理机的 8050 端口绑定(可以从 8050 端口访问容器服务应用) —memory=5.0G 容器最大使用内存为 5.0GB,超出这个限制会被主进程杀死(使用 free -mg 查看并酌情设置你的内存使用) —restart=always 容器退出后无条件重启(满了 5GB 被杀死,然后重启 释放内存) —name splash 容器的名字叫 splash(可以忽略) -v ** 三个-v 参数是将宿主机的目录挂载进容器,便于容器能够直接访问挂载目录中的内容 scrapinghub/splash:master 用于启动容器的镜像 —maxrss 4500 Splash 最大内存使用为 4500MB

查看容器是否启动:

1
2
3
4
[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1b34f7933095 scrapinghub/splash:master "python3 /app/bin/..." 4 hours ago Up 4 hours 5023/tcp, 0.0.0.0:8050->8050/tcp splash
[root@localhost ~]#

访问 Splash 是否正常工作:

请注意:以上操作只需要在你需要运行 splash 的机器上安装即可

安装 HAproxy 实现负载均衡:

安装 zlib-devel(HAproxy 使用 gzip 功能):

1
yum install zlib-devel -y

安装 HAproxy:

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
# 个人喜好 源码放在这个目录
[root@localhost examples]# cd /usr/local/src/

# 安装wget
[root@localhost src]#yum install wget -y

# 下载HAproxy安装包
[root@localhost src]# wget http://www.haproxy.org/download/1.7/src/haproxy-1.7.9.tar.gz

# 解压
[root@localhost src]# tar -zxvf haproxy-1.7.9.tar.gz

# 进入目录
[root@localhost src]# cd haproxy-1.7.9

# 编译
[root@localhost src]# make TARGET=linux2628 PREFIX=/usr/local/haproxy-1.7.9 USE_ZLIB=yes

# 安装
[root@localhost src]# make install

# 拷贝启动文件到目录
[root@localhost src]# cp /usr/local/sbin/haproxy /usr/sbin/

# 测试版本
[root@localhost src]# haproxy -v

# 拷贝启动文件到启动目录
[root@localhost src]# cp examples/haproxy.init /etc/init.d/haproxy

# 赋予可执行权限
[root@localhost src]# chmod 755 /etc/init.d/haproxy

# 创建配置文件目录
[root@localhost src]# mkdir /etc/haproxy

# 创建数据目录
[root@localhost src]# mkdir /var/lib/haproxy

# 创建运行文件目录
[root@localhost src]# mkdir /var/run/haproxy

# 设置日志
[root@localhost src]# vim /etc/rsyslog.conf
# 第15行 $ModLoad imudp #打开注释
# 第16行 $UDPServerRun 514 #打开注释
# 第74行 local3.* /var/log/haproxy.log #local3的路径

# 创建日志文件
[root@localhost src]# touch /var/log/haproxy.log

# 设置权限
[root@localhost src]# chown -R haproxy.haproxy /var/log/haproxy.log

# 启动日志服务
[root@localhost src]# systemctl restart rsyslog.service

配置 HAproxy Conf:

1
[root@localhost src]# vim /etc/haproxy/haproxy.cfg

写入以下内容:

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
# HAProxy 1.7 config for Splash. It assumes Splash instances are executed
# on the same machine and connected to HAProxy using Docker links.
global
# raise it if necessary
maxconn 512
# required for stats page
stats socket /tmp/haproxy

userlist users
user user insecure-password userpass

defaults
log global
mode http

# remove requests from a queue when clients disconnect;
# see https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4.2-option%20abortonclose
option abortonclose

# gzip can save quite a lot of traffic with json, html or base64 data
# compression algo gzip
compression type text/html text/plain application/json

# increase these values if you want to
# allow longer request queues in HAProxy
timeout connect 3600s
timeout client 3600s
timeout server 3600s


# visit 0.0.0.0:8036 to see HAProxy stats page
listen stats
bind *:8036
mode http
stats enable
stats hide-version
stats show-legends
stats show-desc Splash Cluster
stats uri /
stats refresh 10s
stats realm Haproxy\ Statistics
stats auth admin:adminpass


# Splash Cluster configuration
# 代理服务器监听全局的8050端口
frontend http-in
bind *:8050
# 如果你需要开启Splash的访问认证
# 则注释default_backend splash-cluster
# 并放开其余default_backend splash-cluster 之上的其余注释
# 账号密码为user userpass
# acl auth_ok http_auth(users)
# http-request auth realm Splash if !auth_ok
# http-request allow if auth_ok
# http-request deny

# acl staticfiles path_beg /_harviewer/
# acl misc path / /info /_debug /debug

# use_backend splash-cluster if auth_ok !staticfiles !misc
# use_backend splash-misc if auth_ok staticfiles
# use_backend splash-misc if auth_ok misc
default_backend splash-cluster


backend splash-cluster
option httpchk GET /
balance leastconn

# try another instance when connection is dropped
retries 2
option redispatch
# 将下面IP地址替换为你自己的Splash服务IP和端口
# 按照以下格式一次增加其余的Splash服务器
server splash-0 10.10.1.41:8050 check maxconn 5 inter 2s fall 10 observe layer4
server splash-1 10.10.1.42:8050 check maxconn 5 inter 2s fall 10 observe layer4
server splash-2 10.10.1.32:8050 check maxconn 5 inter 2s fall 10 observe layer4

backend splash-misc
balance roundrobin
# 将下面IP地址替换为你自己的Splash服务IP和端口
# 按照以下格式一次增加其余的Splash服务器
server splash-0 10.10.1.41:8050 check fall 15
server splash-1 10.10.1.42:8050 check fall 15
server splash-2 10.10.1.32:8050 check fall 15

启动 HAproxy:

1
2
3
4
5
6
7
8
# 启动HAproxy
[root@localhost src]# /etc/init.d/haproxy start
Restarting haproxy (via systemctl): [ OK ]

# 如果出现错误则使用:
[root@localhost examples]# systemctl status haproxy.service

# 查看报错

查看 HAproxy 状态: 用户名和密码为: admin adminpass

查看 HAproxy 负载是否生效:

完美!!!收工!! 注意:HAproxy 这台服务器没有安装 Splash 服务,是负载到其余安装有 Splash 的服务器上提供的服务器哦!

Python

大家好,我还是小四毛,不是崔老师!!!!崔老师在隔壁,哈哈哈。

写了一个从网上抓取代理 IP,然后构建代理 IP 池的脚本,放在了这里:https://github.com/xiaosimao/IP_POOL

以后应该还会有很多的改动, 欢迎有兴趣的同学 star,以便及时可以收到改动的通知。

目前是从以下几个网站获取 IP:66ip,xicidaili,data5u,proxydb。

具体的使用方法文档在 readme.md 中, 欢迎交流。

如果你需要从别的网站获得, 那么可以在配置文件中进行相关的配置即可, 如果实在不想自己写,也可以提 issue 给我,我会看情况更新到代码中。

一般情况下,只要配置一下配置项就可以从新的网站获取 IP,最大限度减少写代码的时间。

免费的 ip,质量不敢保证,目前测试的目标网站为百度和https://httpbin.org/get, 还是获得了一些通过测试的 IP,下面是截图。

Net

HTTP 2xx 范围内的状态码表明了“客户端发送的请求已经被服务器接受并且被成功处理了”。 HTTP/1.1 200 OK 是 HTTP 请求成功后的标准响应,当你在浏览器中打开某个网站后,你通常会得到一个 200 状态码。HTTP/1.1 206 状态码表示的是“客户端通过发送范围请求头Range抓取到了资源的部分数据” 这种请求通常用来:

  • 学习http头和状态
  • 解决网路问题
  • 解决大文件下载问题
  • 解决CDN和原始HTTP服务器问题
  • 使用工具例如lftp,wget,telnet测试断电续传
  • 测试将一个大文件分割成多个部分同时下载

测试

查看服务器是否支持 HTTP 206:

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
curl -I https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
ETag: "b29f4639b76cd7f94a4b36b05be6c85acfe478f1"
Content-Type: text/plain; charset=utf-8
Cache-Control: max-age=300
X-Geo-Block-List:
X-GitHub-Request-Id: 850A:16D2:30128BA:3341504:59BBC946
Content-Length: 8709
Accept-Ranges: bytes
Date: Fri, 15 Sep 2017 12:36:31 GMT
Via: 1.1 varnish
Connection: keep-alive
X-Served-By: cache-nrt6123-NRT
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1505478991.145862,VS0,VE1
Vary: Authorization,Accept-Encoding
Access-Control-Allow-Origin: *
X-Fastly-Request-ID: ee23d80d2ba507ec0a70c880a075df0d2671aa4d
Expires: Fri, 15 Sep 2017 12:41:31 GMT
Source-Age: 8

其中有两个我们比较关注的请求头: Accept-Ranges: bytes:该响应头表明服务器支持 Range 请求,以及服务器所支持的单位是字节。同时服务器支持断点续传,以及支持同时下载文件的多个部分,也就是说下载工具可以利用范围请求加速下载该文件。Accept-Ranges: none 响应头表示服务器不支持范围请求。 Content-Length: 8709 :Content-Length 响应头表明了响应实体的大小,也就是真实的图片文件的大小是 8709 字节 (8.7K)。

发送

利用 CURL 可以指定请求范围。 获取前 500 字节:

1
curl --header "Range: bytes=0-500" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

后 500 字节:

1
curl --header "Range: bytes=-500" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

从 500 - 1000 字节:

1
curl --header "Range: bytes=500-1000" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

从 500 - 末尾字节:

1
curl --header "Range: bytes=500-" https://raw.githubusercontent.com/Germey/LaravelGeetest/master/README.md

开启

大部分web服务器都原生支持字节范围请求. Apache 2.x用户可以在httpd.conf中尝试 mod_headers:

1
Header set Accept-Ranges bytes

Python

大家好,我是四毛, 不是崔老师。

恩,今天的内容很短, 主要都写在了 README.md 里面。

写了一个将爬虫基本步骤都封装起来的小框架,地址在https://github.com/xiaosimao/AiSpider, 欢迎 Star。

写的很基础,很简单,大道至简(对,其实就是不会写)。

最近也在学一些设计模式的东西。

欢迎有兴趣的同学共同研究,readme.md 中有我的微信(加的时候麻烦注明一下来自静觅),提出存在的问题和你的想法,这样大家可以共同讨论,共同进步。

BE A SPIDERMAN。

Python

Neo4j是一个世界领先的开源图形数据库,由 Java 编写。图形数据库也就意味着它的数据并非保存在表或集合中,而是保存为节点以及节点之间的关系。 Neo4j 的数据由下面几部分构成:

  • 节点
  • 属性

Neo4j 除了顶点(Node)和边(Relationship),还有一种重要的部分——属性。无论是顶点还是边,都可以有任意多的属性。属性的存放类似于一个 HashMap,Key 为一个字符串,而 Value 必须是基本类型或者是基本类型数组。 在Neo4j中,节点以及边都能够包含保存值的属性,此外:

  • 可以为节点设置零或多个标签(例如 Author 或 Book)
  • 每个关系都对应一种类型(例如 WROTE 或 FRIEND_OF)
  • 关系总是从一个节点指向另一个节点(但可以在不考虑指向性的情况下进行查询)

具体介绍可以参考:https://www.w3cschool.cn/neo4j

Neo4j的特点

  • 它拥有简单的查询语言 Neo4j CQL
  • 它遵循属性图数据模型
  • 它通过使用 Apache Lucence 支持索引
  • 它支持 UNIQUE 约束
  • 它包含一个用于执行 CQL 命令的 UI:Neo4j 数据浏览器
  • 它支持完整的 ACID(原子性,一致性,隔离性和持久性)规则
  • 它采用原生图形库与本地 GPE(图形处理引擎)
  • 它支持查询的数据导出到 Json 和 XLS 格式
  • 它提供了 REST API,可以被任何编程语言(如 Java,Spring,Scala 等)访问
  • 它提供了可以通过任何 UI MVC 框架(如 Node JS )访问的 Java 脚本
  • 它支持两种 Java API:Cypher API 和 Native Java API 来开发 Java 应用程序

Neo4j安装

可以到官网直接下载安装包安装即可,链接:https://neo4j.com/download/

Neo4j CQL命令

Neo4j 的 CQL 是非常重要的命令,类似于 SQL 语句,具体的用法可以参考:https://www.w3cschool.cn/neo4j/neo4j_cql_introduction.html

Py2Neo用法

Py2Neo 是用来对接 Neo4j 的 Python 库,接下来对其详细介绍。

相关链接

安装方法

使用 Pip 安装即可:

1
pip3 install py2neo

Node & Relationship

Neo4j 里面最重要的两个数据结构就是节点和关系,即 Node 和 Relationship,可以通过 Node 或 Relationship 对象创建,实例如下:

1
2
3
4
5
6
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
print(a, b, r)

运行结果:

1
(alice:Person {name:"Alice"}) (bob:Person {name:"Bob"}) (alice)-[:KNOWS]->(bob)

这样我们就成功创建了两个 Node 和两个 Node 之间的 Relationship。 Node 和 Relationship 都继承了 PropertyDict 类,它可以赋值很多属性,类似于字典的形式,例如可以通过如下方式对 Node 或 Relationship 进行属性赋值,接着上面的代码,实例如下:

1
2
3
4
a['age'] = 20
b['age'] = 21
r['time'] = '2017/08/31'
print(a, b, r)

运行结果:

1
(alice:Person {age:20,name:"Alice"}) (bob:Person {age:21,name:"Bob"}) (alice)-[:KNOWS {time:"2017/08/31"}]->(bob)

可见通过类似字典的操作方法就可以成功实现属性赋值。 另外还可以通过 setdefault() 方法赋值默认属性,例如:

1
2
a.setdefault('location', '北京')
print(a)

运行结果:

1
(alice:Person {age:20,location:"北京",name:"Alice"})

可见没有给 a 对象赋值 location 属性,现在就会使用默认属性。 但如果赋值了 location 属性,则它会覆盖默认属性,例如:

1
2
3
a['location'] = '上海'
a.setdefault('location', '北京')
print(a)

运行结果:

1
(alice:Person {age:20,location:"上海",name:"Alice"})

另外也可以使用 update() 方法对属性批量更新,接着上面的例子实例如下:

1
2
3
4
5
6
data = {
'name': 'Amy',
'age': 21
}
a.update(data)
print(a)

运行结果:

1
(alice:Person {age:21,location:"上海",name:"Amy"})

可以看到这里更新了 a 对象的 name 和 age 属性,没有更新 location 属性,则 name 和 age 属性会更新,location 属性则会保留。

Subgraph

Subgraph,子图,是 Node 和 Relationship 的集合,最简单的构造子图的方式是通过关系运算符,实例如下:

1
2
3
4
5
6
7
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
s = a | b | r
print(s)

运行结果:

1
({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})}, {(alice)-[:KNOWS]->(bob)})

这样就组成了一个 Subgraph。 另外还可以通过 nodes() 和 relationships() 方法获取所有的 Node 和 Relationship,实例如下:

1
2
print(s.nodes())
print(s.relationships())

运行结果:

1
2
frozenset({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})})
frozenset({(alice)-[:KNOWS]->(bob)})

可以看到结果是 frozenset 类型。 另外还可以利用 & 取 Subgraph 的交集,例如:

1
2
3
s1 = a | b | r
s2 = a | b
print(s1 & s2)

运行结果:

1
({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})}, {})

可以看到结果是二者的交集。 另外我们还可以分别利用 keys()、labels()、nodes()、relationships()、types() 分别获取 Subgraph 的 Key、Label、Node、Relationship、Relationship Type,实例如下:

1
2
3
4
5
6
s = a | b | r
print(s.keys())
print(s.labels())
print(s.nodes())
print(s.relationships())
print(s.types())

运行结果:

1
2
3
4
5
frozenset({'name'})
frozenset({'Person'})
frozenset({(alice:Person {name:"Alice"}), (bob:Person {name:"Bob"})})
frozenset({(alice)-[:KNOWS]->(bob)})
frozenset({'KNOWS'})

另外还可以用 order() 或 size() 方法来获取 Subgraph 的 Node 数量和 Relationship 数量,实例如下:

1
2
3
4
from py2neo import Node, Relationship, size, order
s = a | b | r
print(order(s))
print(size(s))

运行结果:

1
2
2
1

Walkable

Walkable 是增加了遍历信息的 Subgraph,我们通过 + 号便可以构建一个 Walkable 对象,例如:

1
2
3
4
5
6
7
8
9
from py2neo import Node, Relationship

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
c = Node('Person', name='Mike')
ab = Relationship(a, "KNOWS", b)
ac = Relationship(a, "KNOWS", c)
w = ab + Relationship(b, "LIKES", c) + ac
print(w)

运行结果:

1
(alice)-[:KNOWS]->(bob)-[:LIKES]->(mike)<-[:KNOWS]-(alice)

这样我们就形成了一个 Walkable 对象。 另外我们可以调用 walk() 方法实现遍历,实例如下:

1
2
3
4
from py2neo import walk

for item in walk(w):
print(item)

运行结果:

1
2
3
4
5
6
7
(alice:Person {name:"Alice"})
(alice)-[:KNOWS]->(bob)
(bob:Person {name:"Bob"})
(bob)-[:LIKES]->(mike)
(mike:Person {name:"Mike"})
(alice)-[:KNOWS]->(mike)
(alice:Person {name:"Alice"})

可以看到它从 a 这个 Node 开始遍历,然后到 b,再到 c,最后重新回到 a。 另外还可以利用 start_node()、end_node()、nodes()、relationships() 方法来获取起始 Node、终止 Node、所有 Node 和 Relationship,例如:

1
2
3
4
print(w.start_node())
print(w.end_node())
print(w.nodes())
print(w.relationships())

运行结果:

1
2
3
4
(alice:Person {name:"Alice"})
(alice:Person {name:"Alice"})
((alice:Person {name:"Alice"}), (bob:Person {name:"Bob"}), (mike:Person {name:"Mike"}), (alice:Person {name:"Alice"}))
((alice)-[:KNOWS]->(bob), (bob)-[:LIKES]->(mike), (alice)-[:KNOWS]->(mike))

可以看到本例中起始和终止 Node 都是同一个,这和 walk() 方法得到的结果是一致的。

Graph

在 database 模块中包含了和 Neo4j 数据交互的 API,最重要的当属 Graph,它代表了 Neo4j 的图数据库,同时 Graph 也提供了许多方法来操作 Neo4j 数据库。 Graph 在初始化的时候需要传入连接的 URI,初始化参数有 bolt、secure、host、http_port、https_port、bolt_port、user、password,详情说明可以参考:http://py2neo.org/v3/database.html#py2neo.database.Graph。 初始化的实例如下:

1
2
3
4
from py2neo import Graph
graph_1 = Graph()
graph_2 = Graph(host="localhost")
graph_3 = Graph("http://localhost:7474/db/data/")

另外我们还可以利用 create() 方法传入 Subgraph 对象来将关系图添加到数据库中,实例如下:

1
2
3
4
5
6
7
8
from py2neo import Node, Relationship, Graph

a = Node('Person', name='Alice')
b = Node('Person', name='Bob')
r = Relationship(a, 'KNOWS', b)
s = a | b | r
graph = Graph(password='123456')
graph.create(s)

这里必须确保 Neo4j 正常运行,其密码为 123456,这里调用 create() 方法即可完成图的创建,结果如下: 另外我们也可以单独添加单个 Node 或 Relationship,实例如下:

1
2
3
4
5
6
7
8
from py2neo import Graph, Node, Relationship

graph = Graph(password='123456')
a = Node('Person', name='Alice')
graph.create(a)
b = Node('Person', name='Bob')
ab = Relationship(a, 'KNOWS', b)
graph.create(ab)

运行结果如下: 另外还可以利用 data() 方法来获取查询结果:

1
2
3
4
5
from py2neo import Graph

graph = Graph(password='123456')
data = graph.data('MATCH (p:Person) return p')
print(data)

运行结果:

1
[{'p': (e0d0f96:Person {name:"Alice"})}, {'p': (cfe57d0:Person {name:"Bob"})}]

这里是通过 CQL 语句实现的查询,输出结果即 CQL 语句的返回结果,是列表形式。 另外输出结果还可以直接转化为 DataFrame 对象,实例如下:

1
2
3
4
5
6
from py2neo import Graph
from pandas import DataFrame
graph = Graph(password='123456')
data = graph.data('MATCH (p:Person) return p')
df = DataFrame(data)
print(df)

运行结果:

1
2
3
                   p
0 {'name': 'Alice'}
1 {'name': 'Bob'}

另外可以使用 find_one() 或 find() 方法进行 Node 的查找,可以利用 match() 或 match_one() 方法对 Relationship 进行查找:

1
2
3
4
5
6
7
from py2neo import Graph

graph = Graph(password='123456')
node = graph.find_one(label='Person')
print(node)
relationship = graph.match_one(rel_type='KNOWS')
print(relationship)

运行结果:

1
2
(c7402c7:Person {age:21,name:"Alice"})
(c7402c7)-[:KNOWS]->(e2c42fc)

如果想要更新 Node 的某个属性可以使用 push() 方法,例如:

1
2
3
4
5
6
7
8
from py2neo import Graph, Node

graph = Graph(password='123456')
a = Node('Person', name='Alice')
node = graph.find_one(label='Person')
node['age'] = 21
graph.push(node)
print(graph.find_one(label='Person'))

运行结果:

1
(a90a763:Person {age:21,name:"Alice"})

如果想要删除某个 Node 可以使用 delete() 方法,例如:

1
2
3
4
5
6
7
from py2neo import Graph

graph = Graph(password='123456')
node = graph.find_one(label='Person')
relationship = graph.match_one(rel_type='KNOWS')
graph.delete(relationship)
graph.delete(node)

在删除 Node 时必须先删除其对应的 Relationship,否则无法删除 Node。 另外我们也可以通过 run() 方法直接执行 CQL 语句,例如:

1
2
3
4
5
from py2neo import Graph

graph = Graph(password='123456')
data = graph.run('MATCH (p:Person) RETURN p LIMIT 5')
print(list(data))

运行结果:

1
[('p': (b6f61ff:Person {age:20,name:"Alice"})), ('p': (cc238b1:Person {age:20,name:"Alice"})), ('p': (b09e672:Person {age:20,name:"Alice"}))]

NodeSelector

Graph 有时候用起来不太方便,比如如果要根据多个条件进行 Node 的查询是做不到的,在这里更方便的查询方法是利用 NodeSelector,我们首先新建如下的 Node 和 Relationship,实例如下:

1
2
3
4
5
6
7
8
9
10
11
from py2neo import Graph, Node, Relationship

graph = Graph(password='123456')
a = Node('Person', name='Alice', age=21, location='广州')
b = Node('Person', name='Bob', age=22, location='上海')
c = Node('Person', name='Mike', age=21, location='北京')
r1 = Relationship(a, 'KNOWS', b)
r2 = Relationship(b, 'KNOWS', c)
graph.create(a)
graph.create(r1)
graph.create(r2)

运行结果: 在这里我们用 NodeSelector 来筛选 age 为 21 的 Person Node,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person', age=21)
print(list(persons))

运行结果:

1
[(d195b2e:Person {age:21,location:"广州",name:"Alice"}), (eefe475:Person {age:21,location:"北京",name:"Mike"})]

另外也可以使用 where() 进行更复杂的查询,例如查找 name 是 A 开头的 Person Node,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person').where('_.name =~ "A.*"')
print(list(persons))

运行结果:

1
[(bcd8072:Person {age:21,location:"广州",name:"Alice"})]

在这里用了正则表达式匹配查询。 另外也可以使用 order_by() 进行排序:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
persons = selector.select('Person').order_by('_.age')
print(list(persons))

运行结果:

1
[(e3fc3d7:Person {age:21,location:"广州",name:"Alice"}), (da0179d:Person {age:21,location:"北京",name:"Mike"}), (cafa16e:Person {age:22,location:"上海",name:"Bob"})]

前面返回的都是列表,如果要查询单个节点的话,可以使用 first() 方法,实例如下:

1
2
3
4
5
6
from py2neo import Graph, NodeSelector

graph = Graph(password='123456')
selector = NodeSelector(graph)
person = selector.select('Person').where('_.name =~ "A.*"').first()
print(person)

运行结果:

1
(ea81c04:Person {age:21,location:"广州",name:"Alice"})

更详细的内容可以查看:http://py2neo.org/v3/database.html#cypher-utilities

OGM

OGM 类似于 ORM,意为 Object Graph Mapping,这样可以实现一个对象和 Node 的关联,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from py2neo.ogm import GraphObject, Property, RelatedTo, RelatedFrom


class Movie(GraphObject):
__primarykey__ = 'title'

title = Property()
released = Property()
actors = RelatedFrom('Person', 'ACTED_IN')
directors = RelatedFrom('Person', 'DIRECTED')
producers = RelatedFrom('Person', 'PRODUCED')

class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
born = Property()
acted_in = RelatedTo('Movie')
directed = RelatedTo('Movie')
produced = RelatedTo('Movie')

我们可以用它来结合 Graph 查询,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from py2neo import Graph
from py2neo.ogm import GraphObject, Property

graph = Graph(password='123456')


class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
age = Property()
location = Property()

person = Person.select(graph).where(age=21).first()
print(person)
print(person.name)
print(person.age)

运行结果:

1
2
3
<Person name='Alice'>
Alice
21

这样我们就成功实现了对象和 Node 的映射。 我们可以用它动态改变 Node 的属性,例如修改某个 Node 的 age 属性,实例如下:

1
2
3
4
5
person = Person.select(graph).where(age=21).first()
print(person.__ogm__.node)
person.age = 22
print(person.__ogm__.node)
graph.push(person)

运行结果:

1
2
(ccf5640:Person {age:21,location:"北京",name:"Mike"})
(ccf5640:Person {age:22,location:"北京",name:"Mike"})

另外我们也可以通过映射关系进行 Relationship 的调整,例如通过 Relationship 添加一个关联 Node,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from py2neo import Graph
from py2neo.ogm import GraphObject, Property, RelatedTo

graph = Graph(password='123456')

class Person(GraphObject):
__primarykey__ = 'name'

name = Property()
age = Property()
location = Property()
knows = RelatedTo('Person', 'KNOWS')

person = Person.select(graph).where(age=21).first()
print(list(person.knows))
new_person = Person()
new_person.name = 'Durant'
new_person.age = 28
person.knows.add(new_person)
print(list(person.knows))

运行结果:

1
2
[<Person name='Bob'>]
[<Person name='Bob'>, <Person name='Durant'>]

这样我们就完成了 Node 和 Relationship 的添加,同时由于设置了 primarykey 为 name,所以不会重复添加。 但是注意此时数据库并没有更新,只是对象更新了,如果要更新到数据库中还需要调用 Graph 对象的 push() 或 pull() 方法,添加如下代码即可:

1
graph.push(person)

也可以通过 remove() 方法移除某个关联 Node,实例如下:

1
2
3
4
5
person = Person.select(graph).where(name='Alice').first()
target = Person.select(graph).where(name='Durant').first()
person.knows.remove(target)
graph.push(person)
graph.delete(target)

这里 target 是 name 为 Durant 的 Node,代码运行完毕后即可删除关联 Relationship 和删除 Node。 以上便是 OGM 的用法,查询修改非常方便,推荐使用此方法进行 Node 和 Relationship 的修改。 更多内容可以查看:http://py2neo.org/v3/ogm.html#module-py2neo.ogm

结语

以上便是对 Neo4j 的相关介绍。

Python

基本步骤: 1、训练素材分类: 我是参考官方的目录结构: 每个目录中放对应的文本,一个 txt 文件一篇对应的文章:就像下面这样 需要注意的是所有素材比例请保持在相同的比例(根据训练结果酌情调整、不可比例过于悬殊、容易造成过拟合(通俗点就是大部分文章都给你分到素材最多的那个类别去了)) 废话不多说直接上代码吧(测试代码的丑得一逼;将就着看看吧) 需要一个小工具: pip install chinese-tokenizer 这是训练器:

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
import re
import jieba
import json
from io import BytesIO
from chinese_tokenizer.tokenizer import Tokenizer
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.externals import joblib

jie_ba_tokenizer = Tokenizer().jie_ba_tokenizer

# 加载数据集
training_data = load_files('./data', encoding='utf-8')
# x_train txt内容 y_train 是类别(正 负 中 )
x_train, _, y_train, _ = train_test_split(training_data.data, training_data.target)
print('开始建模.....')
with open('training_data.target', 'w', encoding='utf-8') as f:
f.write(json.dumps(training_data.target_names))
# tokenizer参数是用来对文本进行分词的函数(就是上面我们结巴分词)
count_vect = CountVectorizer(tokenizer=jieba_tokenizer)

tfidf_transformer = TfidfTransformer()
X_train_counts = count_vect.fit_transform(x_train)

X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
print('正在训练分类器.....')
# 多项式贝叶斯分类器训练
clf = MultinomialNB().fit(X_train_tfidf, y_train)
# 保存分类器(好在其它程序中使用)
joblib.dump(clf, 'model.pkl')
# 保存矢量化(坑在这儿!!需要使用和训练器相同的 矢量器 不然会报错!!!!!! 提示 ValueError dimension mismatch··)
joblib.dump(count_vect, 'count_vect')
print("分类器的相关信息:")
print(clf)

下面是是使用训练好的分类器分类文章: 需要分类的文章放在 predict_data 目录中:照样是一篇文章一个 txt 文件

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
# -*- coding: utf-8 -*-
# @Time : 2017/8/23 18:02
# @Author : 哎哟卧槽
# @Site :
# @File : 贝叶斯分类器.py
# @Software: PyCharm

import re
import jieba
import json
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.externals import joblib


# 加载分类器
clf = joblib.load('model.pkl')

count_vect = joblib.load('count_vect')
testing_data = load_files('./predict_data', encoding='utf-8')
target_names = json.loads(open('training_data.target', 'r', encoding='utf-8').read())
# # 字符串处理
tfidf_transformer = TfidfTransformer()

X_new_counts = count_vect.transform(testing_data.data)
X_new_tfidf = tfidf_transformer.fit_transform(X_new_counts)
# 进行预测
predicted = clf.predict(X_new_tfidf)
for title, category in zip(testing_data.filenames, predicted):
print('%r => %s' % (title, target_names[category]))

这个样子将训练好的分类器在新的程序中使用时候 就不报错: ValueError dimension mismatch·· 这儿有个 demo 仅供参考:GitHub 地址

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,上面的那个接口其实也是,在返回数据中是可以获得的。 所以综上所述:

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

生成第一步请求

接下来我们要做的第一步当然是请求轮子哥的基本信息,然后获取轮子哥的关注列表了,我们首先构造一个格式化的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 存储 超级简单好使。 至此本篇博文结束。 这个还有一个分布式的版本、现在不想写了··· 等年后再写吧。 另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。另外我真的一个资源都没看。

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"
}
}

以上是版本约束的介绍

参考

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传入参数 你可以当作启动爬虫(就是入口)

改动的地方我都有明确标注哦!仔细看看有什么不同吧。