**开发一个网页分析程序,可以抓取特定网页的内容,加以分析之后将结果保存至数据库。**
V1.5上线,保存至sqlite数据库用时2分13秒。
网页分析程序具体要求描述如下:
1. 使用http技术获取一个博客的首页http://blog.csdn.net/jiangsheng
2. 分析这个网页的内容,从中找到博客中每一篇文章的链接。
3. 通过这些链接,获取文章的正文网页,从内容中提取文章的标题和文章的内容。
4. 将文章的标题与内容分别保存至数据库。
5. 布局要求:提供一个列表框和一个多行文本框。列表框中显示从数据库中获取的文章标题列表;当点击列表框中的某一篇文章时,在文本框中显示该文章的内容。
获取源码
因为需多次调用获取源码功能,将它放入一个函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 CString CGetWebDlg::DownloadCodes (CString path) { CInternetSession session; CHttpFile *file = NULL ; CString strURL = path; CString strHtml = _T("" ); try { file = (CHttpFile*)session.OpenURL (strURL); } catch (CInternetException *m_pException) { file = NULL ; m_pException->m_dwError; m_pException->Delete (); session.Close (); MessageBox ("网络连接错误!" , "提示" ); } CString strLine; char sRecived[1024 ]; if (file != NULL ) { while (file->ReadString ((LPTSTR)sRecived, 1024 ) != NULL ) { strHtml += sRecived; } } else { AfxMessageBox (_T("失败!" )); } session.Close (); file->Close (); delete file; file = NULL ; strHtml = ConvertUtf8ToGBK (strHtml); return strHtml; }
编码问题
注意网页编码问题,因此需要格式转换,编写一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void ConvertUtf8ToGBK (CString &strUtf8) { int len=MultiByteToWideChar (CP_UTF8, 0 , (LPCTSTR)strUtf8, -1 , NULL ,0 ); unsigned short * wszGBK = new unsigned short [len+1 ]; memset (wszGBK, 0 , len * 2 + 2 ); MultiByteToWideChar (CP_UTF8, 0 , (LPCTSTR)strUtf8, -1 , (LPWSTR)wszGBK, len); len = WideCharToMultiByte (CP_ACP, 0 , (LPCWSTR)wszGBK, -1 , NULL , 0 , NULL , NULL ); char *szGBK=new char [len + 1 ]; memset (szGBK, 0 , len + 1 ); WideCharToMultiByte (CP_ACP, 0 , (LPCWSTR)wszGBK, -1 , szGBK, len, NULL ,NULL ); strUtf8 = szGBK; delete [] szGBK; delete [] wszGBK; }
有些类似网页爬虫的感觉。
由于我刚开始是用VC6.0创建项目,现在用VS2013打开,因此,提示报错:
error MSB8031: Building an MFC project for a non-Unicode character set is deprecated. You must change the project property to Unicode or download an additional library. See http://go.microsoft.com/fwlink/p/?LinkId=286820 for more information.
解决:用于多字节字符编码 (MBCS) 的 MFC 库 (DLL) 不再包含于 Visual Studio 中,但是可用作插件,可以在任何装有 Visual Studio Professional、Visual Studio Premium 或 Visual Studio Ultimate 的计算机上下载和安装。下载地址:https://www.microsoft.com/zh-cn/download/details.aspx?id=40770
解析
为了接下来的操作,我去学正则表达式了……
还是学习html解析库 htmlcxx
官方
HtmlCxx用户手册
HTMLCXX
下载htmlcxx库
http://sourceforge.net/projects/htmlcxx/
并解压。
编译
打开htmlcxx.vcproj,右键属性,配置属性-C/C+±代码生成-运行库:多线程调试 DLL (/ MDd)进行编译。编译会报错,将
1 const char *signature = "" ;
改为
1 const char *signature = "\xEF\xBB\xBF" ;
即可编译成功。
导入
把生成的htmlcxx.lib和html文件夹拷贝到所需的工程中。即:
在所开发项目文件夹中,新建”htmlcxx“文件,里面添加两个子文件夹”lib“和”include“。将编译好的htmlcxx.lib拷贝到lib文件夹,将html文件夹中所有的.h头文件和ParserSax.tcc添加到include文件夹。添加库文件htmlcxx.lib到项目中,具体说来:
在VS工程中,添加c/c工程中外部头文件及库的基本步骤:
1、添加工程的头文件目录:工程—属性—配置属性—c/c —常规—附加包含目录:加上头文件存放目录。
2、添加文件引用的lib静态库路径:工程—属性—配置属性—链接器—常规—附加库目录:加上lib文件存放目录。
然后添加工程引用的lib文件名:工程—属性—配置属性—链接器—输入—附加依赖项:加上lib文件名。
3、添加工程引用的dll动态库:把引用的dll放到工程的可执行文件所在的目录下。
注意:第一步可以不用,直接在工程里加入动态库的头文件,在使用代码处引用这个头文件。
所开发的项目的头文件中添加以下内容:
1 2 3 4 5 6 7 #include <string> #include "htmlcxx/include/ParserDom.h" using namespace std;using namespace htmlcxx; #pragma comment(lib,"htmlcxx.lib" )
测试
官方测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <htmlcxx/html/ParserDom.h> ... string html = "<html><body>hey</body></html>" ; HTML::ParserDom parser; tree<HTML::Node> dom = parser.parseTree (html); cout << dom << endl; tree<HTML::Node>::iterator it = dom.begin (); tree<HTML::Node>::iterator end = dom.end (); for (; it != end; ++it) { if (it->tagName () == "A" ) { it->parseAttributes (); cout << it->attributes ("href" ); } } it = dom.begin (); end = dom.end (); for (; it != end; ++it) { if ((!it->isTag ()) && (!it->isComment ())) { cout << it->text (); } }
然而……
加入#include"iostream"头文件即可。
修改为:
1 2 3 4 5 6 if (it->tagName () == "A" ) { it->parseAttributes (); std::pair<bool , std::string> pa = it->attribute ("href" ); cout << pa.second; }
编译通过。
还有其他的库也可以用,比如使用MSHTML解析HTML页面
比如LIBXML2库使用指南
还可以用正则表达式写库……
突然发现 原来的计划里有COM组件 XML和HTML 数据库访问技术
都没怎么接触过 补补补
在填坑的路上不能止步…
参考
C++ 使用Htmlcxx解析Html内容(VS编译库文件)
html与xml解析库htmlcxx使用过程中的若干问题及解决方案
c++ hmtlcxx 学习之旅
MSHTML
因为最近用过MSXML,就试试MSHTML。学有余力的话,htmlcxx之后还是想玩一下…
https://msdn.microsoft.com/en-us/library/aa741317(v=vs.85).aspx
蒋晟-关于MSHTML
https://msdn.microsoft.com/zh-cn/library/mshtml(v=vs.110).aspx
MSHTML导入
系统中自带了mshtml,和msxml一样,在C盘windows/system32中可找到。
如何导入
先看MSDN……
MSDN-MSHTML
再看各种搜集的文章
http://bbs.csdn.net/topics/330214041
http://www.cnblogs.com/speedmancs/archive/2010/08/11/1797442.html
http://blog.csdn.net/jinyaba/article/details/17097323
https://social.msdn.microsoft.com/Search/zh-CN?query=MSHTML&pgArea=header&emptyWatermark=true&ac=4#refinementChanges=117&pageNumber=1&showMore=false
https://wenku.baidu.com/view/d571abc4ec3a87c24028c4bb.html
http://www.codeguru.com/cpp/i-n/ieprogram/article.php/c4385/Lightweight-HTML-Parsing-Using-MSHTML.htm
http://www.yesky.com/403/1938403.shtml?qq-pf-to=pcqq.c2c
http://www.bianceng.cn/Programming/vc/201411/46771.htm
https://wenku.baidu.com/view/299bba4a336c1eb91a375df5.html
思路:下载源码和获取链接是两个独立函数,会被多次调用。先获取首页源码,div id=“archive_list” 遍历该div获取各月份归档链接,再使用多线程(48个线程???)进入每个归档链接里下载源码,获取源码中h1的每篇文章标题,保存到数据库。
虽然有两种方法,一种通过归档获得链接,一种通过翻页获得链接,但根据本html特点,明显通过翻页要简洁方便一些,因为翻页的链接是有规律的,可通过循环搞定。每页5篇直接获得正文链接,比从归档获得少一层。两核4个逻辑处理器,所以是开2个线程好还是4个好呢……
获取每篇正文链接,下载源码,解析得正文,保存到数据库。最后从数据库中提取标题和正文显示到对应窗口(使用ADO)。
解析过程
创建
1.使用CoCreateInstance创建一个接口
1 HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&pDoc);
2.创建一个COM中的数组,将HTML字符串写到数组中
a)SafeArrayCreateVector:函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这将它作为一个字符数组,所以是一个一维数组。
b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,操作这个缓冲区就相当于操作了这个数组。
c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,用来回收这个权限,并使对数组的操作生效。
调用接口的write方法,将接口与HTML字符串绑定
1 2 3 4 5 6 7 8 SAFEARRAY* psa = SafeArrayCreateVector (VT_VARIANT, 0 , 1 ); VARIANT *param; bstr_t bsData = (LPCTSTR)strHtml;hr = SafeArrayAccessData (psa, (LPVOID*)¶m); param->vt = VT_BSTR; param->bstrVal = (BSTR)bsData; hr = pDoc->write (psa); hr = SafeArrayUnaccessData (psa);
目标:
1 2 3 4 5 <span class="link_title"><a href="/jiangsheng/article/details/9870241"> 选择剪贴板格式顺序 </a> </span>
整个 <span> </span> 是元素, <span> 是标签,class是属性名,link_title是属性值,“选择剪贴板格式顺序”是文本。
元素遍历
至少两种方法:
法一:
获取了HTML文档的IID_IHTMLDocument2接口后,开始遍历:
1.get_all方法获取所有标签节点,这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针
2.用IHTMLElementCollection接口的get_length方法获取标签的总数量,据此写一个循环,在循环进行元素的遍历
3.循环中用IHTMLElementCollection接口的item方法进行迭代,获取各元素对应的IDispatch接口指针
4.调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各种信息
以下已能成功获取标题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void CGetWebDlg::EnumElements(IHTMLDocument2* pDoc) { CComPtr<IHTMLElementCollection> pCollection; pDoc->get_all(&pCollection); if (NULL == pCollection) { return; } VARIANT varName; CString strText; long len = 0; pCollection->get_length(&len); for (int i = 0; i < len; i++) { varName.vt = VT_I4; varName.lVal = i; CComPtr<IHTMLElement> pElement; CComPtr<IDispatch> pDisp; pCollection->item(varName, varName, &pDisp); if (NULL == pDisp) { continue; } pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement); if (NULL != pElement) { BSTR bstrClass; pElement->get_className(&bstrClass); CString strClass = _com_util::ConvertBSTRToString(bstrClass); if (strClass.Compare("link_title") == 0) { BSTR bstrText = NULL; pElement->get_innerText(&bstrText); strText = bstrText; m_list.InsertItem(i, strText); } } } }
法二:
利用IHTMLDocument2将字符串形式的HTML转换为DOM对象,利用IHTMLDocument3的getElementByTagName等方法来操作DOM对象。
以下已能成功获取标题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 MSHTML::IHTMLDocument3Ptr pDoc3; MSHTML::IHTMLElementCollectionPtr pCollection; MSHTML::IHTMLElementPtr pElement; pDoc3 = pDoc; pCollection = pDoc3->getElementsByTagName(L"span"); for (long i = 0; i < pCollection->length; i++) { pElement = pCollection->item(i, (long)0); if (pElement != NULL) { BSTR bstrClass; pElement->get_className(&bstrClass); CString strClass = _com_util::ConvertBSTRToString(bstrClass); if (strClass.Compare("link_title") == 0) { BSTR bstrText = NULL; pElement->get_innerText(&bstrText); CString strText = bstrText; m_list.InsertItem(i, strText); } } }
其实两种方法大同小异,相较而言可能数据量大的话,法二效率高些吧。因为法一是直接遍历所有的元素寻找class相同的,而法二是先定位span,然后在span中找寻class。(getElementsByTagName只有IHTMLDocument3Ptr)
使用MSHTML解析HTML页面
变体VARIANT
MSDN-DOM https://msdn.microsoft.com/en-us/library/ms766487(v=vs.85).aspx
使用MSHTML接口获取链接
晚上几个小时做完了一半,抵了之前一两个月。
数据库这块没来得及做,没加多线程,很多细节还得调。但比起之前心有余而力不足的感觉还是好多了。 学过msxml后,学习mshtml确实强一点,比一个月前完全不知道怎么下手好很多了。
今晚总算做出来个半成品
正文解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void CGetWebDlg::ArticleParse (CString strArticle, Blog* blog, int iRow) { IHTMLDocument2* pDoc; MSHTML::IHTMLDocument3Ptr pDoc3; MSHTML::IHTMLElementCollectionPtr pCollection; MSHTML::IHTMLElementPtr pElement; HRESULT hr = CoCreateInstance (CLSID_HTMLDocument, NULL , CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void **)&pDoc); SAFEARRAY* psa = SafeArrayCreateVector (VT_VARIANT, 0 , 1 ); if (psa == NULL || pDoc == NULL ) { MessageBox (_T("创建Document2对象失败!" )); } VARIANT *param; bstr_t bsData = (LPCTSTR)strArticle; hr = SafeArrayAccessData (psa, (LPVOID*)¶m); param->vt = VT_BSTR; param->bstrVal = (BSTR)bsData; hr = pDoc->write (psa); hr = SafeArrayUnaccessData (psa); _bstr_t href; pDoc3 = pDoc; pCollection = pDoc3->getElementsByTagName (L"div" ); for (long k = 0 ; k < pCollection->length; k++) { pElement = pCollection->item (k, (long )0 ); if (pElement != NULL ) { BSTR bstrClass; pElement->get_id (&bstrClass); CString strClass = _com_util::ConvertBSTRToString (bstrClass); if (strClass.CompareNoCase ("article_content" ) == 0 ) { BSTR bstrText = NULL ; pElement->get_innerText (&bstrText); CString strText = bstrText; blog->article = strText; m_list.SetItemData (iRow, (DWORD_PTR)blog); } } } }
建立了一个结构体用来存放每篇文章的标题和正文,方便点击列表控件项时读取对应的结构体。
1 2 3 4 5 struct Blog { CString title; CString article; };
列表控件和编辑框交互
列表控件初始化
1 2 3 4 CRect rectLocal; m_list.GetClientRect (rectLocal); m_list.InsertColumn (0 , "序号" , LVCFMT_LEFT, rectLocal.Width () / 6 , 0 ); m_list.InsertColumn (1 , "文章列表" , LVCFMT_LEFT, rectLocal.Width () / 6 * 5 , 1 );
添加多列后,单击只能选中第一列,这时需要修改风格 LVS_EX_FULLROWSELECT 表示整行。
1 m_list.SetExtendedStyle (LVS_EX_FULLROWSELECT);
添加数据
单纯添加一项的话: m_list.InsertItem(项的索引, 数据);
但要指定列的话:
m_list.InsertItem(项的索引, “”);
m_list.SetItemText(行, 列, 数据);
OnLvnItemchangedList
试了多种方法,只有这一种成功了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void CGetWebDlg::OnLvnItemchangedList1 (NMHDR *pNMHDR, LRESULT *pResult) { LPNMLISTVIEW pNMLV = reinterpret_cast <LPNMLISTVIEW>(pNMHDR); if (pNMLV->uNewState == (LVIS_SELECTED | LVIS_FOCUSED)) { m_edit.SetWindowText ("" ); Blog *pStructure = (Blog*)m_list.GetItemData (pNMLV->iItem); m_edit.SetWindowText (pStructure->article); } *pResult = 0 ; }
实现列表和编辑框交互后,可以说这已经是一个可以完整运行的程序了,V1.0版本出炉。
实测解析时间:1分41秒
能够成功获取该博客111篇文章,能够正确显示正文(包括清晰显示代码)。现在的运行图:
该版本未添加多线程、未和数据库关联。
V2.0实现数据库操作
使用sqlite数据库
下载导入
官网下载sqlite
Source Code sqlite-amalgamation-3200000.zip 有三个东西 shell.c sqlite3.c sqlite3.h
根据VS2010下SQLite3生成lib库文件 文章方法生成sqlite3.lib
Precompiled Binaries for Windows sqlite-dll-win32-x86-3200000.zip 有一个所需 sqlite3.dll
将lib和dll放入项目工程里,在.cpp开头加上
1 2 #include "sqlite3.h" #pragma comment(lib,"sqlite3.lib" )
保存至sqlite数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 int CGetWebDlg::Database (std::vector<Blog*> &vecBlog) { sqlite3 * pDB; char * errMsg; int res = sqlite3_open ("test.db" , &pDB); if (res != SQLITE_OK) { MessageBox (_T("数据库打开失败,请检查后再操作!" ), NULL , MB_ICONSTOP); sqlite3_close (pDB); return -1 ; } string strSQL = "create table blog (title text, article text);" ; res = sqlite3_exec (pDB, strSQL.c_str (), 0 , 0 , &errMsg); if (res != SQLITE_OK) { MessageBox (_T("数据库打开失败,请检查后再操作!" ), NULL , MB_ICONSTOP); } vector<Blog*>::iterator iter; for (iter = vecBlog.begin (); iter != vecBlog.end (); iter++) { CString cstrTitle = (*iter)->title; CString cstrArticle = (*iter)->article; cstrTitle = ConvertGBKToUtf8 (cstrTitle); cstrArticle = ConvertGBKToUtf8 (cstrArticle); char *p1 = cstrTitle.GetBuffer (cstrTitle.GetLength () + 1 ); cstrTitle.ReleaseBuffer (); char *p2 = cstrArticle.GetBuffer (cstrArticle.GetLength () + 1 ); cstrArticle.ReleaseBuffer (); char *strSQL = sqlite3_mprintf ("INSERT INTO blog VALUES('%q','%q')" , p1, p2); sqlite3_exec (pDB, strSQL, 0 , 0 , &errMsg); } }
当然,也可以事先把数据库和表创建好……
因为sqlite数据库是UTF-8格式存储,于是,需要一个转换函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CString ConvertUtf8ToGBK (CString &strUtf8) { int len=MultiByteToWideChar (CP_UTF8, 0 , (LPCTSTR)strUtf8, -1 , NULL ,0 ); unsigned short * wszGBK = new unsigned short [len+1 ]; memset (wszGBK, 0 , len * 2 + 2 ); MultiByteToWideChar (CP_UTF8, 0 , (LPCTSTR)strUtf8, -1 , (LPWSTR)wszGBK, len); len = WideCharToMultiByte (CP_ACP, 0 , (LPCWSTR)wszGBK, -1 , NULL , 0 , NULL , NULL ); char *szGBK=new char [len + 1 ]; memset (szGBK, 0 , len + 1 ); WideCharToMultiByte (CP_ACP, 0 , (LPCWSTR)wszGBK, -1 , szGBK, len, NULL ,NULL ); strUtf8 = szGBK; delete [] szGBK; delete [] wszGBK; return strUtf8; }
出现的问题
如图,数据少时并不会出现。
2.好像是运行了脚本
据查将“script”修改成别的字符便不会运行脚本,并没尝试成功。
效果
下载了一个SQLiteStudio可视化管理工具。可见数据已成功保存至数据库。
从sqlite数据库读取数据
新建工程。
主要用到该函数:
1 2 3 4 5 6 7 8 9 int sqlite3_get_table ( sqlite3 *db, const char *zSql, char ***pazResult, int *pnRow, int *pnColumn, char **pzErrmsg ) ;void sqlite3_free_table (char **result) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void CGetDataFromSqliteDlg::OnBnClickedBtnreaddata () { sqlite3 * pDB; char * errMsg; int res = sqlite3_open ("test.db" , &pDB); if (res != SQLITE_OK){ MessageBox (_T("数据库打开失败,请检查后再操作!" ), NULL , MB_ICONSTOP); sqlite3_close (pDB); } string strSql = "select * from blog" ; char ** pResult; int nRow; int nCol; int nResult; nResult = sqlite3_get_table (pDB, strSql.c_str (), &pResult, &nRow, &nCol, &errMsg); if (nResult != SQLITE_OK) { sqlite3_close (pDB); cout << errMsg << endl; sqlite3_free (errMsg); } std::vector<Blog*>vecBlog; string strOut; int nIndex = 2 ; for (int i = 0 ; i < nRow; i++) { Blog* blog = new Blog; string tempTitle = pResult[nIndex]; string tempArticle = pResult[nIndex + 1 ]; CString number; number.Format ("%d" , i + 1 ); m_list.InsertItem (nRow, "" ); CString strTitle (tempTitle.c_str()) ; CString strArticle (tempArticle.c_str()) ; ConvertUtf8ToGBK (strTitle); ConvertUtf8ToGBK (strArticle); blog->title = strTitle; blog->article = strArticle; vecBlog.push_back (blog); m_list.SetItemText (i, 0 , number); m_list.SetItemText (i, 1 , strTitle); m_list.SetItemData (i, (DWORD_PTR)blog); nIndex = nIndex + 2 ; } sqlite3_free_table (pResult); sqlite3_close (pDB); }
同样地,注意格式转码。
成品
V1.5成功实现从sqlite数据库读取数据并反映在列表和编辑框中。
To be continued…