python爬虫基础(一)

requests+selenium+scrapy

python爬虫

1、爬虫

爬虫:通过编写程序,模拟浏览器上网,然后让去互联网上抓取数据的过程

通用爬虫:抓取的是一整张页面数据

聚焦爬虫:抓取的是页面中特定的局部内容

增量式爬虫:只会抓取网站中最新更新出来的数据

反爬机制:门户网站可以通过制定相应的策略或者技术手段,防止爬虫程序进行网站数据的爬取

反反爬策略:破解门户网站中具备的反爬机制

robot.txt协议:规定了网站中哪些数据可以被爬取,哪些数据不可以被爬取

2、爬虫伪装

为了让我们的爬虫能够成功爬取所需数据信息,我们需要让爬虫进行伪装,简单来说就是让爬虫的行为变得像普通用户访问一样。

有时我们可能会对一些网站进行长期或大规模的爬取,而我们在爬取时基本不会变换 IP,有的网站可能会监控一个 IP 的访问频率和次数,一但超过这个阈值,就可能认作是爬虫,从而对其进行了屏蔽,对于这种情况,我们要采取间歇性访问的策略。

3、http&https协议

http协议:服务器和客户端进行数据交互的方式

Request Headers 中包含 Referer 和 User-Agent 两个属性信息

Referer :告诉服务器该网页是从哪个页面链接过来的

User-Agent(请求载体的身份标识): 中文是用户代理,它是一个特殊字符串头,作用是让服务器能够识别用户使用的操作系统、CPU 类型、浏览器等信息。

Connection:请求完毕后,是断开连接还是保持连接

通常的处理策略是:1)对于要检查 Referer 的网站就加上;2)对于每个 request 都添加 User-Agent。

响应头信息:

Content-type:服务器响应回客户端的数据类型

https协议:安全的超文本传输协议

加密方式:

  • 对称密钥加密
    python爬虫基础(一)_第1张图片
  • 非对称密钥加密
    python爬虫基础(一)_第2张图片
  • 证书密钥加密
    python爬虫基础(一)_第3张图片

4、Requests库

所谓爬虫就是模拟客户端发送网络请求,获取网络响应,并按照一定的规则解析获取的数据并保存的程序。

requests模块:模拟浏览器发请求

掌握了requests模块=掌握了python爬虫中的半壁江山

使用(requests模块的编码流程):

  • 指定url
  • 发起请求
  • 获取响应数据
  • 持久化存储

环境安装:pip install requests

# step:1:指定url
url: str = "http://www.baidu.com/"
# step2:发起请求
response = requests.get(url=url)
# step3:获取响应数据
data = response.text
print(data)
# step4:持久化存储
with open('./baidu.html','w',encoding='utf-8') as file:
    file.write(data)

发送请求

在使用 get 方式发送请求时,我们会将键值对形式参数放在 URL 中问号的后面,如:http://xxx.xxx/get?key=val ,Requests 通过 params 关键字,以一个字符串字典来提供这些参数。比如要传 key1=val1key2=val2http://xxx.xxx/get

import requests

params={"telephone":"15939479856","password":"twt123456","code":"bwcy"}
r=requests.get("http://39.101.67.148:1252/login",params=params)
print(r)

字典里值为 None 的键都不会被添加到 URL 的查询字符串里。

响应内容

import requests


params={"telephone":"15939479856","password":"twt123456","code":"bwcy"}
r=requests.get("http://39.101.67.148:1252/login",params=params)

request=requests.get('https://api.github.com')
print(request.text)
print(request.encoding)
print(request.status_code)
print(request.json())

JSON响应内容 Requests 中已经内置了 JSON 解码器,因此我们可以很容易的对 JSON 数据进行解析

成功调用 r.json() 并不一定响应成功,有的服务器会在失败的响应中包含一个 JSON 对象(比如 HTTP 500 的错误细节),这时我们就需要查看响应的状态码了 r.status_code 或 r.raise_for_status(),成功调用时 r.status_code 为 200,r.raise_for_status() 为 None。

自定义请求头

当我们要给请求添加 headers 时,只需给 headers 参数传递一个字典即可

url = 'http://xxx.xxx'
header= {'user-agent': 'xxx'}
r = requests.get(url, headers=header)

自定义 headers 优先级是低于一些特定的信息的,如:在 .netrc 中设置了用户认证信息,使用 headers 设置的授权就不会生效,而当设置了 auth 参数,.netrc 的设置会无效。所有的 headers 值必须是 string、bytestring 或者 unicode,通常不建议使用 unicode。

