导语:今天闲来没事,爬了一下嘶吼网站内容,发现不少interesting的事情,特发此文。 嘶吼编辑语:希望发这篇文章不要被老板叫去喝茶(双手合十)

近来没什么事做,学漏洞挖掘真的是一个很漫长的过程,很多东西都是一只半解,还需要好好的消化一下,这时候就想让自己做一些别的事情来转一下注意力,稍微放松下。翻了翻安全客,嘶吼近期的文章,发现还是有很多技术点值得参考的,但是有没有时间每天逛一逛这些地方,怎么办?emmmm,现在我有个大胆的想法,把这几个站点整站的技术文章全部爬下来,之后过滤出自己想的文章,用邮箱的形式每天推送给我们。哈,感觉是一个非常牛x的思路。有了思路就跟着我来搞事情吧~

1.开始之前

在开始之前,要先明确如何吧他整站的数据拔下来?爬虫!那肯定是scrapy啊!(不会写爬虫?不怕啊,我教你啊,看完你就会了)之后把爬到的数据保存在mysql里面,至于之后发送邮件的事情,等先把这些数据保存到数据库中在离开考虑之后的事情。

还需要明确的一个问题:

把网站装到爬虫里,要分几步做?

· 新建项目(startproject):新建一个爬虫项目,用scrapy提供的模板生成初始的爬虫文件

· 明确目标: 确定要爬去的站点,并分析他的url,确定要爬下来数据,建立Item(之后说具体啥玩意)

· 制作爬虫(写代码):分析css结构,提取出想要的数据

· 存储数据:将爬下来的数据放到数据库中,这里我采用MySql。因为过滤出想要的文章你会发现数据量没多少。

2.分析目标站点

文章发哪里用那个站点,爬去目标就定嘶吼吧,来看看他的目录结构,分析出想要的url,其实嘶吼标签分类并不是很明确,他实际上有分内网渗透、web安全、系统安全、移动安全等等,但是这些标签又不归在技术那类下面,技术又是一个新的类别,而且,当你去访问技术那一栏…不重要,反正不能从技术那一栏进行提取,只能从首页入手。

可以看到首页最下面有加载更多的按钮,说明他并不是基于Ajax动态加载的,按下F12选择到网络这一栏,点击加载更多,再点下之后迅速按Esc按钮,当然,不按也可以,重点是要找到他请求的Url,可以再网络一栏中找到

1.png

这个请求就是当点击加载更多之后请求的地址,点击一下查看详细的情况可以看到他请求的url

2.png

复制这个请求,直接放到浏览器里访问可以看到,就跟点击加载更多加载在第一页文章之后的地址,那好了。按照这个规则,我们将2改为1,出现的就是文章首页的地址。分析到这里,我们就能知道我们应该如何去发出下一页的请求了,可以直接在页面中定位这个加载更多按钮的css样式,提取他<a>的href地址不就可以找到下一页的请求地址了,至于怎么提取,我们待会再说

3.png

现在分析了如何提取下一页的地址,还有个问题就是刚刚的说道的,他的文章标签我们只能按url来提取,不能直接到技术里面取提取,emm,那就来分析下把,我从中找了两篇文章作为代表

4.png

5.png

从这两篇文章中可以看到他的标签和对应的url地址,现在只需要用和刚刚提取下一页url想用的办法就可以拿到他全部的文章页面url了,但是我们需要从中提取出需要的url,像新闻这类的,直接过滤掉。说了这么多要怎么做囊?正则啊!

6.png

怎么提取稍微等等

3.scrapy的工作原理

既然是要用到scrapy,那就必须说下他的流程,要不然之后在定制需要的功能的时候会懵逼的,下面这张图是scrapy官方给出的工作原理图,这张图给的足够清楚

7.jpg

基本组件:

· 引擎(Engine)

 引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。详细内容查看下面的数据流(Data Flow)部分。

· 调度器(Scheduler)

 调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎

· 下载器(Downloader)

 下载器负责获取页面数据并提供给引擎,而后提供给spider。

· 爬虫(Spiders)

 Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。每个spider负责处理一个特定(或一些)网站。

· 管道(Item Pipeline)

 Item Pipeline负责处理被spider提取出来的item。典型的处理有清理、验证及持久化(例如存取到数据库中)。

· 下载器中间件(Downloader middlewares)

 下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

· Spider中间件(Spider middlewares)

 Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

数据流向

