数据入库之MongoDB(案例二:爬取拉勾)

来自CloudWiki
跳转至: 导航搜索

Python爬虫(入门+进阶) DC学院

本节课程的内容是MongoDB数据库及其界面化工具RoboMongo的安装和基本使用,并且通过爬取拉勾的例子讲授如何通过pymongo包把爬取到的数据存储在MongoDB数据库中。

MongoDB

什么是MongoDB

MongoDB是一个高性能,开源,无模式的文档型数据库

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成

MongoDB相关的安装

Windows的安装方法:

小歪老师在自己的知乎专栏[MongoDB及可视化工具的安装]中给出了详细的MongoDB数据库、可视化工具RoboMongo和MongoDB的PyCharm插件——Mongo Plugin的安装步骤和方法,可按照步骤安装并测试连接

Python用于操作MongoDB的第三方库pymongo安装:

pip install pymongo


Mac OS的安装方法

参考Mac OSX 平台安装 MongoDB安装,可视化工具RoboMongo安装方法与Windows平台大致相同。

MongoDB的PyCharm插件——Mongo Plugin安装: Preferences——Plugins——Mongo Plugin,安装完成后重启PyCharm可发现右侧有Mongo Explorer

Python用于操作MongoDB的第三方库pymongo安装

 pip install pymongo

测试连接

首先需要使用以下方法在终端启动MongoDB

cd /usr/local/mongodb/bin
sudo ./mongod


然后在PyCharm右侧的Mongo Explorer连接localhost:27017即可


Linux系统的安装方法

wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel62-4.0.0.tgz

tar xf mongodb-linux-x86_64-rhel62-4.0.0.tgz

mv mongodb-linux-x86_64-rhel62-4.0.0 /usr/local/mongod

echo "export PATH=$PATH:/usr/local/mongod/bin" >> /etc/profile

source /etc/profile

mkdir -p /data/db #创建暑假目录

mongod

MongoDB管理命令

如果你需要进入MongoDB后台管理,你需要先打开mongodb装目录的下的bin目录,然后执行mongo命令文件。

MongoDB Shell是MongoDB自带的交互式Javascript shell,用来对MongoDB进行操作和管理的交互式环境。

当你进入mongoDB后台后,它默认会链接到 test 文档(数据库):

cd /usr/local/mongod/bin
./mongo

MongoDB在Python中的基本使用

通过一个简单的例子展示使用pymongo连接MongoDB数据库,并插入数据

#! /usr/bin/env python
# -*- coding:utf-8 -*-
from pymongo import MongoClient
client = MongoClient()
db = client.test #连接test数据库,没有则自动创建
my_set = db.set #使用set集合,没有则自动创建
my_set.insert({'name':'Vinie','age':24})#插入一条数据

插入的数据可在MongoDB的test数据库的set集合中找到

实战环节

爬取拉勾网有关“爬虫”的职位信息,并把爬取的数据存储在MongoDB数据库中

  1. 首先前往拉勾网“爬虫”职位相关页面
  2. 确定网页的加载方式是JavaScript加载
  3. 通过谷歌浏览器开发者工具分析和寻找网页的真实请求,确定真实数据在position.Ajax开头的链接里,请求方式是POST
  4. 使用requests的post方法获取数据,发现并没有返回想要的数据,说明需要加上headers
  5. 加上headers的’Cookie’,’User-Agent’,’Referer’等信息,成功返回数据
  6. 再把返回的对应数据存储到MongoDB

准备工作

pip3 install bs4

pip3 install lxml

打印城市名

第一步:模拟发送请求,尝试抓取数据。带上请求头和参数,发送 POST 请求,只会得到如下的结果:

{"status":false,"msg":"您操作太频繁,请稍后再访问","clientIp":"xxx.xxx.xxx.xxx","state":2402}

这说明拉勾网的反爬措施是对 Cookies 有要求的。Cookies 是从浏览器端生成的,但是要从网站的 JavaScript 代码分析出 Cookies 的生成方式,无疑是一件很复杂的事情。这个问题先暂且按下,先考虑把拉勾网支持的所有城市的城市名拉下来,可以找到城市列表页的链接是:https://www.lagou.com/jobs/allCity.html ,只需要带上含有 Accept, Referer 和 User-Agent 三个 Key 的请求头发送 GET 请求,就可以拿到页面的 HTML 代码,使用 BeautifulSoup 解析页面即可获取城市列表

