Bootstrap

项目思路清理

项目一、多ip切换爬虫

1.项目整体结构

python爬虫多ip爬取众筹网站数据,然后通过后端定义的上传接口上传数据,后端将数据存入数据库,用户在前端浏览时,调用显示接口将数据动态加载到html文件中,然后返回给前端进行显示。

2.项目难点

1.爬虫多ip切换问题,一个ip容易被封,如何实现多ip切换呢?答案是,在对应的网络库中有绑定ip的步骤,将这个步骤修改,每次random一下,实现动态绑定。

Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.

研究步骤:首先研究了pyspider,它主要是分为几个部分,一个是database,本地的数据库,存储爬虫成功的结果,或者失败的url,等待另一个组件sheduler来处理,sheduler作为调度的一个组件。特别是库中的fetcher网络库,文档中也说这个库是用来网络请求的嘛,那么该网络库请求是怎么实现的呢?我觉得它在某个节点总会运用别人写好的库(包括第三方开源库或者python自带的库),查看源码发现是,调用tornado来进行网络请求:

from tornado.curl_httpclient import CurlAsyncHTTPClient
from tornado.simple_httpclient import SimpleAsyncHTTPClient

那么很自然的看看tornado网络库中有没有能够动态改变ip的方法。
查看tornado doc。它是一个python的后端web框架和异步网络请求库,查看文档,确实是有http client。想想能不能在httpclient上做文章。
我先看pyspider用的CurlAsyncHTTPClient,它继承自AsyncHTTPClient:

class CurlAsyncHTTPClient(AsyncHTTPClient):

绑定ip应该在initialize函数中,发现其调用了父类的对应方法,即AsyncHTTPClient的:

super(CurlAsyncHTTPClient, self).initialize(io_loop, defaults=defaults)

那么跟踪进去,发现AsyncHTTPClient中initialize和fetch方法中没有绑定ip的迹象。

继续看SimpleAsyncHTTPClient,其也是调用了父类AsyncHTTPClient的initialize函数,另外在其initalize中还发现了tcpclient,啊,tcpclient应该会绑定ip吧。于是往下找:

from tornado.tcpclient import TCPClient

找到TCPClient类,在tornado.tcpclient的TCPClient中的_create_stream()函数中发现调用了tornado.iostream的IOStream来进行处理。
并且在它的构造函数中看到了socket,我知道socket是可以绑定ip的,继续查看:

from tornado.iostream import IOStream

看见IOStream中import socket,应该跟socket有关,看到在其_init只传入了socket,那么通过socket就可以绑定ip啊。

def __init__(self, socket, *args, **kwargs):
        self.socket = socket
        self.socket.setblocking(False)
        super(IOStream, self).__init__(*args, **kwargs)

因为这个IOStream可能是server和client公用的,所以最好不要在这改。幸好可以通过传入参数来构造IOStream,所以在_create_stream()函数中进行修改。

因为socket是python的基础库嘛,应该会有绑定ip的方式,直接Google。

查看python socket官方文档

看到A pair (host, port) is used for the AF_INET address family, where host is a string representing either a hostname in Internet domain notation like ‘daring.cwi.nl’ or an IPv4 address like ‘100.50.200.5’, and port is an integer.

那么,就可以构造socket了,绑定端口0可以自动绑定。然后,为了验证我改动是否生效了,即多ip爬取,自己用框架写了一个小程序,找了个能够返回ip的网站,爬取结果显示确实ip是动态变化的。这样就完成目标了。

所以,正确的步骤大概是:
AsyncHTTPClient接口中有两种实现,一种是默认的,但是跟踪进取没有迹象。另一个SimpleAsyncHTTPClient要自己选择实现,然后看到在这个类中还添加了tcpclient。然后找到了TCPClient中的_create_stream()函数中有传入socket并绑定的IOStream,这样就可以通过socket绑定ip。

socket是tcp和udp上面的一层抽象,它应该是属于传输层吧。

还想谈谈pyspider框架的架构

pyspider的架构主要分为 scheduler(调度器), fetcher(抓取器), processor(脚本执行):

这里写图片描述

