AoboSir 博客

与15年前的我比,我现在是大人;与15年后的我比,我现在还是个婴儿

C++ 编写类文件的时候,需要注意的问题


一段时间不编写程序了,基本上都忘记了。今天我来介绍一下:当我们编写类文件的时候,需要注意的问题:


比如,我现在写了pairAlgin.hpp文件 和 pairAlgin.cpp文件。

我在pairAlign.hpp文件里面

1
2
3
4
class PairAlign{
public:
  PairAlign();
};

你会发现:这个文件的名字和里面的类的名字不同。一个是pairAlign的文件名,一个是PairAlign类。这是没有关系的。这个随便。


但是,我在使用make命令编译程序的时候,上面没有错误的程序竟然编译不了。

后来,我们知道了问题。其实是我们的CMakeLists.txt文件里面的问题。因为 pairAlgin.hpp文件 和 pairAlgin.cpp文件是放在pairAlgin文件夹里面的。而我却没有将这个文件夹添加的被编译的SRC_LIST变量里面。

我们需要在CMakeLists.txt文件里面的aux_source_directory(. SRC_LIST)下面添加下面这个代码,就可以解决问题:

1
aux_source_directory(./pairAlign/ SRC_LIST)

现在我们在重新执行:

1
2
cmake ..
make

就可以编译成功了。

Arduino 005 ADC


我使用的Arduino板子是:Arduino UNO R3 (这里 关于这个板子的引脚介绍。)

参考文献:《Arduino程序设计基础》 3.4 设置ADC参考电压


好的,现在我们知道使用analogRead()函数来获取模拟输入口的电压。

Alt text

设置参考电压

Arduino板子上有一个引脚:AREF 引脚,它就是用来连接参考电压的。如果我们没有设置参考电压的话,Arduino会默认使用工作电压作为参考电压。工作电压一般都是5V,所以默认的参考电压也为5V。

如果我们给Arduino的ADC设置参考电压,除了上面我们说的:要在Arduino板子上的AREF引脚上给一个参考电压外,我们可以还需要在程序初始化的地方使用analogReference()函数来设置Arduino使用外部参考电压。

Alt text


设置参考电压是需要注意

如果你要设置参考电压,注意:这个电压必须大于0,并且小于当前的工作电压(Arduino的工作电压一般为5V),否则可能会损坏Arduino控制器。


C++ 的延时函数


方法一

在Linux下,我们这样使用:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <unistd.h>

int main(void){
  while(1){
      std::cout << "Hello World!" << std::endl;
      sleep(1); //单位是秒
  }
  return 0;
}

我们使用#include <unistd.h>头文件里面的sleep()函数,给这个函数传入的形参是以秒为单位的正整数。


上面的程序执行的效果应该是:以一秒为单位打印Hello World!这个字符串。

或者形参传入以微秒为单位(1000,000微秒 = 1秒)的数据:

usleep()函数的详细介绍:这里

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <unistd.h>

int main(void){
  while(1){
      std::cout << "Hello World!" << std::endl;
      usleep(1000000); //单位是微秒 1000000us = 1s
  }
  return 0;
}

方法三

C++11里面,你可以这样使用:

我使用的是Qt5,在Qt5里面使用C++11,你需要在项目的.pro文件里面添加下面的这句代码:

1
CONFIG += c++11

如果你不在.pro文件中添加上面的这句代码,你就使用不了下面代码里面的std::this_thread

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <chrono>
#include <thread>

int main(void){
  while(1){
      std::cout << "Hello World!" << std::endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //单位是毫秒
  }
  return 0;
}

方法四

如果你使用了Boost库,那么你可以使用下面的代码实现延时的功能:

Linux系统安装Boost库很简单,只需要执行: sudo apt-get install libboost-dev

我使用的是QT项目,所以我们需要在.pro文件中,添加boost库的头文件路径和链接文件的路径:

1
2
3
HEADERS += /usr/include/

LIBS += -L/usr/lib/x86_64-linux-gnu -lboost_system -lboost_thread
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <boost/thread/thread.hpp>

int main(void){
  while(1){
      std::cout << "Hello World!" << std::endl;
      boost::this_thread::sleep( boost::posix_time::milliseconds(1000) ); //单位是毫秒
  }
  return 0;
}

或者:

1
boost::this_thread::sleep( boost::posix_time::seconds(1) ); //单位是秒

参考网站:Sleep for milliseconds

C++ 构造函数使用`:成员变量(形参)`的形式给类里面成员变量赋值,如果成员变量和形参是指针,那么需要注意的事项


我先把结论列出来:

当成员变量和形参是指针,最好不要使用:成员变量(形参)这样的形式。因为你可以不是进行:成员变量 = 形参这个方向的赋值,你可能是执行:形参 = 成员变量这个方向的赋值。因为前提,它们都是指针嘛。


今天我遇到了这样的一个错误:

下面的程序,编译是正常通过的,但是运行却不行。(我只是将相关的代码贴了出来)