5、requests模块实战

实例:爬取百度指定词条对应的搜索结果页面(简易网页采集器)

User-Agent伪装:门户网站的服务器监测对应请求的载体身份标识,如果监测到请求的载体身份标识为浏览器,说明该请求是一个正常的请求;如果监测到载体身份标识不是浏览器,说明该请求是一个爬虫

# -*- coding: utf-8 -*-
# @Time    : 2022/10/14 20:13
# @Author  : 楚楚
# @File    : 02简易网页采集器.py
# @Software: PyCharm
import requests

url = "https://www.baidu.com/"
# 处理url携带的参数,封装到字典中
word = input("enter a word:")
params = {
    "s?wd": word
}

# UA伪装:让爬虫对应的请求载体身份表示伪装成浏览器
header = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}
response = requests.get(url=url, params=params, headers=header)
response.encoding = 'utf-8'
print(response.text)

with open("./word.html", 'w', encoding='utf-8') as file:
    file.write(response.text)
    
url="https://www.zhihu.com/question/373782593/answer/2336744180"
re=requests.get(url=url,headers=header)
print(re.text)
with open("./zhihu.html","w",encoding="utf-8") as file:
    file.write(re.text)

实例:破解百度翻译

post请求:携带参数

响应数据是一组JSON数据

# -*- coding: utf-8 -*-
# @Time    : 2022/10/14 21:17
# @Author  : 楚楚
# @File    : 03破解百度翻译.py
# @Software: PyCharm
import requests
import json

url = "https://fanyi.baidu.com/v2transapi"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}

params = {
    "from": "en",
    "to": "zh",
    "query": "cat",
    "sign":"661701.982004",
    "simple_means_flag":"3",
    "token":"对应的token",
    "domain":"common"
}

response = requests.post(url=url, headers=headers, data=params)

# 下述结果不知道为什么会报错,可以参考:https://blog.csdn.net/bug_tan90/article/details/118252736
# Content-Type:application/json
json_obj=response.json()

# 持久化存储
file=open("./translate.json",'w',encoding="utf-8")
json.dump(json_obj,fp=file,ensure_ascii=False)
# -*- coding: utf-8 -*-
# @Time    : 2022/10/14 21:17
# @Author  : 楚楚
# @File    : 03破解百度翻译.py
# @Software: PyCharm
import requests
import json

# 这个url地址可以正常爬取内容
url = "https://fanyi.baidu.com/sug"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}

params = {
    "kw": "cat",
}

response = requests.post(url=url, headers=headers, data=params)

# Content-Type:application/json
json_obj=response.json()

# 持久化存储
file=open("./translate.json",'w',encoding="utf-8")
json.dump(json_obj,fp=file,ensure_ascii=False)

实例:豆瓣电影分类排行榜

https://movie.douban.com

# -*- coding: utf-8 -*-
# @Time    : 2022/10/15 15:08
# @Author  : 楚楚
# @File    : 04豆瓣电影.py
# @Software: PyCharm
import json

import requests

url = "https://movie.douban.com/j/chart/top_list"
params = {
    'type': '24',
    'interval_id': '100:90',
    'action': '',
    'start': '20',
    'limit': '20'
}

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

response = requests.get(url=url, params=params, headers=headers)
list_data=response.json()

file=open("./movie.json",'w',encoding='utf-8')
json.dump(list_data,fp=file,ensure_ascii=False)

实例:肯德基餐厅查询

# -*- coding: utf-8 -*-
# @Time    : 2022/10/15 15:17
# @Author  : 楚楚
# @File    : 05肯德基餐厅查询.py
# @Software: PyCharm
import json

import requests

url = "http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword"

params = {
    'cname': '',
    'pid': '',
    'keyword': '上海',
    'pageIndex': '1',
    'pageSize': '10'
}

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

response = requests.post(url=url, data=params, headers=headers)

list_data = response.json()
file = open("./kfc.json", 'w', encoding='utf-8')
json.dump(list_data, fp=file, ensure_ascii=False)

6、数据解析

聚焦爬虫:爬取页面中指定的页面内容

编码流程:指定url、发起请求、获取响应数据、数据解析、持久化存储

数据解析分析:正则、bs4、xpath

解析的局部文本内容都会在标签之间或者标签对应的属性中进行存储

解析原理:进行指定标签的定位——>标签或者标签对应的属性中存储的数据值进行提取(解析)

图片数据爬取

import requests

url="https://img1.baidu.com/it/u=3009731526,373851691&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}
response=requests.get(url=url,headers=headers)

#text:字符串 content:二进制 json():对象
image_data=response.content