scheduler 负责整体的调度控制
fetcher 抓取网页内容, processor 执行预先编写的python脚本,输出结果或产生新的提链任务(发往 scheduler),形成闭环
每个脚本可以灵活使用各种python库对页面进行解析,使用框架API控制下一步抓取动作,通过设置回调控制解析动作

scheduler:
任务优先级
周期定时任务

fetcher:
method, header, cookie, proxy, etag, last_modified, timeout 等等抓取调度控制
可以通过适配类似 phantomjs 的webkit引擎支持渲染

但是我这样改ip的方法也不完美,因为改动了程序逻辑,实际上pyspider框架应该定义个自定义的函数最好。我后面又在网上收集了其他方法,如何解决网站限制IP的问题?但是没用过,只是了解:

1.构建http代理
2.基于tor网络的多ip爬虫,最简单的一种方式
3.adsl,在抓取的时候使用phantomjs等方式就也一样是网速要求比较高这个时候就需要另外的方式了,比如adsl.

采用爬虫届大招一样的ADSL动态部署起来,要是,要明白用ADSL就意味着是开始拼成本了。不再像是过去随意一台电脑就爱怎么爬怎么爬。可以买ADSL动态IP服务器。

原理很简单,在家庭网络中宽带上网只要断开再拨号一次,链接成功就会更换一次外网ip。并且链接建立后网速比较稳定。这就是动态ip了,一般这个ip池很大,一个城市一般会有5W-30W的ip。基本属于用不完。

反爬虫的两种策略都遇到了
1.特别难识别的人机识别验证码–如极验的手势验证
2.ip大量的限制

2.关于数据更新的问题

在网站爬取的数据,会因为下架等原因而是数据失效。我是通过在传入数据的时候加入createtime字段,updatetime字段,来控制的,然后起一个定时器每天晚上删除过期的信息。

3.Mybatis问题
这要的几种组合方式不太明白,然后自己实验了一下就豁然开朗了。
1.编写sql2.将sql与方法对应

4.前后段传输json数据的问题,比如Content-type: text/plain是作为字符串传输的,这样我必须按照它是字符串来解析,有很大的灵活度;而如果是Content-type: application/json,我就必须按照json的结构来解析。

5.必须有一些接口是按照get方式来接收数据的,get解析内部应该就是用&符号来标记不同的数据,如果一个网址中含有&,将会引起错误。解决办法,将其替换,然后在后端解析中再替换回来。

6.关于中文乱码的问题,在本机没有问题,tomcat中部署都没有问题,但是在服务器中却乱码。一层一层分析,首先猜测数据库中文编码,无问题,存入数据,显示错误,后面解析数据库数据肯定有问题,更改tomcat,总结就是tomcat版本问题,重新部署,ok。

7.关于调试中前端页面缓存的问题,更改了css,页面不更新,参考Disabling Chrome cache for website development

8.关于thymeleaf模板引擎的问题,将对应的资源要放在特定的指定目录,然后在html文件中也要引用这些地方的文件,就不会出错。

9.关于SpringBoot在部署方面的问题,因为其内置了tomcat,所以在部署时要将maven中对其scope改为provided。

10.用maven自带打包,在项目根目录下使用mvn clean package

11.关于tomcat问题,一定要检测端口是否有冲突,盲目shutdown会将使用这个端口的其他项目弄挂掉。

12.关于url中不能出现空格的问题,参考Is a URL allowed to contain a space?,还有很多unsafe characters。
These characters are “{“, “}”, “|”, “\”, “^”, “~”, “[“, “]”, and “`”.这些符号应该需要encode。

In general URIs as defined by RFC 3986 (see Section 2: Characters) may contain any of the following characters:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=`.

A URL must not contain a literal space. It must either(两种方法):
1.be encoded using the percent-encoding
2.or a different encoding that uses URL-safe characters (like application/x-www-form-urlencoded that uses + instead of %20 for spaces).

2.gdt爬虫项目