1
2
3
4
5
6
7
8
class PclView{
public:
    PclView(pcl::visualization::PCLVisualizer * &p);
    void showCloudsLeft(const pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_source,
                        const pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_target);
private:
    pcl::visualization::PCLVisualizer *p;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PclView::PclView(pcl::visualization::PCLVisualizer * &p):p(p){
    vp_1 = 1;
    vp_2 = 2;
    // Create a PCLVisualizer object
    p = new pcl::visualization::PCLVisualizer ("Pairwise Incremental Registration example");
    p->createViewPort (0.0, 0, 0.5, 1.0, vp_1);
    p->createViewPort (0.5, 0, 1.0, 1.0, vp_2);
    p->removePointCloud ("vp1_target");
}

void PclView::showCloudsLeft(const pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_source,
                             const pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_target)
{
  p->removePointCloud ("vp1_target");
  p->removePointCloud ("vp1_source");

  pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> tgt_h (cloud_target, 0, 255, 0);
  pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> src_h (cloud_source, 255, 0, 0);
  p->addPointCloud (cloud_target, tgt_h, "vp1_target", vp_1);
  p->addPointCloud (cloud_source, src_h, "vp1_source", vp_1);

//  PCL_INFO ("Press Space to begin the registration.\n");
  p-> spinOnce();
}

当我们在main.cpp文件中定义了一个上面的类的实例化对象后,然后调用这个类的showCloudsLeft()方法。编译程序没有问题, 但是运行程序就会出现错误。

1
2
3
4
5
6
7
8
int main(void){
  pcl::visualization::PCLVisualizer *p;
  PclView viewer(p);
  pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_source;
  pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_target;
  viewer.showCloudsLeft(cloud_source, cloud_target);
  return 0;
}

上面的程序运行的时候程序会死在showCloudsLeft()函数里面的p->removePointCloud ("vp1_target");这段代码。原因是:p指针为空。


正确的写法:

问题出现在PclView::PclView(pcl::visualization::PCLVisualizer * &p)这个构造函数里面。

正确的写法有两种:

1
2
3
4
5
6
7
8
9
10
PclView::PclView(pcl::visualization::PCLVisualizer * &p){
    this->p = p;
    vp_1 = 1;
    vp_2 = 2;
    // Create a PCLVisualizer object
    this->p = new pcl::visualization::PCLVisualizer ("Pairwise Incremental Registration example");
    this->p->createViewPort (0.0, 0, 0.5, 1.0, vp_1);
    this->p->createViewPort (0.5, 0, 1.0, 1.0, vp_2);
    this->p->removePointCloud ("vp1_target");
}

或者:

1
2
3
4
5
6
7
8
9
PclView::PclView(pcl::visualization::PCLVisualizer * &p):p(p){
    vp_1 = 1;
    vp_2 = 2;
    // Create a PCLVisualizer object
    this->p = new pcl::visualization::PCLVisualizer ("Pairwise Incremental Registration example");
    this->p->createViewPort (0.0, 0, 0.5, 1.0, vp_1);
    this->p->createViewPort (0.5, 0, 1.0, 1.0, vp_2);
    this->p->removePointCloud ("vp1_target");
}

现在问题就解决了。


但是虽然现在的问题是解决了,但是程序还是有潜在问题的。(现在的程序,还没有达到我们想要的目的。)我们最开始设计这个构造函数,是想将外部通过形参传入的这个指针能够与这个实例化对象里面pcl::visualization::PCLVisualizer * p指针能够指向同一个东西,然后它们两个都可以去改变这个东西。

但是现在我们并没有做到这一点。我们可以使用下面的代码验证:

1
2
3
4
5
6
7
8
9
int main(void){
  pcl::visualization::PCLVisualizer *p;
  PclView viewer(p);
  pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_source;
  pcl::PointCloud<pcl::PointXYZ>::Ptr &cloud_target;
  viewer.showCloudsLeft(cloud_source, cloud_target);
  p->removePointCloud ("vp1_target");
  return 0;
}

编译程序不会出现什么问题,但是现在运行,程序就会在main()函数里面的p->removePointCloud ("vp1_target");这段代码出现问题。


所以,正真正确的解决办法是:

1
2
3
4
5
6
7
8
9
10
PclView::PclView(pcl::visualization::PCLVisualizer * &p){
    vp_1 = 1;
    vp_2 = 2;
    // Create a PCLVisualizer object
    this->p = new pcl::visualization::PCLVisualizer ("Pairwise Incremental Registration example");
    this->p->createViewPort (0.0, 0, 0.5, 1.0, vp_1);
    this->p->createViewPort (0.5, 0, 1.0, 1.0, vp_2);
    this->p->removePointCloud ("vp1_target");
    p = this->p;
}

现在就变成了完美的程序,不会出现问题,也没有潜在的问题。


出现上面的问题的原因

原因很简单,完美的设计思想是:希望两个指针都指向同一个存储空间,但是,实际上两个指针没有指向一个存储空间,一个指针指向了存储空间,而一个是空指针,所以就出现了错误。

Ubuntu 解决更新软件包的时候出现的 “Low Disk Space” 存储空间不足问题


当你的Ubuntu系统的真机或者虚拟机使用时间长了,安装下载的软件包多了。同时你的电脑的存储空间本身又不多。这样,时间一长,就会出现下面的这个问题:

Alt text

系统提示你:现在系统的存储空间已经快要用没有了。

我现在这个情况,我这个Ubuntu系统是安装在一个虚拟机里面的,我只是给它分配了20G的硬盘空间。所以,我使用了大约2个月的时间,就出现了存储空间不足的问题。

下面就来解决这个问题。

其实解决这个问题很简单。我们只需要清理一下系统就可以。

参考网站:http://askubuntu.com/questions/298487/not-enough-free-disk-space-when-upgrading

你需要安装一个ubuntu-tweak软件,具体步骤如下:(如果你的Ubuntu里面有,就可以跳过这一步)

1
2
3
$ sudo add-apt-repository ppa:tualatrix/ppa
$ sudo apt-get update
$ sudo apt-get install ubuntu-tweak

现在启动ubuntu-tweak软件,进行清除缓存:

1
$ ubuntu-tweak

Alt text

然后切换到"Janitor"标签,选择你要清除的Apps、Personal、System的复选框,然后点击"Clean"按钮进行清除。

Alt text

它这里面删除的是:比如, 我们下载的安装包,删除的就是这些安装包。

Alt text

Over 2016-5-11 16:38:02

Windows上使用SecureCRT软件连接Linux终端 — 解决问题;the Remote System Refused the Connection


正常的情况:

1
ifconfig

Alt text

1
whoami

Alt text

1
ps -e | grep ssh

Alt text


secureCRT软件

Alt text

Alt text

Alt text

Alt text


不正常的情况:The remote system refused the connection.

如果你遇到这个问题,说明你的Linux系统里面没有安装openssh-server

1
sudo apt-get install openssh-server
1
2
aobosir@ubuntu:~$ ps -e | grep ssh
 3834 ?        00:00:00 sshd

执行完下面的命令,系统会自动注销(Logout)。

1
2
ssh-agent restart
# 已经没有这个命令了。替代的方法就是:重启系统或者注销系统。

现在查看一下,现在就可以进入了正常使用的状态。

1
2
3
4
aobosir@ubuntu:~$ ps -e | grep ssh
 3834 ?        00:00:00 sshd
 4116 ?        00:00:00 ssh-agent
aobosir@ubuntu:~$ 

现在,我们在Windows系统这段使用SecureCRT软件连接这个Linux系统,就可以添加成功了。

连接成功之后,我们现在在SecureCRT软件连接的Linux系统终端中再次下面命令来查看当前运行着的ssh进程有哪些。(现在,我们已经可以在Windows端的SecureCRT软件里面控制Linux端了。)

1
ps -e | grep ssh

查看Linux这端的当前进程:(正常你会看大下面的4个。(我也不知道是什么东西))

Alt text


如果还是不行,你就执行下面的命令:

1
ssh start

我就不信现在还是不行,现在肯定是可以正常的连接了。

搞定


参考网站:http://blog.csdn.net/lifengxun20121019/article/details/13627757

Python3 大型网络爬虫实战 004 — Scrapy 大型静态商城网站爬虫项目编写及数据写入数据库实战 — 实战:爬取淘宝


[TOC]

开发环境