import requests
from bs4 import BeautifulSoup

headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Referer': 'https://www.lagou.com/jobs/list_%E8%BF%90%E7%BB%B4?city=%E6%88%90%E9%83%BD&cl=false&fromSearch=true&labelWords=&suginput=',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'
}
url = "https://www.lagou.com/jobs/allCity.html?keyword=数据分析&px=default&city=全国"
s = requests.Session()
rsp = s.get(url, headers=headers, verify=False)
doc = rsp.text
soup = BeautifulSoup(doc, 'lxml')
alphabet_list = soup.find_all("ul", class_="city_list")
city_list = []
for alpha in alphabet_list:
    cities = alpha.find_all("li")
    for city in cities:
        city_name = city.a.string
        city_list.append(city_name)
print(city_list)

结果:

['安阳', '安庆', '鞍山', '澳门特别行政区', '安康', '北京', '保定', '蚌埠', '北海                                                                                        ', '包头', '滨州', '巴音郭楞', '保山', '亳州', '宝鸡', '巴中', '巴彦淖尔', '白城                                                                                        ', '本溪', '成都', '长沙', '重庆', '常州', '长春', '沧州', '常德', '赤峰', '潮州                                                                                        ', '承德', '郴州', '滁州', '朝阳', '东莞', '大连', '德州', '东营', '德阳', '大庆                                                                                        ', '达州', '大同', '大理', '儋州', '德宏', '丹东', '定西', '迪庆', '恩施', '鄂尔                                                                                        多斯', '鄂州', '佛山', '福州', '阜阳', '抚州', '抚顺', '防城港', '广州', '贵阳',                                                                                         '桂林', '赣州', '广安', '贵港', '广元', '甘孜藏族自治州', '杭州', '合肥', '哈尔                                                                                        滨', '惠州', '海口', '呼和浩特', '湖州', '淮安', '邯郸', '衡阳', '河源', '黄石',                                                                                         '海外', '怀化', '衡水', '菏泽', '黄山', '淮北', '黄冈', '淮南', '红河', '呼伦贝                                                                                        尔', '汉中', '鹤壁', '哈密', '贺州', '河池', '鹤岗', '济南', '金华', '嘉兴', '江                                                                                        门', '济宁', '揭阳', '荆州', '吉林', '晋中', '九江', '吉安', '焦作', '景德镇', '                                                                                        荆门', '锦州', '酒泉', '晋城', '金昌', '鸡西', '佳木斯', '昆明', '开封', '克拉玛                                                                                        依', '喀什', '廊坊', '兰州', '洛阳', '临沂', '柳州', '拉萨', '聊城', '临汾', '龙                                                                                        岩', '乐山', '吕梁', '泸州', '漯河', '连云港', '六安', '丽水', '丽江', '娄底', '                                                                                        六盘水', '凉山彝族自治州', '莱芜', '辽阳', '林芝', '绵阳', '马鞍山', '茂名', '梅                                                                                        州', '眉山', '牡丹江', '南京', '宁波', '南昌', '南宁', '南通', '南充', '南阳', '                                                                                        宁德', '南平', '内江', '莆田', '濮阳', '攀枝花', '萍乡', '盘锦', '平顶山', '青岛                                                                                        ', '泉州', '秦皇岛', '清远', '衢州', '庆阳', '钦州', '曲靖', '黔西南', '齐齐哈尔                                                                                        ', '黔南', '日照', '深圳', '上海', '苏州', '沈阳', '石家庄', '绍兴', '汕头', '宿                                                                                        迁', '三亚', '商丘', '汕尾', '上饶', '韶关', '十堰', '宿州', '邵阳', '三明', '松                                                                                        原', '三门峡', '遂宁', '三沙', '四平', '绥化', '商洛', '朔州', '天津', '太原', '                                                                                        唐山', '台州', '泰州', '泰安', '台北', '天门', '天水', '铜川', '吐鲁番', '铜仁',                                                                                         '通化', '铁岭', '通辽', '武汉', '无锡', '温州', '乌鲁木齐', '潍坊', '芜湖', '威                                                                                        海', '渭南', '武威', '梧州', '吴忠', '文山', '乌兰察布', '西安', '厦门', '徐州',                                                                                         '新乡', '西宁', '邢台', '香港特别行政区', '咸阳', '湘潭', '襄阳', '许昌', '信阳                                                                                        ', '孝感', '咸宁', '宣城', '湘西土家族苗族自治州', '忻州', '新余', '烟台', '银川                                                                                        ', '盐城', '扬州', '宜昌', '岳阳', '榆林', '运城', '宜宾', '玉溪', '宜春', '云浮                                                                                        ', '永州', '阳江', '营口', '玉林', '益阳', '延安', '雅安', '延边', '伊犁', '鹰潭                                                                                        ', '阳泉', '郑州', '珠海', '中山', '湛江', '株洲', '镇江', '肇庆', '淄博', '张家                                                                                        口', '漳州', '遵义', '舟山', '驻马店', '周口', '长治', '枣庄', '自贡', '资阳', '                                                                                        张家界', '昭通']