在涉及到很多登录加密,并且在请求中会构建一些加密后的字符串和动态的随机数,导致接口很难得到,构建的规则并不明白。因此采用之前那种爬取众筹网站的策略就行不通。因为我没有办法构造出请求,这些加密的数和随机数在js中生成,js经过混淆,当时对js又不熟。所以就寻找了另一种方法,模拟点击。其原理就是开一个无头浏览器,然后用Selenium通过定位页面上的元素进行操作,有点击,填表,上下移动等操作。这个工具好像在自动化测试中用的比较多。

该项目的目的的是,模拟其他广告投放网站的页面,比如腾讯广点通平台,一个在线的投放广告的2B网站。在用户选择了一些条件后,比如年龄,性别,地域等,能够通过爬虫提交相同的选择条件去腾讯广点通平台上爬取结果,然后将结果返回,再将结果嵌入到我们的页面中进行显示。相当于一个代理火车票的网站,只不过是没有授权的。但这样做的目的是验证该商业模式。

前面也说了我们采用的是模拟点击+无头浏览器的方式。特点是,前面登录和切换页面等相对比较耗时的,到了我们想要的页面,通过提交数据返回结果大概也就1秒,可以忍受,而前面登录等大概需要5秒。同时如果超过一定时间没有动作,登录状态下session就会过期,这样需要重新登录,并且如果登录很多次,也会被封QQ。

关于spring多线程:

在Spring的项目中很少有使用多线程处理任务的,没错,大多数时候我们都是使用Spring MVC开发的web项目,默认的Controller,Service,Dao组件的作用域都是单实例,无状态,然后被并发多线程调用。

默认为单实例。

Scope描述的是Spring容器如何新建Bean的实例的。有以下几种,通过@Scope注解来实现:

1.Singleton:一个Spring容器中只有一个Bean的实例,此为Spring的默认配置,全容器共享一个实例。
2.Prototype:每次调用新建一个Bean的实例。
3.Request:Web项目中,给每一个http request新建一个Bean实例。
4.Session:Web项目中,给每一个http session新建一个Bean实例。

刚开始,我想了两种方案,因为我们的QQ号有限,比如有1000个可以使用:

1.用户每次提交了信息后,后端才启动爬虫,缺点是这样返回结果的速度就会很慢,即6秒。优点是不用考虑session过期和QQ被封。
2.后端在部署开始的时候就启动1000个爬虫实例,这样当用户提交信息后,我们找到对应的空闲的爬虫,然后进行爬取,优点是速度快,只需要1秒。缺点是需要考虑session过期,当然可以利用定时任务来定时提交信息来防止session过期,同时内存占用也很大。

当时采用的第二种,主要是为了保证速度,用户体验,牺牲了空间。

这个项目过后,当我看关于mysql的知识时,想到,其实我们可以借鉴数据库连接池的思想。

关于连接池What is a thread pool?

A thread pool is a group of pre-instantiated, idle threads which stand ready to be given work.

These are preferred over instantiating new threads for each task when there is a large number of short tasks to be done rather than a small number of long ones. This prevents having to incur the overhead of creating a thread a large number of times.

适用于很多个小任务的场景,而不适用于少量的大任务的场景。

如果要实现一个线程池,你需要:

1.A way to create threads and hold them in an idle state. This can be accomplished by having each thread wait at a barrier until the pool hands it work. (This could be done with mutexes as well.)可以将这些线程置为等待,直到pool notify他们。

2.A container to store the created threads, such as a queue or any other structure that has a way to add a thread to the pool and pull one out.最常用的数据结构就是queue了。

3.A standard interface or abstract class for the threads to use in doing work. This might be an abstract class called Task with an execute() method that does the work and then returns.这个抽象类有execute方法,然后通过它来执行任务。

When the pool is handed a Task, it takes a thread from the container (or waits for one to become available if the container is empty), hands it a Task, and meets the barrier. This causes the idle thread to resume execution, invoking the execute() method of the Task it was given. Once execution is complete, the thread hands itself back to the pool to be put into the container for re-use and then meets its barrier, putting itself to sleep until the cycle repeats.

参考Thread Pools

Most of the executor implementations in java.util.concurrent use thread pools, which consist of worker threads. This kind of thread exists separately from the Runnable and Callable tasks it executes and is often used to execute multiple tasks.

传递进去的task是实现了Runnable或者Callable接口的,原来如此。

;