avatar

目录
python爬虫(1)-爬虫基础&数据提取

本文档由脑图导出,地址:Spider脑图

参考:heima

[TOC]

Spider

爬虫原理与数据抓取

基本概念

通用爬虫和聚焦爬虫

HTTP和HTTPS

str和bytes的区别

Requests库

python
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
# requests简单实用
import requests

kw = {'wd':'长城'}

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}

# params 接收一个字典或者字符串的查询参数,字典类型自动转换为url编码,不需要urlencode()
response = requests.get("http://www.baidu.com/s?", params = kw, headers = headers)

# 查看响应内容,response.text 返回的是Unicode格式的数据
# print(response.text)

# 查看响应内容,response.content返回的字节流数据
# print(response.content)

# 查看完整url地址
print(response.url)

# 查看响应头部字符编码
print(response.encoding)

# 查看响应码
print(response.status_code)
"""
更推荐使用response.content.deocde()代替response.text

"""

# 爬图片
from io import BytesIO,StringIO
import requests
from PIL import Image
img_url = "http://imglf1.ph.126.net/pWRxzh6FRrG2qVL3JBvrDg==/6630172763234505196.png"
response = requests.get(img_url)
f = BytesIO(response.content)
img = Image.open(f)
print(img.size)

贴吧爬虫

python
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
# 功能:爬贴吧数据,存n页的html内容
import requests


class TiebaSpider:
def __init__(self, tieba_name):
self.tieba_name = tieba_name
self.url_temp = "http://tieba.baidu.com/f?kw="+tieba_name+"&ie=utf-8&pn={}"
self.headers = {"User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"}

def get_url_list(self):
"""构造url_list 1000页"""
# url_list = []
# for i in range(1000):
# url_list.append(self.url_temp.format(i*50))
# return url_list

return [self.url_temp.format(i * 50) for i in range(1000)]

def parse_url(self, url):
"""请求,返回响应内容 content"""
print(url)
response = requests.get(url, headers=self.headers)
return response.content.decode()

def save_html(self, html_str, page_num):
"""保存文档"""
file_path = "{}-第{}页.html".format(self.tieba_name, page_num)
with open(file_path, "w", encoding="utf-8") as f:
f.write(html_str)

def run(self):
# 1 构造url_list 1000页
url_list = self.get_url_list()
# 2 请求,返回响应内容 content
for url in url_list:
html_str = self.parse_url(url)
# 3 保存文档
page_num = url_list.index(url) + 1 # 页码
self.save_html(html_str, page_num)


if __name__ == '__main__':
tieba_spider = TiebaSpider("java")
tieba_spider.run()

"""note
为什么需要在headers添加User-Agent?
- 模拟一个真实的浏览器,以免被后台服务器认为是机器

range(10)
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

列表推导式
- [self.url_temp.format(i * 50) for i in range(1000)]

with open(file_path, "w", encoding="utf-8") as f
- 简化写法,避免每次读写文件都要在finally里面close()

为什么返回结果出现大量注释,但是原网页可以看见界面?
- 查看原网页的源码后,发现源码就注释了的
- 可能是后台服务器解析html时手动去掉了注释 `<!-- -->`

"""

发送post请求

使用代理

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用代理ip爬虫

import requests
proxies = {"http": "http://117.191.11.72:8080"}
url = "http://www.baidu.com"
headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"}
r = requests.get(url, headers=headers, proxies=proxies)
print(r.status_code)

"""note
代理商网址:https://proxy.mimvp.com/freeopen.php

为什么爬虫要使用代理?
- 让服务器以为不是同一个客户端
- 防止真实地址被泄露

使用代理ip
- 准备一堆ip地址,组成ip池
- 如何随机选择ip。让使用次数少的ip地址有更大的可能性被用到
- {"ip":"ip","times":0}
- [{},{},{}]对列表按次数进行排序,选使用次数少的10个IP,从中随机选一个
- 检测ip可用性
"""

模拟登陆

python
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
# 登录人人网
import requests

session = requests.session()
post_url = "http://www.renren.com/PLogin.do"
post_data = {"email":"XXX", "password":"XXX"}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
}
#使用session发送post请求,cookie保存在其中
session.post(post_url,data=post_data,headers=headers)

#在使用session进行请求登陆之后才能访问的地址
r = session.get("http://www.renren.com/327550029/profile",headers=headers)

