我正在抓取一张最终导出为CSV格式的表格。我可能需要考虑几种情况,例如嵌套表,跨越行/单元格等,但是现在我只是忽略这些情况并假设我有一个非常简单的表。 “简单”是指我们只有行和单元格,每行可能有不等数量的单元格,但它仍然是一个相当基本的结构。
<table>
<tr>
<td>text </td>
<td>text </td>
</tr>
<tr>
<td>text </td>
</tr>
</table>
我的方法是简单地遍历行和列
String[] rowTxt;
WebElement table = driver.findElement(By.xpath(someLocator));
for (WebElement rowElmt : table.findElements(By.tagName("tr")))
{
List<WebElement> cols = rowElmt.findElements(By.tagName("td"));
rowTxt = new String[cols.size()];
for (int i = 0; i < rowTxt.length; i++)
{
rowTxt[i] = cols.get(i).getText();
}
}
但是,这很慢。对于包含218行的CSV文件(这意味着,我的表有218行),每行不超过5列,刮掉表格需要45秒。
我试图通过在行元素上使用getText
来避免遍历每个单元格,希望输出将被某些东西分隔,但事实并非如此。
有没有更好的方法刮一张桌子?
答案 0 :(得分:4)
我使用Jsoup而不是使用selenium来解析HTML。虽然Selenium提供了遍历表的功能,但Jsoup效率更高。我决定仅将Selenium用于网页自动化,并将所有解析任务委托给Jsoup。
我的方法如下
我最终编写的代码与selenium版本非常相似
String source = "<table>" + driver.findElement(By.xpath(locator)).getAttribute("innerHTML") + "<table>";
Document doc = Jsoup.parse(source, "UTF-8");
for (Element rowElmt : doc.getElementsByTag("tr"))
{
Elements cols = rowElmt.getElementsByTag("th");
if (cols.size() == 0 )
cols = rowElmt.getElementsByTag("td");
rowTxt = new String[cols.size()];
for (int i = 0; i < rowTxt.length; i++)
{
rowTxt[i] = cols.get(i).text();
}
csv.add(rowTxt);
}
Selenium解析器需要5分钟读取1000行表,而Jsoup解析器需要不到10秒。虽然我没有花太多时间进行基准测试,但我对结果非常满意。
答案 1 :(得分:2)
无论您使用xpath
,id
还是css
来确定您的位置,最明确的是速度很慢。也就是说,如果您使用pageObject
模式,则可以使用@CacheLookup
注释。来自消息来源:
我使用100行和6列的表进行了测试,测试查询了每个td元素的文本。没有@CacheLookup
所花费的时间(元素由XPath
定位) 40秒。使用缓存查找,它下降到大约。 20秒,但仍然太多了。
无论如何,如果你丢失了firefox驱动程序并运行测试无头(使用htmlUnit),速度会急剧增加。运行相同的测试无头,时间在100-200ms之间,所以它甚至可能比Jsoup
更快。
您可以查看/试用我的测试代码here。
答案 2 :(得分:2)
我使用HtmlAgilityPack作为Nuget安装来解析动态html表。它非常快,按this answer你可以使用linq查询结果。我已将其用于将结果存储为DataTable。这是公共扩展方法类: -
public static class HtmlTableExtensions
{
private static readonly ILog Log = LogManager.GetLogger(typeof(HtmlTableExtensions));
/// <summary>
/// based on an idea from https://stackoverflow.com/questions/655603/html-agility-pack-parsing-tables
/// </summary>
/// <param name="tableBy"></param>
/// <param name="driver"></param>
/// <returns></returns>
public static HtmlTableData GetTableData(this By tableBy, IWebdriverCore driver)
{
try
{
var doc = tableBy.GetTableHtmlAsDoc(driver);
var columns = doc.GetHtmlColumnNames();
return doc.GetHtmlTableCellData(columns);
}
catch (Exception e)
{
Log.Warn(String.Format("unable to get table data from {0} using driver {1} ",tableBy ,driver),e);
return null;
}
}
/// <summary>
/// Take an HtmlTableData object and convert it into an untyped data table,
/// assume that the row key is the sole primary key for the table,
/// and the key in each of the rows is the column header
/// Hopefully this will make more sense when its written!
/// Expecting overloads for swichting column and headers,
/// multiple primary keys, non standard format html tables etc
/// </summary>
/// <param name="htmlTableData"></param>
/// <param name="primaryKey"></param>
/// <param name="tableName"></param>
/// <returns></returns>
public static DataTable ConvertHtmlTableDataToDataTable(this HtmlTableData htmlTableData,
string primaryKey = null, string tableName = null)
{
if (htmlTableData == null) return null;
var table = new DataTable(tableName);
foreach (var colName in htmlTableData.Values.First().Keys)
{
table.Columns.Add(new DataColumn(colName, typeof (string)));
}
table.SetPrimaryKey(new[] { primaryKey });
foreach (var values in htmlTableData
.Select(row => row.Value.Values.ToArray<object>()))
{
table.Rows.Add(values);
}
return table;
}
private static HtmlTableData GetHtmlTableCellData(this HtmlDocument doc, IReadOnlyList<string> columns)
{
var data = new HtmlTableData();
foreach (
var rowData in doc.DocumentNode.SelectNodes(XmlExpressions.AllDescendants + HtmlAttributes.TableRow)
.Skip(1)
.Select(row => row.SelectNodes(HtmlAttributes.TableCell)
.Select(n => WebUtility.HtmlDecode(n.InnerText)).ToList()))
{
data[rowData.First()] = new Dictionary<string, string>();
for (var i = 0; i < columns.Count; i++)
{
data[rowData.First()].Add(columns[i], rowData[i]);
}
}
return data;
}
private static List<string> GetHtmlColumnNames(this HtmlDocument doc)
{
var columns =
doc.DocumentNode.SelectNodes(XmlExpressions.AllDescendants + HtmlAttributes.TableRow)
.First()
.SelectNodes(XmlExpressions.AllDescendants + HtmlAttributes.TableHeader)
.Select(n => WebUtility.HtmlDecode(n.InnerText).Trim())
.ToList();
return columns;
}
private static HtmlDocument GetTableHtmlAsDoc(this By tableBy, IWebdriverCore driver)
{
var webTable = driver.FindElement(tableBy);
var doc = new HtmlDocument();
doc.LoadHtml(webTable.GetAttribute(HtmlAttributes.InnerHtml));
return doc;
}
}
html数据对象只是字典的扩展名: -
public class HtmlTableData : Dictionary<string,Dictionary<string,string>>
{
}
IWebdriverCore驱动程序是IWebDriver或IRemoteWebdriver的包装器,它将这些接口中的任何一个公开为readonly属性,但您可以用IWebDriver替换它。
HtmlAttributes是一个静态的lass,它保存常见html属性的const值,以便在c#代码中引用html元素/属性/标签等时保存错别字: -
/// <summary>
/// config class holding common Html Attributes and tag names etc
/// </summary>
public static class HtmlAttributes
{
public const string InnerHtml = "innerHTML";
public const string TableRow = "tr";
public const string TableHeader = "th";
public const string TableCell = "th|td";
public const string Class = "class";
... }
和SetPrimaryKey是DataTable的扩展,它允许轻松设置数据表的主键: -
public static void SetPrimaryKey(this DataTable table,string[] primaryKeyColumns)
{
int size = primaryKeyColumns.Length;
var keyColumns = new DataColumn[size];
for (int i = 0; i < size; i++)
{
keyColumns[i] = table.Columns[primaryKeyColumns[i]];
}
table.PrimaryKey = keyColumns;
}
我发现这是相当高效的 - &lt; 2毫秒来解析一个30 * 80的表,并且它可以轻松使用。