scrapy爬虫

一、基础知识

1.技术选型

  • scrapy为框架,requests与beautifulsoup为库
  • scrapy内置有css和xpath selector,较为方便,beautifulsoup缺点是慢

2.网页分类

1.静态网页

2.动态网页

3.webserver,(ajax方式与后台api交互,也属于动态网页技术)

3.正则表达式

1.开始字符^a

1
2
3
4
5
6
import re
line="boddy123"
regex_str="^b.*" # ^b是以后面的字符b为开头,.表示任意一个字符,*表示匹配0到多个字符
if re.match(regex_str,line):
print("yes")

yes

2.结尾字符a$

1
regex_str="^.*3$" # 3$以3结尾

yes

1
regex_str="^b.3$" # 3$以3结尾,这总共是3个字符

3.?与()

()分组,捕获组,如果目标字符串有(),直接转义即可

假设有一字符串如下

1
line="boooooooooobbbby123"

提取目标为 boooooooooob,构造正则如下

1
2
3
4
5
6
import re
...
regex_str=".*(b.*b).*"
match_obj=re.match(regex_str,line)
if match_obj:
print(match_obj.group(1)) #group(1),第1个括号匹配部分

输出为

bb

分析,上述这种正则为贪婪匹配,从左到右匹配,找出满足正则的情况,bb是最终满足的情况(前面也有满足的情况,但被后面满足的情况覆盖了),熟悉贪心算法的不陌生吧

?控制匹配方式,非贪婪匹配

1
regex_str=".*?(b.*?b).*"

boooooooooob

?附加在其它匹配字符( *?+???等)后面,表示尽可能少地匹配前面的元字符,即非重叠的匹配

否则

regex_str=".*(b.*?b).*"

bb

regex_str=".*?(b.*b).*"

boooooooooobbbb

4.+ 至少匹配1次

+,匹配至少出现一次

1
2
line="boooooobaaoooobbbby123"
regex_str=".*(b.*b).*"

这样的结果,已知为bb,因为 .*中, .单独的作用是匹配一次任意非换行字符,但是 .*表示 *匹配前面的字符 .0次或多次

而如果是

1
2
line="boooooobaaoooobbbby123"
regex_str=".*(b.+b).*"

bbb

.+最少也要匹配 .一次

5.{a},{a,b},限定匹配

{a},限定匹配a个字符

1
2
3
4
5
6
7
8
9
line="boooooobaaoooobbbaaby123" 
regex_str=".*(b.{2}b).*"
# baab
regex_str=".*(b.{3}b).*"
#bbaab
line="boooooobaaoooobbbaaby123"
regex_str=".*(b.{7,10}b).*"
#将.匹配至少7次,最多10次
#baaoooobbb,这是贪婪匹配,仅保留最终结果

6.|或

1
2
3
4
5
6
7
8
9
import re
line="bobby123"
regex_str="((bobby|boaabby)123)"
match_obj=re.match(regex_str,line)
if match_obj:
print(match_obj.group(1))
#bobby123
print(match_obj.group(2))
#bobby

7.[]任取/区间/取反

  • [abcd]括号内取其中一个满足
  • [a=b]取区间内的值
  • [^1]不等于1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import re
line="13992574364"
regex_str="(1[12379][0-9]{9})"
match_obj=re.match(regex_str,line)
if match_obj:
print(match_obj.group(1))
#13992574364

line="16992574364"
#无结果


line="13sssssssss"
regex_str="(1[12379][^1]{9})" #取反,9个字符不为1即可
#13sssssssss

line="yuslei"
regex_str="(yu[A-Za-z0-9_]lei)" #中括号任意一个匹配
#yuslei

8.\s,\S,\w,\W,\d

\s一个空格

\S一个非空格

\w``=[A-Za-z0-9_]

\W取反

\d数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
line="yu lei" 
regex_str="(yu\slei)"
#yu lei

line="yu lei" #两个空格
#无结果

line="yuxiaolei"
regex_str="(yu\slei)"
#无结果

line="yuslei"
regex_str="(yu\Slei)"
#yuslei

line="yusslei"
#无结果

line="we are born in 2021年"
regex_str=".*?(\d+)年"
#2021

9.[\u4E00-\u9FA5] 汉字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
line="你好we" 
regex_str="([\u4E00-\u9FA5]+)"
#你好

line="你 好we"
#你