with open('./日落.jpg','wb') as file:
    file.write(image_data)

正则解析

正则表达式中的贪婪模式和非贪婪模式:

  • 贪婪模式:默认的匹配规则,在满足条件的情况下,尽可能多地去匹配到字符串
  • 非贪婪模式:在满足条件的情况下尽可能少地去匹配

.*.*?.+?的区别:

  1. . 表示 匹配除换行符 \n 之外的任何单字符,*表示零次或多次。所以.*在一起就表示任意字符出现零次或多次。没有?表示贪婪模式。比如a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。
  2. ?跟在*或者+后边用时,表示懒惰模式。也称非贪婪模式。就是匹配尽可能少的字符。就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。
  3. a.+?b匹配最短的,以a开始,以b结束的字符串,但a和b中间至少要有一个字符。如果把它应用于ababccaab的话,它会匹配abab(第一到第四个字符)和aab(第七到第九个字符)。注意此时匹配结果不是ab,abaab。因为a和b中间至少要有一个字符。
import requests
import re
import os

if not os.path.exists('./image'):
    os.mkdir("./image")

url="https://www.jb51.net/article/226159.htm"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}

response=requests.get(url=url,headers=headers)
response.encoding='utf-8'
page=response.text

# 使用聚焦爬虫将页面中的图片进行提取
# content='
.*?.*?src="(.*?)"' content = '' list_image=re.findall(pattern=content,string=page,flags=re.S) for image in list_image: image="https:"+image image_name=image.split("/")[-1] response_image=requests.get(url=image,headers=headers) with open('./image/'+image_name,'wb') as file: file.write(response_image.content)

bs4解析(python独有)

数据解析的原理:标签定位;提取标签、标签属性中存储的数据

bs4数据解析的原理:

  1. 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中
  2. 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

环境安装:pip install bs4、pip install lxml

实例化BeautifulSoup对象:

  1. from bs4 import BeautifulSoup
  2. 对象的实例化:将本地的html文档中的数据加载到该对象中;将互联网上获取的页面源码加载到该对象中
from bs4 import BeautifulSoup

# 将本地的html文档中的数据加载到BeautifulSoup对象中
layout = open("./layout.html", 'r', encoding='utf-8')
soup = BeautifulSoup(layout, 'lxml')

print(soup)
获取标签
soup.div # 返回第一次出现的标签对应的内容

soup.tagName:返回的是文档中第一次出现的tagName对应的标签

soup.find('div') # 返回第一次出现的标签对应的内容

soup.find()等同于soup.tagName

soup.find('div',class_='card') # class="card"对应的标签的内容

soup.find(‘div’,class_/id/attr=‘card’):属性定位

soup.find_all('a') # 返回符合要求的全部标签对应的内容(列表形式)

soup.find_all(‘tagName’):返回符合要求的所有标签

soup.select('.card') # 返回class='card'对应的标签

soup.select(‘id选择器/class选择器/标签选择器/层次选择器’)

获取标签之间的文本数据

soup.tagName.text/string/get_text()

soup.find('p').text
  1. text/get_text():可以获取标签中的所有文本内容(即使该文本内容不是该标签的直系文本内容)
  2. string:只可以获取该标签下面直系文本的内容
获取标签中属性值
soup.a['href']
补充:css层次选择器
后代选择器
/*后代选择器*/
body p{
    /*就是对body后面所有的标签内容进行属性的调节*/
    background: red;
}

后代选择器:爷爷 父亲 儿子

子类选择器
body >p{
    background: blue;
    /*就是一代的关系*/

}
相邻兄弟选择器

同辈的关系(姐姐,妹妹)

/*相邻兄弟选择器*/
.active +p{
    /*向下,只有一个*/
    background: brown;
}
通用选择器

当前选中元素向下的所有兄弟元素

.active ~p {
    background: blanchedalmond;
}
实例:爬取三国演义中的内容

url:https://www.shicimingju.com/book/sanguoyanyi.html

# -*- coding: utf-8 -*-
# @Time    : 2022/10/15 20:03
# @Author  : 楚楚
# @File    : 09bs4解析实例.py
# @Software: PyCharm
import requests
from bs4 import BeautifulSoup
import os
import time

if not os.path.exists("./txt"):
    os.mkdir('./txt')

# 对首页中的页面数据进行爬取
url = "https://www.shicimingju.com/book/sanguoyanyi.html"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}

page_text = requests.get(url=url, headers=headers)
page_text.encoding='utf-8'

