因为互联网的极速发展,全部如今的信息处于大量堆积的状态,咱们既要向外界获取大量数据,又要在大量数据中过滤无用的数据。针对咱们有益的数据须要咱们进行指定抓取,从而出现了如今的爬虫技术,经过爬虫技术咱们能够快速获取咱们须要的数据。可是在这爬虫过程当中,信息拥有方会对爬虫进行反爬处理,咱们就须要对这些困难进行逐个击破。css
恰好前段时间作了爬虫相关的工做,这里就记录下一些相关的心得。html
本文案例代码地址 https://github.com/yangtao9502/ytaoCrawlpython
这里我是使用的 Scrapy 框架进行爬虫,开发环境相关版本号:mysql
Scrapy : 1.5.1 lxml : 4.2.5.0 libxml2 : 2.9.8 cssselect : 1.0.3 parsel : 1.5.1 w3lib : 1.20.0 Twisted : 18.9.0 Python : 3.7.1 (default, Dec 10 2018, 22:54:23) [MSC v.1915 64 bit (AMD64)] pyOpenSSL : 18.0.0 (OpenSSL 1.1.1a 20 Nov 2018) cryptography : 2.4.2 Platform : Windows-10-10.0.15063-SP0
本地开发环境建议使用 Anaconda 安装相关环境,不然可能出现各类依赖包的冲突,相信遇到过的都深有体会,在你配置相关环境的时候就失去爬虫的兴趣。 本文提取页面数据主要使用 Xpath ,因此在进行文中案例操做前,先了解 Xpath 的基本使用。git
scrapy 建立项目很简单,直接一条命令搞定,接下来咱们建立 ytaoCrawl 项目:github
scrapy startproject ytaoCrawl
注意,项目名称必须以字母开头,而且只包含字母、数字和下划线。 建立成功后界面显示:sql
初始化项目的文件有:数据库
其中各个文件的用途:json
了解几个默认生成的文件后再看下面的 scrapy 结构原理图,相对好理解。api
这样咱们的一个 scrapy 爬虫项目就此建立完成。
咱们先建立一个 python 文件 ytaoSpider,该类必须继承 scrapy.Spider 类。接下来咱们就以爬取北京 58 租房信息为例进行分析。
#!/usr/bin/python3 # -*- coding: utf-8 -*- # # @Author : YangTao # @blog : https://ytao.top # import scrapy class YtaoSpider(scrapy.Spider): # 定义爬虫名称 name = "crawldemo" # 容许爬取的域名,但不包含 start_urls 中的连接 allowed_domains = ["58.com"] # 起始爬取连接 start_urls = [ "https://bj.58.com/chuzu/?PGTID=0d100000-0038-e441-0c8a-adeb346199d8&ClickID=2" ] def download(self, response, fName): with open(fName + ".html", 'wb') as f: f.write(response.body) # response 是返回抓取后的对象 def parse(self, response): # 下载北京租房页面到本地,便于分析 self.download(response, "北京租房")
经过执行命令启动爬虫,指定爬虫名字:
scrapy crawl crawldemo
当咱们有多个爬虫时,能够经过 scrapy list
获取全部的爬虫名。
开发过程当中固然也能够用 mian 函数在编辑器中启动:
if __name__ == '__main__': name = YtaoSpider.name cmd = 'scrapy crawl {0} '.format(name) cmdline.execute(cmd.split())
这时将在咱们启动的目录中下载生成咱们爬取的页面。
上面咱们只爬取到了第一页,可是咱们实际抓取数据过程当中,一定会涉及到分页,因此观察到该网站的分页是将最后一页有展现出来(58最多只展现前七十页的数据),如图。
从下图观察到分页的 html 部分代码。
接下来经过 Xpath 和正则匹配获取最后一页的页码。
def pageNum(self, response): # 获取分页的 html 代码块 page_ele = response.xpath("//li[@id='pager_wrap']/div[@class='pager']") # 经过正则获取含有页码数字的文本 num_eles = re.findall(r">\d+<", page_ele.extract()[0].strip()) # 找出最大的一个 count = 0 for num_ele in num_eles: num_ele = str(num_ele).replace(">", "").replace("<", "") num = int(num_ele) if num > count: count = num return count
经过对租房连接进行分析,能够看出不一样页码的连接为https://bj.58.com/chuzu/pn
+num
这里的num
表明页码,咱们进行不一样的页码抓取时,只需更换页码便可,parse 函数可更改成:
# 爬虫连接,不含页码 target_url = "https://bj.58.com/chuzu/pn" def parse(self, response): print("url: ", response.url) num = self.pageNum(response) # 开始页面原本就是第一页,因此在遍历页面时,过滤掉第一页 p = 1 while p < num: p += 1 try: # 拼接下一页连接 url = self.target_url + str(p) # 进行抓取下一页 yield Request(url, callback=self.parse) except BaseException as e: logging.error(e) print("爬取数据异常:", url)
执行后,打印出的信息如图:
由于爬虫是异步抓取,因此咱们的打印出来的并不是有序数据。 上面所介绍的是经过获取最后一页的页码进行遍历抓取,可是有些网站没有最后一页的页码,这时咱们能够经过下一页来判断当前页是否为最后一页,若是不是,就获取下一页所携带的连接进行爬取。
这里咱们就获取标题,面积,位置,小区,及价格信息,咱们须要先在 item 中建立这些字段,闲话少说,上代码。
# 避免取xpath解析数据时索引越界 def xpath_extract(self, selector, index): if len(selector.extract()) > index: return selector.extract()[index].strip() return "" def setData(self, response): items = [] houses = response.xpath("//ul[@class='house-list']/li[@class='house-cell']") for house in houses: item = YtaocrawlItem() # 标题 item["title"] = self.xpath_extract(house.xpath("div[@class='des']/h2/a/text()"), 0) # 面积 item["room"] = self.xpath_extract(house.xpath("div[@class='des']/p[@class='room']/text()"), 0) # 位置 item["position"] = self.xpath_extract(house.xpath("div[@class='des']/p[@class='infor']/a/text()"), 0) # 小区 item["quarters"] = self.xpath_extract(house.xpath("div[@class='des']/p[@class='infor']/a/text()"), 1) money = self.xpath_extract(house.xpath("div[@class='list-li-right']/div[@class='money']/b/text()"), 0) unit = self.xpath_extract(house.xpath("div[@class='list-li-right']/div[@class='money']/text()"), 1) # 价格 item["price"] = money+unit items.append(item) return items def parse(self, response): items = self.setData(response) for item in items: yield item # 接着上面的翻页操做 .....
至此,咱们以获取咱们想要的数据,经过打印 parse 中的 item 可看到结果。
咱们已抓取到页面的数据,接下来就是将数据入库,这里咱们以 MySQL 存储为例,数据量大的状况,建议使用使用其它存储产品。 首先咱们先在 settings.py 配置文件中设置 ITEM_PIPELINES 属性,指定 Pipeline 处理类。
ITEM_PIPELINES = { # 值越小,优先级调用越高 'ytaoCrawl.pipelines.YtaocrawlPipeline': 300, }
在 YtaocrawlPipeline 类中处理数据持久化,这里 MySQL 封装工具类 mysqlUtils 代码可在 github 中查看。 经过再 YtaoSpider#parse 中使用 yield 将数据传输到 YtaocrawlPipeline#process_item 中进行处理。
class YtaocrawlPipeline(object): def process_item(self, item, spider): table = "crawl" item["id"] = str(uuid.uuid1()) # 若是当前爬取信息的连接在库中有存在,那么就删除旧的再保存新的 list = select(str.format("select * from {0} WHERE url = '{1}'", table, item["url"])) if len(list) > 0: for o in list: delete_by_id(o[0], table) insert(item, table) return item
在数据库中,能够看到成功抓取到数据并入库。
既然有数据爬虫的需求,那么就必定有反扒措施,就当前爬虫案例进行一下分析。
经过上面数据库数据的图,能够看到该数据中存在乱码
,经过查看数据乱码规律,能够定位在数字进行了加密。
同时,经过打印数据能够看到\xa0
字符,这个(表明空白符)在 ASCII 字符 0x20~0x7e 范围,可知是转换为了 ASCII 编码。
由于知道是字体加密,因此在下载的页面查看font-family
字体时,发现有以下图所示代码:
看到这个fangchan-secret
字体比较可疑了,它是在js中动态生成的字体,且以 base64 存储,将如下字体进行解码操做。
if __name__ == '__main__': secret = "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" # 将字体文件编码转换为 UTF-8 编码的字节对象 bytes = secret.encode(encoding='UTF-8') # base64位解码 decodebytes = base64.decodebytes(bytes) # 利用 decodebytes 初始化 BytesIO,而后使用 TTFont 解析字体库 font = TTFont(BytesIO(decodebytes)) # 字体的映射关系 font_map = font['cmap'].tables[0].ttFont.tables['cmap'].tables[0].cmap print(font_map)
经过将 fontTools 库的 TTFont 将字体进行解析,都到以下字体映射结果:
{ 38006: 'glyph00007', 38287: 'glyph00005', 39228: 'glyph00006', 39499: 'glyph00003', 40506: 'glyph00010', 40611: 'glyph00001', 40804: 'glyph00009', 40850: 'glyph00004', 40868: 'glyph00002', 40869: 'glyph00008' }
恰好十个映射,对应的 0~9 的数量,可是查找相应规律,1~9 后,出现了个 10,那么这里对应的数字究竟是一个怎么样的规律呢?还有上面映射对应的 key 不是16进制的 ASCII 码,而是一个纯数字,是否是多是十进制的码呢? 接下来验证咱们的设想,将页面上获取的十六进制的码转换成十进制的码,而后去匹配映射中的数据,发现映射的值的非零数字部分恰好比页面上对应的数字字符大 1 ,可知,真正的值须要咱们在映射值中减 1。 代码整理后
def decrypt(self, response, code): secret = re.findall("charset=utf-8;base64,(.*?)'\)", response.text)[0] code = self.secretfont(code, secret) return code def secretfont(self, code, secret): # 将字体文件编码转换为 UTF-8 编码的字节对象 bytes = secret.encode(encoding='UTF-8') # base64位解码 decodebytes = base64.decodebytes(bytes) # 利用 decodebytes 初始化 BytesIO,而后使用 TTFont 解析字体库 font = TTFont(BytesIO(decodebytes)) # 字体的映射关系 font_map = font['cmap'].tables[0].ttFont.tables['cmap'].tables[0].cmap chars = [] for char in code: # 将每一个字符转换成十进制的 ASCII 码 decode = ord(char) # 若是映射关系中存在 ASCII 的 key,那么这个字符就有对应的字体 if decode in font_map: # 获取映射的值 val = font_map[decode] # 根据规律,获取数字部分,再减1获得真正的值 char = int(re.findall("\d+", val)[0]) - 1 chars.append(char) return "".join(map(lambda s:str(s), chars))
如今,咱们将全部爬取的数据进行解密处理,再查看数据:
上图中,进行解密后,完美解决数据乱码!
验证码通常分为两类,一类是刚开始进入时,必须输入验证码的,一类是频繁请求后,须要验证码验证再继续接下来的请求。 对于第一种来讲,就必须破解它的验证码才能继续,第二种来讲,除了破解验证码,还可使用代理进行绕过验证。 对于封禁IP的反爬,一样可以使用代理进行绕过。好比仍是使用上面的网址爬虫,当它们识别到我多是爬虫时,就会使用验证码进行拦截,以下图:
接下来,咱们使用随机 User-Agent 和代理IP进行绕行。 先设置 settings.USER_AGENT,注意PC端和移动端不要混合设置的 User-Agent,不然你会爬取数据会异常,由于不一样端的页面不一样:
USER_AGENT = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.10 Safari/537.36", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", # ...... ]
在请求中设置随机 User-Agent 中间件
class RandomUserAgentMiddleware(object): def __init__(self, agents): self.agent = agents @classmethod def from_crawler(cls, crawler): return cls( agents=crawler.settings.get('USER_AGENT') ) def process_request(self, request, spider): # 随机获取设置中的一个 User-Agent request.headers.setdefault('User-Agent', random.choice(self.agent))
设置动态IP中间件
class ProxyIPMiddleware(object): def __init__(self, ip=''): self.ip = ip def process_request(self, request, spider): # 若是当前的地址重定向到了验证码地址,就使用代理ip进行从新请求 if self.ban_url(request.url): # 获取被重定向的地址 redirect_urls = request.meta.get("redirect_urls")[0] # 将当前重定向到验证码的地址改成原始请求地址 request._set_url(redirect_urls) # 设置动态代理,这里在线上通常使用接口动态生成代理 request.meta["proxy"] = "http://%s" % (self.proxy_ip()) def ban_url(self, url): # settings中设置的验证码或被禁止的页面连接,当遇到该连接时,爬虫会进行绕行重爬 dic = settings.BAN_URLS # 验证当前请求地址是否为验证码地址 for d in dic: if url.find(d) != -1: return True return False # 代理动态生成的 ip:port def proxy_ip(self): # 模拟动态生成代理地址 ips = [ "127.0.0.1:8888", "127.0.0.1:8889", ] return random.choice(ips); def process_response(self, request, response, spider): # 若是不是成功响应,则从新爬虫 if response.status != 200: logging.error("失败响应: "+ str(response.status)) return request return response
最后在 settings 配置文件中开启这些中间件。
DOWNLOADER_MIDDLEWARES = { 'ytaoCrawl.middlewares.RandomUserAgentMiddleware': 500, 'ytaoCrawl.middlewares.ProxyIPMiddleware': 501, 'ytaoCrawl.middlewares.YtaocrawlDownloaderMiddleware': 543, }
如今为止,设置随机 User-Agent 和动态IP绕行已完成。
使用 scrapyd 部署爬虫项目,能够对爬虫进行远程管理,如启动,关闭,日志调用等等。 部署前,咱们得先安装 scrapyd ,使用命令:
pip install scrapyd
安装成功后,能够看到该版本为 1.2.1
。
部署后,咱们还须要一个客户端进行访问,这里就须要一个 scrapyd-client 客户端:
pip install scrapyd-client
修改 scrapy.cfg 文件
[settings] default = ytaoCrawl.settings [deploy:localytao] url = http://localhost:6800/ project = ytaoCrawl # deploy 可批量部署
启动 scrapyd:
scrapyd
若是是 Windows,要先在X:\xx\Scripts
下建立scrapyd-deploy.bat
文件
@echo off "X:\xx\python.exe" "X:\xx\Scripts\scrapyd-deploy" %1 %2
项目部署到 Scrapyd 服务上:
scrapyd-deploy localytao -p ytaoCrawl
远程启动 curl http://localhost:6800/schedule.json -d project=ytaoCrawl -d spider=ytaoSpider
执行启动后,能够在http://localhost:6800/
中查看爬虫执行状态,以及日志
除了启动可远程调用外,同时 Scrapyd 还提供了较丰富的 API:
curl http://localhost:6800/daemonstatus.json
curl http://localhost:6800/cancel.json -d project=projectName -d job=jobId
curl http://localhost:6800/listprojects.json
curl http://localhost:6800/delproject.json -d project=projectName
curl http://localhost:6800/listspiders.json?project=projectName
curl http://localhost:6800/listversions.json?project=projectName
curl http://localhost:6800/delversion.json -d project=projectName -d version=versionName
更多详情 https://scrapyd.readthedocs.io/en/stable/api.html
本文篇幅有限,剖析过程当中不能面面俱到,有些网站的反爬比较棘手的,只要咱们一一分析,都能找到破解的办法,还有眼睛看到的数据并不必定是你拿到的数据,好比有些网站的html渲染都是动态的,就须要咱们去处理好这些信息。当你走进crawler的世界,你就会发现,其实挺有意思的。最后,但愿你们不要面向监狱爬虫,数据千万条,遵纪守法第一条。
我的博客: https://ytao.top
个人公众号 ytao