line="we are 家人侠"
regex_str=".*([\u4E00-\u9FA5]+侠)"#因为+至少取1个,已经满足了
#人侠

line="we are 家人侠"
regex_str=".*?([\u4E00-\u9FA5]+侠)"
#家人侠

实例-出生日期提取

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
import re
line="XXX生于2023年9月2日"
# line="XXX生于2023/9/2"
# line="XXX生于2023-9-2"
# line="XXX生于2023-09-02"
regex_str=".*?(\d+).?(\d+).?(\d+)"
match_obj=re.match(regex_str,line)
if match_obj:
print(match_obj.group(1)+match_obj.group(2)+match_obj.group(3))

import re
line="XXX生于2023年9月2日"
line="XXX生于2023/9/2"
line="XXX生于2023-9-2"
line="XXX生于2023-09-02"
#line="XXX生于2023-09"
regex_str=".*生于(\d{4}[年/-]\d{1,2}[月/-](\d{1,2}|$))"
match_obj=re.match(regex_str,line)
if match_obj:
print(match_obj.group(1))
#2023年9月2
#2023/9/2
#2023-9-2
#2023-09-02

line="XXX生于2023-09"
regex_str=".*生于(\d{4}[年/-]\d{1,2}[月/-](\d{1,2}|$))"
#无结果

line="XXX生于2023-09"
regex_str=".*生于(\d{4}[年/-]\d{1,2}([月/-]|\d{1,2}|$))"
#2023-09 但是满足不了前面的了


line="XXX生于2023年9月2日"
line="XXX生于2023/9/2"
line="XXX生于2023-9-2"
line="XXX生于2023-09-02"
line="XXX生于2023-09"
regex_str=".*生于(\d{4}[年/-]\d{1,2}([月/-]\d{1,2}|[月/-]$|$))"

#都可以了
#为什么[月-/]会报错嘞

4.深度优先/广度优先

网站url的树结构

深度优先/广度优先

学过数据结构都知道了,不赘述

5.爬虫去重

1.访问过的url存进数据库

2.url->hashset,但占用内存大,性能低

3.md5(url)->hashset,这是scrapy的方法

6.字符串编码

ASCII(一个字节)编码

但中文很多汉字,所以有GB2312编码

于是Unicode出现了,统一编码

但是如果内容全是英文,Unicode编码比ASCII多一倍存储空间

所以有utf-8编码

所以内存为unicode编码,文件为utf-8编码

二、实例

我用的vscode,新建一个文件夹,在终端下输入 pip install scrapy

1.新建项目

终端输入

1
2
3
4
5
6
#scrapy startproject <project_name> [project_dir]
scrapy startproject name kaipa

cd kaipa
scrapy genspider example example.com #生成eample.py的开始地址为example.com的基础模板
scrapy crawl 项目名

此处,我的项目名为 name

2.调试

我用的vscode,参考http://t.csdn.cn/PKqWc

Scrapy 2.11.0 - no active project

Unknown command: crawl

Use “scrapy” to see available commands

参考https://blog.csdn.net/qq_45476428/article/details/108707622

settings中使得

ROBOTSTXT_OBEY = False #否则爬虫会过滤一些url

3.各模块作用

items.py

将需要爬取的信息封装成一个类,方便后期数据的保存

pipelines.py

随着项目的创建而自动创建,该模块主要是完成数据保存到外部文件中。

在完整的代码中,一共有三个pipelines模块:

pipelines.py将数据保存到txt文件中,

pipelines_excel.py将数据保存到excel表中,

pipelines_mysql.py将数据保存到mysql数据库中

分成多个pipelines时,需要修改setting.py中的代码,在65行附件有一段注释掉的代码:

1
2
3
4
5
#ITEM_PIPELINES = {

#qkhouse.pipelines.QkhousePipeline': 300,

#}

若你只有一个pipelines,直接取消注释即可,代码中的300表示优先级,因为只有一个pipelines,所以优先级设置的大小没有影响.

若有多个pipelines时,你就需要设置pipelines优先级的大小:

1
2
3
4
5
ITEM_PIPELINES = {
'qkhouse.pipelines.QkhousePipeline': 1,
'qkhouse.pipelines_excel.QkhousePipeline': 2,
'qkhouse.pipelines_mysql.QkhousePipeline': 3,
}

4.Xpath作用

先点进目标网页其中一个页面

尝试提取标题,日期,评论个数

