Python爬虫(7):多进程抓取拉钩网十万数据

由于拉钩网页面做了一些更新,之前的程序无法正常工作,本篇文章做一次更新。只更新一些程序和一些程序的实现方法。由于没有仔细修改,可能前后语言不通顺,大家谅解。


大家好,几天没有更新了。相信大家经过前两篇的练手爬虫,大家已经知道如何抓取一个简单网站。

这篇文章我们来抓取 拉钩网 的招聘信息。全部抓取大概十几万条全国招聘信息,并且保存数据库。

准备

安装Mongodb数据库

其实不是一定要使用MongoDB,大家完全可以使用MySQL或者Redis,全看大家喜好。这篇文章我们的例子是Mongodb,所以大家需要 下载 它。

Windows中。由于MongoDB默认的数据目录为C:\data\db,建议大家直接在安装的时候更改默认路径为C:\MongoDB.

然后创建如下目录文件:

C:\data\log\mongod.log  //用于存储数据库的日志
C:\data\db    //用于存储数据库数据

然后在C:\MongoDB文件夹下(安装 Mongodb 路径)创建配置文件mongod.cfg。并且在配置文件里写入以下配置:

systemLog:
 destination: file
    path: C:\data\log\mongod.log
storage:
    dbPath: C:\data\db

大家记住要打开文件后缀名,不然我们可能创建了一个mongod.cfg.txt文件。

最后我们需要打开管理员权限的 CMD 窗口,执行如下命令,安装数据库成服务:

"C:\mongodb\bin\mongod.exe" --config "C:\mongodb\mongod.cfg" --install

设置为服务后,需要在管理员权限打开的windows cmd窗口用服务的方式启动或停止MongoDB

net start mongodb    //启动mongodb服务
net stop mongodb     //关闭mongodb服务

好了,安装好Mongodb数据库后,我们需要安装PyMongo,它是MongoDBPython接口开发包。

pip install pymongo

开始

准备完成后,我们就开始浏览拉勾网。我们可以发现拉勾网所有的招聘职位都在左侧分类里。如图:

image

我们先获取首页HTML文件:

import requests
from bs4 import BeautifulSoup
from requests.exceptions import RequestException

url = 'https://www.lagou.com/'
# 获取页面源码函数
def get_page_resp(url):
    headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'}
    try:
        resp = requests.get(url, headers=headers)
        if resp.status_code == 200:
            return resp.text
        return None
    except RequestException:
        return None

soup = BeautifulSoup(get_page_resp(url), 'lxml')

然后我们打开开发者工具,找到招聘职业的位置。

image

大家还记得BeautifulSoup的CSS选择器吧,我们直接使用.select()方法获取标签信息。

all_positions = soup.select('div.menu_sub.dn > dl > dd > a')
print(all_positions)
print(len(all_positions))

输出结果:

[<a class="curr" href="https://www.lagou.com/zhaopin/Java/" data-lg-tj-cid="idnull" data-lg-tj-id="4O00" data-lg-tj-no="0101">Java</a>, <a class="curr" href="https://www.lagou.com/zhaopin/C%2B%2B/" data-lg-tj-cid="idnull" data-lg-tj-id="4O00" data-lg-tj-no="0102">C++</a>, # ... 省略部分 <a class="" href="https://www.lagou.com/zhaopin/fengxiankongzhizongjian/" data-lg-tj-cid="idnull" data-lg-tj-id="4U00" data-lg-tj-no="0404">风控总监</a>, <a class="" href="https://www.lagou.com/zhaopin/zongcaifuzongcai/" data-lg-tj-cid="idnull" data-lg-tj-id="4U00" data-lg-tj-no="0405">副总裁</a>] 
260

获取到所有职位标签的a标签后,我们只需要提取标签的href属性和标签内内容,就可以获得到职位的招聘链接和招聘职位的名称了。我们准备信息生成一个字典。方便我们后续程序的调用。

# 解析首页获得所有职位信息的函数
def parse_index():
    url = 'https://www.lagou.com/'
    soup = BeautifulSoup(get_html(url), 'lxml')
    all_positions = soup.select('div.menu_sub.dn > dl > dd > a')
    joburls = [i['href'] for i in all_positions]
    jobnames = [i.get_text() for i in all_positions]

    for joburl, jobname in zip(joburls, jobnames):
        data = {
            'url' : joburl,
            'name' : jobname
        }
        # 这里使用yield语法糖,不熟悉的同学自己查看资料哦
        yield data

这里我们用zip函数,同时迭代两个list。生成一个键值对。

接下来我们可以随意点击一个职位分类,分析招聘页面的信息。

分页

我们首先来分析下网站页数信息。经过我的观察,每个职位的招聘信息最多不超过 30 页。也就是说,我们只要从第 1 页循环到第 30 页,就可以得到所有招聘信息了。但是也可以看到有的职位招聘信息,页数并不到 30 页。以下图为例:

image

如果我们访问页面:https://www.lagou.com/zhaopin/Java/31/

也就是第 31 页。我们会得到 404 页面。所以我们需要在访问到404页面时进行过滤。

if resp.status_code == 404:
    pass

这样我们就可以放心的 30 页循环获得每一页招聘信息了。

我们的每一页url使用format拼接出来:

for page in (1, 31):
    link = '{}{}/'.format(url, str(page))

获取信息

image

我们可以看到上面划线的信息。这就是我们要抓取的信息了。

当然。抓取的方法千篇一律,我们可以使用find()find_all()

选择器,当然也可以使用 CSS选择器。但是 CSS选择器相对于前两者代码量稍微少一些。这里大家自己动手抓取,我们直接上代码供大家借鉴。

# 职位信息 
positions = soup.select('ul > li > div.list_item_top > div.position > div.p_top > a > h3') 
# 工作地址 
adds = soup.select('ul > li > div.list_item_top > div.position > div.p_top > a > span > em') 
# 发布时间 
publishs = soup.select('ul > li > div.list_item_top > div.position > div.p_top > span') 
# 薪资信息
moneys = soup.select('ul > li > div.list_item_top > div.position > div.p_bot > div > span') 
# 工作需求 
needs = soup.select('ul > li > div.list_item_top > div.position > div.p_bot > div') 
# 发布公司 
companys = soup.select('ul > li > div.list_item_top > div.company > div.company_name > a') 
tags = [] 
# 由于我发现有的招聘信息没有标签信息,if判断防止没有标签报错 
if soup.find('div', class_='li_b_l'): 
    # 招聘信息标签 
    tags = soup.select('ul > li > div.list_item_bot > div.li_b_l') 
# 公司福利 
fulis = soup.select('ul > li > div.list_item_bot > div.li_b_r')

获取到全部信息后,我们同样的把他们组成键值对字典。

for position,add,publish,money,need,company,tag,fuli in \
        zip(positions,adds,publishs,moneys,needs,companys,tags,fulis):
    data = {
        'position' : position.get_text(),
        'add' : add.get_text(),
        'publish' : publish.get_text(),
        'money' : money.get_text(),
        'need' : need.get_text().split('\n')[2],
        'company' : company.get_text(),
        'tag' : tag.get_text().replace('\n','-'),
        'fuli' : fuli.get_text()
    }

组成字典的目的是方便我们将全部信息保存到数据库。

保存数据库

保存数据库前我们需要配置数据库信息:

import pymongo

client = pymongo.MongoClient('localhost', 27017)
lagou = client['lagou']
url_list = lagou['url_list']

这里我们导入了pymongo库,并且与MongoDB建立连接,这里是默认连接本地的MongoDB数据。创建并选择一个数据库lagou,并在这个数据库中,创建一个table,即url_list。然后,我们进行数据的保存:

if url_list.insert_one(data):
    print('保存数据库成功', data)

如果保存成功,打印出成功信息。

多进程抓取

十万多条数据是不是抓取的有点慢,有办法,我们使用多进程同时抓取。由于Python的历史遗留问题,多线程在Python中始终是个美丽的梦。

from multiprocessing import Pool
def main(datas):
        url = data['url']
        print(url)
        mongo_table = data['name']
        # 因为有的职位是以'.'开头的,比如.Net,数据库表名不能以.开头
        if mongo_table[0] == '.':
            mongo_table = mongo_table[1:]
        # 我们把之前抓取职位所有招聘信息的程序整理为parse_link()函数
        # 这个函数接收职位url,页码,和数据库表名为参数
        parse_link(url, mongo_table)

我们把之前提取职位招聘信息的代码,写成一个函数,方便我们调用。这里的parse_link()就是这个函数,他就收职位的 url 和所有页数为参数。我们get_alllink_data()函数里面使用for循环 30 页的数据。然后这个作为主函数传给多进程内部调用。

if __name__ == '__main__':
    pool = Pool(processes=6)
    datas = (data for data in parse_index())
    pool.map(main, datas)
    pool.close() 
    pool.join()

这里是一个pool进程池,我们调用进程池的map方法.

map(func, iterable[,chunksize=None])

多进程Pool类中的map方法,与Python内置的map函数用法行为基本一致。它会使进程阻塞,直到返回结果。需要注意,虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。join()

方法等待子进程结束后再继续往下运行,通常用于进程间的同步.

针对反爬

如果大家就这样整理完代码,直接就开始抓取的话。相信在抓取的不久后就会出现程序中止不走了。我刚刚第一次中止后,我以为是网站限制了我的 ip。于是我做了如下改动。

import random
import time

user_agent_list = [
    'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    ' Mozilla/5.0 (Windows; U; Windows NT 5.2) Gecko/2008070208 Firefox/3.0.1',
    'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)'
]
user_agent = random.choice(user_agent_list)
headers  = {
    'User-Agent':user_agent,
    'Connection':'keep-alive'
}
proxy_list = [
    'http://140.224.76.21:808',
    'http://60.178.14.90:8081',
    'http://121.232.146.13:9000',
    ]
proxy_ip = random.choice(proxy_list)
proxies = {
    'http': proxy_ip,
    'https': proxy_ip,
}
resp = requests.get(url, headers=headers, proxies=proxies)
time.sleep(1)

这里我是直接在在网上找了一些免费的ip,还自己找了几个浏览器的user-agent。利用Python内置的random库,开始随机选择列表里的user-agentip。并且将参数传入requests内继续抓取。为了防止请求频率过快,我们设置每次请求结束停留一秒。然后我以为问题这么结束了。就开始继续抓取,但是在之前中断的位置突然有中断了。

于是,我在代码抓取信息位置添加了一句打印url的代码。我得到了中断爬取的url,然后我手动多点进去,发现了这个网页。当然出现这个情况并不是更换ip不可以,而是我们的ip太少了,一个ip可能仍然出现多次抓取的情况,后续我将会和大家一起写一个自己的ip代理池。

image

模拟登录

没错这是一个登录界面,不知道是不是这里的浏览器头,或者ip请求过多。如果继续增多ipuser_agent就不会出现这种情况。

但是如何面对这种需要登录才能持续爬取的网站呢,很简单,我们只要打开浏览器的开发者工具。登录自己的账号,在Network标签找一个请求,查看你的请求头部信息,找到自己的cookies。这个cookies就是你的登录信息了,我们需要将他和你的user-agent一样,添加到请求头就可以了。如果大家不明白这个cookies是什么,没关系,后面会有专门的讲解。

headers = {
    'Cookie':'user_trace_token=20170603115043-d0c257a054ee44f99177a3540d44dda1; LGUID=20170603115044-d1e2b4d1-480f-11e7-96cf-525400f775ce; JSESSIONID=ABAAABAAAGHAABHAA8050BE2E1D33E6C2A80E370FE9167B; _gat=1; PRE_UTM=; PRE_HOST=; PRE_SITE=; PRE_LAND=https%3A%2F%2Fwww.lagou.com%2F; index_location_city=%E5%85%A8%E5%9B%BD; login=false; unick=""; _putrc=""; _ga=GA1.2.922290439.1496461627; X_HTTP_TOKEN=3876430f68ebc0ae0b8fac6c9f163d45; _ga=GA1.3.922290439.1496461627; LGSID=20170720174323-df1d6e50-6d2f-11e7-ac93-5254005c3644; LGRID=20170720174450-12fc5214-6d30-11e7-b32f-525400f775ce; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1500541369; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1500543655',
    'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
}

好了我们继续抓取,发现这次程序并没有中止。~~~但是由于博主网速过慢,抓取太慢了,还没有全部抓取下来,不知道后面会不会出现问题。~~~

这次更新,我是完全抓取玩全站才过来的,没错我已经抓取了全部的信息。用时1412 .9008133411407 秒。

image

好了,说了这么多,基本也都说完了。

项目地址

对整理代码有疑惑的伙伴可以点击 这里

最后

这是我已经抓取的一部分。这里如果大家希望看到Mongodb数据库里的保存内容。

我们需要安装一个Mongodb可视化应用 robomango

安装没什么要说的,大家链接到我们的lagou数据库,就可以看到里面的数据了。

image

如果博主后面把全部数据抓下来后,可以和大家一起进行数据分析,分析Python招聘的一些信息啊什么的。大家加油。

谢谢阅读

本文作者:俞坤

本文链接:http://www.yukunweb.com/2017/6/python-spider-crawl-data-Multiprocess

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0许可协议。转载请注明出处!

返老还童
13 条评论
  1. author
    2018-09-06
    cai

    mongodb太不友好了,安装卸载安装卸载几十次都失败了

    • 2018-09-06

      俞坤回复 cai:

      我也不是很清楚这个问题,帮你谷歌了下,建议你将

      "C:\mongodb\bin\mongod.exe" --config "C:\mongodb\mongod.cfg" --install

      中的将"\"换成"/"

      或者看看配置文件总是不含有制表符TAB,将他换成空格试试看。QAQ~

  2. author
    2018-09-06
    cai

    您好,请问,我在执行这一步的时候总是出错,“最后我们需要打开管理员权限的 CMD 窗口,执行如下命令,安装数据库成服务:

    "C:\mongodb\bin\mongod.exe" --config "C:\mongodb\mongod.cfg" --install 设置为服务……” 运行指令后返回:Error parsing YAML config file: yaml-cpp: error at line 5, column 8: illegalmap value 这意思是不是配置文件有问题呀?我是按照您的步骤一样安装一样设置的呀。求解惑,翻遍百度都没有解决办法,试了一个上午。。。

    • 2018-09-07

      cai回复 cai:

      不怕您笑话,我昨天晚上回家,在笔记本Ubuntu双系统上装了mongodb,也失败了,我有毒

  3. author
    2018-07-06
    郝梦真

    博主你好,我想请问下,我下载了源代码,执行以后,用robomango可视化只有副总裁这一项,只有50条?为什么看不到其他的呢?谢谢你

    • 2018-07-07

      yukun回复 郝梦真:

      还以为是拉钩改版了,特意下载源码跑了一下。

      原来是自己代码有问题,真是惭愧,这么久没人说,哈哈。是一个代码行 yield 缩进问题,导致抓了首页所有分类却只能返回最后一个结果,我已经改了代码提交了,你重新跑源码,应该没问题了。

    • 2018-07-09

      郝梦真回复 yukun:

      嗯嗯,后来打印了下网址,只打印了副总裁这一个网址,yield跑出了循环,改正了缩进,在数据库了显示的完整啦!谢谢你

  4. author
    2018-01-16
    crawl knight

    楼主您好,又是我。您的文中:

    if name == 'main': pool = Pool(processes=6) pages = ([p for p in range(1, 31)]) pool.map(main,pages) pool.close() pool.join()

    这里的 pool.map(main,pages),main接收pages,这个pages是一个int类型吧?可是您pages = ([p for p in range(1, 31)])这里给他赋上的是一个列表类型额,这样传给main的时候不会有问题吗?

    • 2018-01-17

      俞坤回复 crawl knight:

      你应该了解一下map的用法

    • 2018-01-18

      crawl knight回复 俞坤:

      (捂脸)谢谢提醒。。。。。

  5. author
    2018-01-15
    crawl knight

    感谢楼主耐心解答,我想实现的功能是抓取淘宝某商品下的评论信息,具体是这样的:通过抓包已经得到评论信息的url,那么我就通过构造url,将全部的url放到一个列表里。

    编写提取信息函数以及保存到Excel的函数,编写main函数调用以上两个函数,然后开启多个进程同时对url列表内的url进行抓取。

    抓取回来的数据,通过打印输出显示,有很多重复的〒▽〒没有办法在评论里放代码可读性太差了,就不放代码了。我这两天再试试,照着您的思路(其实是半个抄袭)再重构一遍,灰常感谢~

  6. author
    2018-01-14
    crawl knight

    谢谢楼楼~,真的很详细,本人最近入坑爬虫,在多进程这一步卡着了,有个疑问始终在我脑海里:我理解的多进程应该是很多个进程同时对多个url进行爬取吧。那么多个进程是如何确保不会同时拿到同一个url的?看了好多博客,有提到什么消息队列,Redis,真的要那么复杂吗?(本人爬虫几乎零基础,python几乎零基础,编程几乎零基础。。)另外,期待楼主的文章更新哦~

    • 2018-01-15

      俞坤回复 crawl knight:

      有这种问题就要想办法实现出来,看看会不会出现多个进程拿到同一个URL的情况,比如说我们写一个循环,分别打印出每个进程每次循环的进程id,以及执行的任务,比如使用5个进程,一次循环是不是task1,task2,3,4,5。有没有出现执行相同任务的进程。至于消息队列是进程间通信的,比如一个进程生产一个数据,一个进程负责去消费掉这个数据,那就有进程间的数据传递。进程间通信是通过Queue、Pipes实现的,你可以了解了解。

已登录,注销 取消