Python爬取StackOverflow问题页面并转换为WordPress可用格式(2021/9/21更新)

推荐参考:Python爬虫入门

这篇文章是在做出来后第六天写的,
写着写着我又想写爬虫了,因此又写了个WordPress转CSDN的,就不用我每次加前缀了。
python真好用噢。

2021/9/11更新:代码更新,增加处理注销用户的功能。
2021/9/21更新:代码更新,修复十天前加上的bug。

项目介绍

  • 现在开了个栏目,翻译StackOverflow文章。
  • 但每次转换有点麻烦,复制粘贴改格式。
  • 于是就有了这篇。
  • 输入StackOverflow的问题网址,爬取后,转换成我的行文格式,并自动复制到剪切板。
  • 值得一提的是,WordPress的格式与html的格式还是挺像的,所以不需要做太大的改变。
  • 最后的成果也不错,只有两个问题:
    • 链接不是在新页面打开,而是在当前页面打开,这就要求我每次手动修改,但也只是十几秒的时间。
    • 这里的内容没有换行符,所以转换过来还需要我自己加,但我选的问题都是我能看懂的,所以加点换行符也不是大问题。

项目思路

  1. 爬取网页

    • 使用了urllib库。
    • 相关函数是ask_url_get_html(url)
    • 是很基础的用法,因为只爬取这一个页面,还是不用登陆就可以看到的页面,所以连cookie都不需要。
    • 提交一个链接后,爬取它的html文本。
  2. 网页与WordPress文章格式分析

    • 用到了BeautifulSoup库、re库。
    • 相关函数是get_data_from_html(html, answer_num)
    • 首先我的文章需要几个部分:
    • 标题及其链接。
    • 提问者ID,提问内容,提问者主页链接。
    • 回答者ID,回答内容,回答者主页链接,投票数。
    • 回答数量。
      • 这个后来删去了,爬取倒不是说麻烦,但就是想偷懒。
    • 然后就是观察在页面的哪些部分:
    • 先是比较简单的:
      • 标题:
        • 很容易获取,游览器右键审查元素找到的就是它。
        • 或者bs4的title也能获取。
      • 标题链接:
        • 这个是我给它的,所以不用获取。
      • 提问者ID:
        • 右键审查元素,它的类是‘user-details’。
      • 提问内容:
        • 类是‘s-prose js-post-body’。
      • 提问者链接:
        • 和前面提问者ID在一起。
    • 来到了回答者的部分,就比较麻烦了:
      • ID、内容、主页、投票数,它们的类与提问者都一致。
      • 也就是说,找到的第一个并不是我们需要的。
      • 但这还好,用re表达式匹配后,变成列表后也可以直接取得,所以还好。
    • 更麻烦的是内容匹配,因为有换行符,所以re不能完全匹配上所有内容,只有第一句。
      • 后来尝试了几个函数,最后使用str.replace(\n, ).replace('\r', '')以及re.DOTALL来删除所有换行符。
      • 然后可以正常匹配到完整内容了,但因为匹配用的表达式,会用上与下一个内容的标签,所以在匹配的时候需要多匹配一个内容。
        • 即,如果我要找第三个回答,因为提问内容标签与回答标签一致,
        • 第三个回答实际上是第四个内容,
        • 并且还因为要用到下一个内容的标签来匹配,所以实际上要匹配五个内容,
        • 才能找到第三个回答的内容。
    • 之后是转换成WordPress格式:
    • 只有内容需要转换,其他像ID、主页这些信息,实际上只是拼接而已。
    • 就来测试获取的能不能直接用:
      • 测试后发现可以,内容中代码的内容,恰好也是WordPress的代码块格式。
      • 所以匹配到的东西,复制粘贴就能用。
    • 但因为我是用WordPress里的列表来展示的,所以还需要进一步转换:
      • 选择一句复制粘贴来的内容,转换成WordPress列表,然后复制这个区块来分析。
      • 发现与html格式类似,是加了前缀与后缀,然后多个
          标签。
    • 大部分的内容是能直接用的,但如果回答者以代码块开头,就会导致格式出错:
      • StackOverflow里的代码块用到了
      • 而每次我给WordPress列表添加内容的时候,还会加上
        • 就使得WordPress列表的第一行会直接向右移动两个缩进。
          • 有点像首行缩进,正常两个字符。
          • 而两个
              会导致四个字符作为首行缩进。
        • 但WordPress不允许这种情况出现,就会导致格式损坏。
          • 而且我也不可能用这么丑的格式。
        • 最终在这里的格式处理比较麻烦,有的格式不报错,但是不能正常显示。
        • 最后还是用较为复杂的内容转换成WordPress列表,加上换行和缩进,再一行行比较。
    • 格式转换

      • 相关函数是convert_to_wordpress_format(page_info, url)
      • 就是字符串拼接。
      • 主要的难点在列表格式生成。
      • 这点就是前面最后内容匹配讲的。
      • 最后是用爬取到的内容,转换成WordPress列表格式,加上换行与缩进,再与自己生成的格式一行行比较。
    • 输出为可用格式

      • 用到了pyperclip库。
      • 测试的时候使用print输出的,然后复制粘贴,但太过麻烦。
      • 于是选择转换成文本,但从pycharm文本中复制的内容,虽然和WordPress需要的格式一模一样吧,但是不会自动转换。
      • 于是找了一个pyperclip库,可以将文本粘贴至剪切板。
      • 虽然每次爬取页面后必须要及时粘贴吧,但也不是麻烦事。
      • 即便希望在处理后自行选择是否粘贴,也只需要加个选项,循环这个粘贴函数就好了。
      • 所以这个项目就大功告成了。
    • 转换成exe文件

      • 用pyinstaller将文件转换成exe文件,避免每次都需要打开pycharm。

    代码

    import re
    import urllib.request
    import urllib.error
    from bs4 import BeautifulSoup
    import pyperclip
    
    def main():
        url = input("Input stackoverflow question url:")
        html = ask_url_get_html(url)
        print('Getting html file from url...')
        answer_num = 3
        page_info = get_data_from_html(html, answer_num)
        print('Select the first 3 answers, processing...')
        wordpress_format = convert_to_wordpress_format(page_info, url)
        print('Success, the result will paste to the shear plate.')
        pyperclip.copy(wordpress_format)
        print('Copy success.')
        input("Press the ENTER key to close.")
    
    def ask_url_get_html(url):
        #   从url中获取html文件
        head = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
        }
        request = urllib.request.Request(url, headers=head)
        try:
            response = urllib.request.urlopen(request)
            html = response.read().decode("utf-8")
        except urllib.error.URLError as e:
            if hasattr(e, "code"):
                print(e.code)
            if hasattr(e, "reason"):
                print(e.reason)
        if html is None:
            input("Enter any key to continue")
    
        return html
    
    def get_data_from_html(html, answer_num):
        #   html文本处理,收集标题、内容、作者名、作者主页链接、回答投票,得到并返回页面信息
    
        #   正则表达式
        find_title = re.compile('">(.*)</a>')
        find_description = re.compile('">(.*)</div>', re.DOTALL)
        find_author_name = re.compile('name">(.*?)</span>')
        find_author_link = re.compile('(href="(.*)">.*</a>)|(Person">(.*)<span)')
        find_vote_count = re.compile('data-value="(\d*?)"')
    
        soup = BeautifulSoup(html, "html.parser")
        #   标题匹配
        item = str(soup.find_all("a", class_="question-hyperlink", limit=1))
        title = re.findall(find_title, item)
        #   内容匹配,并修改/去除部分标签,方便后续处理
        item = str(soup.find_all("div", class_="s-prose js-post-body", limit=answer_num+2))
        item = item.replace("\n", "").replace('\r', '')
        item = item.replace('<ul>', '').replace('</ul>', '')
        item = item.replace('<li>', '').replace('</li>', '')
        item = item.replace("/pre>", "/p>").replace('<pre', '<p')
        item = item.replace("/p>", "/li>").replace("<p", "<li")
        # print(item)
        description = re.findall(find_description, item)
        description = description[0].split('</div>, <div class="s-prose js-post-body" itemprop="text">')
        #   作者名及作者连接匹配
        item = str(soup.find_all('div', class_="user-details", itemprop="author", limit=answer_num+1))
        # item = item.replace()
        author_name = re.findall(find_author_name, item)
        author_link = re.findall(find_author_link, item)\
        #   投票数匹配
        item = str(soup.find_all("div", class_='js-vote-count', limit=answer_num+1))
        vote_count = re.findall(find_vote_count, item)
    
        #   按序整理匹配信息
        page_info = []
        page_info.append(title[0])
        for pos in range(0, answer_num + 1):
            page_info.append(author_name[pos])
            if len(author_link) == answer_num + 1:
                page_info.append(author_link[pos][1])
            else:
                page_info.append("")
                if pos == 0: print('Link loss, maybe account cancellation?\nAll link replace with ""')
            page_info.append(description[pos])
            page_info.append(vote_count[pos])
        return page_info
    
    def convert_to_wordpress_format(page_info, url):
        #   页面信息转WordPress格式文本
        wordpress_content = """
    <blockquote class="wp-block-quote"><p><a rel="noreferrer noopener" 
    href="https://mwhls.top/programming-language-learning/stackoverflow" 
    target="_blank">stackoverflow热门问题目录</a></p><p>如有翻译问题欢迎评论指出,谢谢。</p></blockquote>
    <!-- /wp:quote --><!-- wp:heading {"textAlign":"center","level":4} -->
    <h4 class="has-text-align-center"><a rel="noreferrer noopener" href="
        """ + url + '" target="_blank">' + page_info[0] + '''</a></h4><!-- /wp:heading --><!-- wp:list --><ul><li><a rel="noreferrer noopener" href="https://stackoverflow.com''' + page_info[2] + '" target="_blank">' + page_info[1] + '''</a> asked:
        <ul>''' + page_info[3] + '</ul></li>' + '<li>Answers:'
    
        for pos in range(1, len(page_info) // 4):
            wordpress_content += '<ul><li><a rel="noreferrer noopener" href="https://stackoverflow.com'
            wordpress_content += page_info[4 * pos + 2]
            wordpress_content += '" target="_blank">'
            wordpress_content += page_info[4 * pos + 1]
            wordpress_content += '</a> - vote: '
            wordpress_content += page_info[4 * pos + 4]
            wordpress_content += '<ul>'
            wordpress_content += page_info[4 * pos + 3]
            wordpress_content += '</ul></li></ul>'
    
        wordpress_content += '</li></ul><!-- /wp:list -->'
        return wordpress_content
    
    if __name__ == '__main__':
        main()
    

    You may also like...

    发表评论

    您的电子邮箱地址不会被公开。 必填项已用*标注