· 引擎从Spider中获取到初始Requests。

· 引擎将该Requests放入调度器,并请求下一个要爬取的Requests。

· 调度器返回下一个要爬取的Requests给引擎

· 引擎将Requests通过下载器中间件转发给下载器(Downloader)。

· 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件

· (返回(response)方向)发送给引擎。

· 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。

· Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。

· 引擎将(Spider返回的)爬取到的Item交给ItemPipeline处理,将(Spider返回的)Request交给调度器,并请求下一个Requests(如果存在的话)。

· (从第一步)重复直到调度器中没有更多地Request。

原文地址点我    这种东西我是不会自己写的。因为我解释不清楚T_T

4.创建项目设计数据库结构

怎么安装我就不说了,大家稍微百度下,要不这篇文章会巨长

安装好之后在终端输入

scrapy startproject "项目名称"

这个项目名称就是目录了,他会在你当前目录下生成一个你输入名字的文件夹

scrapy genspider 4hou 
http://www.4hou.com/page/1

之后他会在spiders目录下生成一个a4hou.py的文件。

为什么不是4hou.py?因为他默认不允许用数字来作为开头.

之后需要Mysql怎么新建数据库我就不多说了,直接提供一份数据库结构的sql文件,来说说他每个字段的含义就行。

7.png

title 为文章题目,

url_id为文章url经过md5加密过后的值,作为主键

author为作者名字

tags为文章的类别

watch_num为观看的次数,comment_num,praise_nums类似

content为文章的内容

image_url为文章封面图,你们不觉得文章的封面图很好看么?非常高端的样子

image_local是将文章封面图下载到本地之后的存放的地址