# 在首页中解析出章节的标题和详情页的url
soup = BeautifulSoup(page_text.text, 'lxml')

li_list = soup.select(".book-mulu > ul > li")

file=open('./txt/三国演义.txt','a+',encoding='utf-8')

for li in li_list:
    title = li.a.string
    file.write(title+'\n')

    content_url = 'https://www.shicimingju.com' + li.a['href']
    print(content_url)

    # 对详情页发起发起发起请求
    content = requests.get(url=content_url, headers=headers)
    content.encoding='utf-8'
    content_soup=BeautifulSoup(content.text)

    detail=content_soup.find_all('div',class_='chapter_content')[0].text
    file.write(detail)
    print("title:"+title)

    time.sleep(0.5)

异常处理:

  1. TypeError: object of type ‘Response’ has no len() 这是为何?
  2. Python "ResultSet object has no attribute ‘%s’. 问题解决
  3. ModuleNotFoundError: No module named 'certifi’问题

xpath解析

xpath解析:最常用且最便捷高效的一种解析方式

xpath解析原理:

  1. 实例化一个etree对象,且需要将被解析的页面源码数据加载到该对象中
  2. 调用etree对象中的xpath方法结合xpath表达式实现标签的定位和内容的捕获

环境的安装:pip install lxml

实例化一个etree对象(from lxml import etree):将本地的html文档中的源码数据加载到etree对象中:etree.parse(filePath);从互联网上获取的源码数据加载到该对象中:etree.HTML(page_text)

xpath(‘xpath表达式’)