  • Python第三方库:lxml、Twisted、pywin32、scrapy
  • Python 版本:python-3.5.0-amd64
  • PyCharm软件版本:pycharm-professional-2016.1.4
  • 电脑系统:Windows 10 64位

如果你还没有搭建好开发环境,请到这篇博客


本篇博文的重点内容:

  • 有一些数据,在源码上找不到的,这个时候需要使用 — 抓包。
  • Python调用MySQL数据库

本爬虫项目的目的是:某个关键字在淘宝上搜索到的所有商品,获取所有商品的: 商品名字、商品链接、商品价格、商品的评论。


开始实战

创建一个爬虫项目

1
scrapy startproject thirdDemo

Alt text

设置防反爬机制(settings.py 文件)

请参考这篇博客:给 Scrapy 爬虫项目设置为防反爬

分析网站

  • 分析网页界面
  • 分析网址结构
  • 分析网页源代码

1 . 分析网页界面:

我们在淘宝网的搜索栏里面搜索关键字,比如“小吃”。它一共会输出100页。

可见:100页的搜索结果是淘宝的上限。(最多100页)

Alt text

2 . 分析网址结构:

当我们点击页面进行浏览时,我们发现不同的页面的网址有规律,并且下面是我们找到的规律:

  1. 红色部分是一模一样的。
  2. 删除红色部分,将剩下的组成网址,一样可以正常的浏览原网页。
  3. q= 后面是“小吃”的编码形式。
  4. s= 后面的数值等于 44*(当前页面-1)

Alt text

开始写爬虫程序(taobao.py 文件)

创建一个爬虫文件(taobao.py 文件)

1
2
cd thirdDemo
scrapy genspider -t basic taobao taobao.com

Alt text

使用PyCharm软件开发,使用PyCharm软件打开 thirdDemo项目。

Alt text

添加需要使用的存储容器对象(items.py文件)

先到 items.py 文件里面的ThirddemoItem()函数里面创建存储用到容器(类的实例化对象)

1
2
3
4
5
6
7
8
class ThirddemoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    link = scrapy.Field()
    price = scrapy.Field()
    comment = scrapy.Field()
    pass

Alt text

得到搜索关键字对应的所有搜索页面(taobao.py文件)

在回调函数parse()中,建立一个变量(key)来存储关键词(零食)。然后在使用一个for循环来爬取所有的网页。然后使用scrapy.http里面的Request 来在parse()函数返回(返回一个生成器(yield))一个网页源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-
import scrapy
from scrapy.http import Request

class TaobaoSpider(scrapy.Spider):
    name = "taobao"
    allowed_domains = ["taobao.com"]
    start_urls = ['http://taobao.com/']

    def parse(self, response):
        key = '小吃'
        for i in range(0, 2):
            url = 'https://s.taobao.com/search?q=' + str(key) + '&s=' + str(44*i)
            print(url)
            yield Request(url=url, callback=self.page)
        pass

