从零开始学习python爬虫(一):获取58同城二手房信息

时间:2024-02-18 15:00:09

  大数据时代下,编写爬虫程序已经成为信息收集的必备技能;python在数据挖掘方面具有极大优势且简单易学,是新手入坑爬虫程序编写的极佳语言。

  由于在校期间本人主要应用java和matlab进行数据挖掘,因此借助刚入职的学习期,简单开发了一个最基本的python爬虫获取58同城二手房信息,一来是自己借此练手python和爬虫开发,二来是爬取的数据可以实际用于自己之后的学习,也算是做个小小的预研吧。在两个工作日的开发后,终于实现了用自己开发的爬虫在58同城上爬取了1500条本地二手房数据。这篇随笔将介绍这个简单pyhon爬虫的实现过程、及需要学习的知识和搭建的环境,希望能给同样刚接触python和爬虫开发的学习者们一点点参考,存在不足请大家批评指正。


  开发前准备:

   必备知识:python基本语法、web前端基本知识、数据库基本知识

   要编写爬虫程序,必要的python语法知识还是不可少的。python简单易学,对于用过其他开发语言的开发者来说能很快上手(当然,只是简单上手)。然后由于我们要从网页上爬取,所以对web前端相关知识还是需要大致了解下;最后我们要将爬取的数据存入数据库(本次开发采用oracle),所以数据库基本知识也是不可少的。

       搭建环境:python、oracle、pycharm

  python和oracle的按照这里不再过多叙述,网上的资料已经有非常之多。本次开发python选择的是2.7版本,开发工具我尝试了pycharm和jupyter notebook,可以说是各有千秋,实际开发中使用pycharm的人应该还是更多的,同样安装配置过程在网上也可以轻松找到。需要注意的是oracle字符集的清晰设定可以避免之后的开发出现乱码的坑。

  整体思路:

  我们的目标是从网页中爬取需要的资料,所以整个开发思路可以按此递进展开:1.获得某一页面的文本数据,截取需要的字段;2.将截取的字段进行整理,存放在数据库中;3.实现爬取多页或者某一列表的多条信息;4.组合完成一个可以按页依次爬取58同城二手房信息,并存入oracle的简单爬虫。思路明确后,接下来便介对其进行实现:


  第一步,先安装好必要的模块

  在cmd下进入python的scripts文件夹,按照BeautifulSoup4、requests这两个模块。这两个模块都是python开发爬虫的利器,BeautifulSoup4用于从网页抓取数据,而requests则用于进行HTTP连接。具体操作如下。

cd C:\Python27\Scripts
pip install BeautifulSoup4
pip install requests


      除此之外,还有pandas、sqlalchemy 、re需要安装(同样也是pip install),pandas用来整理数据、sqlalchemy 用来访问数据库、re用来实现正则表达式。安装好后,我们new一个python文件,在文件开头导入。

import requests
import pandas
import re
from bs4 import BeautifulSoup
from sqlalchemy import create_engine

  设定os编码可以避免之后数据库的乱码