/*
Navicat MySQL Data Transfer
Source Server         : TT_ubuntu16.04
Source Server Version : 50720
Source Host           : 192.168.250.66:3306
Source Database       : ArticleSpider
Target Server Type    : MYSQL
Target Server Version : 50720
File Encoding         : 65001
Date: 2017-12-05 15:03:35
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for 4hou_Article
-- ----------------------------
DROP TABLE IF EXISTS `4hou_Article`;
CREATE TABLE `4hou_Article` (
  `image_local` varchar(255) COLLATE utf8_bin NOT NULL,
  `image_url` varchar(255) COLLATE utf8_bin NOT NULL,
  `title` varchar(200) COLLATE utf8_bin NOT NULL,
  `url_id` varchar(32) COLLATE utf8_bin NOT NULL,
  `create_date` date DEFAULT NULL,
  `url` varchar(100) COLLATE utf8_bin NOT NULL,
  `author` varchar(200) COLLATE utf8_bin NOT NULL,
  `tags` varchar(50) COLLATE utf8_bin NOT NULL,
  `watch_num` int(10) DEFAULT '0' COMMENT '0',
  `comment_num` int(10) DEFAULT '0',
  `praise_nums` int(10) DEFAULT '0',
  `content` longtext COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`url_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;

5.设计Item

这个设计起来真的是太简单了,Scrapy Item的定义方式和Django Moudle很像,因为Scrapy就是模仿Django写的,很多地方就很像。但是他设计起来比Django简单的多,为啥?因为他只有一种字段类型,我们只需要做的就是在items.py下添加一个类就好,但是我们这个类需要集成scrapy的Item

class ArticleSpider4hou(scrapy.Item):
    image_local = scrapy.Field() #图片本地地址
    image_url =scrapy.Field() #图片地址
    title = scrapy.Field()  #文章标题
    create_date = scrapy.Field() #发布日期
    url = scrapy.Field()  #原文地址
    url_id = scrapy.Field() #经过md5加密过后的url  作为主键
    author = scrapy.Field() #作者
    tags = scrapy.Field() #标签
    watch_num = scrapy.Field() #观看数量
    comment_num = scrapy.Field() #评论数量
    praise_nums =scrapy.Field() #点赞数量
    content = scrapy.Field() #文章正文
    ArticlecontentImage = scrapy.Field()#文章中的背景图处理

可以看到写法基本都是定死的,因为scrapy之后一种属性就是scrapy.Field(),前面的名字跟数据库中的名字都是一毛一样的,但是发现多了一个字段叫ArticlecontentImage字段,这个字段是干嘛的,这个字段并不需要保存到数据库中,这个字段保存的是文章中出现的图片,把文章中出现的图片也下载下来,保存到本地,之后在要存到数据库的html源码中图片的地址改为本地的地址,如果不改当你你从数据库中检索文章的时候你会发现为毛很多图片读不出来。举个例子。

8.png

9.png

他有的是存在专门的图片服务器上的,有的就存放在本地服务器上的,我至今不知道他是怎么做到的,肯定不是老系统跟新系统的问题,感觉是看脸的。所以我才决定干脆全部下载下来,放在自家保险柜比较安心。

6.解析HTML页面

这里我用css选择器选择,有同学可能会说干么干嘛不用beautifulsoup4这个库,这个库用起来确实简单,但是他速度慢,他是用python写的,还有就是xpath选择器,这玩意是c写的,效率比beautifulsoup4高了快1000倍(忘了,印象中是),还有就是正则表达式,正则表达式的效率是最快的,但是,你会用么?会用的熟练么?emm,css选择器是比较折中的,速度可以,还简单。所以我选择css选择器。

在终端下执行

scrapy shell 
http://www.4hou.com/page/1

之后可以进入scrapy shell方便调试,嘶吼对爬虫比较友好,并没有验证User-Agent头,所以,直接用scrapy自带的User-Agent头就可以了。接下来分析下他的html页面,先来看看获取更多这个按钮的属性,用这个按钮,就能便利嘶吼全部的文章了

03.png

可以搜索一下这个loadMoreNew是不是全局唯一的,如果是,就可以直接使用这个样式寻找他的地址,如果不是,就再向上找一层。

04.png

可以看到这里他的.loadMoreNew就是全局唯一的,那就可以直接提取出他的href地址,之后让scrapy再去访问这个地址,从中取出再下一页的地址,知道scrapy找不到这个属性,那就说明已经到最后一页了,已经便利了嘶吼的整站。在scrapy shell下的操作是

05.png

可以看到他已经获取到了这个按钮的链接地址,但是他返回的是一个SelectorList对向的属性,需要把他变成一个List,就直接调用.extract()方法即可。如果想要获取a标签中的某一个值的选项,比如说href的值,就可以调用attr()方法

response.css(".loadMoreNew")
response.css(".loadMoreNew::attr(href)").extract()

这是在scrapy shell中的写法,在真正书写代码的时候和这段代码是一样的,

class A4houSpider(scrapy.Spider):
    name = '4hou'
    allowed_domains = ['www.4hou.com']
    start_urls = ['http://www.4hou.com/page/1']
    def parse(self, response):
        #提取出下一页的url
        next_url = response.css(".post-read-more-new a::attr(href)").extract()[0]
        if next_url:
            yield scrapy.Request(url=parse.urljoin(response.url,next_url),callback=self.parse)

提取出next之后判断下他是否为空,不为空再继续执行,这个yield的作用是发送一个请求,他的意思也就是请求下一页的url,在提交的参数中有一个callback,他是一个回调函数,还是调用了本身的这个函数,进行一个地递归的过程,知道没有下一个请求地址为止。

提取出了下一页的url,接下来就是提取文章的url了,每个文章肯定都有一个url。接下来来提取文章的url

06.png

使用.new_img_title就可以获取到每一篇文章的url,这里刚好有10个,在这里,我们并不需要关注除了最新资讯以外的文章,开头的那几张图片,还有最后的那些精选文章都不需要关注,因为他都在这全部的文章中,家下来,为了确保我们找到的类是可用的,我建议最好在scrapy shell下尝试一下,如果直接写代码的时候,如果没有值,也很难找到具体是哪里出了问题。

07.png

拿到了url,但是还差一个封面图啊,封面图的地址是没有提取的,这时候要思考一个问题,要从这些列表中提取出想要的url,但是封面图没办法过滤啊,封面图又和文章地址是一一对应的,那怎么办?说简单点,我们只需要提取出文章url地址和封面图共同的上一层标签,之后判断,当文章是我们想要的,再进行进一步的提取,否则,什么都不做,看我提供的代码

Article_Boxs  = response.css(".main-box .ehover1")
        for Article_box in Article_Boxs:
            Article_url = Article_box.css(".new_img_title::attr(href)").extract_first("")
            #过滤出技术文章,不要新闻
            match_obj = re.match("(.*4hou.com/(technology|reverse|penetration|web|vulnerable)/(d+).html$)", Article_url)
            if match_obj:
                Image_url = Article_box.css(".new_img .wp-post-image::attr(data-original)").extract_first("")
                yield scrapy.Request(url = parse.urljoin(response.url,Article_url),
                                     headers=self.headers
                                     ,meta={"image_url":parse.urljoin(response.url,Image_url)}
                                     ,callback=self.parse_detail)

先提取出他们共同的福标签之后,循环判断是不是想要的地址,这里正则表达式代表的是,只匹配技术文章,新闻的就直接不要了,当匹配到了,才会取获取图片的地址,最后yield一个请求,注意:这里的url我使用了parse.urljoin方法,这个方法有什么用囊?打个比方,我并没有找到势力,以防万一!

如果获取到的url中有一个的地址是./aaaaa/3389.html,那scrapy将没有办法处理这个请求,因为地址不是完整的,自然是获取不到的,但是用了这个方法之后,如果我们的url不是完整的,这个类库会帮我们判断,如果不是完整的,他会和response.url拼接出一个完整的url地址,response.url就是http://www.4hou.com。这个中间多了一个meta的传递,这个meta的作用就是传递一个图片的地址,之后会对图片进行下载,这里先不说,只需要知道meta需要传递进去的是一个字典类型的,字典中的name,就是在item中设置的保存图片url地址的名字,value也和之前一样,以防万一。

因为嘶吼真的全是坑,稍不留神就一片错,虽然他们没有验证User-Agent头,也没有限制访问速度,但这或许就是一种反爬机制.

这里提交函数当然不能在返回给我们当前函数了,现在发送进去的地址就是文章详情页了,要在另一个函数中对文章的各个关键字段进行解析,先来分析下文章的具体页面

文章题目获取

08.png

这里实际上之后一个,另一个是js里面的东西,可以直接进行提取的

09.png

这里要提的就是,如果想要标签中的文本字段,就可以使用::text来获取

这里再说下文章内容的提取,后面的以此类推

10.png

这个字段并不能直接提取::text如果你提取了,你会发现什么都没有提取到,因为这中间还有很多html标签,直接保存这个就好,因为到后面还需要从数据库中检索出来显示,如果去掉了标签,一大坨文字..emmm,我没有看下去的欲望了,熟悉python的同学可以直接将文字转成md格式的,不往数据库里写.我直接提供代码,

    def parse_detail(self,response):
        image_url = response.meta.get("image_url","") #文章封面图
        item_loader =ItemLoader(item=ArticleSpider4hou(),response=response)
        item_loader.add_css("title",".art_title::text")
        item_loader.add_css("create_date",".art_time::text")
        item_loader.add_value("url",response.url)
        item_loader.add_value("url_id",get_md5(response.url))
        item_loader.add_css("author",".article_author_name .upload-img::text")
        item_loader.add_xpath('tags',"//*[@class='art_nav']/a[2]/text()")
        item_loader.add_value('image_url',[image_url])
        item_loader.add_css("watch_num",".newtype .read span::text")
        item_loader.add_css("comment_num",".newtype .comment span::text")
        item_loader.add_css("praise_nums",".newtype .Praise span::text")
        item_loader.add_css("content",".article_cen")
        #文章中引用的图片
        item_loader.add_css("ArticlecontentImage",".article_cen img::attr(data-original)")
        article_item = item_loader.load_item()
        yield article_item

这里可以看到刚刚用meta传进来的参数直接获取到了,这里又出现了新的东西,我这里直接使用了item loader机制,否则现在的代码就不会这么简单了,而是一大坨,这里没有提供之前的代码,我就用一个例子来演示下,当我们不使用我们的item loader是一个什么样的代码

title = response.css(".art_title::text").extract()[0]
article_item = ArticleSpider4hou()
article_item["title"] = title

这里我只举了一个例子,用了三行,需要将字段直接提取出来,之后实例化ArticleSpider4hou这个item,然后在把提取出来的值给article_item["title"]这个字段.明眼人都能看出来,这样代码的复用率非常底,而且,当有很多个字段需要提取的时候,这对我们来说就是一噩梦,scrapy也考虑到了这个问题,所以,给我们提供了一个item loader机制,我们的代码瞬间缩减了一半,这里只负责提取,时候我们再处理,这里需要介绍的就是这三个方法add_css(用来选择css),add_value(用来设置值),add_xpath(用来选择xpath)

这里处理完之后.又yield了一个article,这个时候代码就到了item里面了,因为用了item loader机制,我们要将提取的这一块全部放到item里面来做,先来看下代码,之后再说具体要怎么做

def splitspace(value):
    value = value.strip()
    value = value.replace('n','')
    value = value.replace('r','')
    return value
def remove_comma(value):
    if "," in value:
        return value.replace(",","")
    else:
        return value
def remove_Keywords(value):
    if "发布" in value:
        value = value.replace("发布", "")
    if "前" in value:
        #now_time = time.strftime("%Y-%m-%d")
        now_time = time.strftime('%Y-%m-%d',time.localtime(time.time()))
        return now_time
    else:
        time = value.replace("年","-").replace("月","-").replace("日","")
        return time
def return_value(value):
    return value
def return_intvalue(value):
    value =  int(value)
    return value
  
def add_tt(value):
  return value + "--TT"
  
def seturl(value):
    if value == None:
        return value
    elif value.startswith("http://") or value.startswith("https://"):
        return value
    else:
        return "http://www.4hou.com"+value
#嘶吼文章Item
class ArticleSpider4hou(scrapy.Item):
    image_local = scrapy.Field(
     output_processor = TakeFirst()
    ) #图片本地地址
    image_url =scrapy.Field(
        output_processor=MapCompose(return_value)
    ) #图片地址
    title = scrapy.Field(
       input_processor=MapCompose(add_tt),
     output_processor = TakeFirst()
    )  #文章标题
    create_date = scrapy.Field(
        input_processor=MapCompose(remove_Keywords),
       output_processor = TakeFirst()
    ) #发布日期
    url = scrapy.Field(
     output_processor = TakeFirst()
    )  #原文地址
    url_id = scrapy.Field(
     output_processor = TakeFirst()
    ) #经过md5加密过后的url  作为主键
    author = scrapy.Field(
        input_processor =MapCompose(splitspace),
       output_processor = TakeFirst()
    ) #作者
    tags = scrapy.Field(
     output_processor = TakeFirst()
    ) #标签
    watch_num = scrapy.Field(
        input_processor=MapCompose(remove_comma,return_intvalue),
       output_processor = TakeFirst()
    ) #观看数量
    comment_num = scrapy.Field(
        input_processor=MapCompose(remove_comma,return_intvalue),
       output_processor = TakeFirst()
    ) #评论数量
    praise_nums =scrapy.Field(
        input_processor=MapCompose(remove_comma,return_intvalue),
      output_processor = TakeFirst()
    ) #点赞数量
    content = scrapy.Field(
     output_processor = TakeFirst()
    ) #文章正文
    #文章中的背景图处理
    ArticlecontentImage = scrapy.Field(
        input_processor = MapCompose(seturl),
        output_processor = Identity(),
    )

在这些函数中实际上我已经帮处理好了全部的值,为了给大家说清楚,我专门对title字段进行了一个处理,因为这个字段本来是可以不经过任何处理的,他调用了add_tt这个方法,然而这个方法的作用就是,在title字段后面加上–tt这个标识,现在回到a4hou.py中,在yield article_item打上一处断点,来看看现在这个title的值

11.png

可以看到,title后面就加上了想要添加到值,现在相信大家明白了input_processor字段的意思就是当值传递进来的时候可以在这个值上做一些预处理,相反output_processor就是传出时做的处理,而MapCompose的作用就是,直接传递函数进去,可以传递多个,他会用你里面提供的函数对该项做预处理.

代码写到这里,在会头看看上面的方法,是不是基本每一个output_processor都要调用一个TakeFirst()这个方法,这个函数的作用就是取数组中的第一个,这个方法被写了很多次,代码显的特别累赘,那怎么办?简单!在items.py中添加

class ArticleItemLoader(ItemLoader):
    # 自定义itemloader
    default_output_processor = TakeFirst()

这个类集成了ItemLoader给他设置一个默认的output_processor,如果想要重写,自然是可以的,之后修改spiders.pt中的代码

def parse_detail(self,response):
        image_url = response.meta.get("image_url","") #文章封面图
        item_loader =ItemLoader(item=ArticleSpider4hou(),response=response)
  ......
#修改为
 def parse_detail(self,response):
        image_url = response.meta.get("image_url","") #文章封面图
        item_loader =ArticleItemLoader(item=ArticleSpider4hou(),response=response)
        ......

这样,就不用每个都去写output_processor这个属性了

7.定制pipline

当数据留过了item,也就到最后了,保存数据库啊,下载图片啊,保存json啊等等的,都在这一步进行处理,

1.保存文章封面图到本地

在settings.py中写入

ITEM_PIPELINES = {
    'Technical_Artical_Spider.pipelines.ArticleImagePipeline': 1,
}
IMAGES_URLS_FIELD = "image_url"
project_dir = os.path.abspath(os.path.dirname(__file__))
IMAGES_STORE = os.path.join(project_dir, 'images')

IMAGES_URLS_FIELD这个为需要下载的字段名

IMAGES_STORE的作用就是设置保存文件路径,因为要写入数据库,所以在中间获取了一下当前脚本所在的路径.

ITEM_PIPELINES这一项就是配置我们自定义的pipline,后面这个数字小,优先级就越高,我们设置成了1,那他的优先级相比域别的pipline就都要高,最先执行他,之后才会执行别的pipline

做了这样的配置之后,就可以下载图片了,这里注意了,在item中还有一个属性没有值了,image_local这个属性还没有值了,这个代表的是图片保存在本地之后的地址,而且还要写如数据库中,要不怎么读取封面图啊,有学过的同学肯定会说,为什么是这个ArticleImagePipeline,不应该是ImagesPipeline这个么,我这里直接继承了ImagesPipeline,所以,他有的功能我都有,自然是不用scrapy自带的了,来看看我的ArticleImagePipeline做了什么

class ArticleImagePipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        if "image_url" in item:
            for ok,value in results:
                image_file_path = value["path"]
            item["image_local"] = image_file_path
        return item

这里我只是重写ImagesPipeline中的item_completed这个方法,在这个里面给image_local赋值了,并返回了item,来看看这个results干了什么的

12.png

这就是为什么用两个值取遍历他的原因,我就不做过多的解释了,有兴趣的同学可以看官方文档

2.保存文章中的图片到本地

就是文章开始提的,需要保存图片到本地来,要不然…全是坑..

ITEM_PIPELINES = {
    'Technical_Artical_Spider.pipelines.ArticleImagePipeline': 1,
    'Technical_Artical_Spider.pipelines.ArticlecontentImagePipline': 10,
}

看我的代码:

class ArticlecontentImagePipline(ImagesPipeline):
    def get_media_requests(self, item, info):
        if len(item["ArticlecontentImage"]):
            for image_content_url in item["ArticlecontentImage"]:
                print(image_content_url)
                yield scrapy.Request(image_content_url)
    def item_completed(self, results, item, info):
        return_list = []
        if "ArticlecontentImage" in item:
            for ok,value in results:
                image_content_path = value["path"]
                return_list.append(image_content_path)
            item["ArticlecontentImage"] = return_list
        return item

跟之前一样重写了item_completed这个方法,而且,这次多重写了个get_media_requests这个方法,因为没有办法直接设置在settings.py文件中了,传入的这些参数都是写死的,所以并不需要管,判item["ArticlecontentImage"]是否为空,如果不是空的话进行循环下载.(这里有把文章中的图片和文章封面图保存在了一起,这并不是一个好的选择,但是之前是没有考虑到这个东西的.)

3.修改文章的html中的图片引用

既然图片都下载下来了,自然是需要修改本地html的图片引用了,同样,在settings.py中设置,注意,ArticleHTMLreplacePipline优先级一定要比ArticlecontentImagePipline和ArticleImagePipeline低,否则,你可以试一下会出什么错误..

ITEM_PIPELINES = {
    'Technical_Artical_Spider.pipelines.ArticleImagePipeline': 1,
    'Technical_Artical_Spider.pipelines.ArticlecontentImagePipline': 10,
    'Technical_Artical_Spider.pipelines.ArticleHTMLreplacePipline': 20,
}

看看,ArticleHTMLreplacePipline代码

class ArticleHTMLreplacePipline(object):
    # exchange html <img>
    def process_item(self,item,spider):
        if "content" not in item:
            return item
        content = item["content"]
        sum = len(re.findall('<p style="text-align.*<img.*[</noscript>$]',content))
        if sum != len(item["ArticlecontentImage"]):
           return item
        if item["ArticlecontentImage"]:
            for exf in range(sum):
                html = item["ArticlecontentImage"][exf]
                html = '<center><p><img src="../images/{0}" /></p></center>'.format(html)
                content = re.sub('<p style="text-align.*<img.*[</noscript>$]',html,content,1)
        item["content"] = content
        return item

这里使用了正则表达式,我自己都觉的自己牛逼,能写出这么牛的正则,这里注意下,这段代码process_item是必须要写的函数在这段函数中,先判断content在不在item中,之后,用正则取找引用图片的地方,并给sum变量,如果这个数字和item["ArticlecontentImage"]这个图片的数量是不一样的,那就直接什么都不做,因为你少下载图片了,如果强制更换,文章说就没看了,直接返回(反正也看不了..因为他有个加载的图片)

如果不是相同,就去挨个替换文章中的图片

4.使用twised异步机制插入数据库

ITEM_PIPELINES = {
    'Technical_Artical_Spider.pipelines.MysqlTwistedPipline': 30,
    'Technical_Artical_Spider.pipelines.ArticleImagePipeline': 1,
    'Technical_Artical_Spider.pipelines.ArticlecontentImagePipline': 10,
    'Technical_Artical_Spider.pipelines.ArticleHTMLreplacePipline': 20,
}
#设置数据库信息
MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "sql_dbname"
MYSQL_USER = "root"
MYSQL_PASSWORD = "password"

我这里并没有使用普通的数据库插入操作,而是采用了异步插入,来看下代码

class MysqlTwistedPipline(object):
    def __init__(self, dbpool):
        self.dbpool = dbpool
    @classmethod
    def from_settings(cls, settings):
        dbparms = dict(
            host = settings["MYSQL_HOST"],
            db = settings["MYSQL_DBNAME"],
            user = settings["MYSQL_USER"],
            passwd = settings["MYSQL_PASSWORD"],
            charset='utf8',
            cursorclass=MySQLdb.cursors.DictCursor,
            use_unicode=True,
        )
        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
        return cls(dbpool)
    def process_item(self, item, spider):
        #使用twisted将mysql插入变成异步执行
        query = self.dbpool.runInteraction(self.do_insert, item)
        query.addErrback(self.handle_error, item, spider) #处理异常
    def handle_error(self, failure, item, spider):
        #处理异步插入的异常
        print(failure)
    def do_insert(self, cursor, item):
        #执行具体的插入
        #根据不同的item 构建不同的sql语句并插入到mysql中
        insert_sql, params = item.get_insert_sql()
        cursor.execute(insert_sql, params)

这一段代码我是从别人的代码中看到的,这个代码是基于twiesd框架的,因为scrapy本身就是基于twiesd框架来写的,要不然他也不会有这么高的执行效率啊,这个框架挺想研究下,因为听说他是python全部框架中代码量排名前三的…

言归正传,看看我这段代码,我实际上并没有在这里执行任何的sql语句,大家看do_insert这个函数,我直接调用了item.get_insert_sql方法,为什么要这样做?因为这样可以不仅减少代码量,而且增加了代码复用率,你想,如果以后再写个爬freebuf的爬虫,你觉得这里的代码需要动不?肯定不用动啊,我们只需要修改item.get_insert_sql()方法就行了吧,我把这个get_insert_sql()代码贴上

    def get_insert_sql(self):
        insert_sql = """
            insert into 4hou_Article(
            image_local,
            image_url,
            title,
            url_id,
            create_date,
            url,
            author,
            tags,
            watch_num,
            comment_num,
            praise_nums,
            content
            )
            VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE watch_num=VALUES(watch_num),
            comment_num=VALUES(comment_num),praise_nums=VALUES(praise_nums)
        """
        params= (
              self["image_local"],
              self["image_url"],
              self["title"],
              self["url_id"],
              self["create_date"],
              self["url"],
              self["author"],
              self["tags"],
              self["watch_num"],
              self["comment_num"],
              self["praise_nums"],
              self["content"]
                  )
        return insert_sql,params

这是标准的sql语句,没什么好说的吧~说句实在话,我看这几行代码总感觉能来个注入什么的

8.总结

代码写下来相对而言还是比较容易的,因为嘶吼没有任何的反爬机制。

想要获取这个代码的地址,可以点击这里,这里有还提供了一份嘶吼12月6日的一份文章数据,包括图片

现在代码还没写安全客、freebuf等地方的文章爬去,而且搜索引擎肯定是要有的,还要有的就是用邮箱接受每个人自己关注的点,每天推送一下,等我写好了其他模块,再来介绍搜索引擎.

如果这篇文章你没看明白,或者想要交流下,邮箱找我[email protected]

源链接

Hacking more

...