    def page(self, response):
        pass

Alt text

(注意:我们上面通过观察网页已经知道了,搜索得到的页面有100页,但是我们现在是测试阶段,不爬这么多页,上面的代码我们只爬了2页)


运行一下:

Alt text

程序没有问题。


得到所有商品的id

我们现在的目标是得到搜索页面中所有商品的链接。

Alt text

现在,我们观察搜索页面的源代码,我们找这些商品链接的规律,能否通过什么商品id之类的信息,然后通过商品id来构造出商品的链接网址。

幸运的是:确实可以这么做。

我发现,不管是搜索的商品,不管是在淘宝里、天猫里、天猫超市里,商品的链接网址都可以用下面的格式去构造:

1
https://item.taobao.com/item.htm?id=商品的id

所以,现在我们要做的是:先提取商品的id:(使用正则表达式)

对搜索结果的网页随便一个地方右键:查看网页源代码(V)

(我发现:通过在浏览器中按F12 和 右键来 查看网页源代码 这两种查看源代码得到的源代码不一样,后者得到的源代码和爬虫爬取的源代码一致,而前者和爬虫爬取的不一致。)

所有我们不能使用Xpath表达式来用过标签获取商品id了。只能使用正则表达式了。

我想可能的原因是:搜索页面可能是动态构造出来的,所以使用Xpath表达式是不能对这种网址的源码进行提取信息的。(我瞎想的,不知道是否正确。不过事实其实是:使用Xpath表达式是提取不了有效信息的。)

Alt text

然后随便点击进入一个商品的链接网页,在这个网页的网址里面就可以找到这个商品的id:

Alt text

然后在刚刚打开的源代码中,查找这个id:

Alt text

我们通过观察发现,使用"nid":"就可以找到这个搜索结果页面里面所有商品的id:

Alt text

这个页面里面一共是36个商品,没错。

Q: 你可能发现了,这个搜索网页里面搜索到结果是48个商品,我们得到的是36个,是不是少了?

A: 没有少,这就是淘宝的营销策略。一个搜索页面一共有48个商品,但是其中有10多个商品是重复的!其中甚至有个商品在这个搜索页面中重复出现了三次,不信,你可以仔细的找找。

所以,商品的id可以使用下面的正则表达式获取:

1
'"nid":"(.*?)"'

我们在page()方法中得到爬取到的网页源代码的 body 标签里面的所有信息:

先声明一点:

爬取到的网页源代码是:以网页源代码中指定的编码方式编码得到的bytes信息。

Alt text

我们需要得到对应的解码信息:

参考网站:Python3 bytes.decode()方法

1
    body = response.body.decode('utf-8')

response.body 它默认是二进制格式,所以我们在使用它之前要给它解码:decode('utf-8'),为了避免出错,我给它传第二个参数:ignore

page()函数中的代码现在是下面这个样子的:

1
2
3
4
5
6
def page(self, response):
    body = response.body.decode('utf-8','ignore')
    pattam_id = '"nid":"(.*?)"'
    all_id = re.compile(pattam_id).findall(body)
    print(all_id)
    pass

运行试试看:

Alt text


得到所有商品的链接网址

现在得到了所有商品的ip,现在通过这些ip,构造出所有商品的网址。得到了链接后,就可以去爬这个网页的源代码了:(下面的代码中,在next()方法中将商品的网页网址打印了出来)

1
import re
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def page(self, response):
    body = response.body.decode('utf-8','ignore')
    pattam_id = '"nid":"(.*?)"'
    all_id = re.compile(pattam_id).findall(body)
    # print(all_id)
    for i in range(0, len(all_id)):
        this_id = all_id[i]
        url = 'https://item.taobao.com/item.htm?id=' + str(this_id)
        yield Request(url=url, callback=self.next)
        pass
    pass

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

运行试试看:(自动的将网址调整到正确的网址上。比如天猫或者天猫超市之类的子域名)

Alt text


获取商品的具体信息(taobao.py 文件)

获取商品的名字

现在在next()回调函数中实例化一个开始时在items.py文件里面创建的项目的存储容器对象。然后,我们就可以直接使用它了。

所以现在在 taobao.py 文件的上面添加这个文件:

1
from thirdDemo.items import ThirddemoItem

现在我们要得到商品的标题。

我们尽量从源代码中的信息提取,如果源代码中没有的信息,我们在使用抓包的凡是提取。

标题是可以直接在源代码中提取的:(观察网页源代码,直接中Xpath表达式)

天猫或者天猫超市的商品的标题可以使用下面的Xpath表达式提取:

1
title = response.xpath("//div[@class='tb-detail-hd']/h1/text()").extract()

淘宝的商品的标题可以使用下面的Xpath表达式提取:

1
title = response.xpath("//h3[@class='tb-main-title']/@data-title").extract()

所以,这里提取标题,我们需要一个判断语句,判断这个商品的网址链接是天猫的还是淘宝的。

伪码如下:

1
2
3
4
if 不是淘宝的网址:
  title = response.xpath("//div[@class='tb-detail-hd']/h1/text()").extract() # 天猫或者天猫超市
else:
  title = response.xpath("//h3[@class='tb-main-title']/@data-title").extract() # 淘宝

我们的判断标准就是商品网址的子域名。子域名大致一共有三种:detail.tmall(天猫)、chaoshi.detail.tmall(天猫超市)、item.taobao(淘宝)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def next(self, response):
    # print(response.url)
    url = response.url
    pattam_url = 'https://(.*?).com'
    subdomain = re.compile(pattam_url).findall(url)
    # print(subdomain)
    if subdomain[0] != 'item.taobao':
        title = response.xpath("//div[@class='tb-detail-hd']/h1/text()").extract()
        pass
    else:
        title = response.xpath("//h3[@class='tb-main-title']/@data-title").extract()
        pass
    self.num = self.num + 1;
    print(title)
    pass

运行试试看:

Alt text

有的时候,偶尔会得到几个 [],这是因为,你爬的太快的,淘宝的服务器没有同意你爬取这个商品的网页。(所以提高防反爬机制,效果会好一些。)

Alt text

获取商品的链接网址(taobao.py 文件)

(直接得到)

1
    item['link'] = response.url

获取商品的价格信息(原价)(taobao.py 文件)

正常的价格可以在商品网页的源代码里面获取,但是淘宝价(促销价)在商品源代码里面没有,这时就需要通过抓包来获取。

淘宝:

Alt text

天猫:

Alt text

我们先获取正常的价格。这里也需要分淘宝和天猫,它们获取正常价格的Xpath表达式或者正则表达式不同。

注意:这里总结表达式,通过对商品页面右键 -> 查看网页源代码 的方式查看源代码。

1
2
3
4
5
6
7
8
    if subdomain[0] != 'item.taobao':
        pattam_price = '"defaultItemPrice":"(.*?)"'
        price = re.compile(pattam_price).findall(response.body.decode('utf-8', 'ignore')) # 天猫
        pass
    else:
        price = response.xpath("//em[@class = 'tb-rmb-num']/text()").extract() # 淘宝
        pass
    print(price)

提取商品的累计评论数量:(使用抓包的方式)(taobao.py 文件)

淘宝:

Alt text

天猫:

Alt text

可以使用 : Fiddler4抓包软件 或者 浏览器按F12->Network->Name->Response查看抓包信息

这里,我通过浏览器进行抓包,找到了评论数所在的包:(一个一个的找)

淘宝:

Alt text

观察这个包的网址:

Alt text

这个网址,我们可以在浏览器中复制,再访问以下:(是可以正常访问的)

Alt text

https://rate.taobao.com/detailCommon.htm?auctionNumId=533237707421&userNumId=1990097437&ua=097UW5TcyMNYQwiAiwQRHhBfEF8QXtHcklnMWc%3D%7CUm5Ockt%2BQ3dDfkB8R35Eey0%3D%7CU2xMHDJ7G2AHYg8hAS8XKQcnCVU0Uj5ZJ11zJXM%3D%7CVGhXd1llXGlUYFRpV2tQaVFvWGVHekV8RHtBf0Z%2FQXRKdUx1T3VOYDY%3D%7CVWldfS0TMw8xBD8fIAAubQslcyU%3D%7CVmJCbEIU%7CV2lJGSQEORklGCMYOAI%2FADkZJREuEzMPMgc6GiYSLRAwDDEJNGI0%7CWGFcYUF8XGNDf0Z6WmRcZkZ8R2dZDw%3D%3D&callback=json_tbc_rate_summary

我发现上面的这个网址可以缩减为:

https://rate.taobao.com/detailCommon.htm?auctionNumId=533237707421
https://rate.taobao.com/detailCount.do?_ksTS=1480993623725_99&callback=jsonp100&itemId=533237707421

而这个533237707421就是商品的id。好了,找到这个网址的规律,现在可以> 手动构造这个评论数的网址了:

天猫:

Alt text

https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=35338957824&spuId=235704813&sellerId=628189716&_ksTS=1480992656788_203&callback=jsonp204

可以缩减为:

https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=35338957824

最后,我们发现:不管是淘宝还是天猫,都可以使用下面这个构造方式来得到含有正确评论数量的网址:

1
https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=商品id

注意:使用https://rate.taobao.com/detailCommon.htm?auctionNumId=商品id 这种网址也可以,但是在对天猫商品得到评价数量和网页里面显示的不同。所以我们不使用这个构造方法。

Alt text

Alt text

所以通过商品id就可以得到含有评论数量信息的包的网址。现在在next()方法中需要通过商品的URL获取商品的id。

Alt text

我们从上面的图中看到:天猫和淘宝的网址不同,所以,从网址中获取商品id的正则表达式也就不同。下面的代码的功能就是从商品的url中提取商品id:

1
2
3
4
5
6
7
8
9
10
11
    # 获取商品的id(用于构造商品评论数量的抓包网址)
    if subdomain[0] != 'item.taobao':  # 如果不属于淘宝子域名,执行if语句里面的代码
        pattam_id = 'id=(.*?)&'
        this_id = re.compile(pattam_id).findall(url)[0]
        pass
    else:
        # 这种情况是不能使用正则表达式的,正则表达式不能获取字符串最末端的字符串
        pattam_id = 'id=(.*?)$'
        this_id = re.compile(pattam_id).findall(url)[0]
        pass
    print(this_id)

注意:$ : 在正则表达式里面的作用是:匹配字符串末尾。

举例:当url = 'https://item.taobao.com/item.htm?id=535023141744' 时,这是一个淘宝网站里面的一个商品,现在我们想得到这个网址里面的商品id。

如果你把正则表达式写成这个样子:pattam_id = 'id=(.*?)',是匹配不到结果的(商品id)。

正则表达式是通过字符串上下文来匹配你需要的信息的,如果只有“上文”,没有“下文”时,对于使用正则表达式匹配字符串末端字符串,需要在正则表达式中使用$


运行试试看,一切都在掌控之中。

构造具有评论数量信息的包的网址,并获取商品的评论数量

得到目标抓包网址,获取它的源代码,然后提取评论数量:

1
import urllib
1
2
3
4
5
6
7
8
9
10
    # 构造具有评论数量信息的包的网址
    comment_url = 'https://dsr-rate.tmall.com/list_dsr_info.htm?itemId=' + str(this_id)

