网络数据抓取-JS动态生成数据-Python-requests爬虫

智能决策上手系列教程索引

前面三篇文章介绍了如何利用Headers模拟浏览器请求,如何嵌套For循环抓取二级页面。但针对的都是Html文件数据,这一篇我们来看一下另外一种情况的数据以及更加复杂的Headers模拟。

案例是拉勾网(一个招聘网站)抓取某个公司全部招聘信息,然后分析中大型人工智能公司的人才需求分布情况。

这次我们使用Anaconda的Jupyter Notebook。

Anaconda安装教程

1. 理解页面

打开这个页面,这是思必驰科技(一家专注于人工智能语音技术的科技公司)在拉勾网的全部招聘职位列表。

思必驰招聘职位

我们可以看到共有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面板留下痕迹。

还是刚才的页面,【右键-检查】切换到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了!

3. 发送数据请求

上面看到,我们需要的数据都在searchPosition.json这个Request请求里面,【右键-Copy-Copy link address】复制请求地址。

复制请求地址

打开Notebook,新建Python 3文件,粘贴过去。

#单元1
url='https://www.lagou.com/gongsi/searchPosition.json'

向这个地址发送请求:

#单元2
import requests
jsonData=requests.get(url)
print(jsonData.text)

全部运行后得到下图结果,我们的爬虫请求被服务器识别了!


直接发起数据请求失败

并不是所有数据请求都会被识别,拉勾网服务器做了这方面的检测,有些网站就没有检测机制,可以直接获取有效数据。

回顾上面截图的浏览器Request请求的Headers信息,实际上浏览器发送请求的时候还携带了很多Request headers(包含Cookie),以及Form data数据(对应我们以前提到过的Parameters信息)。

4. 添加params和headers

params就是Form Data,从浏览器的Network面板直接手工复制,然后修改成为Python的字典对象(就是大括号包含的一些属性数据),注意都要加上引号,每行结尾有逗号。
header可以用右键searchPosition.json然后【Copy-Copy Request headers】复制到,但是注意这个字符很多而且换行,所以要用三个单引号才能包括起来。

注意!这里我删除了其中一行Content-Length: 86,因为在发送Request请求的时候Python会自动计算生成Content-Length数值(不一定是86)。如果这里不删除就会导致重复引发错误。

修改单元1的代码:

#单元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%20set)|utmcmd=(not%20set); X_HTTP_TOKEN=b53ce1f559f492d4aa675d08aaffa8d93; _putrc=67FE3A6CCEBE7074123F83D1B170EADC; login=true; hasDeliver=0; index_location_city=%E5%85%A8%E5%9B%BD; unick=%E6%8B%89%E5%8B%BE%E7%94%A8%E6%88%B75537; 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
'''

这里的jobheader是给二级页面使用的。你必须复制自己浏览器https://www.lagou.com/jobs/5151679.html?source=pl&i=pl-6页面的Request请求5151679.html?source=pl&i=pl-6的信息header信息,我这里只是示意,不能直接复制使用。

#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',': ')

这里只是最后一行,也转化jobheaders对象。

#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) 

这里没有直接使用,而是def了一个函数getJobs,带有三个参数compId公司序号,school是否社招,pageCount一共有多少页。

#cell-4
print('\t'.join(hud))
getJobs(94,'false',5)

启动。

本篇小节

  • 页面可以不直接包含数据,而是通过运行JavaScript代码,从服务器重新获取数据,再填充到页面上。
  • 任何向服务器发起的请求都可以在Network面板找到信息,带了哪些参数params(Form Data),带了什么样的headers,等等
  • json数据和字典对象用起来一样,从Request获取的文本text数据需要用json.load转换一下,然后就可以用shuju['aa']['bb']的方法一层层找到我们需要的信息

智能决策上手系列教程索引

每个人的智能决策新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,573评论 18 139
  • 前端开发者丨http请求 https:www.rokub.com 前言见解有限, 如有描述不当之处, 请帮忙指出,...
    麋鹿_720a阅读 10,885评论 11 31
  • 声明:本文讲解的实战内容,均仅用于学习交流,请勿用于任何商业用途! 一、前言 强烈建议:请在电脑的陪同下,阅读本文...
    Bruce_Szh阅读 12,672评论 6 28
  • 多头翻转bar应具有的最小特征是收盘价高于开盘价,或者收盘价高于其终点,最佳多头反转bar具有下列特征的一个或者多...
    日光光阅读 328评论 0 1
  • 買了一條充電線⋯粉紅色的。 那不是女孩們喜歡的顏色嗎? 我回答⋯好像是! 可是⋯我覺得好看,我喜歡! 大家都在突顯...
    蔡振源阅读 198评论 0 3