想爬取一个有很多页的网站,但是我不知道这个网站的准确页数。请问 Python 中如何用循环来实现呢? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
solider245

想爬取一个有很多页的网站,但是我不知道这个网站的准确页数。请问 Python 中如何用循环来实现呢?

  •  
  •   solider245 2019 年 7 月 27 日 6131 次点击
    这是一个创建于 2463 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,假如网址是

    www.baidu.com/page_01 www.baidu.com/page_02 www.baidu.com/page_03 www.baidu.com/page_0{i} 

    字母 i 代表页数,以前我爬取的网站,i 的数字是明确的,一般是 100,200,或者 300 以内。 这个时候我可以用 range(1,300)这样生成循环数来搞定。

    现在有一个网站,这个自增数量太大,我应该如何用条件判断和循环来解决这个问题呢?

    41 条回复    2019-07-28 16:18:46 +08:00
    wzwwzw
        1
    wzwwzw  
       2019 年 7 月 27 日
    scrapy rule .
    xiaoming1992
        2
    xiaoming1992  
       2019 年 7 月 27 日
    设置一个尽可能大的值,循环过程中进行判断,没爬到希望的内容就跳出循环并通知你,你再人肉看看后面还有没有,再进行下一步操作?
    limuyan44
        3
    limuyan44  
       2019 年 7 月 27 日
    这种算最简单的爬取规则了吧你一直爬到没有不就好了。
    ranleng
        4
    ranleng  
       2019 年 7 月 27 日 via Android
    while true。
    然后 404 的时候 break 掉
    lihongjie0209
        6
    lihongjie0209  
       2019 年 7 月 27 日
    while 不行啊
    solider245
        7
    solider245  
    OP
       2019 年 7 月 27 日
    @ranleng #4 请问可以稍微写一个例子吗?
    solider245
        8
    solider245  
    OP
       2019 年 7 月 27 日
    @xiaoming1992 #2 设置最大值会遇到一种情况,就是以后当网站的页数超过你的最大值时,你的爬虫就要去更新了
    solider245
        9
    solider245  
    OP
       2019 年 7 月 27 日
    @wzwwzw #5 谢谢,我去看下
    xiaoming1992
        10
    xiaoming1992  
       2019 年 7 月 27 日 via Android
    本来就是啊,所以说尽可能大啊,比方说一百万,不够就设个三十亿(我怎么会说是我忘了 while true 呢)。

    另外,爬虫肯定要不定期更新的啊,因为人家也会更新反爬规则啊。
    cherbim
        11
    cherbim  
       2019 年 7 月 27 日 via iPhone
    while ture,然后判断获取 http 的状态码,是 200 就继续循环,不是就跳出循环
    solider245
        12
    solider245  
    OP
       2019 年 7 月 27 日
    @xiaoming1992 #10 我其实已经说了,这个设置最大值这块其实我已经会了。
    感觉看别人的代码,似乎有三种写法,if/else,try/expect,while ture 这个例子我还真没见过。
    solider245
        13
    solider245  
    OP
       2019 年 7 月 27 日
    @cherbim #11 能大概写个小例子吗?
    cherbim
        14
    cherbim  
       2019 年 7 月 27 日   1
    @solider245 当然可以啊,比如查询从你发布的帖子到现在发布了多少新帖
    代码如下:
    import requests

    url = r"t/"
    header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
    }
    i = 586668
    while True:
    page_url = url + str(i)
    print(page_url)
    r = requests.get(page_url, header)
    if r.status_code == 200:
    i = i + 1
    else:
    break
    print("自楼主发帖起一共更新了" + str(i - 586668 - 1) + "贴")
    cherbim
        15
    cherbim  
       2019 年 7 月 27 日
    我靠,这破站真是“小气鬼”,我就查了几次,把我 IP ban 了
    loading
        17
    loading  
       2019 年 7 月 27 日   1
    我用人工都能很快地尝试出来。
    二分法。
    solider245
        18
    solider245  
    OP
       2019 年 7 月 27 日
    @cherbim #14 原来是这个原理,好的,谢谢哈,我按照这个逻辑,尝试一下
    JasonEWNL
        19
    JasonEWNL  
       2019 年 7 月 27 日 via iPad
    比如你找到当前页的 “下一页” 特征(标签啊各种,一般靠 re 扒出来)定义为 next_url 啥的,然后在 while True 循环里 if next_url 就行了,只要一直有就会一直爬,没有就终止了。(以上来自以前爬多页漫画网站的经验)
    dongyx
        20
    dongyx  
       2019 年 7 月 27 日   1
    楼主,这个可以用二分搜索解决啊。
    dongyx
        21
    dongyx  
       2019 年 7 月 27 日
    我给楼主写一个实现,借助 bisect 标准模块,我们连二分搜索都不用自己实现,bisect_left()会求一个升序数组的 lower_bound,所以我们只需要写一个小类,覆盖__getitem__模拟数组行为,每次取下标 i 都去读站点的第 i 页,如果页面合法就返回 0,否则返回 1,然后调用 bisect_left 就可以了。

    下面的例子是二分搜索探寻 Hacker news 的新闻版 ( https://news.ycombinator.com/news?p={page}) 的最大页码。对于 Hacker News 来说,一个页面有帖子当且仅当 HTML 中含有字符串'class='title'",所以可以以这个标准来确定页面是否合法。

    V2EX 的回复似乎会忽略缩进,所以我也贴一个 gist: https://gist.github.com/dongyx/cdb7df063c6a4825a51571bd429d0157

    import requests
    import bisect

    def valid(resp):
    return int('class="title"' in resp.text)

    class PageStatus:
    def __getitem__(self, page):
    r = requests.get("https://news.ycombinator.com/news",
    params={"p": page})

    return 0 if valid(r) else 1

    end = bisect.bisect_left(PageStatus(), 1, lo=1, hi=1024)

    print("the page range is [1, {0})".format(end))
    solider245
        22
    solider245  
    OP
       2019 年 7 月 27 日
    @dongyx #20 二分查找是什么意思?
    jugelizi
        23
    jugelizi  
       2019 年 7 月 27 日
    话说 这个代码上来说二分是最简便的算法啊
    solider245
        24
    solider245  
    OP
       2019 年 7 月 27 日
    @dongyx #21 你这个好像有点复杂啊,我得消化下。想问下,你这种写法和前面朋友给的 while True 方式相比,有哪些优点吗?
    因为 while true 我现在可以理解,而且也可以写出来了。
    dongyx
        25
    dongyx  
       2019 年 7 月 27 日   1
    @solider245 就是一般意义上我们说的二分查找,一般的程序员应该都了解的。 如果你这方面有缺失,我时间不多,只能在这里简单解释一下:假设我有一个有序的整数数组,我想要查询里面一个数的下标,我先拿数组中间的数和目标数对比,如果相等那就找到了,如果小于目标值,就说明目标值在数组的后半部分,那我就去后半部分继续二分,如果大于目标值,说明目标值在数组的前半部分,我就去前部分用同样的方法找。

    这样不断地分割数组,我只需要正比于数组长度的对数的时间就能找到值,也就是 1024 长度的数组我只需要找 10 次左右。

    对于你的问题,你可以把所有页面看成是一个数组,合法是 0,不合法是 1,这个数组就是[0,0,0,....,1,1,1,1]这样的形式,你要找到最右边的 0,二分搜索就可以了。二分搜索的实现是很容易出 bug 的(除非你很好地掌握了循环不变式的思想, 但是如果你不知道二分搜索,我估计你也不知道循环不变式),所以不建议自己实现。bisect 标准模块已经实现了数组的二分搜索,所以你只需要构造这么一个数组,但是你不能真的去构造数组,不然相当于每个页面都去读取了一次,所以你可以覆盖__getitem__方法,构造一个假的数组,每次读数组的第 i 个值,你去取第 i 个页面就行了。

    我给的代码只是简单的 demo,要达到工业强度,还需要一些改进,比如请求失败的处理之类的,祝好运。

    PS:while 的做法是每个页码都尝试去读一次,比二分搜索慢了很多,但是如果你的性能需求没有你帖子里说的那么高,那就怎么简单怎么来吧。
    solider245
        26
    solider245  
    OP
       2019 年 7 月 27 日
    @dongyx #25 哦,有点理解了。
    比如我有一个要爬取的网站,他的最大页面值等于 1W,但是我并不知道。
    如果用 while true 的话,我相当于要依次载入 9999 次,最终到达一万次,然后 10001 次的时候,页面返回的不是 200,然后这个循环就中止了。

    用二分的话,速度就要快一点?

    可问题是我的目的是爬取,求最大值只是为了让我的爬虫知道要爬多少个页面,这样的话,二分法用在这里,似乎并没有大的作用?除了可以让我的爬虫可以更快的获取网页的数量。

    不过感觉可以用来作为前置使用。比如我想爬取一下这个网站,然后我通过二分法,快速知道这个网站大概有多少个页面,这样的话,心里会更有数?
    xuanbg
        27
    xuanbg  
       2019 年 7 月 27 日
    写个死循环,条件判断跳出不就好了。。。
    dongyx
        28
    dongyx  
       2019 年 7 月 27 日
    @solider245 抱歉,我以为你的需求是确定最大页码,如果你是要爬去所有页面的话,用 while 就行了,二分搜索多此一举。
    daozhihun
        29
    daozhihun  
       2019 年 7 月 27 日   5
    如果你要爬取所有页面,就写个死循环,一直到 404 或者没东西返回的时候再 break。
    如果你只需要确定页数,就可以先倍增再二分,比如 1、2、4、8、16 …… 假如到 1024 的时候没东西,那么页数就在 512-1024 之间,然后在这个区间二分。
    不过你要注意人家可能会反爬,甚至会投毒,这个就得你自己处理了。说不定人家发现你是爬虫,会返回假数据让你永远也爬不完,所以你要监控你的数据。
    反正做爬虫就要准备打持久战,小网站还好,大网站的反爬很恶心,你得做好每天都改的准备。
    dongyx
        30
    dongyx  
       2019 年 7 月 27 日
    @daozhihun 倍增确定上界好赞,送一个感谢给你
    solider245
        31
    solider245  
    OP
       2019 年 7 月 27 日
    @daozhihun #29 谢谢了,又涨知识了。
    solider245
        32
    solider245  
    OP
       2019 年 7 月 27 日
    @xuanbg #27 让我很尴尬的是,网上几乎所有的教程都没有涉及到这方面。看来是这个问题太简单了
    zgl263885
        33
    zgl263885  
       2019 年 7 月 27 日 via iPhone
    类似递归二分法定位问题,先随便给个比较大的数值,确认无效后开始递归二分法查找边界值。
    cherbim
        34
    cherbim  
       2019 年 7 月 27 日
    @solider245 我给出代码的判断条件是是否 404,万一某个网站不给 404,直接给你跳转某个网页,那这代码就陷入死循环,就没用了,重新回到你的问题,你不知道一个网页有多少页,为什么要靠猜有多少页,为啥不让代码看一下网页有多少页,建议了解一下 xlml 这个库
    顺带给个范例,查询本贴共有多少人回复:
    import requests
    from lxml import etree

    url = r"t/586668"
    header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
    }

    html = requests.get(url, headers=header)
    xml_cOntent= etree.HTML(html.content)
    href_list = xml_content.xpath("//span[@class='no']/text()")
    print("当前回复最大楼层" + href_list[-1] + "楼")
    soho176
        35
    soho176  
       2019 年 7 月 27 日
    http 的状态码 200
    solider245
        36
    solider245  
    OP
       2019 年 7 月 28 日
    @cherbim #34 我去,还有这种套路啊?我都没遇到过,一般都是 200 或者非 200 的状态码。
    看来大家爬取的都是很高端的网站啊。我爬取的一般都是一些表格网站居多。
    谢谢你提供的范例,我琢磨和研究下。
    感觉上论坛问了之后,发现了以前很多根本没有接触到的东西。又触及到了我的知识盲区了。
    kppwp
        37
    kppwp  
       2019 年 7 月 28 日 via iPhone
    是我没有理解楼主已经会设置最大页码的意思吗
    获取这个目录下的最大页码 源代码肯定有接口或者写死在标签里的
    这样直接循环遍历不挺好的么
    二分实在多此一举了
    kppwp
        38
    kppwp  
       2019 年 7 月 28 日 via iPhone
    “但是我不知道这个网站的准确页数”
    如果真的是这样的话对用户太不友好了 设计再差的网站项目也不会忽略这点的 因此绝对有接口存在
    但是那种不打算对用户开放的站点可能不会考虑......但是如果是这种站点 他的入口又是哪里呢 从哪里搞到这个 url 的,从最初的地方去找逻辑

    总之这个不知道准确页数让我很迷惑
    应该就是知道的

    另外说一下
    判断不一定要用状态码啊 如果用 xpath 为空就让他为空好了 最后不会返回顺便判断一下

    二分我也有点迷惑 一样要遍历判断是否合法 二分完全是多余的
    Les1ie
        39
    Les1ie  
       2019 年 7 月 28 日
    这种单纯确定页数一般不需要写代码 :)

    这种情况我一般是在浏览器手动二分试试最大的页数。楼上说的先倍增再二分,这个 tips 学到了 :) 比我直接瞎猜上界靠谱
    locoz
        40
    locoz  
       2019 年 7 月 28 日 via Android
    最大页数未知的并不影响你爬啊,正常地一页一页翻下去不就好了吗?下一页给的哪个就跟着翻下去,拟人的操作不就是这样吗?
    还是说你想要知道所有的页码,直接并发请求列表页?没必要啊,你都说了是列表页了,主要影响速度的应该是在详情页上,跟列表页没啥关系。
    solider245
        41
    solider245  
    OP
       2019 年 7 月 28 日
    @locoz #40 谢谢你的答复,今天刚看到这种操作
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2612 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 68ms UTC 15:31 PVG 23:31 LAX 08:31 JFK 11:31
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86