import os
os.environ[\'NLS_LANG\'] = \'SIMPLIFIED CHINESE_CHINA.UTF8\'

  第二步,获得某一页面的文本数据

  在爬取前,我们应该先明确爬取的原理。在google chrome打开开发者工具,访问58同城二手房页码,查看网页源代码,我们可以发现我们需要的字段都包含在各个标签里。 

·············   

     我们的可以先将这些html代码获取下来,再进行筛选取得我们需要的字段。这里我定义了一个函数,传入网页的url,采用requests进行连接,然后在用BeautifulSoup获取到文本的信息。

new_message_total = get_message_total(\'url\')
df = pandas.DataFrame(new_message_total)
def get_message_total(message_uri):
res = requests.get(message_uri)
res.encoding = \'utf-8\'
soup = BeautifulSoup(res.text, \'html.parser\')

  通过观察,我们发现每一套房屋信息中我们需要的大部分字段都包含在一个list-info的class里,于是我们便可以利用soup.select获取所有list-info的信息,具体的用法可以去看看BeautifulSoup的相关文档,我们输出了soup.select(\'.list-info\')的数组长度,即每一页出现房屋信息的次数;同时定义一个message_total为之后的存储预留空间。

def get_message_total(message_uri):
res = requests.get(message_uri)
res.encoding = \'utf-8\'
soup = BeautifulSoup(res.text, \'html.parser\')
num = len(soup.select(\'.list-info\'))
message_total = []
print(num)

   第三步,将截取的字段进行整理

  这里建立了一个循环去获取每个soup.select(\'.list-info\')数组的元素,即每个房屋信息,如soup.select(\'.list-info\')[0]即第一条房屋信息。接下来再用soup.select获取每个需要的字段,然后对字段进行了简单处理(字段去掉空格换行,面积价格去单位),这里我几乎没有用到正则表达式。取得的字段我们存放在一个result里,在每次循环结束一并存放到message_total中,在获取全部的数据后我们将其作为函数的返回。

for i in range(0, num):
message = soup.select(\'.list-info\')[i]
pricetext = soup.select(\'.price\')[i]
title = message.select(\'a\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
housetype = message.select(\'span\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
areatext = message.select(\'span\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
area=re.findall(r"\d+\.?\d*",areatext)[0]
orientation = message.select(\'span\')[2].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
floor = message.select(\'span\')[3].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
estate = message.select(\'a\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
region = message.select(\'a\')[2].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
position = message.select(\'a\')[3].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
totalpricetext = pricetext.select(\'p\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
totalprice=re.findall(r"\d+\.?\d*",totalpricetext)[0]
unitpricetext= pricetext.select(\'p\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
unitprice=re.findall(r"\d+\.?\d*",unitpricetext)[0]

result = {}
result[\'title\'] = title
result[\'housetype\'] = housetype
result[\'area\'] = area
result[\'orientation\'] = orientation
result[\'floor\'] = floor
result[\'estate\'] = estate
result[\'region\'] = region
result[\'position\'] = position
result[\'totalprice\'] = totalprice
result[\'unitprice\'] = unitprice
message_total.append(result)
return message_total

  第四步,将数据存入数据库中       我们获取返回的数组后,采用pandas将其进行整理,然后建立与数据库的连接,采用to_sql方法将整个dateframe存入数据库。to_sql方法有多参数可以设定,很重要的一个是if_exists,如果设为\'append\'则表示表如果存在则进行插入操作。

new_message_total = get_message_total(url\')
df = pandas.DataFrame(new_message_total)
print(df)

db_engine=create_engine(\'oracle://name:password@url/databasename\')
df.to_sql(name=tablename, con=db_engine, if_exists=\'append\', index=False)

   第五步,实现实现爬取多页或者某一列表的多条信息

  多页爬取的实现比较简单,将url中关于页码的参数进行赋值(在这里为/ershoufang/pn,pn1、pn2....表示第一页第二页)存放到一个数组中,接着循环调用get_message_total函数去查询整个url数组即可。而要爬取某一页中的每个房产信息的详细情况则(多层次的爬取)比较麻烦。一个方法是采用soup.select获取相应<a>标签中记录房产信息详情的地址,然后将其url记录,用和多页爬取相同的方法进行爬取。第二种方法是在开发者工具-Network-js中进行寻找可能存放url的js文件,我们发现这边的query_list中涵盖了多个房产信息详情的地址,我们将其获取去掉开头的函数名和括号及结尾的括号,获得一个json字符串,再将json字符串中每个url获取出来进行记录,然后在用get_message_total函数去查询整个url数组。

res = requests.get(
\'http://api.fang.58.com/aurora/query_list.............\')
res.encoding = \'utf-8\'
num = 0
jd = json.loads(res.text.lstrip(\'jQuery112408648859133127407_1528161168333(\').rstrip(\')\\'\'))
for ent in jd[\'data\'][\'houseList\']:
print num
num = num + 1

res = requests.get(jd[\'data\'][\'houseList\'][0][\'url\'])
res.encoding = \'utf-8\'
soup = BeautifulSoup(res.text, \'html.parser\')
print(soup.text)

      第六步,整合

  这里我只将爬取单个页面的源码全部粘贴出来,不将所有代码贴出的原因是同城的反爬虫机制使得目前的多列表查询或者多页查询形同虚设,且思路明确后多页查询和多列表查询的第一种方法只是在重复之前的工作,而多列表查询第二种方法的关键代码也已经贴出。有需要的同学改下数据库参数便可用于爬取58二手房信息,将中间的获取规则部分进行修改后(可以用正则表达式,会简单很多)也可以用于其他场景。

# encoding: utf-8
import requests
import pandas
import re
from bs4 import BeautifulSoup
from sqlalchemy import create_engine
import os
os.environ[\'NLS_LANG\'] = \'SIMPLIFIED CHINESE_CHINA.UTF8\'

def get_message_total(message_uri):
res = requests.get(message_uri)
res.encoding = \'utf-8\'
soup = BeautifulSoup(res.text, \'html.parser\')
num = len(soup.select(\'.list-info\'))
message_total = []
print(num)

for i in range(0, num):
message = soup.select(\'.list-info\')[i]
pricetext = soup.select(\'.price\')[i]
title = message.select(\'a\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
housetype = message.select(\'span\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
areatext = message.select(\'span\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
area=re.findall(r"\d+\.?\d*",areatext)[0]
orientation = message.select(\'span\')[2].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
floor = message.select(\'span\')[3].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
estate = message.select(\'a\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
region = message.select(\'a\')[2].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
position = message.select(\'a\')[3].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
totalpricetext = pricetext.select(\'p\')[0].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
totalprice=re.findall(r"\d+\.?\d*",totalpricetext)[0]
unitpricetext= pricetext.select(\'p\')[1].text.replace(\'\t\', \'\').replace(\'\n\', \'\').replace(\' \', \'\')
unitprice=re.findall(r"\d+\.?\d*",unitpricetext)[0]

result = {}
result[\'title\'] = title
result[\'housetype\'] = housetype
result[\'area\'] = area
result[\'orientation\'] = orientation
result[\'floor\'] = floor
result[\'estate\'] = estate
result[\'region\'] = region
result[\'position\'] = position
result[\'totalprice\'] = totalprice
result[\'unitprice\'] = unitprice
message_total.append(result)
return message_total

new_message_total = get_message_total(url\')
df = pandas.DataFrame(new_message_total)
print(df)

db_engine=create_engine(\'oracle://name:password@url/databasename\')
df.to_sql(name=tablename, con=db_engine, if_exists=\'append\', index=False)

 

   下一步:目前的爬虫程序还只是一个简单的小demo,首要解决的还是各大网站的反爬虫限制问题。比如58同城爬取数据中,每爬取几十条数据便会要求输验证码,这使得我们的多页爬取失去了意义(每爬取一两页就要手动输入验证码),而安居客则会直接检测爬虫程序(之后可能会采用伪装成浏览器的方法进行解决)。因为我们不可能像一些平台那样采用ip池的方法避免ip限制,所以还需加深对反反爬虫的研究。(如果接下来还有需要及空闲时间,下一篇随笔可能就是相关内容,但是自己感觉大概率要鸽...)此外,多线程爬取、采集数据的增量处理等都是需要深入研究的内容。

   感悟:从熟悉python语法到开发完成花了两个工作日,很大一部分时间都用在解决各种坑上(搭建环境、更改数据库字符集等),这段时间的学习我确实感受到python的简单好用,但是我目前对python还停留在初步认识的阶段,很多要用到的知识点都是遇到了再去查;因为时间紧凑,目前只是按照之前的编程思路运用python这个工具将自己的目标实现,对python的特性还有待探索,如果能善用正则表达式和一些高效的模块,想必开发能简单不少。虽然本人很早就接触博客园,但是这是我在博客园的第一篇文章,主要也是为了记录自己刚刚开始的工作生涯中对各种技术的探索,同时也希望能和大家一起交流学习,为同样的萌新提供点参考,当然我更欢迎大家提出意见及批评指正。最后,本人爬取的数据仅用于个人的学习,绝不用于商业用途。