搜索城市页面

为了拿到某个城市的搜索结果,并验证是否有30页(如果有的话,还需要顺便拿到该城市下面的行政区列表),我们还需要继续获取搜索结果的城市页面。以北京为例,使用上面的方法访问链接:

 https://www.lagou.com/jobs/list_%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90?city=%E5%8C%97%E4%BA%AC&cl=false&fromSearch=true&labelWords=&suginput=

,确实可以拿到北京的搜索结果首页,在该页面上也能找到结果的总页数30以及北京所有的行政区划。

import requests
from bs4 import BeautifulSoup

headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Referer': 'https://www.lagou.com/jobs/list_%E8%BF%90%E7%BB%B4?city=%E6%88%90%E9%83%BD&cl=false&fromSearch=true&labelWords=&sugin                               put=',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.3                               6'
}
city_url = "https://www.lagou.com/jobs/list_数据分析?city=北京&cl=false&fromSearch=true&labelWords=&suginput="
#result='''
ss = requests.session()
# 获取cookies
rsp = ss.get(city_url, headers=headers, verify=False)
# 获取总页数
doc = rsp.text
soup = BeautifulSoup(doc, 'lxml')
num_soup = soup.find("span", class_="totalNum")
total_num = int(num_soup.string)
#'''
# 如果总页数为30,就抓取行政区
#print("hello")
total_num =30
if total_num == 30:
        district_list = [ ]
        #查找北京市内所有的行政区域,代码中特征为满足"class":"contents","data-type":"district"的div标签
        district_soup = soup.find("div",attrs={"class":"contents","data-type":"district"}).find_all("a")[1:]
        #[1:]的含义是把取数组里的第2位~最后一位,因为第1位是不限
        for district in district_soup:
                district_name = district.string
                district_list.append(district_name)
        print(district_list)
        

发起POST请求

在第二步中获取到了城市首页的 Cookies,尝试使用这个 Session 发送 POST 请求抓取 AJAX 的接口:

import requests
from bs4 import BeautifulSoup

headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Referer': 'https://www.lagou.com/jobs/list_%E8%BF%90%E7%BB%B4?city=%E6%88%90%E9%83%BD&cl=false&fromSearch=true&labelWords=&suginput=',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'
}
city_url = "https://www.lagou.com/jobs/list_数据分析?city=北京&cl=false&fromSearch=true&labelWords=&suginput="
#result='''
ss = requests.session()
# 获取cookies

city_api_url = "https://www.lagou.com/jobs/positionAjax.json?city=北京&needAddtionalResult=false"

r = ss.post(city_api_url, headers=headers, verify=False)
print(r.json())

的确拿到了 JSON 格式的结果。但是使用这个方法尝试抓取几页之后,依然会得到"您操作太频繁,请稍后再访问"的结果,在尝试使用代理 IP 以及抓取间隔控制等多种方法后,仍然是这样。问题到底出在哪里?为什么还是会被反爬?

通过对网络交互数据的仔细追踪,不难发现每次向 API 发送 POST 请求之后,网站会自动发送一条 GET 请求:

Bd3-15.png

这条请求并没有什么实质性的作用,所带参数只不过是上一条 POST 请求拿回来结果的公司 ID,故猜想这个 GET 请求是用来确认或者更新 Cookies 的,那么为了确保 Cookies 持续有效,我们只需在每次发送 POST 请求之前,使用 GET 请求刷新 Cookies 即可。为了验证想法,尝试使用下面的代码抓取北京30页的结果:

# encoding: utf-8
import requests
import time
import random
from urllib.request import quote

def get_data(page_num, keyword):
    if page_num == 1:
        first = "true"
    else:
        first = "false"
    data = {
        "first": first,
        "pn": page_num,
        "kd": keyword
    }
    return data