1.概要

  • 路径表达式在xml和html中导航
  • 包含标准函数库
  • w3c标准

节点

1
2
3
4
5
6
7
8
9
10
<html> //父节点 Parent  //html是meta的先辈节点 Ancestor
<head> //是html的子节点 Child

</head>
<body>
<meta/> //meta与link是同胞节点 Sibling

<link/>
</body>
</html>

2.语法

表达式 说明
article 选取所有article元素的所有子节点
/article 选取根元素article
article/a 选取所有属于article元素的a元素
//div 选取所有div子元素
article//div 选取所有属于article元素的后代div元素
//@class 选取所有名为class的属性 <img src=”…”> src就是属性

参考链接

https://www.cnblogs.com/wendyw/p/11633588.html#_label3

菜鸟教程https://www.runoob.com/xpath/xpath-syntax.html

找标题

首先已经指定某一个静态网页url了

写法1
1
/html/body/main/div[2]/div[2]/div/h1

按路径找的,/可以理解为是嵌套关系

/html/body,在html标签下有个body

依次类推

main/div[2],表示main标签下的第二个div

实际上,f12,浏览器是提供复制Xpath功能的

1
re_selector=response.xpath("/html/body/main/div[2]/div[2]/div/h1")

debug一下

鼠标移到上面,应该有东西,哈,有点问题

加个 /text()才有东西,具体原因,请往下看,技巧部分

写法2

更简短吧,当然了,要确保class的值是全局唯一

//a[@href="..."]

如果class名字过长怎么办,有函数contains,如

//span[contains(@class,"vo-up1")]

1
//div[@class="article-header mb-0"]/h1/text()
1
re_selector=response.xpath('//div[@class="article-header mb-0"]/h1/text()')

再debug一下

技巧

这里有个技巧,你看,上面两种写法,调试的时候,每次都要重新启动debug,别慌,在cmd下,cd到有scrapy脚本的地方,

输入 scrapy shell 爬取网址

But!!!

我的朋友,如果你和我一样,用的也是vscode,那可真是,太酷啦!!! 或者其它平台,就在下方的终端去启动就行了

比如我的cd kaipa(我的项目名),去启动scrapy shell

然后

1
2
3
4
5
scrapy shell 爬取网址
>>> title=responses.xpath("...")
>>> title #打印一下
>>> title.extract() #提取数值元组
>>> title.extract()[0] #提取第一个数值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [1]: title=response.xpath("/html/body/main/div[2]/div[2]/div/h1")

In [2]: title
Out[2]: [<Selector query='/html/body/main/div[2]/div[2]/div/h1' data='<h1 class="post-title mb-2 mb-lg-3">A...'>]

In [3]: title.extract()
Out[3]: ['<h1 class="post-title mb-2 mb-lg-3">Apple 苹果产品参数中心 收集苹果全部产品的详细参数</h1>']

In [4]: title.extract()[0]
Out[4]: '<h1 class="post-title mb-2 mb-lg-3">Apple 苹果产品参数中心 收集苹果全部产品的详细参数</h1>'

In [5]: title=response.xpath("//div[@class='article-header mb-0']/h1/text()")

In [6]: title
Out[6]: [<Selector query="//div[@class='article-header mb-0']/h1/text()" data='Apple 苹果产品参数中心 收集苹果全部产品的详细参数'>]

In [7]: title.extract()[0]
Out[7]: 'Apple 苹果产品参数中心 收集苹果全部产品的详细参数'
1
2
3
4
5
6
7
In [8]: title=response.xpath("/html/body/main/div[2]/div[2]/div/h1/text()")

In [9]: title
Out[9]: [<Selector query='/html/body/main/div[2]/div[2]/div/h1/text()' data='Apple 苹果产品参数中心 收集苹果全部产品的详细参数'>]

In [10]: title.extract()[0]
Out[10]: 'Apple 苹果产品参数中心 收集苹果全部产品的详细参数'

爬日期

1
//div[@class="article-meta"]/span[1]/text()

去终端试一下

1
2
3
4
5
6
7
8
9
10
In [11]: title1=response.xpath("//div[@class='article-meta']/span[1]/text()")       

In [12]: title1
Out[12]: [<Selector query="//div[@class='article-meta']/span[1]/text()" data='2023-09-14'>]

In [13]: title1.extract()
Out[13]: ['2023-09-14']

In [14]: title1.extract()[0]
Out[14]: '2023-09-14'

爬评论