    # 这个获取网址源代码的代码永远也不会出现错误,因为这个URL的问题,就算URL是错误的,也可以获取到对应错误网址的源代码。
    # 所以不需要使用 try 和 except urllib.URLError as e 来包装。
    comment_data = urllib.request.urlopen(comment_url).read().decode('utf-8', 'ignore')
    pattam_comment = '"rateTotal":(.*?),"'
    comment = re.compile(pattam_comment).findall(comment_data)
    # print(comment)
    item['comment'] = comment

现在返回item对象:

1
   yield item

Alt text


现在,我们就可以在pipline.py文件里面来对我们得到的这些商品数据进行一些操作了,比如打印到终端或者保存到数据库中。

但在这之前,我们需要设置一下settings.py文件,将下面的代码的注释去掉:

Alt text


pipline.py文件中对taobao.py爬虫文件返回的item对象进行处理(比如打印到终端,或者保存到数据库中)

将得到的信息打印到终端中:

1
2
3
4
5
6
7
8
9
10
11
12
class ThirddemoPipeline(object):
    def process_item(self, item, spider):
        title = item['title'][0]
        link = item['link']
        price = item['price'][0]
        comment = item['comment'][0]
        print('商品名字', title)
        print('商品链接', link)
        print('商品正常价格', price)
        print('商品评论数量', comment)
        print('------------------------------\n')
        return item

运行试试看:

Alt text


下面是将得到的信息保存到数据库中的操作:

第一件事情就是 启动数据库,启动数据库的代码一般我们是将它写到默认的__init__(self)函数中,这个方法就是最开始做的事情。

要想连接到数据库,首先要有数据库:使用MySQL数据库

在python上要想使用MySqL数据库,需要先安装pymysql库这个模块。

Alt text

有打开数据库的函数,就要有关闭数据库的方法。

Alt text

现在,我们在process()函数中处理数据,将数据插入到数据库里面。并且加一个异常处理,因为我不希望程序运行的时候会出现错误而终止,并且我也不想

Alt text

Python3 大型网络爬虫实战 003 — Scrapy 大型静态图片网站爬虫项目实战 — 实战:爬取 169美女图片网 高清图片


[TOC]

开发环境