def main():
    kd = "数据分析"
    for i in range(1, 31):
        # 随即休眠5到10秒
        time.sleep(random.randint(5, 10))
        ss = requests.session()
        # 先使用 GET 获取 Cookies
        rsp = ss.get(city_url, headers=headers, verify=False)
        # 检测返回的页面是否出错,如果有要求登录账号,就重新发起请求
        doc = rsp.text
        if "网络出错啦" in doc:
            print("retry...")
            rsp = ss.get(city_url, headers=headers, verify=False)
            doc = rsp.text
        # 再使用 POST 获取数据
        r = ss.post(city_api_url, headers=headers, data=get_data(i, kd), verify=False)
        print(r.json())

if __name__ == '__main__':
    city_url = "https://www.lagou.com/jobs/list_{}?city={}&cl=false&fromSearch=true&labelWords=&suginput=".format(quote("数据分析"), quote("北京"))
    city_api_url = "https://www.lagou.com/jobs/positionAjax.json?city=北京&needAddtionalResult=false"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
        "Referer": city_url,
        "Content-Type": "application/x-www-form-urlencoded;charset = UTF-8"
    }
    main()

在尝试的过程中,也曾加入代理 IP 地址进行测试,但是基于某种原因(可能是代理 IP 已经被污染了)不成功,故而放弃;而如果只用本地 IP ,一段时间内抓取太频繁亦会被对方检测出来,严重情况下可能会被封 IP,所以这里每次 GET 请求之前加入了随机的等待时间(5~10 s);之后发现连续请求若干次之后,GET 请求的页面又会被要求登录账号,这种情况下我们只需要检查返回页面,并重新发起一次 GET 请求就可以了。

在研究反反爬的套路时,我们要大胆假设,小心求证,一步步逼近反爬工程师挖下的坑,架好板子走过去或者找好岔路绕过去。从最终的代码来看,既无法使用代理也无法加入多线程,导致抓取速度很慢,只能说对方网站为了确保自己的数据不被反反爬,确实牺牲了很多性能和网络资源(包括自己的和对方的) -_-!!!

最终的抓取代码加入了对行政区的抓取,并对抓取动作进行一定的整合,有兴趣的同学可以参见 Github 代码。

完整代码:爬取同时,存储进MongoDB

# encoding: utf-8
import requests
import time
import random
from urllib.request import quote
from pymongo import MongoClient
import time
def get_data(page_num, keyword):
    if page_num == 1:
        first = "true"
    else:
        first = "false"
    data = {
        "first": first,
        "pn": page_num,
        "kd": keyword
    }
    return data

def main():
    kd = "数据分析"
    for i in range(1, 31):
        # 随即休眠5到10秒
        time.sleep(random.randint(5, 10))
        ss = requests.session()
        # 先使用 GET 获取 Cookies
        rsp = ss.get(city_url, headers=headers, verify=False)

        # 检测返回的页面是否出错,如果有要求登录账号,就重新发起请求
        doc = rsp.text
        if "网络出错啦" in doc:
            print("retry...")
            rsp = ss.get(city_url, headers=headers, verify=False)
            doc = rsp.text
        # 再使用 POST 获取数据
        r = ss.post(city_api_url, headers=headers, data=get_data(i, kd), verify=False)
        job_json = r.json()['content']['positionResult']['result']
        lagou.insert(job_json)
        print(job_json)