1
//span[@class="meta-comment"]/a/text()
1
2
3
4
5
6
7
8
In [20]: response.xpath("//span[@class='meta-comment']/a/text()")
Out[20]: [<Selector query="//span[@class='meta-comment']/a/text()" data='1'>]

In [21]: response.xpath("//span[@class='meta-comment']/a/text()").extract()
Out[21]: ['1']

In [22]: response.xpath("//span[@class='meta-comment']/a/text()").extract()[0]
Out[22]: '1'

爬收藏

1
//div[@class="article-meta"]/span[contains(@class,"meta-fav")]/text()
1
2
3
4
5
In [23]: response.xpath('//div[@class="article-meta"]/span[contains(@class,"meta-fav")]/text()').extract()
Out[23]: ['3']

In [24]: response.xpath('//div[@class="article-meta"]/span[contains(@class,"meta-fav")]/text()').extract()[0]
Out[24]: '3'

爬内容

1
2
In [35]: response.xpath('//div[@class="card"]/article').extract()
Out[35]: ... #太多了,不展示了

综上

对于likes和comment部分,其实根据实际情况判断,

如果某网页显示的是, 3 评论 ,没有评论时,只显示 评论

需要逻辑判断一下

比如:

1
2
3
4
5
match_re=re.match(".*?(\d+).*",comment)
if match_re:
comment=match_re.group(1)
else:
comment=0

其他例子

有个例子就是

如果一个div标签下有多个同级span,

1
2
3
4
5
6
tag=response.xpath('//div[@class="articlemeta"]/span/text()').extract() 
Out[44]: ['2023-09-14', '0 评论', '3', '0']

#假设除了tag[1]的内容,我都要,且拼接成字符串
#我当然可以tag[0],tag[2],tag[3]那样去取

1
2
3
4
5
6

#这里有另一种方法
tag_list=response.xpath('//div[@class="articlemeta"]/span/text()').extract()

tag_list=[elem for elem in tag if not elem.strip().endwith("评论")]
tags=",".join(tag_list)

5.css选择器实现字段解析

https://www.runoob.com/cssref/css-selectors.html

爬取实例

标题

1
2
3
4
<div class="container py-3 py-md-5">
<div class="article-header mb-0">
<h1 class="post-title mb-2 mb-lg-3">Apple 苹果...
...
1
.article-header h1

这种用法,前提是class唯一;

如果标签不同,但class相同,那就加上标签 :

div.article-header h1