  • Python第三方库:lxml、Twisted、pywin32、scrapy
  • Python 版本:python-3.5.0-amd64
  • PyCharm软件版本:pycharm-professional-2016.1.4
  • 电脑系统:Windows 10 64位

如果你还没有搭建好开发环境,请到这篇博客


  • 本篇博客源代码GitHub里:这里

这一篇博客的目的是爬取 169美女图片网 里面的所有的“西洋美女”的高清图片。

Alt text


爬虫程序设计思路:

1 . 先得到 http://www.169bb.com/xiyangmeinv/ 页面里面所有的照片后面对应的URL网页链接()。

2 . 接着在得到的URL链接网页里面得到里面所有高清图片的下载地址,进行下载。

3 . 得到所有 “西洋美女” 网页的页数。

Alt text


观察网页 和 网页源代码

1 . 打开 169美女图片网:http://www.169bb.com/

2 . 我们的目的是爬取这个站点里面所有 “西洋美女” 的高清图片。所以点击进入“西洋美女” 标签里。(http://www.169bb.com/xiyangmeinv/%EF%BC%89

Alt text

3 . 观察这个页面,在页面最下面,显示了,当前一共311页。

Alt text

4 . 我们再来观察每页的网址有什么共同点,我们发现:

这样每页的网址是有规律的,按照这个规律,我们可以推测出“西洋美女” 的第120页的网址就应该是:http://www.169bb.com/xiyangmeinv/list_4_120.html 。事实的确是这样的。好。

5 . 现在,我们随便点击一个图片,进去看看这个美女的高清图片集。

里面都是高清的图片,并且有很多,并且,不止一页。就我随机点击的这个美女的链接就有11页,并且一页里面有5张左右的高清图片。

Alt text

6 . 并且它的每页的网址也是有规律的。

但是有的美女的网址里只有一页,比如这个:http://www.169bb.com/xiyangmeinv/2016/0103/5974.html


好了,现在这个目标网页,我们已经分析完了。现在就可以编程。


写程序

源代码GitHub里:这里

接下来我们为大家讲解大型图片爬虫项目编写实战。

Step 1 .

创建一个Scrapy爬虫项目:

1
scrapy startproject secondDemo

Alt text

创建一个scrapy爬虫文件,我们就在这个文件里面写爬取目标网站里面图片的爬虫程序。

1
2
cd secondDemo
scrapy genspider -t basic pic_169bb 169bb.com

Alt text


PyCharm 软件打开刚刚创建的 secondDemo 工程。

Alt text

Step 2 . items.py 文件里面的SeconddemoItem()函数里面创建一个对象,这个对象在其他的文件里面会使用到。

Alt text

Step 3 . 现在开始爬虫的编写。进入pic_169bb.py文件。

爬虫(pic_169bb.py文件)会自动的先爬首页(169bb.com),爬完首页之后,会自动的进入parse()回调函数。

这个回调函数中,我们需要写些东西。

先获取所有栏目的名字和地址。

Alt text

查看源代码:

Alt text

pic_169bb.py 文件中的 parse()回调函数中添加下面的代码:

1
2
    urldata = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/@href").extract()
    print(urldata)

Alt text

现在运行一下,输出:

Alt text


继续进行下一次的爬取:

先导入一个模块

1
from scrapy.http import Request

爬取子栏目的第一页,即西洋美女网址的第一页。

1
2
3
4
5
6
    xiyangurldata = urldata[4]  # 获取西洋美女首页网址
    print(xiyangurldata)
    yield Request(url=xiyangurldata, callback=self.next)

def next(self, response):
    pass

Alt text

1
2
3
4
5
6
def next(self, response):
    page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
    print(page_title_list)
    page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
    print(page_url_list)
    pass

Alt text

运行输出一下:

Alt text


Alt text

1
2
3
4
5
6
7
    page_num = response.xpath("//span[@class='pageinfo']//strong/text()").extract()[0] # 得到西洋美女总页数
    print(page_num)
    print(response.url)
    for i in range(1, int(page_num)+1):
        page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
        print(page_url)
    pass

Alt text

运行输出一下:

Alt text

Alt text


继续下一次:

1
2
3
4
5
6
7
8
    for i in range(1, int(page_num)+1):
        page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
        print(page_url)
        yield Request(url=page_url, callback=self.next2)
    pass

def next2(self, response):
    pass

Alt text


现在获取每一个美女的网页网址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def next2(self, response):
    page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
    # print(page_title_list)
    page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
    # print(page_url_list)

    for i in range(0, len(page_url_list)):
        gril_page_url = page_url_list[i]
        print(gril_page_url)
        yield Request(url=gril_page_url, callback=self.next3)
    pass

def next3(self, response):
    pass

Alt text

运行程序看看:

Alt text


next3() 这个回调函数的功能就是得到一个美女网页里面的所有的页面的网址。

有的美女的网页里面只有一个页面,有的美女的网页里面有多个页面:

Alt text

Alt text

可以统一解决。


测试:

Alt text

Alt text

Alt text

输出:

Alt text


Alt text

Alt text

同样的回调函数

输出:

Alt text


所以,我们可以这样写程序:

当得到的页码为-3,说明这个美女的网页是单页的;如果得到的页码数不等于-3,说明这个美女的网页是多也的。

测试程序:

对于多页面的美女网页网址

Alt text

Alt text

运行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>scrapy crawl pic_169bb --nolog
10
http://www.169bb.com/xiyangmeinv/2016/0717/36463.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_5.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_10.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_9.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_6.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_8.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_7.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_3.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_4.html
http://www.169bb.com/xiyangmeinv/2016/0717/36463_2.html

D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>

对于单页面的美女网页:

1
2
3
def parse(self, response):
  ...
    yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

回调函输一样。

运行输出:

1
2
3
4
5
D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>scrapy crawl pic_169bb --nolog
-3
http://www.169bb.com/xiyangmeinv/2016/0103/2268.html

D:\WorkSpace\python_ws\python-large-web-crawler\secondDemo>

成功。

所以现在的爬虫代码应该是这样的:

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
# -*- coding: utf-8 -*-
import scrapy
from scrapy.http import Request

class Pic169bbSpider(scrapy.Spider):
    name = "pic_169bb"
    allowed_domains = ["169bb.com"]
    start_urls = ['http://169bb.com/']

    def parse(self, response):
        title_list = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/text()").extract()
        # print(title_list)
        urldata = response.xpath("/html/body/div[@class='header']/div[@class='hd_nav']/div[@class='w1000']//a/@href").extract()
        #print(urldata)
        xiyang_title = title_list[4] # 获取西洋美女标签的文本内容
        xiyang_urldata = urldata[4]  # 获取西洋美女首页网址
        # print(xiyang_title, xiyang_urldata)
        yield Request(url=xiyang_urldata, callback=self.next)
        # yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)
        # yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

    def next(self, response):
        page_num_str = response.xpath("//span[@class='pageinfo']//strong/text()").extract()[0] # 得到西洋美女总页数
        # print(page_num_str)
        # print(response.url)
        for i in range(1, int(page_num_str)+1):
            page_url = response.url + 'list_4_'+ str(i) + '.html' # 得到西洋美女每一个页面的网址
            # print(page_url)
            yield Request(url=page_url, callback=self.next2)
        pass

    def next2(self, response):
        page_title_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@alt").extract()
        # print(page_title_list)
        page_url_list = response.xpath("/html/body//div[@class='w1000 box03']/ul[@class='product01']//li/a/@href").extract()
        # print(page_url_list)

        for i in range(0, len(page_url_list)):
            gril_page_url = page_url_list[i] # 得到西洋美女页面里面每一个美女的网页网址
            print(gril_page_url)
            yield Request(url=gril_page_url, callback=self.next3)
        pass

    def next3(self, response):
        rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
        pages_num = len(rela_pages_list) - 3
        # print(pages_num)
        self.getPic(response)
        if pages_num == -3:
            # pages_num = 1
            return
        for i in range(2, pages_num+1):
            girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
            # print(girl_page_url)
            yield Request(url=girl_page_url, callback=self.next4)
        pass

    # def demo(self, response):
    # #     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    # #     pages_num = len(rela_pages_list)-3
    # #     print(pages_num)
    # #     pass
    #     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    #     pages_num = len(rela_pages_list) - 3
    #     # print(pages_num)
    #     self.getPic(response)
    #     if pages_num == -3:
    #         # pages_num = 1
    #         return
    #     for i in range(2, pages_num+1):
    #         girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
    #         # print(girl_page_url)
    #         yield Request(url=girl_page_url, callback=self.next4)
    #     pass

    def next4(self, response):
        self.getPic(response)
        pass

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

现在,我们需要在getPic() 函数中获取每一个美女网页的每一个页面里面的所有高清图片。

Alt text

1
2
3
4
5
6
def getPic(self, response):
    # print(response.url)
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    print(item['url'])
    pass

测试运行:

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.demo)

Alt text

成功。


下载高清图片

OK,现在我们就已经得到了所有西洋美女的所有高清图片的下载地址,我们在piplines.py 文件中使用它们。

删除了next4()回调函数。在demo()回调函数里面调用的都是getPic()回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def demo(self, response):
#     rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
#     pages_num = len(rela_pages_list)-3
#     print(pages_num)
#     pass
    rela_pages_list = response.xpath("//div[@class='dede_pages']/ul//li/a/text()").extract()
    pages_num = len(rela_pages_list) - 3
    # print(pages_num)
    self.getPic(response)
    if pages_num == -3:
        # pages_num = 1
        return
    for i in range(2, pages_num+1):
        girl_page_url = response.url.replace('.html', '_') + str(i) + '.html'
        # print(girl_page_url)
        yield Request(url=girl_page_url, callback=self.getPic)
    pass

# error : yield 经过了一个中间函数,运行就有问题。我现在还不知道为什么
# def next4(self, response):
#     self.getPic(response)
#     pass

并将getPic()函数里面的item写到生成器里面:

1
2
3
4
5
6
7
def getPic(self, response):
    # print(response.url)
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    # print(item['url'])
    # pass
    yield item

测试:

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.getPic)