if __name__ == '__main__':
    city_url = "https://www.lagou.com/jobs/list_{}?city={}&cl=false&fromSearch=true&labelWords=&suginput=".format(quote("数据分析"), quote("北京
"))
    city_api_url = "https://www.lagou.com/jobs/positionAjax.json?city=北京&needAddtionalResult=false"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
        "Referer": city_url,
        "Content-Type": "application/x-www-form-urlencoded;charset = UTF-8"
    }
    client = MongoClient()
    db = client.lagou
    lagou = db.test #创建test集合
    main()
 

课后作业

  • 安装MongoDB、RoboMongo可视化工具、Mongo Plugin插件、pymongo包
  • 尝试爬取多个职位数据
  • 改进爬虫,爬取详情页数据

补充资料

1. MongoDB

       前往MongoDB 3.4 中文文档,学习更多关于MongoDB语法的知识
       可以在Github的PyMongo中学习到更多pymongo包的用法

2. POST请求

       阅读更加复杂的 POST 请求,学习关于requests的POST请求方法的用法

3. fake-useragent包

       前往fake-useragent 0.1.7官方文档学习更多fake-useragent的用法

4. 爬取微博热点评论

ModeHeader是很好的测试httpheader,支持在线修改http header的小工具。我们可以用它来模拟移动端的Request Headers。下面将为大家展示如何添加此扩展程序。

使用Chrome浏览器,访问扩展程序:chrome://extensions/,点击获取更多扩展程序。(注意这里需要翻墙)

Alt text 搜索ModHeader

Alt text

点击添加至Chrome

Alt text

右上角点开它,添加名字和User-Agent

Alt text

接下来就可以用它去爬取移动端的应用数据了。 下面是爬取手机端的微博热评数据,大家可以学习一下。

import pymongo
import requests
import pandas as pd

class weibo:
    def __init__(self):
        # 初始化爬取条数
        self.count = 0

        #实例化mongo client连接对象
        # client = pymongo.MongoClient('127.0.0.1', 27001)
        # self.coll = client['spider']['weibo']
    def write_mongo(self,item):
        '''将item写入数据库中'''
        self.coll.update({'id': count},{'$set': item}, upsert=True)
        print ('已入库:', self.count, '条。')
        self.count += 1

    def write_file(self, item):
        '''将item写入到文件中'''

        # 写入文件
        df = pd.DataFrame.from_dict(item,orient='index')
        df.to_csv('./weibo.csv')

    def get_info(self):
        comment = {}
        for i in range(1,11):
            url = 'https://m.weibo.cn/single/rcList?format=cards&id=4160547165300149&type=comment&hot=1&page={}'.format(i)
            #创建Headers,cookie和user_agent需要换成自己的
            headers = {
                'Cookie':'_T_WM=a759aca8fd5fd4e20cbf876930d6ee91; H5_wentry=H5; backURL=https%3A%2F%2Flogin.sina.com.cn%2Fsso%2Flogin.php%3Furl%3Dhttps%3A%2F%2Fm.weibo.cn%2F%26_rand%3D1509006100.3267%26gateway%3D1%26service%3Dsinawap%26entry%3Dsinawap%26useticket%3D1%26returntype%3DMETA%26sudaref%3D%26_client_version%3D0.6.26; SCF=Aoy6fc80dcpiX-IQbgAI9tL-MQ91qWXakLPbJUlZgUbpCQaX3yqGci9_RUtAh6HGAhJukUqNqn5Cw28oR5h00t8.; SUB=_2A2509dQuDeRhGeNN61UT-S7LyDWIHXVUGfxmrDV6PUJbktBeLWnykW0W2p_Y0olCbF4MlLyQ_ooUjClbyg..; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9W5TIMn1sFAPOAnJGH2adIyi5JpX5K-hUgL.Fo-0ehME1K5Ne0.2dJLoI7feIgUQUGUDUs4LMNSk; SUHB=0rr76wcx9aInWq; H5:PWA:UID=1',
                'Host':'m.weibo.cn',
                'qq':'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',
                'referer':'https://m.weibo.cn/single/rcList?id=4160547165300149&type=comment&hot=1&tab=1',
                'Upgrade-Insecure-Requests':'1',
                'User-Agent':'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
            }
            res = requests.get(url,headers = headers,verify = False).json()
            #选取名字和评论,生成字典
            for index in range(len(res[-1]['card_group'])):
                comment[res[-1]['card_group'][index]['user']['screen_name']] = res[-1]['card_group'][index]['text']

                self.count += 1
                print('已爬取:', self.count, '条。')
        #调用写文件函数,写进文件
        self.write_file(comment)
        #调用数据库函数,写进MongoDB数据库
#       self.write_mongo(comment)
if __name__ == '__main__':
    # 实例化weibo类,生成wb实例
    wb = weibo()
    # 调用get_info方法
    wb.get_info()

参考文档:

[1] https://www.cnblogs.com/zhangxinqi/p/9242687.html

[2] https://mp.weixin.qq.com/s?__biz=MzI5MTM2Njk3Mg==&mid=2247483901&idx=1&sn=852359e0bf9eafe4db6cc0604eb9abf4&chksm=ec10f0aedb6779b8324142b737bd7ebd86256ecd9176944e43dc0876290d559017695ee7acf8&mpshare=1&scene=23&srcid=0707eHGwZjFACjwiyEDVXXHL#rd