xpath表达式:

  1. /:表示从根节点开始定位,表示的是一个层级
  2. //:表示的是多个层级;可以表示从任意位置开始
  3. 属性定位://div[@class=‘className’]
  4. 索引定位://div[@class=“className”]/p[3](索引是从1开始的)
  5. 取文本
    1. /text():content=tree.xpath(‘//div[@class=“card”]//li[5]/a/text()’)[0]
    2. /text():获取的是标签中直系的文本内容
    3. //text():获取的是标签中所有的文本内容
  6. 取属性
    1. /@attrName img/@src
    2. content=tree.xpath(“//div[@class=‘card’]/img/@src”)
from lxml import etree

# 实例化etree对象,且将别解析的源码加载到该对象中
tree=etree.parse('./layout.html')

# content:Element对象
content=tree.xpath('/html/head/title')
# 属性定位
content_attr=tree.xpath('//div[@class="card"]')

# 索引定位
content_index=tree.xpath('//div[@class="card"]/p[0]')

print(content)
print(content_attr)
print(content_index)
实例:千千音乐

url:https://music.91q.com/songlist/295822

# -*- coding: utf-8 -*-
# @Time    : 2022/10/15 21:38
# @Author  : 楚楚
# @File    : 11二手房.py
# @Software: PyCharm
import requests
from lxml import etree

url = "https://music.91q.com/songlist/295822"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}
# proxies={"https": '222.110.147.50:3128'}

response = requests.get(url=url, headers=headers).text

# 数据解析
tree = etree.HTML(response)
titles = tree.xpath('//div[@class="song-box"]/a/text()')

print(titles)

titles_div = tree.xpath("//div[@class='song-box']")
for div in titles_div:
    # 局部解析
    titles_div_a = div.xpath("./a/text()")
    print(titles_div_a)
xpath路径

xpath通过"路径表达式"(Path Expression)来选择节点。在形式上,"路径表达式"与传统的文件系统非常类似

"."表示当前节点

"…"表示当前节点的父节点

“/”:表示选择根节点

“//”:表示选择任意位置的某个节点

“@”: 表示选择某个属性

xml实例文档:



<bookstore>

  <book>
    <title lang="eng">Harry Pottertitle>
    <price>29.99price>
  book>

  <book>
    <title lang="eng">Learning XMLtitle>
    <price>39.95price>
  book>

bookstore>

/bookstore :选取根节点bookstore

//book :选择所有 book 子元素,而不管它们在文档中的位置

//@lang :选取所有名为 lang 的属性

xpath的谓语条件:所谓"谓语条件",就是对路径表达式的附加条件。所有的条件,都写在方括号"[]"中,表示对节点进行进一步的筛选

/bookstore/book[1] :表示选择bookstore的第一个book子元素

/bookstore/book[last()] :表示选择bookstore的最后一个book子元素

/bookstore/book[last()-1] :表示选择bookstore的倒数第二个book子元素

/bookstore/book[position() < 3] :表示选择bookstore的前两个book子元素

//title[@lang] :表示选择所有具有lang属性的title节点

//title[@lang=‘eng’] :表示选择所有lang属性的值等于"eng"的title节点

/bookstore/book[price] :表示选择bookstore的book子元素,且被选中的book元素必须带有price子元素

/bookstore/book[price>35.00] :表示选择bookstore的book子元素,且被选中的book元素的price子元素值必须大于35

/bookstore/book[price>35.00]/title:在结果集中选择title子元素

/bookstore/book/price[.>35.00] :表示选择值大于35的"/bookstore/book"的price子元素

通配符:"*“表示匹配任何元素节点、”@*"表示匹配任何属性值。

//* :选择文档中的所有元素节点

/*/* :表示选择所有第二层的元素节点

/bookstore/* :表示选择bookstore的所有元素子节点

//title[@*] :表示选择所有带有属性的title元素

选择多个路径:用"|"选择多个并列的路径

//book/title | //book/price :表示同时选择book元素的title子元素和price子元素

实例:4k图片解析下载
# -*- coding: utf-8 -*-
# @Time    : 2022/10/16 9:53
# @Author  : 楚楚
# @File    : 12图片解析.py
# @Software: PyCharm
import requests
from lxml import etree
import os

if not os.path.exists('./picture'):
    os.mkdir("./picture")

url = "https://pic.netbian.com/"
headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
}

response = requests.get(url=url, headers=headers)
# response.encoding='utf-8'
text = response.text
tree = etree.HTML(text)

images = tree.xpath("//div[@class='slist']//li")

for image, i in zip(images, range(1, len(images))):
    src = "https://pic.netbian.com" + image.xpath('./a/span/img/@src')[0]
    # alt=image.xpath('./a/span/img/@alt')[0]+'.jpg'
    name = str(i) + '.jpg'

    # 请求图片
    picture = requests.get(url=src, headers=headers).content
    with open('./picture/'+name,'wb') as file:
        file.write(picture)
        print(f"{name}下载成功")

7、验证码识别

反爬机制:验证码

识别验证码图片中的数据,用于模拟登录操作

8、模拟登录Cookie操作

http/https协议特性:无状态

Cookie的作用:我们在浏览器中,经常涉及到数据的交换,比如登录邮箱,登录一个页面。我们经常会在此时设置30天内记住我,或者自动登录选项。那么它们是怎么记录信息的呢,答案就是今天的主角cookie了, Cookie是由HTTP服务器设置的,保存在浏览器中,但HTTP协议是一种无状态协议,在数据交换完毕后,服务器端和客户端的链接就会关闭,每次交换数据都需要建立新的链接。

session机制采用的是在服务器端保持状态的方案,而cookie机制则是在客户端保持状态的方案,cookie又叫会话跟踪机制。打开一次浏览器到关闭浏览器算是一次会话。HTTP协议是一种无状态协议,在数据交换完毕后,服务器端和客户端的链接就会关闭,每次交换数据都需要建立新的链接。此时,服务器无法从链接上跟踪会话。

cookie分为会话cookie和持久cookie,会话cookie是指在不设定它的生命周期expires时的状态,前面说了,浏览器的开启到关闭就是一次会话,当关闭浏览器时,会话cookie就会跟随浏览器而销毁。当关闭一个页面时,不影响会话cookie的销毁。持久cookie则是设定了它的生命周期expires,此时,cookie像商品一样,有个保质期,关闭浏览器之后,它不会销毁,直到设定的过期时间。对于持久cookie,可以在同一个浏览器中传递数据,比如,你在打开一个淘宝页面登陆后,你在点开一个商品页面,依然是登录状态,即便你关闭了浏览器,再次开启浏览器,依然会是登录状态。

session会话对象:

  1. 可以进行请求的发送
  2. 如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中
import requests

session=requests.Session()
# 使用session进行post请求的发送
# 使用携带cookie的session对象进行get请求的发送

9、代理IP

代理的作用:破解封IP这种反爬机制

代理(代理服务器)的作用:

  1. 突破自身IP访问的限制
  2. 隐藏自身真实IP

代理IP的类型:

  1. http:应用到http协议对应的url中
  2. https:应用到https协议对应的url中

代理ip的匿名度:

  1. 透明:服务器知道了该次请求使用了代理,也知道请求对应的真实ip
  2. 匿名:知道使用了代理,不知道真实ip
  3. 高匿:不知道使用了代理,更不知道真实的ip

参考文献

1、关于正则表达式中的.*,.*?,.+?的理解

2、xpath路径表达式

3、Cookie的原理、作用,区别以及使用

你可能感兴趣的:(python,爬虫,python,爬虫)