Scrapy入门

174 阅读7分钟

Scrapy入门

最近因为工作上有数据采集相关内容,特地研究了一下Scrapy相关使用。总结了一下Scrapy的一些入门和实践经验。

考虑到想在武汉上车,因此就去采集了贝壳找房上的一些数据。具体的字段可以存到数据库或者文件中。这里是我定义的一些字段。

data_structure.png

最终采集完数据之后,一般会对数据进行一些分析。如果会sql的直接写语句处理就好了,更友好的方式是用可视化展示出来,这样更直观。因此这里采用了pandas来处理数据,用pyecharts展示数据。这里是一个简单的效果图。

bar_loupan.png

Scrapy安装

对于使用过Python的同学而言,这些内容可以忽略。

pip install scrapy

使用scrapy创建工程,这个脚本很方便,可以一键生成模板。

scrapy genspider [你的工程名称]

最终的工程结构是这样的:

project_structrure.png

Scrapy使用

生成的文件都有自己的作用。items.py中定义数据字段。

import scrapy
class BeikeWuhanItem(scrapy.Item):
    name = scrapy.Field()
    lp_type = scrapy.Field()
    image = scrapy.Field()
    block = scrapy.Field()
    address = scrapy.Field()
    room_type = scrapy.Field()
    spec = scrapy.Field()
    ava_price = scrapy.Field()
    total_range = scrapy.Field()
    tags = scrapy.Field()
    detail_url = scrapy.Field()
    create_time = scrapy.Field()

可以发现这里的字段和在数据库中定义的一模一样。

settings.py中定义一些设置属性。具体如何使用可以点击注释中的链接详细研究,scrapy的文档写的很全,对新手很友好。

# 下载间隔 10秒一次 太频繁了网站会给你封掉 要么用代理池
DOWNLOAD_DELAY = 10
# 后面的数字代表顺序
DOWNLOADER_MIDDLEWARES = {
   'BeikeSpider.middlewares.IPProxyMiddleware': 100,
   'BeikeSpider.middlewares.BeikespiderDownloaderMiddleware': 543,
}

pipelines.py中定义对采集到的数据的处理。


class BeikespiderPipeline:
    def __init__(self):
        self.connection = pymysql.connect(
            host=MYSQL_HOST,
            user=MYSQL_USER,
            password=MYSQL_PASS,
            db=MYSQL_DB,
            charset="utf8mb4",
            cursorclass=pymysql.cursors.DictCursor,
        )

    def process_item(self, item, spider):
        if isinstance(item, BeikeWuhanItem):
            self.insert(item)
        return item

    def insert(self, item):
        cursor = self.connection.cursor()
        keys = item.keys()
        values = tuple(item.values())
        fields = ",".join(keys)
        temp = ",".join(["%s"] * len(keys))
        sql = "INSERT INTO wh_loupan (%s) VALUES (%s)" % (fields, temp)
        cursor.execute(sql, values)
        self.connection.commit()

这里的逻辑是将采集到的数据写入mysql,当然也可以写文件到excel也是没问题的。需要注意的是,这个pipeline写完了一定要在settings中配置一下。

最后就是自己的爬取逻辑了,beike_wuhan.py这个文件不会被自动生成出来,需要自己定义。这里是爬虫的入口。 start_requests方法是父类中的定义,这个方法中定义爬虫的名称,以及从哪个url出发。最后通过yield关键字将这个请求交给Scrapy的下载器处理,其中还有一个callback函数,表示处理完请求后要做什么操作。 parse函数也是父类的,通过参数response可以看出在这里做返回数据的解析。

在解析页面数据的时候使用xpath是比较快捷的。不过对于眼神不好的还是考虑别的选择器,因为层级多了容易看花。至于如何使用xpath这里不细说,不过浏览器中打开调试器能直接选择元素复制其xpath,非常好用。提取出元素信息将其值填充到BeikeWuhanItem中,这个过程还是比较繁琐的,需要点耐心。item构建完依然通过yield出去。这里的item就传递到了pipline中了,可以直接被process_item函数处理。

因为爬取的不仅仅只有这一页数据,还要继续接着爬。这里涉及到了翻页动作。贝壳页面中虽然有下一页按钮,但是这个按钮的渲染是通过JS动态生成的,直接通过请求地址是不会返回这个按钮的元素的。这点值得注意一下,最简答的办法是查看网页源代码,如果能找到这个元素说明通过xpath可以取到,否则怎么都取不到。因此这里不能去通过判断是不是有下一页按钮去做翻页,而是得通过具体的总数去判断到底要不要继续爬。观察到如果到了最后一页,页面元素上的总条数就变成了0,这时候就不用继续翻页了。翻页的url也是通过yield传递给下载器,类似for循环,一直去下载解析。 这里也发现了贝壳的bug,显示有80页,其实到了40页就没数据了。导致我开始计算总页数的时候以为总页数就那么多,实际上的bug在有的页显示10条,有的页显示20条,不讲武德。

p_room_type = re.compile('[0-9]+')
p_room_spec = re.compile('[\s\S]*㎡$')

root_url = 'https://wh.fang.ke.com/'
default_schema = 'https:'

