智能决策上手系列教程索引
前面三篇文章介绍了如何利用 Headers 模拟浏览器请求, 如何嵌套 For 循环抓取二级页面. 但针对的都是 html 文件数据, 这一篇我们来看一下另外一种情况的数据以及更加复杂的 Headers 模拟.
案例是拉勾网 (一个招聘网站) 抓取某个公司全部招聘信息, 然后分析中大型人工智能公司的人才需求分布情况.
这次我们使用 Anaconda 的 Jupyter Notebook.
Anaconda 安装教程
1. 理解页面
打开这个页面 https://www.lagou.com/gongsi/j94.HTML , 这是思必驰科技 (一家专注于人工智能语音技术的科技公司) 在拉勾网的全部招聘职位列表.
思必驰招聘职位
我们可以看到共有 47 个招聘职位. 但是, 如果我们[右击 - 查看网页源代码] , 然后[Ctrl+F] 搜索第一个职位的名称 "运维技术专家" 却什么也搜不到, 实际上整个页面只有 600 行左右, 并没有包含任何职位信息.
数据不在请求的 HTML 文件里面, 数据在哪?
这几年的网站很多都采用了类似游戏的模式: 你打开游戏软件的时候, 本机电脑里面没有任何玩家信息, 但是游戏软件启动后会向服务器请求数据(而不是 HTML 文件), 拿到这些数据之后, 游戏软件就把各种在线玩家数据显示在屏幕上, 让你能够看到他们.
换成网页就是: 你刚打开网页的时候, 请求的 HTML 文件没有数据, 但是网页在浏览器运行之后, 网页自己就会向服务器请求数据, 网页拿到数据之后, 它就会把各种数据填充到页面上, 你就看到了这些数据,-- 但这些数据并不是像以前那样直接写在 HTML 文件里的.
动态填充数据页面流程
这些能够动态请求数据和填充数据的代码就是 HTML 网页内运行的 JavaScript 脚本代码, 它们可以做各种事情, 尤其善于玩弄数据.
JS(JavaScript)从服务器获取的数据大多是 JSON 格式的, 类似下面这种对象(Python 里面也叫 dict 字典), 也有 xml 格式的, 这里暂时用不到就不介绍了.
- data={
- 'title':'内容标题',
- 'text':'文字内容'
- }
这个格式看上去比 HTML 一堆尖括号标记看上去舒服多了. 但如何拿到这个数据呢?
2. 理解数据请求 Request
我们知道 Elements 面板显示了所有标记元素, 而 Network 面板显示了所有浏览器发出的请求 Request, 既然 JS 是向服务器发出请求的, 那么就一定会在 Network 面板留下痕迹.
还是刚才的页面 https://www.lagou.com/gongsi/j94.HTML ,[右键 - 检查] 切换到 Network 面板, 点击红色小按钮清空, 然后点击上面的第 2 页按钮, 查看 Network 里面的变化.
Network 查看 JS 的 xhr 请求
我们注意到 searchPosition.JSON 这行, 它的类型 (Type) 是 xhr, 数据请求都是这个类型的.
点击 searchPosition.JSON 可以看到这个请求的详细信息.
Headers 详细信息
和之前的稍有不同, 它没有 Parameters 数据 (因为地址栏没有? aaa=xxx&bbb=yyy 这类结尾了), 但是多了 Form Data 表单数据, 其实和 Parameters 作用相同, 就是向服务器说明你要哪个公司(companyId) 的数据, 第几页 (pageNo), 每页多少个职位(pageSize) 等等.
再点击上面的[preview] 预览, 可以看到这个请求实际获得了什么数据:
数据结构预览
如图, 小三角一路点下去, 就能看到这个数据实际和页面展示的职位列表是一一对应的. 所以我们只要拿到这个数据就 OK 了!
- # 单元 1
- url='https://www.lagou.com/gongsi/searchPosition.JSON'
- # 单元 2
- import requests
- jsonData=requests.get(url)
- print(jsonData.text)
- # 单元 1
- url='https://www.lagou.com/gongsi/searchPosition.JSON'
- params={
- 'companyId': '94',
- 'positionFirstType': '全部',
- 'schoolJob': 'false',
- 'pageNo': '2',
- 'pageSize': '10'
- }
- headers='''
- POST /gongsi/searchPosition.JSON HTTP/1.1
- Host: www.lagou.com
- Connection: keep-alive
- Origin: https://www.lagou.com
- X-Anit-Forge-Code: 38405859
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) ApplewebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8
- Accept: application/JSON, text/JavaScript, */*; q=0.01
- X-Requested-With: XMLHttpRequest
- X-Anit-Forge-Token: fcd0cae2-af8a-44b7-ae08-6cc103677fc1
- Referer: https://www.lagou.com/gongsi/j94.HTML
- Accept-Encoding: gzip, deflate, br
- Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
- Cookie: JSESSIONID=ABAAABAAAGFRGDA8929AE8AEDDF675B0A416152D50F1155; user_trace_token=20180914214240-a4f27a86-ee75-49d4-a447-7d7ec6386510; _ga=GA1.2.764376373.1536932562; LGUID=20180914214241-0d64224c-b824-11e8-b93f-6544005c3644; WEBTJ-ID=20180917170602-165e6c78d78209-0f57b51c336360b-3461790f-1296000-165e6c78d7953; __utmc=14951595; __utmz=14951595.1537175176.1.1.utmcsr=m_cf_cpt_sogou_pc|utmccn=(not set)|utmcmd=(not set); X_HTTP_TOKEN=b53ce1f559f492d4aa675d08aaffa8d93; _putrc=67FE3A6CCEBE7074123F83D1B170EADC; login=true; hasDeliver=0; index_location_city=全国; unick=拉勾用户5537; showExpriedIndex=1; showExpriedCompanyHome=1; showExpriedMyPublish=1; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf677e6=1536922564,1537493466; TG-TRACK-CODE=hpage_code; _gid=GA1.2.969240417.1537831173; gate_login_token=2b25e668e5c44f984fa699aa1142cccd6a9c3d914111e874bf297af1b325c383; __utma=14951595.764376373.1536932562.1537589263.1537831174.12; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1537831174; LGRID=20180925071933-4b7c4ce8-c3330-11e8-bb1c-5254005c3644
- '''
- 注意, 不要直接复制使用上面的 headers 代码, 其中的信息涉及到我的个人隐私, 所以都被修改过了, 不能正常使用. 必须自己复制你的浏览器里面 searchPosition.JSON 的真实 Request Headers
- 5. 把 headers 转化为字典对象
- headers 是一长串字符, 不符合 Python 要使用的字典对象格式 {'key':'value'} 的格式, 我们必须转化它一下.
- 你可以像处理 params 那样手工加引号加逗号, 也可以使用下面这个代码实现自动转化, 有兴趣的话可以参考代码里的注释理解, 或者不管什么意思直接使用也行.
- # 单元 1.5
- def str2obj(s,s1=';',s2='='):
- li=s.split(s1)
- res={}
- for kv in li:
- li2=kv.split(s2)
- if len(li2)>1:
- res[li2[0]]=li2[1]
- return res
- headers=str2obj(headers,'\n',':')
- print(headers)
- 把这个放在紧跟单元 1 后面, 然后全部运行, 可以看到输出的结果大致如下:
- 转化后的 header
- 6. 重新发送请求
- 这次我们模拟浏览器, 携带我们复制来的 Headers 和 Form Data 数据重新发送请求, 查看输出结果:
- # 单元 2
- import requests
- jsonData=requests.get(url,params=params,headers=headers)
- print(jsonData.text)
- 我们运行全部代码, 可以看到正常输出的结果数据:
- 获取数据成功
- 7. 解析 JSON 数据
- JSON 数据格式其实和我们一直用的字典对象几乎是一样的, 类似这样:
- zidian={
- 'a':'1',
- 'b':{
- 'b1':'2-1',
- 'b2':'2-2'
- }
- }
- JSON 数据和字典对象都是可以一层层嵌套的(上面 b1 就是嵌套在 b 对象里面的). 如果我们要获取 b2 的值就可以 print(zidian['b']['b2']), 它会输出'2-1'
- 我们可以用下面的代码把刚才 Request 获得的很多 JSON 数据整齐的显示出来:
- import JSON
- import requests
- jsonData=requests.get(url,params=params,headers=headers)
- data=JSON.loads(jsonData.text)
- print(JSON.dumps(data,indent=2,ensure_ascii=False))
- 这里我们 import 引入了 JSON 功能模块, 然后使用 data=JSON.loads(jsonData.text)的 loads 方法把 Request 获得的字符串数据转换为正式的 JSON 对象格式, dumps 方法就是把 JSON 对象再变为字符串输出. 是的, loads 和 dumps 是相反的功能, 但是我们的 dumps 加了 indent=2,ensure_ascii=False 就能让输出的字符串显示的很整齐了, 如下图:
- 整齐显示的 JSON 对象
- 这样, 我们就可以从图中的层级一层层找到需要的数据信息了, 比如 data['content']['data']['page']['result']就是我们需要的职位的列表对象, 我们可以用 for 循环输出这个列表的每一项:
- import JSON
- import requests
- jsonData=requests.get(url,params=params,headers=headers)
- data=JSON.loads(jsonData.text)
- #print(JSON.dumps(data,indent=2,ensure_ascii=False))
- jobs=data['content']['data']['page']['result']
- for job in jobs:
- print(job['positionName'])
- 得到的结果是:
- 输出职位名称
- 8. 输出数据到 Excel
- 我们只要针对每个 job 进行详细的处理, 就可以输出更多内容了:
- import JSON
- import requests
- import time
- hud=['职位','薪酬','学历','经验']
- print('\t'.join(hud))
- for i in range(1,6):
- params['pageNo']=i
- jsonData=requests.get(url,params=params,headers=headers)
- data=JSON.loads(jsonData.text)
- jobs=data['content']['data']['page']['result']
- for job in jobs:
- jobli=[]
- jobli.append(job['positionName'])
- jobli.append(job['salary'])
- jobli.append(job['education'])
- jobli.append(job['workYear'])
- print('\t'.join(jobli))
- time.sleep(1)
- 从浏览器可以看到总共有 47 个职位, 每页 10 个共 5 页, 所以这里都抓取了:
- 最终输出数据
- 直接鼠标选中, 然后复制, 打开 Excel 表格新建, 选择足够大区域, 右键, 选择性粘贴, 选择 Unicode, 就能得到数据表格了.
- 10. 抓取二级职位详情页面
- 最后附上抓取职位详情页面的代码, 综合了我们这几节前面使用的很多内容, 仅供参考和理解:
- #cell-1
- url='https://www.lagou.com/gongsi/searchPosition.JSON'
- params={
- 'companyId': '94',
- 'positionFirstType': '全部',
- 'schoolJob': 'true',
- 'pageNo': '1',
- 'pageSize': '10'
- }
- headers='''
- POST /gongsi/searchPosition.JSON HTTP/1.1
- Host: www.lagou.com
- ...
- LGRID=20180925071933-4b7c4ce8-c050-11e8-bb5c-5254005c3644
- '''jobheaders='''
- Accept: text/HTML,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
- ...
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
- '''
- #cell-2
- def str2obj(s,s1=';',s2='='):
- li=s.split(s1)
- res={}
- for kv in li:
- li2=kv.split(s2)
- if len(li2)>1:
- res[li2[0]]=li2[1]
- return res
- headers=str2obj(headers,'\n',':')
- jobheaders=str2obj(jobheaders,'\n',':')
- #cell-3
- import JSON
- import requests
- import time
- from bs4 import BeautifulSoup
- hud=['页数','职位','薪酬','学历','经验','描述']
- def getJobs(compId=94,school='true',pageCount=1):
- for i in range(1,1+pageCount):
- params['pageNo']=str(i)
- params['companyId']=compId
- params['schoolJob']=school
- params['pageNo']=i
- jsonData=requests.get(url,params=params,headers=headers)
- data=JSON.loads(jsonData.text)
- #print(JSON.dumps(data,indent=2,ensure_ascii=False))
- jobs=data['content']['data']['page']['result']
- for job in jobs:
- jobli=[str(i)]
- jobli.append(job['positionName'])
- jobli.append(job['salary'])
- jobli.append(job['education'])
- jobli.append(job['workYear'])
- #请求二级详情页面
- pid=job['positionId']
- joburl='https://www.lagou.com/jobs/'+str(pid)+'.HTML'
- jobhtml=requests.get(joburl,headers=jobheaders)
- jobsoup= BeautifulSoup(jobhtml.text, 'HTML.parser')
- desc=jobsoup.find('dd','job_bt').div.text
- desc=desc.replace('\n','')
- jobli.append(desc)
- time.sleep(1)
- print('\t'.join(jobli))
- time.sleep(1)
- #cell-4
- print('\t'.join(hud))
- getJobs(94,'false',5)
来源: http://www.jianshu.com/p/9de3be54abc1