成功。

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

也算算成功,因为有重复的文件名,所以自动替换,所以这里需要做修改。

我们可以使用美女图片网址里面的数字作为图片文件名的固定前缀来给图片命名,使用正则表达式获取网址的数字。


pipelines.py 文件 中的 process_item() 函数中使用正则表达式得到图片下载网址的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re
import urllib.request

class SeconddemoPipeline(object):
    def process_item(self, item, spider):
        # print(len(item['url']))
        for i in range(0, len(item['url'])):
            this_url = item['url'][i]
            id = re.findall('http://724.169pp.net/169mm/(.*?).jpg', this_url)[0]
            id = id.replace('/', '_')
            print(id)
            # file = 'D:/WorkSpace/python_ws/python-large-web-crawler/xiyangmeinv/' + str(i) + '.jpg'
            # print('Downloading :' , file)
            # urllib.request.urlretrieve(this_url, filename=file)
            # print('Final Download :' , file)
        return item

Alt text

要想使用 pipelines.py 文件 中的 SeconddemoPipeline 类,需要在 settings.py 文件里面设置 ITEM_PIPELINES 项:

1
2
3
ITEM_PIPELINES = {
   'secondDemo.pipelines.SeconddemoPipeline': 300,
}

Alt text

这里,我有一件事情不懂,关于正则表达式的: 网址里面的. 也是正则表达式中的工具字符,也是数据中中的内容,那么正则表达式是如何分辨它在这里是功能字符还是内容字符?