#保存页面
with open("renren1.html","w",encoding="utf-8") as f:
f.write(r.content.decode())

"""note
爬虫携带cookie
- 一堆cookie请求,组成cookie池
- 后台若判断同一个用户频繁请求,可能会拦截
- 请求登陆后的网站需要携带cookie

- 请求登录后网站思路
- (1)
- 使用session发送请求,将返回的cookie保存在session中
- 再使用该session请求网站,session会自动携带cookie进行请求
- (2)
- 直接在headers里携带cookie进行登录

- 字典推导式
- `cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split("; ")}`

- 模拟登录三种方式
- 使用session发送post
- 在headers添加cookie键
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Cookie":"XXX"}
r = requests.get("http://www.renren.com/327550029/profile",headers=headers)`
- 在请求方法中添加cookie参数
- r = requests.get("http://www.renren.com/327550029/profile",headers=headers,cookies=cookies)
"""

保存图片

python
1
2
3
4
5
6
# 保存图片(二进制文件)

import requests
r = requests.get("https://www.baidu.com/img/bd_logo1.png")
with open("logo.png","wb") as f:
f.write(r.content)

requests的小技巧

python
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
"""note
- 处理HTTPS请求 SSL证书验证
- "你的链接不是私密链接" "SSLError"
- r = requests.get("https://www.12306.cn/mormhweb/", verify = False)
- 设置超时
- r = requests.get(url, timeout = 10)
- 请求异常的处理

"""
# 请求异常的处理 重试机制
import requests
from retrying import retry