1
2
3
4
5
6
7
8
9
In [46]: response.css(".article-header h1").extract()
Out[46]: ['<h1 class="post-title mb-2 mb-lg-3">Apple 苹果产品参数中心 收集苹果全部产
品的详细参数</h1>']

In [47]: response.css(".article-header h1::text").extract()
Out[47]: ['Apple 苹果产品参数中心 收集苹果全部产品的详细参数']

In [48]: response.css(".article-header h1::text").extract()[0]
Out[48]: 'Apple 苹果产品参数中心 收集苹果全部产品的详细参数'

日期

1
2
3
4
5
<div class="article-meta">
<span class="meta-date xh-highlight">
<i class="far fa-clock me-1"></i>
2023-09-14
</span>
1
2
3
4
5
In [50]: response.css(".meta-date").extract()[0]
Out[50]: '<span class="meta-date"><i class="far fa-clock me-1"></i>2023-09-14</span>'

In [51]: response.css(".meta-date::text").extract()[0]
Out[51]: '2023-09-14'

评论

1
2
3
4
5
6
<span class="meta-comment">
<a href="https://www.ahhhhfs.com/18496/#comments" class="">
<i class="far fa-comments me-1"></i>
1
</a>
</span>
1
2
3
4
5
6
7
8
9
10
11
In [55]: response.css(".meta-comment").extract()
Out[55]: ['<span class="meta-comment"><a href="https://www.ahhhhfs.com/18496/#comments"><i class="far fa-comments me-1"></i>1</a></span>']

In [56]: response.css(".meta-comment a").extract()
Out[56]: ['<a href="https://www.ahhhhfs.com/18496/#comments"><i class="far fa-comments me-1"></i>1</a>']

In [57]: response.css(".meta-comment a::text").extract()
Out[57]: ['1']

In [58]: response.css(".meta-comment a::text").extract()[0]
Out[58]: '1'

收藏

1
2
3
4
5
6
7
8
9
10
11
12
<div class="article-meta">
<span class="meta-date">
<i class="far fa-clock me-1"></i>
2023-09-14
</span>
<a href="https://www.ahhhhfs.com/it/apple/" class="">Apple
</a>
</span>
<span class="meta-fav d-none d-md-inline-block">
<i class="far fa-star me-1"></i>3
</span>
...

因为我这个页面下面会展示些其它的首页,封面也有这个收藏数,所以如果,仅仅是 .fa-clock会出现很多个数字,当然可以extract后取[0]

但是,多练练也没错,可以额外加一个div.article-meta

1
2
3
4
5
In [59]: response.css("span.meta-fav::text").extract()
Out[59]: ['3', '0', '2', '0', '2', '3', '4', '2', '9']

In [60]: response.css("div.article-meta span.meta-fav::text").extract()
Out[60]: ['3']

内容

1
response.css("article.post-content").extract()[0]
1
2
3
4
5
6
7
8
9
10
#   #标题
# title=response.css(".article-header h1::text").extract()[0]
# #日期
# create_date= response.css(".meta-date::text").extract()[0]
# #评论
# comment=response.css(".meta-comment a::text").extract()[0]
# #喜欢数
# likes=response.css("div.article-meta span.meta-fav::text").extract()[0]
# #内容是个粗提取,没有进一步的操作
# content=response.css("article.post-content").extract()[0]

6.爬取所有网页

一个静态网页,搞好了,如何爬取所有静态网页呢?

目的为:

  1. 获取某列表页的所有文章url,交给scrapy进行具体字段解析
  2. 获取下一页的url并交给scrapy重复1步骤

目的1:

1
url_list=response.css("h2.entry-title a::attr(href)").extract()

取的是当前页所有的超链接的 href属性,这里面装着url

现在有一大串的url了

处理代码

url=parse.urljoin(response.url,post_url)防止取的href里,没有域名,也就是url不完整,那就需要去拼接一下

目的2:

解析一下 下一页这个跳转页

1
<a class="page-link page-next" href="https://www...">下一页<i class="fas fa-angle-right ms-1"></i></a>

使用两个class进行定位:就是中间不留空格即可

1
response.css(".page-link.page-next::attr(href)").extract_first("")

extract_first("")等价于 extract()[0],但是多了一个默认为空

然后对它的进一步处理呢:

完整域名拼接,以及调用列表页的函数

探讨

1
yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)

parse.urljoin,如果post_url有域名呢,不起作用;没域名,就拼接

封面图的添加

封面图一般出现在列表页上

由于列表页呢,超链接a中,它的属性href就是我们要的post_url;同时属性data-bg呢,存放的是封面链接image_url

所以对于上面的代码做个修改

对于列表页

1
2
3
post_node=response.css("a.media-img.lazy.bg-cover.bg-center")
#对于封面链接image_url
image_url=post_node.css("::attr(data-bg)").extract_first("")

7.item设计

1
2
3
4
5
6
7
8
9
10
class WebItem(scrapy.Item):
title=scrapy.Field()
url=scrapy.Field()
url_object_id=scrapy.Field()
create_data=scrapy.Field()
comment=scrapy.Field()
likes=scrapy.Field()
content=scrapy.Field()
front_image_url=scrapy.Field()
front_image_path=scrapy.Field()

Webspider.py

然后再里导入

1
from name.items import WebItem

def parse_detail中添加

1
2
3
4
5
6
7
8
9
10
11
12
article_item=WebItem()
...
article_item["title"]=title
article_item["url"]=response.url
article_item["comment"]=comment
article_item["likes"]=likes
article_item["content"]=content
article_item["create_date"]=create_date
article_item["create_date"]=create_date
article_item["front_image_url"]=front_image_url

yield article_item #传入到pipelines去

settings.py

图片保存

去settings.py取消以下代码的注释

1
2
3
ITEM_PIPELINES = {
"name.pipelines.NamePipeline": 300,
}

添加一下代码

1
2
3
4
5
6
7
8
9
10
11
12
import os

...

ITEM_PIPELINES = {
#数字越小,越早进
"name.pipelines.NamePipeline": 300,
"scrapy.pipelines.images.ImagesPipeline":1,
}
IMAGES_URLS_FIELD="front_image_url" #使得它去items找该"front_image_url"字段
project_dir=os.path.abspath((os.path.dirname(__file__)))
IMAGES_STORE=os.path.join(project_dir,'images')