pic_169bb.py 文件的demo() 回调函数中,这样写才能获取到美女网页的第一页的图片地址:

1
2
3
4
5
6
7
8
    # error
    # self.getPic(response)
    # succes 为啥将下面的代码用self.getPic(response)的形式不能正常的获取到,而使用下面的代码却能获取到?
    item = SeconddemoItem()
    item['url'] = response.xpath("//div[@class='big-pic']/div[@class='big_img']//p/img/@src").extract()
    # print(item['url'])
    # pass
    yield item

Alt text

运行程序试试:

测试单页的美女网页:

parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0103/2268.html', callback=self.getPic)

Alt text

成功。

测试多页的美女网页:

先将parse()函数的最后一行改为:yield Request(url='http://www.169bb.com/xiyangmeinv/2016/0717/36463.html', callback=self.demo)

Alt text

都成功。


先测试下载图片:

pipelines.py 文件 中的 SeconddemoPipeline 类的process_item() 函数里面,添加代码:

1
2
3
4
5
6
7
8
9
10
11
12
def process_item(self, item, spider):
    # print(len(item['url']))
    for i in range(0, len(item['url'])):
        this_url = item['url'][i]
        id = re.findall('http://724.169pp.net/169mm/(.*?).jpg', this_url)[0]
        id = id.replace('/', '_')
        # print(id)
        file = 'D:/WorkSpace/python_ws/python-large-web-crawler/xiyangmeinv/' + id + '.jpg'
        print('Downloading :' , file)
        urllib.request.urlretrieve(this_url, filename=file)
        print('Final Download :' , file)
    return item

Alt text

运行程序,没有毛病:(除了下载速度有点慢)

Alt text


下载 169美女图片网 的所有西洋美女的图片

pic_169bb.py文件里, 将parse()函数的最后一行改为:yield Request(url=xiyang_urldata, callback=self.next)

Alt text

demo() 函数里面的所有代码复制一份到 next3()函数里:

Alt text

现在,运行程序:(最终的程序)

Alt text

Alt text

成功!


防反爬技术

Step 4 . 不遵循 robots.txt 协议。

settings.py 文件里面的 ROBOTSTXT_OBEY 项设置为:False

Alt text


Step 6 . 模仿浏览器

请先查看这篇博客:http://blog.csdn.net/github_35160620/article/details/52489709 里面是:六 . 设置 用户代理(user_agent)

settings.py 文件里面的 USER_AGENT 项设置为:浏览器的用户代理信息。

1
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0'

Step 7 . 禁止缓存

settings.py 文件里面的 COOKIES_ENABLED 项设置为:False

1
COOKIES_ENABLED = False

搞定


需要升级的地方:(2016-11-27 19:34:34)

  1. 在易错的代码段加上异常检测程序
  2. 在下载图片的代码加上:超时异常检测程序
  3. 记录成功下载的、超时失败下载的、链接失败下载的 信息
  4. 添加断点续下功能。