class BeikeWuhanSpider(Spider):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.curr_page = 1
        self.url_template = "https://wh.fang.ke.com/loupan/nhs1pg%s"

    name = "beike_wuhan"
    allowed_domains = ["wh.fang.ke.com"]

    def start_requests(self):
        # 武汉楼盘的首页
        baseurl = "https://wh.fang.ke.com/loupan/nhs1pg1"

        UserAgents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 Edg/92.0.902.62',
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36']
        UserAgent = random.choice(UserAgents)

        headers = {'User-Agent': UserAgent}
        yield Request(
            url=baseurl,
            headers=headers,
            callback=self.parse)

    def parse(self, response):
        # next_page = response.xpath('/html/body/div[7]/div[2]/')
        # if next_page is not None:
        #     next_page = response.urljoin(next_page)
        #     yield Request(next_page, callback=self.parse)
        # 每页的数量还不是固定的 有的是10个每页 有的是20个每页
        content = response.xpath('/html/body/div[6]/ul[2]/li')
        for item in content:
            image = item.xpath('./a/img/@src')[0].extract()
            name = item.xpath('./div[1]/div[1]/a/@title')[0].extract()
            detail_url = item.xpath('./div[1]/div[1]/a/@href')[0].extract()
            lp_type = item.xpath('./div[1]/div[1]/span[2]/text()').extract_first()
            address = item.xpath('./div[1]/a[1]/text()')[1].extract()
            block = ''
            if address:
                block = address.split('/')[0]
            room_type_texts = item.xpath('./div[1]/a[2]/span/text()').extract()
            room_types = []
            room_spec = ''
            for room_type_text in room_type_texts:
                if re.match(p_room_type, room_type_text):
                    room_types.append(room_type_text)
                if re.match(p_room_spec, room_type_text):
                    room_spec = room_type_text.split(' ')[1]

            room_type = '/'.join(room_types)
            t = item.xpath('./div[1]/div[3]/span/text()').getall()
            tags = ','.join(t)

            ava_price = item.xpath('./div[1]/div[4]/div[1]/span[1]/text()').extract_first()
            total = item.xpath('./div[1]/div[4]/div[2]/text()').extract_first()
            if not total:
                total = ''

            wuhan_item = BeikeWuhanItem()
            wuhan_item['name'] = name
            wuhan_item['lp_type'] = lp_type
            wuhan_item['image'] = default_schema + image
            wuhan_item['block'] = block
            wuhan_item['address'] = address
            wuhan_item['room_type'] = room_type
            wuhan_item['spec'] = room_spec
            wuhan_item['ava_price'] = ava_price
            wuhan_item['total_range'] = total
            wuhan_item['tags'] = tags
            wuhan_item['detail_url'] = root_url + detail_url
            wuhan_item['create_time'] = datetime.datetime.now()
            yield wuhan_item

        # 楼盘总数  如果为0了 那么说明没有下一页了
        count = response.xpath('/html/body/div[6]/div[2]/span[2]/text()').get()
        print('===================================================================', count)
        if int(count) > 0:
            self.curr_page += 1
        else:
            self.crawler.engine.close_spider(self, '任务完成')
        next_page = self.url_template % self.curr_page
        yield Request(next_page, callback=self.parse)

贝壳对频繁请求做了限制,因此最好使用代理池,否则很容易被禁IP,得苦哈哈去点击“我不是人机”的识别码才能解除限制。 到这里,基本上整个采集的环节都描述完了。接下来就是对数据进行处理和展示。

pandas数据处理

pandas也能读取数据库中的记录,几行代码就搞定。

import pandas as pd
from sqlalchemy import create_engine
import matplotlib.pyplot as plt
from pyecharts.charts import Bar

plt.rcParams['font.sans-serif'] = 'SimHei'
engine = create_engine('mysql+pymysql://root:123456@localhost:3306/beike_spider')
sql = ''' select * from wh_loupan; '''
# 将查到的数据 load到pd
df = pd.read_sql_query(sql, engine)

接着对数据进行过滤,因为我们采集的楼盘有很多类型,如写字楼,底商等等。目前我只关心住宅,因此通过这个操作实现过滤。

# 过滤掉其他的类型 只拿住宅做分析 同时只保留了4个列
houses = df.loc[df["lp_type"] == "住宅", ['id', 'name', 'ava_price', 'block']]

不过话说回来,干嘛不用sql直接写过滤条件呢?当然可以,但这里是为了展示一下pandas的使用。

接着对住宅的区域进行分组,看看这些区的住宅楼盘的分布情况。


# 通过区来做分组 这里返回的是一个series类型 索引为区,值为区下楼盘的数量 因此需要将对应的值取出来
group_num = houses.groupby("block").size()
index = []
val = []
for i in group_num.items():
    index.append(i[0])
    val.append(i[1])

bar = Bar()
bar.add_xaxis(index)
bar.add_yaxis("楼盘数", val)
# render 会生成本地 HTML 文件,默认会在当前目录生成 render.html 文件
# 也可以传入路径参数,如 bar.render("mycharts.html")
bar.render("./figures/楼盘分布.html")

最后生成图表,直观的呈现出来。

完整代码请参考 github.com/Mr-Vincent/…

总结

在很早以前都是使用request库直接请求网页,需要处理连接,解析,重试之类的业务无关的脏活累活。代码写的又多又丑,目前使用了scrapy后发现瞬间清爽很多,效率提升了不少。 在习惯了Java这种强类型的语言后,再去写Python这种弱类型的有时候会很抓狂,比如做除法运算,虽然你知道你的除数是数字,但是解释器告诉你这是字符类型,不给你除,还得手动转一下类型,太气人了。还有在使用类方法的时候,根本不知道怎么去传参,各种花里胡哨的参数列表,对于强类型语言使用者来说,编程思维转变过来还是需要点时间。

更多不精彩内容,关注公众号也不一定获取到

qrcode_for_gh_c3a95f61b3b1_258.jpg

参考资料

pandas doc scrapy tutorial 贝壳新房 代码地址