去根文件夹name新建images文件夹

由于是对图片操作,需要一个PIL库

在终端安装

1
pip install -i https://pypi.douban.com/simple pillow

然后由于会将 IMAGES_URLS_FIELD="front_image_url"值当做数组

需要去webspider中将该值中括号括起

1
article_item["front_image_url"]=[front_image_url]

启动

启动运行的是main.py文件,不要搞错了,我当时才没搞错

问题

运行完后,在images文件夹下有一个full文件夹,图片只有13个,根据当时实际列表页来看,有13页,一页有16个封面;这远远不够诶。当时调试时,也有发现,有的post_url,貌似并没有调入到 parse_detail方法

它不会是13个页面,一个页面搞一个图吧

我注释掉了 next_url那串代码

运行后,只下载到第一个图

1
2
3
In [50]: for post_url in post_nodes:
...: post_url=post_nodes.css("::attr(href)").extract_first("")
...: print(post_url)

果然,出现在了 extract_first("")上,这样一直遍历的是第一个

更改如下:

1
2
3
4
5
6
7
8
9
post_nodes=response.css("a.media-img.lazy.bg-cover.bg-center")
i=0
for post_url in post_nodes:
if i<len(post_nodes):
#yield Request(url=parse.urljoin(response.url,post_url),callback=self.parse_detail)
image_url=post_nodes.css("::attr(data-bg)").extract()[i]
post_url=post_nodes.css("::attr(href)").extract()[i]
yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)
i=i+1

好吧,进行到后面我才发现,应该这样改

1
2
3
4
5
6
7
for post_node in post_nodes:

image_url=post_node.css("::attr(data-bg)").extract_first("")

post_url=post_node.css("::attr(href)").extract_first("")

yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)

Items字段填充

填充front_image_url

自定义pipelines

为了保存图片路径,代码如下

1
2
3
4
5
6
7
8
9
10
11
from scrapy.pipelines.images import ImagesPipeline
...
class NameImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
if "front_image_url" in item:
for ok,value in results:
image_file_path=value["path"]

item["front_image_path"]=image_file_path

return item

管道里,填充了 front_image_path

自定义settings
1
2
3
4
5
6
ITEM_PIPELINES = {
#数字越小,越早进
"name.pipelines.NamePipeline": 300,
#"scrapy.pipelines.images.ImagesPipeline":1,
"name.pipelines.NameImagePipeline": 1,
}

填充url_object_id

新建utils文件夹中一个common.py,用来做md5加密

1
2
3
4
5
6
7
8
import hashlib

def get_md5(url):
if isinstance(url,str):#python3 里的str 相当于python2的unicode
url=url.encode("utf-8")
m=hashlib.md5()
m.update(url)
return m.hexdigest()
自定义爬虫文件webspider
1
2
3
from name.utils.common import get_md5
...
article_item["url_object_id"]=get_md5(response.url)

8.数据表设计和保存item到json文件

1.保存item到json文件

自定义json导出

pipelines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import json

...

class JsonwithEncodingPipeline(object):
def __init__(self):
self.file=codecs.open('article.json','w',encoding="utf-8")
def process_item(self, item, spider): #写入文件
lines=json.dumps(dict(item),ensure_ascii=False)+"\n"
self.file.write(lines)
return item
def spider_closed(self,spider):#关闭文件
self.file.close()
#ensure_ascii=False有中文不出错
settings
1
2
3
4
5
6
ITEM_PIPELINES = {
#数字越小,越早进
"name.pipelines.JsonwithEncodingPipeline": 2,
#"scrapy.pipelines.images.ImagesPipeline":1,
"name.pipelines.NameImagePipeline": 1,
}
结果

生成了article.json文件,有json格式,看着很清楚

scrapy导出json

pipelines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from scrapy.exporters import JsonItemExporter
...
class JsonExporterPipeline(object):
#调用由scrapy提供的json_export导出json文件
def __init__(self):
self.file=open('articleexport.json','wb')
self.exporter=JsonItemExporter(self.file,encoding="utf-8",ensure_ascii=False)
self.exporter.start_exporting()

def close_spider(self,spider):
self.exporter.finish_exporting()
self.file.close()

def process_item(self,item,spider):
self.exporter.export_item(item)
return item

显示的结果嘛,比自定义的json导出,多了个 []

然后settings记得改一改,就对应的管道那

2.数据库设计