headers={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"}

@retry(stop_max_attempt_number=3)
def _parse_url(url,method,data,proxies):
print("*"*20)
if method=="POST":
response = requests.post(url,data=data,headers=headers,proxies=proxies)
else:
response = requests.get(url,headers=headers,timeout=3,proxies=proxies)
assert response.status_code == 200
return response.content.decode()


def parse_url(url,method="GET",data=None,proxies={}):
try:
html_str = _parse_url(url,method,data,proxies)
except:
html_str = None

return html_str

if __name__ == '__main__':
url = "www.baidu.com"
print(parse_url(url))

chrome抓包的技巧

爬虫数据提取

数据的分类

  • 结构化数据
    • 处理方法:正则、xpath
  • 非结构化数据
    • 处理方法:转化为python数据类型

json数据处理

类型转化

  • 如何找到json的url

  • 使用手机版

  • 提取方法

    • python数据类型 = json.loads(json字符串)

    • json字符串 = json.dumps(python数据类型)

    • python数据类型 = json.loads(包含json的类文件对象)

    • json字符串 = json.dumps(python数据类型)

    • ps: 具有read()或者write()方法的对象就是类文件对象
      f = open(“a.txt”,”r”) f就是类文件对象

  • demo

    python
    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
    import json
    from parse_url import parse_url
    from pprint import pprint

    url = "https://m.douban.com/rexxar/api/v2/subject_collection/movie_showing/items?start=0&count=18&loc_id=108288"
    html_str = parse_url(url)

    # json.loads把json字符串转化为python类型
    ret1 = json.loads(html_str)
    # pprint(ret1) # 格式化输出
    # print(type(ret1))

    # json.dumps能够把python类型转化为json字符串
    with open("douban.json","w",encoding="utf-8") as f:
    # ensure_ascii 中文编码 indent 格式化json 4个空格
    f.write(json.dumps(ret1,ensure_ascii=False,indent=4))
    # f.write(str(ret1))

    # with open("douban.json","r",encoding="utf-8") as f:
    # ret2 = f.read()
    # ret3 = json.loads(ret2)
    # print(ret3)
    # print(type(ret3))

    # 使用json.load提取类文件对象中的数据
    with open("douban.json","r",encoding="utf-8") as f:
    ret4 = json.load(f)
    print(ret4)
    print(type(ret4))

    #json.dump能够把python类型放入类文件对象中
    with open("douban1.json","w",encoding="utf-8") as f:
    json.dump(ret1,f,ensure_ascii=False,indent=2)

正则过滤获取json

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re
from parse_url import parse_url
import json

url = "http://36kr.com/"
html_str = parse_url(url)

# 正则匹配
ret = re.findall("<script>var props=(.*?),locationnal=",html_str)[0]

with open("36kr.json","w",encoding="utf-8") as f:
f.write(ret)

ret = json.loads(ret)
print(ret)

正则处理数据

正则表达式基础

案例-豆瓣小组爬虫

python
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
# 爬豆瓣某小组的讨论 title

# <a href="https://www.douban.com/group/topic/134170743/" title="广州演唱会是几号?"
# r"<a href=\"https://www.douban.com/group/topic/.*?/\" title=\"(.*?)\""

import requests
import re


class DBXiaozuSpider:
def __init__(self):
self.url_temp = "https://www.douban.com/group/649504/discussion?start={}"
self.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"}

def start_url(self, page_num):
return self.url_temp.format((page_num-1) * 25)

def parse_url(self, url):
print(url)
r = requests.get(url, headers=self.headers)
return r.content.decode()

def get_content_list(self, html_str):
title_list = re.findall(r"<a href=\"https://www.douban.com/group/topic/.*?/\" title=\"(.*?)\"", html_str)
return title_list

def save_content_list(self, title_list, page_num):
# a: 追加

with open("title_list.txt", "a", encoding="utf-8") as f:
for title in title_list:
index = (page_num-1) * 25 + title_list.index(title) + 1
title_line = "{} {}".format(index, title)
f.write(title_line + "\n")

def run(self):
page_num = 0
while True:
page_num += 1
# 1 start_url
url = self.start_url(page_num)

# 2 parse_url
html_str = self.parse_url(url)

# 3 get_content_list
title_list = self.get_content_list(html_str)

# 4 save_content_list
self.save_content_list(title_list, page_num)


if __name__ == '__main__':
db_xiaozu_spider = DBXiaozuSpider()
db_xiaozu_spider.run()

xpath处理数据

xpath

获取某贴吧的 标题 url 图片

  • chrome工具:xpath helper

  • 常用语法:

    获取文本 a/text()
    获取属性 a/@href
    当前目录 /.
    上一级 /..
    不考虑位置 //ul[@id="detail-list"]/li
    选列表中某一个
    ​ 第一个 //div[@id='page']/a[1]
    ​ 最后一个 //div[@id='page']/a[last()]
    ​ 倒数第2个 //div[@id='page']/a[last()-1]
    ​ 前3个 //div[@id='page']/a[position<4]
    任意节点
    //*[id='page']
    //node()[id='page']
    |
    //div[@id='page']/a[1] | //div[@id='page']/a[3]

    包含

    class包含i的div `//div[contains(@class,'i')]`

lxml

lxml

  • 能自动修正格式不规范的html代码

  • demo

    python
    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
    # coding=utf-8
    from lxml import etree

    text = ''' <div> <ul>
    <li class="item-1"><a>first item</a></li>
    <li class="item-1"><a href="link2.html">second item</a></li>
    <li class="item-inactive"><a href="link3.html">third item</a></li>
    <li class="item-1"><a href="link4.html">fourth item</a></li>
    <li class="item-0"><a href="link5.html">fifth item</a>
    </ul> </div> '''

    html = etree.HTML(text)
    print(html)
    #查看element对象中包含的字符串
    # print(etree.tostring(html).decode())

    #获取class为item-1 li下的a的herf
    ret1 = html.xpath("//li[@class='item-1']/a/@href")
    print(ret1)

    #获取class为item-1 li下的a的文本
    ret2 = html.xpath("//li[@class='item-1']/a/text()")
    print(ret2)

    #每个li是一条新闻,把url和文本组成字典
    for href in ret1:
    item = {}
    item["href"] = href
    item["title"] = ret2[ret1.index(href)]
    print(item)

    print("*"*100)
    #分组,根据li标签进行分组,对每一组继续写xpath
    ret3 = html.xpath("//li[@class='item-1']")
    print(ret3)
    for i in ret3:
    item= {}
    item["title"] = i.xpath("a/text()")[0] if len(i.xpath("./a/text()"))>0 else None
    item["href"] = i.xpath("./a/@href")[0] if len( i.xpath("./a/@href"))>0 else None
    print(item)
  • 用xpath改进豆瓣小组爬虫

    python
    1
    2
    3
    4
    5
    6
    def get_content_list(self, html_str):
    # 变成对象
    html = etree.HTML(html_str)
    title_list = html.xpath("//table[@class='olt']//tr/td/a/@title")

    return title_list

通用爬虫案例

案例

贴吧爬虫

  • 保存 每条贴吧的 title\ url\ 每条贴吧里所有img的url
  • demo :略

糗百爬虫

  • demo:略

爬虫思路总结

实现爬虫的套路

  • 准备url

    • 准备start_url
      • url地址规律不明显,总数不确定
      • 通过代码提取下一页的url
        • xpath
        • 寻找url地址,部分参数在当前的响应中(比如,当前页码数和总的页码数在当前的响应中)
    • 准备url_list
      • 页码总数明确
      • url地址规律明显
  • 发送请求,获取响应

    • 添加随机的User-Agent,反反爬虫
    • 添加随机的代理ip,反反爬虫
    • 在对方判断出我们是爬虫之后,应该添加更多的headers字段,包括cookie
    • cookie的处理可以使用session来解决
    • 准备一堆能用的cookie,组成cookie池
      • 如果不登录
        • 准备刚开始能够成功请求对方网站的cookie,即接收对方网站设置在response的cookie
        • 下一次请求的时候,使用之前的列表中的cookie来请求
      • 如果登录
        • 准备多个账号
        • 使用程序获取每个账号的cookie
        • 之后请求登录之后才能访问的网站随机的选择cookie
  • 提取数据

    • 确定数据的位置

      • 如果数据在当前的url地址中

        • 提取的是列表页的数据
          • 直接请求列表页的url地址,不用进入详情页
        • 提取的是详情页的数据
            1. 确定url
            1. 发送请求
            1. 提取数据
            1. 返回
      • 如果数据不在当前的url地址中

        • 在其他的响应中,寻找数据的位置
            1. 从network中从上往下找
            1. 使用chrome中的过滤条件,选择出了js,css,img之外的按钮
            1. 使用chrome的search all file,搜索数字和英文
    • 数据的提取

      • xpath,从html中提取整块的数据,先分组,之后每一组再提取
      • re,提取max_time,price,html中的json字符串
      • json
  • 保存

    • 保存在本地,text,json,csv
    • 保存在数据库

多线程爬虫

python
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
# 爬10页豆瓣某小组的内容,使用多线程

from queue import Queue
import requests
from lxml import etree
import threading
import time

class DBSpider:
def __init__(self):
self.url_temp = "http://www.douban.com/group/649504/discussion?start={}"
self.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"}
self.proxies = {"http": "http://117.191.11.72:8080"}
self.url_queue = Queue()
self.html_queue = Queue()
self.content_queue = Queue()

def get_url_list(self):
for i in range(10):
url = self.url_temp.format(i * 25)
self.url_queue.put(url)

def parse_url(self): # 守护线程
while True:
url = self.url_queue.get()
print("GET请求:", url)
r = requests.get(url, headers=self.headers, proxies=self.proxies)
self.html_queue.put(r.content.decode())
self.url_queue.task_done()

def get_content_list(self): # 守护线程
while True:
html_str = self.html_queue.get()
html = etree.HTML(html_str)
title_list = html.xpath("//table[@class='olt']//tr/td/a/@title")
for title in title_list:
self.content_queue.put(title)
self.html_queue.task_done()

def save_content_list(self): # 守护线程
# a: 追加
while True:
title = self.content_queue.get()
with open("title_list.txt", "a", encoding="utf-8") as f:
f.write(title + "\n")

self.content_queue.task_done()


def run(self):
thread_list = []

t_url = threading.Thread(target=self.get_url_list())
thread_list.append(t_url)

for i in range(10):
t_parse = threading.Thread(target=self.parse_url)
thread_list.append(t_parse)

for i in range(1):
t_html = threading.Thread(target=self.get_content_list)
thread_list.append(t_html)

for i in range(1):
t_save = threading.Thread(target=self.save_content_list)
thread_list.append(t_save)

for t in thread_list:
t.setDaemon(True) # 把子线程设置为守护线程,主线程结束,子线程结束
t.start()

# 主线程何时结束?
for q in [self.url_queue, self.html_queue, self.content_queue]:
q.join() # 让主线程等待阻塞,等待队列的任务完成之后再完成

print("主线程结束")


if __name__ == '__main__':
db_spider = DBSpider()

time_start = time.time()

db_spider.run()

time_end = time.time()
print('time cost', time_end - time_start, 's')
# 之前没加线程的代码是4s左右
# 增加请求url线程(并发请求) 1.32 1.48 1.37
文章作者: Machine
文章链接: https://machine4869.gitee.io/2019/02/20/20190220155214243/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 哑舍
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论