欢迎关注微信公众号:FSA全栈行动 👋javascript
Scrapy 是一个 python 编写的,被设计用于爬取网络数据、提取结构性数据的开源网络爬虫框架。html
补充:Scrapy 使用了 Twisted 异步网络框架,能够加快下载速度java
其流程描述以下:node
注意:python
模块 | 做用 | 是否实现 |
---|---|---|
Scrapy Engine(引擎) | 总指挥:负责数据和信号在不一样模块之间传递 | scrapy 已经实现 |
Scheduler(调度器) | 一个队列,存放引擎发过来的 request 请求 | scrapy 已经实现 |
Downloader(下载器) | 下载把引擎发过来的 request 请求,并返回给引擎 | scrapy 已经实现 |
Spider(爬虫) | 处理引擎发来的 response,提取数据、url,并交给引擎 | 须要手写 |
Item Pipeline(管道) | 处理引擎传过来的数据,好比存储 | 须要手写 |
Downloader Middlewares(下载中间件) | 能够自定义的下载扩展,好比设置代理 | 通常不用手写 |
Spider Middlewares(爬虫中间件) | 能够自定义 requests 请求和进行 response 过滤 | 通常不用手写 |
注意:爬虫中间件 和 下载中间件 只是运行逻辑的位置不一样,做用是重复的:如替换 UA 等。git
scrapy 有 2 种安装方式:es6
命令:github
sudo apt-get install scrapy
复制代码
pip:web
pip/pip3 install scrapy
复制代码
建立项目:ajax
scrapy startproject <项目名称>
eg:
scrapy startproject myspider
复制代码
生成一个爬虫:
scrapy genspider <爬虫名字> <容许爬取的域名>
eg:
cd myspider
scrapy genspider example example.com
复制代码
爬虫名字:做用爬虫运行时的参数
容许爬取的域名:为对于爬虫设置的爬取范围,设置以后用于过滤要爬取的 url,若是爬取的 url 与容许的域不一样则被过滤掉
注意:执行完命令后,myspider/spiders 目录下,会多出一个 example.py 爬虫文件
提取数据:
根据网站结构在 spider 中实现数据采集相关内容
保存数据:
使用 pipeline 进行数据后续处理和保存
运行 scrapy
scrapy crawl <爬虫名字>
scrapy crawl <爬虫名字> --nolog
eg:
scrapy crawl example
复制代码
注意 :须要在项目录下执行命令;
--nolog
能够关闭框架日志信息输出
解析并获取 scrapy 爬虫中的数据:利用 xpath 规则字符串进行定位和提取
response.xpath()
:返回一个相似 List 的类型,其中包含的是 selector 对象,操做和列表同样,可是有一些额外方法:
extract()
:返回一个包含有字符串的列表extract_first()
:返回列表中的第一个字符串,列表为空则返回 Noneresponse.url
:当前响应的 url 地址response.request.url
:当前响应对应的请求的 url 地址response.headers
:响应头response.request.headers
:当前响应的请求头response.body
:响应体,也就是 html 代码,byte 类型response.status
:响应状态码scrapy startproject myspider
cd myspider
scrapy genspider itcast itcast.cn
复制代码
在/myspider/myspider/spiders/itcast.py 中修改内容以下:
import scrapy
class ItcastSpider(scrapy.Spider):
name = 'itcast'
# 2.检查域名
allowed_domains = ['itcast.cn']
# 1.修改起始url
start_urls = ['http://itcast.cn/channel/teacher.shtml'] # 设置起始的url,咱们只须要设置就好,一般会被自动的建立成请求发送
# 3.在parse方法中实现爬取逻辑
def parse(self, response):
# 解析方法,一般用于起始url对应响应的解析
# 定义对于网站的相关操做
# with open('itcast.html', 'wb') as f:
# f.write(response.body)
# 获取全部老师节点
node_list = response.xpath('//div[@class="li_txt"]')
# 遍历老师节点列表
for node in node_list:
temp = {}
# xpath方法返回的是选择器对象列表,extract()用于从选择器对象中提取数据
# temp['name'] = node.xpath('./h3/text()')[0].extract()
# temp['title'] = node.xpath('./h4/text()')[0].extract()
# temp['desc'] = node.xpath('./p/text()')[0].extract()
# xpath结果为只含有一个值的列表,可使用extract_first(),若是为多个值则使用extract()
temp['name'] = node.xpath('./h3/text()').extract_first()
temp['title'] = node.xpath('./h4/text()').extract_first()
temp['desc'] = node.xpath('./p/text()').extract_first()
# {'name': [<Selector xpath='./h3/text()' data='黄老师'>], 'title': [<Selector xpath='./h4/text()' data='高级讲师'>], 'desc': [<Selector xpath='./p/text()' data='15年+的软件开发与教学经验,参与中国联通VOP系统,中国联通在线计费系统...'>]}
# extract()以后:
# {'name': '黄老师', 'title': '高级讲师', 'desc': '15年+的软件开发与教学经验,参与中国联通VOP系统,中国联通在线计费系统,道路交通事故救助基金管理系统等的开发工做;\r\n拥有丰富的教学经验。'}
print(temp)
yield temp
复制代码
注意:
scrapy.Spider
爬虫类中必须有名为 parse 的解析allowed_domains
范围内,可是 start_urls
中的 url 地址不受这个限制parse()
函数中使用 yield
返回数值。process_item()
方法process_item()
方法处理完 item 以后必须返回给引擎这里直接使用建立项目时默认建立好的管道类:/myspider/myspider/pipelines/MyspiderPipeline,修改内容以下:
import json
class MyspiderPipeline:
def __init__(self):
self.file = open('itcast.json', 'w')
def process_item(self, item, spider):
# print('item:', item)
# 将字典数据序列化
json_data = json.dumps(item, ensure_ascii=False) + ',\n'
# 将数据写入文件
self.file.write(json_data)
# 默认使用完管道以后须要将数据返回给引擎
return item
def __del__(self):
self.file.close()
复制代码
ITEM_PIPELINES = {
'myspider.pipelines.MyspiderPipeline': 300,
# 'myspider.pipelines.MyspiderPipelineOther': 301,
}
复制代码
.
进行分割,分别为:项目目录.文件.管理类
。在控制台运行以下命令便可运行爬虫:
scrapy crawl itcast
scrapy crawl itcast --nolog
复制代码
一般在项目开发过程当中,须要在 items.py
中进行数据建模
在 items.py
文件中定义发提取的字段:
import scrapy
class MyspiderItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field() # 讲师的名字
title = scrapy.Field() # 讲师的职称
desc = scrapy.Field() # 讲师的介绍
# if __name__ == '__main__':
# item = MyspiderItem()
# item['name'] = 'lqr' # ok
# item['nama'] = 'lqr' # KeyError: 'MyspiderItem does not support field: nama'
复制代码
注意:
scrapy.Item
能够理解为一个更高级的 “字典”,能够对键名进行限制、校验。但切记它不是字典,若是你须要对字典进行操做,可使用dict()
将scrapy.Item
进行强制转换。
模板类定义之后,须要在爬虫中导入而且实例化,以后的使用方法和字典相同:
# 若是报包报错有2种解决办法:
# 一、PyCharm以myspider为根目录从新打开项目
# 二、不以myspider为根目录的话,能够右键myspider-->Make Directory as-->Sources Root
from myspider.items import MyspiderItem
def parse(self, response):
item = MyspiderItem()
...
item['name'] = node.xpath('./h3/text()').extract_first()
item['title'] = node.xpath('./h4/text()').extract_first()
item['desc'] = node.xpath('./p/text()').extract_first()
...
yield item
复制代码
建立项目
scrapy startproject 项目名
复制代码
明确目标
建立爬虫
建立爬虫
scrapy genspider 爬虫名 容许的域
复制代码
编写爬虫
修改start_urls
检查修改allowed_domains
编写解析方法
复制代码
保存数据
pipelines.py
文件中定义对数据处理的管道settings.py
文件中注册启用管道肯定 url 地址
构造请求
scrapy.Request(url, callback)
复制代码
callback
:指定解析函数名称,表示该请求返回的响应使用哪个函数进行解析把请求交给引擎
scrapy startproject wangyi
cd wangyi
scrapy gespider job 163.com
复制代码
import scrapy
class WangyiItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
link = scrapy.Field()
depart = scrapy.Field()
category = scrapy.Field()
type = scrapy.Field()
address = scrapy.Field()
num = scrapy.Field()
date = scrapy.Field()
复制代码
import scrapy
from wangyi.items import WangyiItem
class JobSpider(scrapy.Spider):
name = 'job'
# 2.检查修改allowed_domains
allowed_domains = ['163.com']
# 1.修改start_urls
start_urls = ['https://hr.163.com/position/list.do']
def parse(self, response):
# 提取数据
# 获取全部职位节点列表
node_list = response.xpath('//*[@class="position-tb"]/tbody/tr')
# 遍历节点列表
for num, node in enumerate(node_list):
# 设置过滤条件,将目标节点获取出来
if num % 2 == 0:
item = WangyiItem()
item['name'] = node.xpath('./td[1]/a/text()').extract_first()
# item['link'] = 'https://hr.163.com' + node.xpath('./td[1]/a/@href').extract_first()
# response.urljoin()用于拼接相对路径的url,能够理解为自动补全
item['link'] = response.urljoin(node.xpath('./td[1]/a/@href').extract_first())
item['depart'] = node.xpath('./td[2]/text()').extract_first()
item['category'] = node.xpath('./td[3]/text()').extract_first()
item['type'] = node.xpath('./td[4]/text()').extract_first()
item['address'] = node.xpath('./td[5]/text()').extract_first()
item['num'] = node.xpath('./td[6]/text()').extract_first().strip()
item['date'] = node.xpath('./td[7]/text()').extract_first()
# print(item)
yield item
# 模拟翻页
part_url = response.xpath('/html/body/div[2]/div[2]/div[2]/div/a[last()]/@href').extract_first()
# 判断终止条件
if part_url != 'javascript:void(0)':
next_url = response.urljoin(part_url)
# 构建请求对象,而且返回给引擎
yield scrapy.Request(
url=next_url,
callback=self.parse
)
复制代码
注意:引擎根据爬虫
yield
的对象类型,将对象分配给对应的模块处理,好比:若是是模板类对象或字典,则会交给管道(Item Pipeline);若是是 Request 对象,则会交给队列(Scheduler)。
import json
class WangyiPipeline:
def __init__(self):
self.file = open('wangyi.json', 'w')
def process_item(self, item, spider):
item = dict(item)
str_data = json.dumps(item, ensure_ascii=False) + ',\n'
self.file.write(str_data)
return item
def __del__(self):
self.file.close()
复制代码
ITEM_PIPELINES = {
'wangyi.pipelines.WangyiPipeline': 300,
}
复制代码
scrapy crawl job --nolog
复制代码
能够在 settings 中设置 ROBOTS 协议
# False表示忽略网站的robots.txt协议,默认为True
ROBOTSTXT_OBEY = False
复制代码
能够在 settings 中设置 User-Agent:
# scrapy发送的每个请求的默认UA都是设置的这个User-Agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36'
复制代码
scrapy.Request(url[, callback, method="GET", headers, body, cookies, meta, dont_filter=False])
复制代码
注意:中括号[]里的参数为可选参数
参数解释:
meta 的使用:
meta 的注意事项:
使用举例:
def parse(self, response):
...
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
...
def parse_detail(self,response):
# 获取以前传入的item
item = response.meta['item']
复制代码
应用场景:
scrapy 中 start_url 是经过 start_requests 来进行处理的,可能经过重写该方法让 start_url 携带上请求头信息,实现代码以下:
import scrapy
class Git1Spider(scrapy.Spider):
name = 'git1'
allowed_domains = ['github.com']
start_urls = ['https://github.com/GitLqr']
def start_requests(self):
""" 重写start_requests,发送携带cookies的Request。 默认start_requests只是普通的get请求,不会携带自定义的头信息 """
url = self.start_urls[0]
temp = '_octo=GH1.1.1045146750.1615451260; _device_id=cd8d64981fcb3fd4ba7f587873e97804'
# 把cookies字符串转成字典
cookies = {data.split('=')[0]: data.split('=')[-1] for data in temp.split('; ')}
yield scrapy.Request(
url=url,
callback=self.parse,
cookies=cookies
)
def parse(self, response):
print(response.xpath('/html/head/title/text()').extract_first())
复制代码
注意:
scrapy.Request 发送 post 请求有两种方式:
经过 scrapy.Request()
指定 method、body 参数来发送 post 请求(不推荐)
使用 scrapy.FormRequest()
来发送 post 请求(推荐)
注意:
scrapy.FormRequest()
可以发送表单和 ajax 请求,参考阅读 www.jb51.net/article/146…
举例:
import scrapy
class Git2Spider(scrapy.Spider):
name = 'git2'
allowed_domains = ['github.com']
start_urls = ['http://github.com/login']
def parse(self, response):
username = 'GitLqr'
password = 'balabala'
# 从登陆页面响应中解析出post数据
token = response.xpath('//input[@name="authenticity_token"]/@value').extract_first()
post_data = {
'commit': 'Sign in',
'authenticity_token': token,
'login': username,
'password': password,
'webauthn-support': 'supported',
}
print(post_data)
# 针对登陆url发送post请求
yield scrapy.FormRequest(
url='https://github.com/session',
callback=self.after_login,
formdata=post_data
)
def after_login(self, response):
yield scrapy.Request('https://github.com/GitLqr', callback=self.check_login)
def check_login(self, response):
print(response.xpath('/html/head/title/text()').extract_first())
复制代码
注意:在 settings.py 中经过设置
COOKIES_DEBUG = True
可以在终端查看到 cookie 的传递过程
process_item(self, item, spider)
:
return item
open_spider(self, spider)
:在爬虫开启的时候仅执行一次close_spider(self, spider)
:在爬虫关闭的时候仅执行一次注意:以上 3 个方法,能够经过
spider.name
获取爬虫的名字
scrapy 的 Item Pipeline
模块能够有多个管道,当有一个 spider 把数据对象经过引擎交给 Item Pipeline
模块时, Item Pipeline
模块中的全部管道会按 settings.py
中指定的管道顺序一一被执行。但不少时候,咱们须要管道针对特定爬虫作数据存储的,这时就须要在管道中对数据对象的来源作判断。
注意:不一样的 pipeline 可以对一个或多个爬虫进行不一样的数据处理的操做,好比一个进行数据清洗,一个进行数据的保存
举例:
import scrapy
from wangyi.items import WangyiItem
class JobSpider(scrapy.Spider):
name = 'job'
...
def parse(self, response):
...
yield item
复制代码
import scrapy
from wangyi.items import WangyiSimpleItem
class JobSimpleSpider(scrapy.Spider):
name = 'job_simple'
...
def parse(self, response):
...
yield item
复制代码
class WangyiPipeline:
def open_spider(self, spider):
if spider.name == 'job':
self.file = open('wangyi.json', 'w')
def process_item(self, item, spider):
if spider.name == 'job':
item = dict(item)
str_data = json.dumps(item, ensure_ascii=False) + ',\n'
self.file.write(str_data)
return item
def close_spider(self, spider):
if spider.name == 'job':
self.file.close()
class WangyiSimplePipeline:
def open_spider(self, spider):
if spider.name == 'job_simple':
self.file = open('wangyi_simple.json', 'w')
def process_item(self, item, spider):
if spider.name == 'job_simple':
item = dict(item)
str_data = json.dumps(item, ensure_ascii=False) + ',\n'
self.file.write(str_data)
return item
def close_spider(self, spider):
if spider.name == 'job_simple':
self.file.close()
复制代码
process_item
的方法必须 return item
,不然后一个 pipeline 取到的数据为 None 值process_item
方法必需要有,不然 item 没有办法接收和处理process_item
方法接收 item 和 spider,其中 spider 表示当前传递 item 过来的 spideropen_spider(spider)
:可以在爬虫开启的时候执行一次close_spider(spider)
:可以在爬虫关闭的时候执行一次根据 scrapy 运行流程中所在位置不一样,对 scrapy 中间件进行分类:
scrapy 中间件的做用是:预处理 request 和 response 对象
默认状况下,两种中间件都在 middlewares.py
一个文件中
爬虫中间件使用方法和下载中间件相同,且功能重复,一般使用下载中间件
注意:结合 scrapy 流程图可方便理解
中间件使用步骤以下:
在 middlerware.py
中定义中间件类
在中间件类中,重写处理请求或者响应的方法(以Downloader Middlewares
为例)
process_request(self, request, spider)
当每一个 request 经过下载中间件时,该方法被调用【Scrapy Engine --> Downloader】
process_request
方法,若是全部中间件都返回 None,则请求最终被交给下载器处理。注意:没有 return 也是返回 None。process_request
方法process_response(self, request, response, spider)
当下载器完成 http 请求,传递响应给引擎的时候调用【Scrapy Engine <-- Downloader】
process_response
方法process_response
方法在 settings
文件中开启中间件,权重值越小越优先执行(同管道注册同样)
在 settings.py
文件中定义一个存放了大量 UA 的列表 USER_AGENT_LIST
:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36'
USER_AGENT_LIST = [
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;',
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)',
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11',
'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
'Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
'Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
'Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
'Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
'MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
'Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10',
'Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13',
'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+',
'Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0',
'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124',
'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)',
'UCWEB7.0.2.37/28/999',
'NOKIA5700/ UCWEB7.0.2.37/28/999',
'Openwave/ UCWEB7.0.2.37/28/999',
'Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999',
'UCWEB7.0.2.37/28/999',
'NOKIA5700/ UCWEB7.0.2.37/28/999',
'Openwave/ UCWEB7.0.2.37/28/999',
'Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999'
]
复制代码
注意:该
USER_AGENT_LIST
变量名能够自定义,也能够写在其余 py 文件中,写在 settings.py 文件中,只是为了规范而已。
在 middlewares.py
文件中定义随机 UA 的下载中间件:
import random
from Douban.settings import USER_AGENT_LIST
class RandomUserAgentMiddleware(object):
def process_request(self, request, spider):
# print(request.headers['User-Agent'])
ua = random.choice(USER_AGENT_LIST)
request.headers['User-Agent'] = ua
复制代码
在 settings.py
文件中配置开启自定义的下载中间件:
DOWNLOADER_MIDDLEWARES = {
'Douban.middlewares.RandomUserAgentMiddleware': 543,
}
复制代码
scrapy 的 Downloader 模块只会根据请求获取响应,但实际开发过程当中,有些页面上的数据是经过 ajax 延迟加载出来的,Downloader 模块没法应对这种状况,这时就须要用到 Selenium 来处理这类请求,等页面渲染完成后,再把渲染好的页面返回给爬虫便可:
from selenium import webdriver
from scrapy.http import HtmlResponse
class SeleniumMiddleware(object):
def __init__(self):
self.driver = webdriver.Chrome()
def process_request(self, request, spider):
url = request.url
# 过滤须要使用selenium渲染的request
if 'daydata' in url:
self.driver.get(url)
time.sleep(3)
data = self.driver.page_source
self.driver.close()
# 返回HtmlResponse,请求不会达到Downloader,而是直接经过引擎交给爬虫
response = HtmlResponse(url=url, body=data, encoding='utf-8', request=request)
return response
def __del__(self):
self.driver.quit()
复制代码
若是文章对您有所帮助, 请不吝点击关注一下个人微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不只有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~