数据库管理工具用的navicat,mysql用的是phpstudy自带的哈

数据库设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `article_spider` (
`title` varchar(200) COLLATE utf8_unicode_ci NOT NULL COMMENT '标题',
`create_date` date DEFAULT NULL COMMENT '时间',
`url` varchar(300) COLLATE utf8_unicode_ci NOT NULL,
`url_object_id` varchar(50) COLLATE utf8_unicode_ci NOT NULL,
`front_image_url` varchar(300) COLLATE utf8_unicode_ci DEFAULT NULL,
`front_image_path` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL,
`comment` int(11) NOT NULL,
`likes` int(11) NOT NULL,
`content` longtext COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`url_object_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

然后webspider里对create-date处理一下,字符串转datetime

1
2
3
4
try:
create_date=datetime.datetime.strptime(create_date,"%Y/%m/%d").date()
except Exception as e:
create_date=datetime.datetime.now.date()

终端安装一下mysqlclient pip install mysqlclient

同步类

pipelines
1
2
3
4
5
6
7
8
9
10
11
12
13
class Mysqlpipeline(object):
def __init__(self):
#'host','user','password','dbname'
self.conn=MySQLdb.connect('localhost','root','root','mydb',charset="utf8",use_unicode=True)
self.cursor=self.conn.cursor()

def process_item(self,item,spider):
insert_sql="""
insert into article_spider(title,url,create_date,likes)
values(%s,%s,%s,%s)
"""
self.cursor.execute(insert_sql,(item["title"],item["url"],item["create_date"],item["likes"]))
self.conn.commit()

然后settings里,添加这个管道的,注释其它管道的

为了演示,只添上面四个字段 title,url,create_date,likes,避免mysql报错,将主键先取消掉,不为空的字段加上默认值

结果,成功了

问题

MySQLdb.OperationalError: (2013, 'Lost connection to server during query')

MySQLdb.OperationalError: (2006, 'Server has gone away')

这个其实是断点,debug太久导致了数据库连接超时

异步化类

防止scrapy解析过快,数据库写入速度跟不上而阻塞

pipelines
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
import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi

....

class Mysqltwistedpipeline(object):
def __init__(self,dbpool):
self.dbpool=dbpool

@classmethod
def from_settings(cls,settings):
#dict()是一个类
dbparams= 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", host=settings["MYSQL_HOST"],
#db=settings["MYSQL_DBNAME"],user=settings["MYSQL_USER"]...)
#或者
dbpool=adbapi.ConnectionPool("MySQLdb",**dbparams)

return cls(dbpool) #实例化对象
def process_item(self,item,spider):
#使用twist将Mysql插入变为异步执行
query=self.dbpool.runInteraction(self.do_insert,item)
#错误处理
query.addErrback(self.handle_error)

def handle_error(self,failure):
#处理异步插入的异常
print(failure)

def do_insert(self,cursor,item):
#执行具体插入
insert_sql="""
insert into article_spider(title,url,create_date,likes)
values(%s,%s,%s,%s)
"""
cursor.execute(insert_sql,(item["title"],item["url"],item["create_date"],item["likes"]))

settings
1
2
3
4
5
6
7
8
"name.pipelines.Mysqltwistedpipeline":1,

...

MYSQL_HOST="localhost"
MYSQL_DBNAME="db"
MYSQL_USER="root"
MYSQL_PASSWORD="root"
问题

问题1

1
2
3
4
instance = objcls.from_settings(settings, *args, **kwargs)
File "", line 70, in from_settings
return cls(dbpool) #实例化对象
builtins.TypeError: Mysqltwistedpipeline() takes no arguments

解决方法,纯粹是 __init__拼写错了

问题2'Deferred' object has no attribute 'addErrorback'

改成addErrBack()

9.item loader机制

如果有个爬虫代码有大量的xpath和css很多的时候,不好改

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scrapy.loader import ItemLoader 
...
#通过Itemloader加载item

item_loader=WebItemLoader(item=WebItem(),response=response)
item_loader.add_xpath("title",'//div[@class="article-header mb-0"]/h1/text()')
#item_loader.add_css()
item_loader.add_value("url",response.url)
item_loader.add_value("url_object_id",get_md5(response.url))
item_loader.add_xpath("comment",'//span[@class="meta-comment"]/a/text()')
item_loader.add_xpath("likes",'//div[@class="article-meta"]/span[contains(@class,"meta-fav")]/text()')
item_loader.add_xpath("content",'//div[@class="card"]/article')
item_loader.add_xpath("create_date","//div[@class='article-meta']/span[1]/text()")

front_image_url=response.meta.get("front_image_url","")
item_loader.add_value("front_image_url",[front_image_url])

article_item=item_loader.load_item()

但有个问题:

1.它这里每个值加载的是list形式了

2.像字段由字符串处理成date格式,即一些处理怎么做

处理方式

MapCompose

用来对原始值进一步处理

在items.py中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import scrapy,datetime
from scrapy.loader.processors import MapCompose

...

def add_webspider(value):
return value+"-yuleiyun"

class WebItem(scrapy.Item):
title=scrapy.Field(
#从左到右拼接
input_processor=MapCompose(lambda x:x+"-webspider",add_webspider)
)
...

这样会使得webspider中

1
2
article_item=item_loader.load_item()
其title字段会拼接 "-webspider-yuleiyun"

TaskFirst

1
2
3
4
5
6
7
from scrapy.loader.processors import MapCompose,TakeFirst

title=scrapy.Field(
#从左到右拼接
input_processor=MapCompose(lambda x:x+"-webspider",add_webspider),
output_processor=TakeFirst()
)

这样结果就不是列表形式了

create_date字段同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def date_convert(val):
try:
create_date=datetime.datetime.strptime(val,"%Y/%m/%d").date()
except Exception as e:
create_date=datetime.datetime.now().date()

return create_date

...

create_date=scrapy.Field(
input_processor=MapCompose(date_convert),
output_processor=TakeFirst()
)

...

输出效果为:处理过的,且非list类型而是date类型的字段

处理方式改进

当然了,如果一个字段就加一个output_processor的话,如果有100个呢?

那为了让所有字段都取第一个值,那就自定义一个ItemLoader类

items

1
2
3
4
5
from scrapy.loader import ItemLoader
...
class WebItemLoader(ItemLoader):
#自定义ItemLoader
default_output_processor=TakeFirst()

去掉原来的output_processor

webspider

使用自定义的Itemloader类

1
2
3
4
5
from name.items import WebItem,WebItemLoader
...

item_loader=WebItemLoader(item=WebItem(),response=response)
...

Debug,可以明显看到,所有字段不再是list形式了

正则表达式处理

如果之前有的字段,scrapy提取后,需要正则进一步提取

同上,新建一个有参的函数,函数体复制过来,return一下处理结果就行了

特殊处理

上面有一个这样的代码

1
2
tag_list=[elem for elem in tag if not elem.strip().endwith("评论")]
tags=",".join(tag_list)

这个tags实际就是个字符串

join

1
2
3
4
5
6
7
8
9
10
11
12
from scrapy.loader.processors import MapCompose,TakeFirst,join
...
def remove_tags_comment(val):
if "评论" in val:
return ""
else:
return val

...
input_processor=MapCompose(remove_tags_comment),
output_processor=join(",")
...

front_image_url

当时处理这个字段的时候,是要将它作为list的,由于自定义处理使得它不为list了,所以需要处理一下

1
2
3
4
5
6
7
8
9
#对于需要保持list的字段所做处理
def return_val(val):
return val
...

front_image_url=scrapy.Field(
#覆盖default_output_processor
output_processor=MapCompose(return_val)
)

10.总结

学习了正则表达式应用

学习了scrapy框架的安装,项目初步建立

学习了使用vscode调试scrapy项目

学习了scrapy文件结构,主要文件为 主爬虫脚本、Items.py、pipelines.py、settings.py

主爬虫脚本:

  1. 设置爬虫目标网站
  2. 使用response.xpath、response.css解析目标网站字段
  3. 对于字段的存储,使用items中的实体类保存->使用默认scrapy item_loader->使用自定义scrapy item_loader
  4. 对于ItemLoader,主要有三个方法,add_xpath(),add_css(),add_value()

items.py

  1. 封装爬取目标的信息
  2. 运用自定义方法,与类中各字段的搭配
  3. 搭配方法有MapComposeTakeFirst
  4. 传递到pipelines

pipelines.py

  1. 接收items的返回值
  2. 进行保存,json存储->同步化数据库->异步化数据库

settings

  1. 对管道pipelines中的类进行,优先度分配
  2. 系统常量的定义,如MYSQL_HOST、IMAGES_URLS_FIELD
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 是羽泪云诶
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信